Compare commits

..

545 Commits

Author SHA1 Message Date
Ben Potter e491217a12 docs: add v2.3.3 changelog (#10435) 2023-10-30 15:06:11 -05:00
Steven Masley 9d2b805fb7 fix: prevent infinite redirect oauth auth flow (#10430)
* fix: prevent infinite redirect oauth auth flow
2023-10-30 14:45:06 -05:00
Kyle Carberry 7fc1a65b14 fix: add new aws regions to instance identity (#10434)
Fixes #10433
2023-10-30 19:44:29 +00:00
Kayla Washburn fdf035cd06 chore: remove fly template (#10429) 2023-10-30 13:16:43 -06:00
dependabot[bot] fc1d823cae chore: bump github.com/go-logr/logr from 1.2.4 to 1.3.0 (#10423)
Bumps [github.com/go-logr/logr](https://github.com/go-logr/logr) from 1.2.4 to 1.3.0.
- [Release notes](https://github.com/go-logr/logr/releases)
- [Changelog](https://github.com/go-logr/logr/blob/master/CHANGELOG.md)
- [Commits](https://github.com/go-logr/logr/compare/v1.2.4...v1.3.0)

---
updated-dependencies:
- dependency-name: github.com/go-logr/logr
  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>
2023-10-30 21:36:01 +03:00
dependabot[bot] 8fe3dcf18a chore: bump go.uber.org/goleak from 1.2.1 to 1.3.0 (#10398)
Bumps [go.uber.org/goleak](https://github.com/uber-go/goleak) from 1.2.1 to 1.3.0.
- [Release notes](https://github.com/uber-go/goleak/releases)
- [Changelog](https://github.com/uber-go/goleak/blob/master/CHANGELOG.md)
- [Commits](https://github.com/uber-go/goleak/compare/v1.2.1...v1.3.0)

---
updated-dependencies:
- dependency-name: go.uber.org/goleak
  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>
2023-10-30 21:35:40 +03:00
Kyle Carberry 5abfe5afd0 chore: rename dbfake to dbmem (#10432) 2023-10-30 17:42:20 +00:00
Spike Curtis 7a8da08124 feat: add api_version column to workspace_agents (#10418)
Adds api_version to workspace_agents table

Part of #10399
2023-10-30 21:30:49 +04:00
dependabot[bot] 6b7858c516 ci: bump the github-actions group with 2 updates (#10420)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-30 11:25:37 +00:00
Mathias Fredriksson 9d3785def8 test(cli/cliui): make agent tests more robust (#10415)
Fixes #10408
2023-10-30 13:20:10 +02:00
Spike Curtis 2a6fd90140 feat: add tailnet and agent API definitions (#10324)
Adds API definitions and packages for Tailnet and Agent APIs (API version 2.0)
2023-10-30 12:14:45 +04:00
Spike Curtis c2e3648484 fix: disable tests broken by daylight savings (#10414) 2023-10-30 06:44:30 +00:00
dependabot[bot] 3b50530a63 chore: bump gopkg.in/DataDog/dd-trace-go.v1 from 1.55.0 to 1.56.1 (#10403)
Bumps gopkg.in/DataDog/dd-trace-go.v1 from 1.55.0 to 1.56.1.

---
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>
2023-10-27 22:41:33 +03:00
Mathias Fredriksson 7fecd39e23 fix(agent/agentscripts): display informative error for ErrWaitDelay (#10407)
Fixes #10400
2023-10-27 19:07:26 +03:00
Muhammad Atif Ali 99fda4a8e2 docs: replace gituth with externalauth (#10409) 2023-10-27 10:53:56 -04:00
Muhammad Atif Ali 51aa32cfcf chore: limit history to the last 30 runs/days for PR deploy and cleanup workflows (#10406) 2023-10-27 11:15:21 +00:00
Muhammad Atif Ali 6ae8bfed94 chore(examples): fix a small typo (#10404) 2023-10-26 09:42:46 +00:00
dependabot[bot] 35e7d7854a chore: bump google.golang.org/grpc from 1.58.2 to 1.59.0 (#10381)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-26 11:36:50 +03:00
dependabot[bot] edcbd4f394 chore: bump github.com/coreos/go-oidc/v3 from 3.6.0 to 3.7.0 (#10397)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-26 11:36:10 +03:00
dependabot[bot] ea578ceabb chore: bump github.com/prometheus/common from 0.44.0 to 0.45.0 (#10399)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-26 11:35:47 +03:00
Mathias Fredriksson 0ddd54d34b fix(coderd/provisionerdserver): avoid error log during shutdown (#10402) 2023-10-25 18:31:28 +03:00
Josh Vawdrey fdc9097d6c feat(provisioner): expose template version to provisioner (#10306) 2023-10-25 14:44:08 +03:00
dependabot[bot] e7fd2cb1a6 chore: bump github.com/djherbis/times from 1.5.0 to 1.6.0 (#10380)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-24 23:03:08 +03:00
dependabot[bot] 670ee4d54f chore: bump google.golang.org/api from 0.147.0 to 0.148.0 (#10383)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-24 22:58:41 +03:00
dependabot[bot] 39fbf74c7d ci: bump the github-actions group with 1 update (#10379)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-24 22:14:48 +03:00
Mathias Fredriksson eac155aec2 test(cli): fix TestServer flake due to DNS lookup (#10390) 2023-10-24 22:12:03 +03:00
Michael Smith 7732ac475a refactor: update logic for all metadata query factories (#10356)
* refactor: simplify metadata patterns

* fix: add return type to me factory

* fix: make sure query key for me is always defined at type level
2023-10-24 08:42:38 -04:00
Josh Vawdrey 6b2aee4133 feat(cli): make the dotfiles repository directory configurable (#10377) 2023-10-24 12:00:04 +03:00
Devarshi Shimpi d8592bf09a fix(README.md): update installation link (#10275) 2023-10-24 11:36:38 +03:00
Asher 4af8446f48 fix: initialize terminal with correct size (#10369)
* Fit once during creation

This does not fix any bugs (that I know of) but we only need to fit once
when the terminal is created, not every time we reconnect.  Granted,
currently we do not support reconnecting without refreshing anyway so it
does not really matter, but this just seems more correct.

Plus now we will not have to pass the fit addon around.

* Pass size when connecting web socket URL

I think this will solve an issue where screen does does not correctly
handle an immediate resize.  It seems to ignore the resize, but even if
you send it again nothing changes, seemingly thinking it is already at
that size?

* Use new struct for decoding reconnecting pty requests

Decoding a JSON message does not touch omitted (or null) fields so once
a message with a resize comes in, every single message from that point
will cause a resize.

I am not sure if this is an actual problem in practice but at the very
least it seems unintentional.
2023-10-23 23:42:39 -04:00
Mathias Fredriksson 1286904de8 test(agent): improve TestAgent_Session_TTY_MOTD_Update (#10385) 2023-10-23 17:32:28 +00:00
Mathias Fredriksson 09f7b8e88c fix(agent/agentscripts): track cron run and wait for cron stop (#10388)
Fixes #10289
2023-10-23 17:08:52 +00:00
Mathias Fredriksson 1a2aea3a6b fix(agent): prevent metadata from being discarded if report is slow (#10386) 2023-10-23 17:02:54 +00:00
Mathias Fredriksson 6683ad989a test(coderd): fix TestWorkspaceBuild flake (#10387)
Fixes #10335
2023-10-23 19:45:54 +03:00
Mathias Fredriksson 8f1b4fb061 test(agent): fix service banner trim test flake (#10384) 2023-10-23 18:06:59 +03:00
Ben Potter a7243b3f3b docs: add v2.3.2 changelog (#10371) 2023-10-20 21:26:43 +00:00
Jon Ayers 1372bf82f5 chore: revert "chore: remove workspace_actions experiment (#10030)" (#10363) 2023-10-20 13:21:53 -05:00
Asher 57c9d88703 chore(site): remove terminal xservice (#10234)
* Remove terminalXService

This is a prelude to the change I actually want to make, which is to
send the size of the terminal on the web socket URL after we do a fit.
I have found xstate so confusing that it was easier to just rewrite it.

* Fix hanging tests

I am not really sure what ws.connected is doing but it seems to somehow
block updates.  Something to do with `act()` maybe?

Basically, the useEffect creating the terminal never updates once the
config query finishes, so the web socket is never created, and the test
hangs forever.

It might have been working before only because the web socket was
created using xstate rather than useEffect and once it connected it
would unblock and React could update again but this is just a guess.

* Ignore other config changes

The terminal only cares about the renderer specifically, no need to
recreate the terminal if something else changes.

* Break out port forward URL open to util

Felt like this could be broken out to reduce the component size.  Also
trying to figure out why it is causing the terminal to create multiple
times.

* Prevent handleWebLink change from recreating terminal

Depending on the timing, handleWebLink was causing the terminal to get
recreated.  We only need to create the terminal once unless the render
type changes.

Recreating the terminal was also recreating the web socket pointlessly.
2023-10-20 10:18:17 -08:00
Muhammad Atif Ali 5ebb702e00 chore: add OIDC provider logos (#10365)
* chore: add OIDC provider logos

* Add files via upload

* fmt
2023-10-20 19:30:05 +03:00
Eric Paulsen 9dbc913798 fix: additional cluster SA, role names (#10366) 2023-10-20 11:44:16 -04:00
Kira Pilot ed5567ba28 fix: show dormant and suspended users in groups (#10333)
* fix: show dormant and suspended users in groups

* added status column
2023-10-20 11:36:00 -04:00
Bruno Quaresma ac322724b0 chore(site): replace custom LoadingButton from the one in MUI (#10351) 2023-10-20 09:57:27 -03:00
Bruno Quaresma 3d9bfdd5dc chore(site): remove update check service (#10355) 2023-10-20 09:41:34 -03:00
Bruno Quaresma 1ba5169109 chore(site): remove search users and groups xservice (#10353) 2023-10-20 09:33:07 -03:00
Jon Ayers d33526108f feat: add frontend support for mandating active template version (#10338) 2023-10-19 18:21:52 -05:00
Jon Ayers f5f150d568 feat: add cli support for --require-active-version (#10337) 2023-10-19 17:16:15 -05:00
Ammar Bandukwala b799014832 docs: rework telemetry doc and add CLI warning (#10354) 2023-10-19 15:50:20 -05:00
Kira Pilot 9c9319f81e fix: resolve User is not unauthenticated error seen on logout (#10349)
* fix: do not cache getAuthenticatedUser call

* use initialQuery, add back meta tag for initial load of users

* lift initialUserData
2023-10-19 14:50:53 -04:00
Michael Smith ab2904a676 feat: add user groups column to users table (#10284)
* refactor: extract UserRoleCell into separate component

* wip: add placeholder Groups column

* fix: remove redundant css styles

* refactor: update EditRolesButton to use Sets to detect selections

* wip: commit progress for updated roles column

* wip: commit current role pill progress

* fix: update state sync logic

* chore: add groupsByUserId query options factory

* fix: update return value of select function

* chore: drill groups data down to cell component

* wip: commit current cell progress

* fix: remove redundant classes

* wip: commit current styling progress

* fix: update line height for CTA

* fix: update spacing

* chore: add tooltip for Groups column header

* fix: remove tsbuild file

* refactor: consolidate tooltip components

* fix: update font size defaults inside theme

* fix: expand hoverable/clickable area of groups cell

* fix: remove possible undefined cases from HelpTooltip

* chore: add popover functionality to groups

* wip: commit progress on groups tooltip

* fix: remove zero-height group name visual bug

* feat: get basic version of user group tooltips done

* perf: move sort order callback outside loop

* fix: update spacing for tooltip

* feat: make popovers entirely hover-based

* fix: disable scroll locking for popover

* docs: add comments explaining some pitfalls with Popover component

* refactor: simplify userRoleCell implementation

* feat: complete main feature

* fix: prevent scroll lock for role tooltips

* fix: change import to type import

* refactor: simplify how groups are clustered

* refactor: update UserRoleCell to use Popover

* refactor: remove unnecessary fragment

* chore: add id/aria support for Popover

* refactor: update UserGroupsCell to use Popover

* chore: redo visual design for UserGroupsCell

* fix: shrink UserGroupsCell text

* fix: update UsersTable test to include groups info
2023-10-19 14:31:48 -04:00
Bruno Quaresma 557adab224 chore(site): remove template ACL XService (#10332) 2023-10-19 14:59:08 -03:00
dependabot[bot] 21f87313bd chore: bump github.com/aws/smithy-go from 1.14.2 to 1.15.0 (#10282)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-19 16:08:56 +03:00
Muhammad Atif Ali 42c21d400f fix(docs): update external-auth docs to use coder_external_auth (#10347) 2023-10-19 12:30:48 +00:00
Bruno Quaresma f677c4470b chore(site): add custom popover component (#10319) 2023-10-19 09:13:21 -03:00
Bruno Quaresma b8c7b56fda fix(site): fix tabs in the template layout (#10334) 2023-10-19 09:12:41 -03:00
Marcin Tojek c4f590581e feat: expose template insights as Prometheus metrics (#10325) 2023-10-19 08:45:12 +00:00
Jon Ayers 997493d4ae feat: add template setting to require active template version (#10277) 2023-10-18 17:07:21 -05:00
Colin Adler 1ad998ee3a fix: add requester IP to workspace build audit logs (#10242) 2023-10-18 15:08:02 -05:00
Colin Adler 504cedf15a feat: add telemetry for external provisioners (#10322) 2023-10-18 14:20:30 -05:00
Mathias Fredriksson 9b73020f11 ci(.github): set DataDog upload timeout (#10328) 2023-10-18 20:07:52 +03:00
Bruno Quaresma c93fe8ddbe chore(site): remove template version machine (#10315) 2023-10-18 09:18:03 -03:00
Muhammad Atif Ali fe05fd1e6e docs: update vscode web docs (#10327) 2023-10-18 12:13:44 +00:00
Kayla Washburn 2b5e02f5b2 refactor: improve e2e test reporting (#10304) 2023-10-17 16:11:42 -06:00
Muhammad Atif Ali ab456276dc docs: add v2.3.1 changelog (#10313)
* add v2.3.1 changelog

* fmt

* explain

* simplify

* update

* simplify

* cleanup

* refresh

* update

* cleanup and fmt

* Update v2.3.1.md

cleanup

* add new commits

---------

Co-authored-by: Ben <me@bpmct.net>
2023-10-17 21:45:38 +00:00
Eric Paulsen 09d995c8dc fix: set K8s deployment strategy to Recreate (#10321) 2023-10-17 21:06:35 +00:00
Kayla Washburn 619df23ad1 chore: fix linting issues and generated files (#10317) 2023-10-17 14:41:35 -06:00
Muhammad Atif Ali 492da15890 chore: delete filebrowser.db (#10320) 2023-10-17 18:59:35 +00:00
Kira Pilot 1656249e07 feat: add all safe experiments to the deployment page (#10276)
* added new option table type for experiments

* added tests

* fixed go tests

* added go test for new param

* removing query change

* clearing ExperimentsAll

* dont mutate ExperimentsAll

* added new route for safe experiments

* added new route for safe experiments

* added test for new route

* PR feedback

* altered design

* alias children
2023-10-17 14:49:19 -04:00
Bruno Quaresma 35f9e2ef7f refactor(site): refactor create workspace button (#10303) 2023-10-17 13:31:51 -03:00
Michael Smith 0f2d4fdb6d fix: prevent metadata queries from short-circuiting (#10312)
* fix: prevent metadata queries from short-circuiting

* fix: use correct type definitions
2023-10-17 10:20:56 -06:00
Mathias Fredriksson 8f39ec5cc3 fix(scaletest/templates): fix bugs and improve debugging (#10316) 2023-10-17 15:18:49 +00:00
Kayla Washburn 2f4ca0f566 chore: use emotion for styling (pt. 6) (#10298) 2023-10-17 09:14:13 -06:00
Muhammad Atif Ali a49e6b88f9 docs: reorganize template docs (#10297)
* docs: rework our "templates" section

* wikistuff

* fix formatting

* add diagram

* reorganize some things

* docs: improve workspaces and templates doc (#9139)

* Reorg, updated/new screenshots, consistent terminology

* First pass

* Another pass

* Added integration section

* New outline for template pages, small updates

* Revised outline for templates, added tutorial

* First pass at tutorial

* Some feedback from Ben.

* Update docs/workspaces.md

Co-authored-by: Muhammad Atif Ali <matifali@live.com>

* Update docs/workspaces.md

Co-authored-by: Muhammad Atif Ali <matifali@live.com>

* Update docs/workspaces.md

Co-authored-by: Muhammad Atif Ali <matifali@live.com>

* Fixed typos

* Expanded tutorial

I have read the CLA Document and I hereby sign the CLA

* New screenshots, improved tutorial, revised anatomy

* Improved tutorial. Anatomy is now a guided tour.

* First pass at guided tour

* Updated authentication info

* Reorganized the guided tour

* Edited more template pages

* Update docs/templates/tour.md

Co-authored-by: Muhammad Atif Ali <matifali@live.com>

* Update docs/templates/tour.md

Co-authored-by: Muhammad Atif Ali <matifali@live.com>

* Update docs/templates/tour.md

Co-authored-by: Muhammad Atif Ali <matifali@live.com>

* Update docs/templates/tutorial.md

Co-authored-by: Muhammad Atif Ali <matifali@live.com>

* Update docs/templates/tour.md

Co-authored-by: Muhammad Atif Ali <matifali@live.com>

* Update docs/templates/tour.md

Co-authored-by: Muhammad Atif Ali <matifali@live.com>

* Update docs/templates/tour.md

Co-authored-by: Muhammad Atif Ali <matifali@live.com>

* Update docs/templates/tour.md

Co-authored-by: Muhammad Atif Ali <matifali@live.com>

* Update docs/templates/tour.md

Co-authored-by: Muhammad Atif Ali <matifali@live.com>

* Revised devcontainers and docker-in-workspaces

* Edited and added screenshots

* Prepared first draft, except docs/templates/open-in-coder.md

* Fix typo

* remove legacy parameters and migration guide

* Use coder templates create

* Added screenshot for workspace template variables

* Made it prettier

* Fixed minor typos and markdown problems

* edits to repairing workspaces

* fix broken links in product

* Added troubleshooting, minor corrections.

* fix terminal links

* fmt

---------

Co-authored-by: Muhammad Atif Ali <matifali@live.com>
Co-authored-by: Ben Potter <me@bpmct.net>
Co-authored-by: Atif Ali <atif@coder.com>

* make fmt

* fix merge conflict

* make fmt

* make gen

* update

* lint

* Discard changes to coderd/database/queries.sql.go

* Discard changes to cli/templates.go

* Discard changes to cli/templateversionarchive.go

* Discard changes to cli/templateversions.go

* Update docker-in-workspaces.md

* replace ```sh with ```shell

* open-in-coder

* fmt

* mention coder_metadata in icons.md

* resource_metadata

* use shell

* modules.md

* mention coder registry module

* workspace.md

* resource_metadata

* remove duplication

* address comments

* cleanup

* fmt

* fix broken links

* fix numbering

* mention module registry

* add example

* demote heading

* remove top level entry from manifest

* fmt

---------

Co-authored-by: Ben <me@bpmct.net>
Co-authored-by: Marc Paquette <22124737+marcpaq@users.noreply.github.com>
2023-10-17 14:47:12 +00:00
dependabot[bot] b5e5b39de2 chore: bump @babel/traverse from 7.22.8 to 7.23.2 in /offlinedocs (#10294)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-16 23:39:04 +03:00
dependabot[bot] 2acf195b13 chore: bump @babel/traverse from 7.22.11 to 7.23.2 in /site (#10295)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-16 23:38:51 +03:00
Kayla Washburn eaea918a59 chore: use emotion for styling (pt. 5) (#10261) 2023-10-16 12:41:15 -06:00
Bruno Quaresma 4240200b5d fix(site): fix state used to check if creating was loading (#10296) 2023-10-16 15:53:50 +00:00
Mathias Fredriksson 43f26dfec5 feat(scaletest/templates): run all scenarios even on failure (#10290)
We now also end all failed phases and add the `error` tag in Grafana.
2023-10-16 18:18:05 +03:00
Ben Potter 9a0aac88e0 chore: fix broken links in docs (#10291)
https://github.com/coder/coder/actions/runs/6531396781/job/17732557812
2023-10-16 14:29:53 +00:00
Steven Masley 6ebe9b0402 feat: add UI for autostart workspace days (#10263)
* feat: add ui for selecting auto start days
2023-10-16 09:29:42 -05:00
Steven Masley 5a90228c60 feat: fix 404 on the first app loads when unauthenticated (#10262)
* feat: fix 404 on the first app loads when unauthenticated
* Update site/src/pages/LoginPage/LoginPage.tsx
2023-10-16 09:29:25 -05:00
Mathias Fredriksson 8ffe0e22b6 feat(scaletest/templates): gather pod logs at the end of a scale test (#10288) 2023-10-16 13:50:04 +00:00
Bruno Quaresma 8efa1239e7 fix(site): do not return next page if the current size is lower than the limit (#10287) 2023-10-16 13:39:48 +00:00
Mathias Fredriksson 3c49290dd7 feat(scaletest/templates): add comment parameter (#10285) 2023-10-16 13:28:23 +00:00
Cian Johnston 6875faf238 fix(coderd/provisionerdserver): pass through api ctx to provisionerdserver (#10259)
Passes through coderd API ctx to provisionerd server so we can cancel workspace updates when API is shutting down.
2023-10-16 13:50:07 +01:00
Bruno Quaresma 01792f064e fix(site): display empty component when workspace has no parameters (#10286) 2023-10-16 09:49:53 -03:00
Bruno Quaresma f64b9cab90 feat(site): decrease the number of statuses in the workspaces filter (#10283) 2023-10-16 12:46:06 +00:00
Mathias Fredriksson 493e2bd2ac feat(scaletest/templates): add repo branch parameter (#10279) 2023-10-16 12:25:32 +00:00
Cian Johnston dd86100f33 fix(scaletest): fix flake in Test_Runner/Cleanup (#10252)
* fix(scaletest/createworkspaces): address flake in Test_Runner/CleanupPendingBuild

* fix(scaletest): pass io.Writer to Cleanup()

* add some extra logs to workspacebuild cleanup

* fixup! fix(scaletest): pass io.Writer to Cleanup()

* remove race

* fmt

* address PR comments
2023-10-16 12:37:12 +01:00
Cian Johnston 1be24dcb5c feat(helm/provisioner): add extraTemplates (#10256)
Adds support for extraTemplates to the coder-provisioner chart to bring it in line with the coder chart.
2023-10-16 12:17:05 +01:00
Stephen Kirby 2029543eba chore(site): clarify autostop description (#10260)
* updated autostop description based on customer request

* edited for specificity

* make fmt
2023-10-13 12:24:37 -05:00
Steven Masley 39c0539d42 feat: add controls to template for determining startup days (#10226)
* feat: template controls which days can autostart
* Add unit test to test blocking autostart with DaysOfWeek
2023-10-13 11:57:18 -05:00
Ammar Bandukwala 98b6c8bcb0 chore(coderd): report full license in telemetry (#10258)
Will aid in cross-referencing deployments to sales accounts.
2023-10-13 11:43:06 -05:00
Kayla Washburn cbc0c39792 fix: display health alert in DeploymentBannerView (#10193) 2023-10-13 10:39:20 -06:00
Muhammad Atif Ali def980b973 chore(docs): fix syntax highlighting (#10247)
Due to some reason, our docs are not rendering syntax highlighting when the code block type is 'terraform'. Changing the type to `hcl` fixes this.
2023-10-13 18:50:24 +03:00
Mathias Fredriksson 76c65b1e1b fix(agent): send metadata in batches (#10225)
Fixes #9782

---

I recommend reviewing with ignore whitespace.
2023-10-13 17:48:25 +03:00
Mathias Fredriksson 4857d4bd55 feat(codersdk/agentsdk): use new agent metadata batch endpoint (#10224)
Part of #9782
2023-10-13 17:32:28 +03:00
Mathias Fredriksson 7eeba15d16 feat(coderd): add support for sending batched agent metadata (#10223)
Part of #9782
2023-10-13 16:37:55 +03:00
dependabot[bot] 1b1ab97c24 chore: bump github.com/google/go-cmp from 0.5.9 to 0.6.0 (#10248)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-13 13:27:22 +00:00
dependabot[bot] 13036dd088 chore: bump google.golang.org/api from 0.145.0 to 0.147.0 (#10249)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-13 16:17:35 +03:00
dependabot[bot] ab7dd24d97 ci: bump the github-actions group with 2 updates (#10250)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-13 16:17:21 +03:00
Cian Johnston d56f49f619 fix(coderd): make activitybump aware of default template ttl (#10253)
The refactored ActivityBump query did not take into account the
template-level TTL, resulting in potentially incorrect bump
amounts for workspaces that have both a user-defined and template-
defined TTL that differ.

This change is ported over from PR#10035 to reduce the overall
size of that PR.

Also includes a drive-by unit test in autobuild for checking template autostop/TTL.

Co-authored-by: Dean Sheather <dean@deansheather.com>
2023-10-13 13:53:02 +01:00
Marcin Tojek 2a4ac2a53c feat: expose user seat limits as Prometheus metrics (#10169) 2023-10-13 08:10:16 +00:00
Eric Paulsen 570f963aea clarify external auth regex (#10243)
* docs: clarify external auth regex

* cleanup
2023-10-13 04:27:12 +00:00
Kayla Washburn 5fc9ff29d1 docs: mention /icons in the template documentation (#10230)
Co-authored-by: Muhammad Atif Ali <atif@coder.com>
2023-10-12 15:32:23 -06:00
Steven Masley 88605b9d01 chore: add display name to member role (#10239)
* chore: add display name to member role
* Do not let member role be assignable
* Ignore org member role for assignability atm
2023-10-12 10:52:32 -05:00
Cian Johnston e5198a25a6 feat(scaletest): annotate scaletest pod when scaletest is in progress (#10235)
This PR modifies the scaletest-runner template to add a pod annotation to the scaletest runner pod.

The annotation key is set to com.coder.scaletest.phase and the annotation value is one of preparing, running, or complete.

This will allow checking if a scaletest is in progress, and preventing any operations that would interrupt a running scaletest.

Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2023-10-12 14:36:15 +01:00
Mathias Fredriksson 782c22a293 test(coderd/database/dbtestutil): allow access to *sql.DB (#10238) 2023-10-12 15:07:16 +03:00
Mathias Fredriksson 7df40b85f2 chore(coderd/coderdtest): use testing.TB for use in benchmarks (#10237) 2023-10-12 15:03:16 +03:00
Cian Johnston 1e75762cb4 fix(cli): scaletest: create-worksapces: remove invalid character for kubernetes provider in implicit plan (#10228) 2023-10-12 09:21:40 +01:00
Muhammad Atif Ali 3adf86b608 chore(dogfood): use coder_extrenal_auth over deprecated coder_git_auth (#10208)
@kylecarbs does this need any changes on the Coder server?
For example adopting to the new `CODER_EXTERNAL_AUTH_X` variables?
2023-10-12 10:30:00 +03:00
dependabot[bot] 5f0457f160 chore: bump golang.org/x/net from 0.16.0 to 0.17.0 (#10232)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.16.0 to 0.17.0.
- [Commits](https://github.com/golang/net/compare/v0.16.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-11 21:43:59 -05:00
Michael Smith 9bf3b35bbf fix: update create workspace button to recognize template names+display names (#10233)
* fix: only used 'unnamed' as a last resort for templates

* fix: update filter logic to read from name field
2023-10-12 00:23:45 +00:00
Kayla Washburn 6ef1beec13 chore: recrop some icons for more consistent sizing (#10229) 2023-10-11 15:59:47 -06:00
Asher a9077812e2 fix: use UTF-8 encoding with screen (#10190)
This will make characters like ❯ and ⇣ work, for example.
2023-10-11 13:25:04 -08:00
Asher a67a5a8105 Prevent terminal being created twice (#10200)
I missed this in code review.
2023-10-11 12:47:32 -08:00
Bruno Quaresma 301c045aad chore(site): remove create workspace xservice (#10217)
* Move xstate transitions to provider

* Centrlize auth logic in the provider

* Remove actor

* Remove auth xservice

* Add loader while AuthProvider is loading

* Simplify and fix a few computed states

* Add a few replaces

* Fix logout

* Remove unused import

* Fix RequireAuth test

* Fix wait loader

* Fix tests

* Remove unecessary type

* Rename workspace queries module

* Remove auto create from workspace xservice

* Move external auth into its own hook

* Remove permissions fetching from templateByName query

* Remove form load from service

* Remove create workspace service entirely

* refactor: update workspace automation to use useEffectEvent

---------

Co-authored-by: Parkreiner <michaelsmith@coder.com>
Co-authored-by: Michael Smith <throwawayclover@gmail.com>
2023-10-11 20:42:25 +00:00
Bruno Quaresma 5be4b12378 chore(site): refactor AuthProvider to not use authXService (#10184)
* Move xstate transitions to provider

* Centrlize auth logic in the provider

* Remove actor

* Remove auth xservice

* Add loader while AuthProvider is loading

* Simplify and fix a few computed states

* Add a few replaces

* Fix logout

* Remove unused import

* Fix RequireAuth test

* Fix wait loader

* Fix tests

* Wrap signout with callback
2023-10-11 16:13:32 -04:00
Kira Pilot 7c6687813d fix(site): ensure empty string error shows default message (#10196)
* fix(site): ensure empty string error shows default message

* added test

* added logging

* asserting axios message

* cleanup and add test
2023-10-11 13:54:58 -04:00
Cian Johnston 59ae69b7f2 chore(enterprise/cli): fix test flake in TestServerDBCrypt (#10222)
* increase randomness in names generated by server dbcrypt

* more randomness

* close PTYs when we are done with them
2023-10-11 17:38:20 +01:00
Ben Potter 04e67836a5 docs: add v2.3.0 changelog (#10221)
* docs: add v2.3.0 changelog

* mention archive version
2023-10-11 11:05:53 -05:00
Cian Johnston 98a076fb46 chore(pty/ptytest): add sync.Once to close (#10220) 2023-10-11 16:47:02 +01:00
Steven Masley ac623b4717 feat: implement basic archive ui to make archiving failed versions easy (#10182)
* feat: implement basic archive ui to make archiving failed versions easy.
2023-10-11 15:06:10 +00:00
Steven Masley 1e950fa9a8 feat: archive template versions to hide them from the ui (#10179)
* api + cli implementation
2023-10-11 09:26:22 -05:00
Bruno Quaresma edbd51955c chore(site): fix inconsistent fetching results on tests (#10215) 2023-10-11 11:17:56 -03:00
Colin Adler 43fa4349d6 chore: move single_tailnet experiment to * (#10188) 2023-10-11 08:50:45 -05:00
Mathias Fredriksson a2cd6640f3 fix(codersdk/agentsdk): improve ctx cancel in agent logs flush, fix test (#10214)
Fixes #9719
Related #9865
2023-10-11 12:42:30 +00:00
Marcin Tojek a1ee4d44aa fix: test: TestSSH_RemoteForward wait for startup script (#10211) 2023-10-11 14:17:04 +02:00
Cian Johnston e829cbf2db fix(scaletest/dashboard): fix early exit due to validate (#10212) 2023-10-11 11:51:06 +00:00
Cian Johnston ed8092c83d fix(scaletest/createworkspaces): address race condition between agent closer and cleanup (#10210) 2023-10-11 12:10:51 +01:00
Cian Johnston b3471bd23a fix(scaletest/dashboard): increase viewport size and handle deadlines (#10197)
- Set viewport size to avoid responsive mode
- Added way more debug logging
- Added facility to write a screenshot on error in verbose mode.
- Added a deadline for each iteraction of clicking on and waiting for a thing.
2023-10-11 11:10:08 +01:00
Cian Johnston dc117051e6 chore(docs): update admin/scale.md (#10168) 2023-10-11 09:40:43 +01:00
Muhammad Atif Ali fafecbd9b3 fix(dogfood): fix slackme module source (#10207)
This was pinned to the branch ref and is no more needed after the branch has been merged.
2023-10-11 08:29:25 +00:00
Spike Curtis 3c43216e99 fix: remove Parallel() call after timeout context (#10203)
Fixes test flake seen here: https://github.com/coder/coder/runs/17562370632

It's inherently flaky to create a context with a timeout and then later call `t.Parallel()` since it causes the test to wait until all non-parallel tests have completed before resuming execution.  By the time execution has resumed, the context may 
have expired.  The amount of time before resuming is dependent on machine resources and number of test cases, which are inherently variable.
2023-10-11 11:39:15 +04:00
Jon Ayers 4452a1484d fix: fix log spam related to skipping custom nice scores (#10206) 2023-10-11 02:32:50 -05:00
Spike Curtis 7c71053eab fix: stop leaking User into API handlers unless authorized
Fixes an issue where we extracted the `{user}` parameter from the URL and added it to the API Handler context regardless of whether the caller had permission to read the User.
2023-10-11 09:41:14 +04:00
Spike Curtis fbabb43cbb fix: ignore spurious node updates while waiting for errors (#10175)
fixes #9921
2023-10-11 09:22:20 +04:00
Kyle Carberry b0d2828f9e fix: append external auth env vars (#10201) 2023-10-11 05:17:08 +00:00
Jon Ayers ec9b480ac0 fix: use is-dormant instead of dormant_at (#10191) 2023-10-10 19:00:09 -05:00
Kyle Carberry 652e1a7d43 feat: add slackme module to dogfood (#10198) 2023-10-10 22:46:47 +00:00
Dean Sheather e7d9b8d858 feat: allow prefixes at the beginning of subdomain app hostnames (#10150) 2023-10-10 20:02:39 +00:00
Dean Sheather f48bc33e00 chore: remove cron schedule from quiet hours schedule page (#10187) 2023-10-10 19:55:28 +00:00
Jon Ayers 91555c3a85 feat: support configurable web terminal rendering (#10095)
* feat: support configurable web terminal rendering

- Added a deployment option for configuring web terminal rendering.
  Valid values are 'webgl', 'canvas', and 'dom'.
2023-10-10 13:18:02 -05:00
Jon Ayers 05a393cd06 feat: only display license warnings to privileged users (#10096) 2023-10-10 12:48:51 -05:00
Colin Adler 7e6b549170 chore: upgrade Terraform to 1.5.7 (#10186) 2023-10-10 12:46:59 -05:00
Colin Adler 21e0d540dc chore: upgrade Go to 1.20.10
https://groups.google.com/g/golang-announce/c/iNNxDTCjZvo/m/UDd7VKQuAAAJ?utm_medium=email&utm_source=footer
2023-10-10 12:23:54 -05:00
Kayla Washburn 7ea58eac18 chore: use emotion for styling (pt. 4) (#10149) 2023-10-10 10:46:45 -06:00
Colin Adler 00589d6422 chore: fix lint failures 2023-10-10 11:26:53 -05:00
Steven Masley 69d13f1676 chore: add archive column to template versions (#10178)
* chore: add archive column to template versions
2023-10-10 10:52:42 -05:00
Colin Adler c11f241622 feat: add --version flag to coder templates pull, default to active version (#10153)
Fixes https://github.com/coder/coder/issues/9837
2023-10-10 10:20:31 -05:00
Cian Johnston 2506415def chore(scaletest/templates/scaletest-runner): fix dashboard command invocation, autoscale provisioners (#10177)
add --retries on kubectl cp
remove --count parameter to scaletest dashboard
scale provisioners up and down

Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2023-10-10 15:33:55 +01:00
Spike Curtis db8592fa93 chore: refactor workspace conversion to accept ownerName (#10171)
Refactors workspace conversion to accept the ownerName, rather than a slice of users, since all it does is search the slice for the owner and use the username.

This is in preparation for a fix to `postWorkspacesByOrganization()` that will remove the need to pass the user object.

Also avoids panicing if the required user is not in the slice, since `findUser` could return nil in the old code, which would then get dereferenced for the username.
2023-10-10 16:55:28 +04:00
Bruno Quaresma 19400d6794 fix(site): fix week range for insights (#10173) 2023-10-10 09:33:46 -03:00
Spike Curtis b780bff429 chore: drop unused redirectToLoginOnMe parameter (#10164)
The parameter seems to be vestigial from an earlier use of the middleware, but is always set to `false` in the code.
2023-10-10 16:13:00 +04:00
Michael Smith 5ae6cda89f feat: add warning message when trying to delete active template (#10142)
* refactor: clean up TemplatePageHeader

* chore: add react query configs for workspace lists

* feat: add delete-intercept functionality

* refactor: improve readability

* refactor: rename entities for readability/accuracy

* refactor: clean up variable names again

* refactor: remove redudant function calls

* fix: update logic check for safe deletions

* fix: update workspaces query logic

* fix: update call site for workspaces key
2023-10-10 08:04:54 -04:00
Spike Curtis 78b9201b31 chore: move AsSystemRestricted to caller (#10163)
Moves escalation to SystemRestricted out of the function that queries the database for the User. This is in prepartion for a refactor such that we don't need SystemRestricted in `ExtractUserParam` middleware.
2023-10-10 15:57:51 +04:00
Spike Curtis 8a47262faf fix: ignore logged errors in TestWorkspaceAgent/Timeout
fixes #10167

Annoyingly, there isn't a good way to stop the publish from being sent on shutdown, and subscribing to them in the test is too fragile because empty messages are sent in a bunch of places, so we can't reliably tell it's regarding timeouts.
2023-10-10 15:45:47 +04:00
Spike Curtis a0485c00ac chore: refactor ExtractUserParam to call function
Refactors `ExtractUserParam` to separate the part that actually obtains the user from the database and the part that sets it on the middleware context.  This is in preparation for further refactor that removes `ExtractUserParam` middleware from 
`organizations/{organization}/members/{user}` paths.
2023-10-10 15:00:53 +04:00
Cian Johnston c83af5e627 chore(cli): add linter to detect potential spurious usage of owner user in cli tests (#10133)
* Detects the following pattern where the CLI is initialized with a client authenticated as the "first user":

    client := coderdtest.New(t, ...)
    [...]
    user := coderdtest.CreateFirstUser(t, client)
    [...]
    clitest.SetupConfig(t, client, root)

* Updates documentation regarding role permissions on workspaces.
2023-10-10 11:14:20 +01:00
dependabot[bot] 017d7e9dad chore: bump github.com/prometheus/client_model to 0.5.0 (#10129)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-10 12:33:32 +03:00
Spike Curtis 211718f95a fix: fix MaliciousTar test case (#10158)
fixes #9895

Problem was that provisionerd tries to acquire the next job, and races with shutdown, triggering the assert in the handler.  Switches this test case to use the more robust handler.
2023-10-10 13:24:43 +04:00
Marcin Tojek f36fba2486 fix: revert: use CRC32 to shorten app subdomain
This reverts commit 0e28397c82.
2023-10-10 18:12:46 +10:00
Spike Curtis b039dc6989 fix: correct escaping in test regex (#10138)
Fixes regex escaping.  Spotted during a code read.
2023-10-10 08:42:39 +04:00
Kyle Carberry 9c098b218f feat: allow external auth providers to expose extra metadata (#10157) 2023-10-09 23:02:16 -05:00
Kyle Carberry 3eb9a43190 fix: use query to get external-auth by id (#10156) 2023-10-09 22:25:50 -05:00
Kyle Carberry a61f8ee45c fix: apply default ExtraTokenKeys to oauth (#10155) 2023-10-09 22:11:05 -05:00
Kyle Carberry 863c2e7b64 feat: allow storing extra oauth token properties in the database (#10152) 2023-10-09 18:49:30 -05:00
Kyle Carberry 35538e1051 feat: add external-auth cli (#10052)
* feat: add `external-auth` cli

* Add subcommands

* Improve descriptions

* Add external-auth subcommand

* Fix docs

* Fix gen

* Fix comment

* Fix golden file
2023-10-09 23:04:35 +00:00
Colin Adler 20438ae6c2 chore: run go mod tidy 2023-10-09 15:10:46 -05:00
Kayla Washburn 42fb6cab12 chore: add icons for popular programming languages (#10141) 2023-10-09 13:52:06 -06:00
dependabot[bot] cb3b617ee9 ci: bump the github-actions group with 2 updates (#10131)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-09 22:39:40 +03:00
dependabot[bot] af63909134 chore: bump google.golang.org/api from 0.143.0 to 0.145.0 (#10130)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-09 22:39:29 +03:00
dependabot[bot] 583d44e60e chore: bump the golang-x group with 6 updates (#10128)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-09 22:39:13 +03:00
Kayla Washburn 1cdc62b332 chore: reorganize storybook (#10144) 2023-10-09 13:26:38 -06:00
Kyle Carberry 54648b90ca fix: properly trim spaces so multi-line shebang executes (#10146) 2023-10-09 19:19:57 +00:00
Colin Adler 3bbfcc593e feat: add request_id to HTTP trace spans (#10145) 2023-10-09 14:05:10 -05:00
Muhammad Atif Ali 2881b8b252 chore: add vault icon (#10125) 2023-10-09 18:25:07 +00:00
Spike Curtis b9c7bc4d3c fix: check for nil pointer in AwaitWorkspaceAgents
CompletedAt is a pointer and can be nil, need to check before calling IsZero() on it

c.f. https://github.com/coder/coder/runs/17534657301
2023-10-09 22:12:28 +04:00
Bruno Quaresma 584a2e87c9 chore(site): remove create template xservice (#10112) 2023-10-09 14:10:48 -03:00
Spike Curtis 54fd350913 feat: improve logging for speedtest connections
part of #7963

improve connection logging for speedtest connections
2023-10-09 20:48:28 +04:00
Colin Adler 9e622d00a6 feat(cli): add coder users delete command (#10115) 2023-10-09 11:47:57 -05:00
Spike Curtis 24c80bf532 fix: remove AwaitWorkspaceAgents in goroutines
AwaitWorkspaceAgent calls testify.require which isn't allowed from a goroutine and causes cascading failures in the test suite such as: https://github.com/coder/coder/actions/runs/6458768855/job/17533163316

I don't believe these functions serve a direct purpose since nothing else is "waiting" for the functions to return before doing other things.
2023-10-09 20:37:23 +04:00
Spike Curtis 17e889af16 feat: improve logging for reconnectingPTY connections
part of #7963

improves connection logging on reconnectingPTY
2023-10-09 20:35:50 +04:00
Kyle Carberry b402f2a816 feat: add shebang support to scripts (#10134)
This enables much greater portability!
2023-10-09 10:57:57 -05:00
Kayla Washburn 17869ecb74 feat: select icons from emoji picker (#10119) 2023-10-09 09:50:24 -06:00
Kayla Washburn bda68b143a feat: add /icons page (#10093) 2023-10-09 09:49:26 -06:00
Spike Curtis 236e84c4d6 feat: add logging for forwarded TCP connections
part of #7963

log TCP connections as they are forwarded by gVisor
2023-10-09 19:41:26 +04:00
Kira Pilot 791144ddfd feat(site): disable rich parameters when using open in coder (#10114)
* feat(site): disable rich parameters when using open in coder

* updated docs

* chore(site): increase refetch interval for deployment health

* Revert "chore(site): increase refetch interval for deployment health"

This reverts commit 8e642be3fb.
2023-10-09 09:47:22 -04:00
Cian Johnston 5673aca408 feat(cli): add --parameter flag to exp scaletest command (#10132) 2023-10-09 14:08:24 +01:00
Marcin Tojek c6cf719f6c feat: show user limit on active users chart (#10101) 2023-10-09 13:38:41 +02:00
Michael Smith 38bb854c8b fix: update ErrorDialog logic and tests (#10111)
* fix: make error text less naggy

* fix: make input colors sync with confirmation text state

* fix: more color sync fixes

* fix: remove flaky warning messages in test

* fix: remove needless braces

* refactor: clean up code

* refactor: clean up code more
2023-10-06 19:40:37 -04:00
Kayla Washburn ae113179b3 chore: alias react-query (#10118) 2023-10-06 17:15:03 -06:00
Kira Pilot da47ac87db chore(site): increase refetch interval for deployment health (#10117) 2023-10-06 16:26:46 -04:00
Colin Adler 19dbf19177 fix(coder): properly check for missing organization membership
Fixes a possible panic introduced in https://github.com/coder/coder/pull/9781.
2023-10-06 14:57:21 -05:00
Kayla Washburn 71ad5909f2 chore: add icons.json file (#10085) 2023-10-06 13:46:37 -06:00
Colin Adler 36f3151b71 fix(enterprise/tailnet): properly detect legacy agents (#10083) 2023-10-06 16:49:26 +00:00
Colin Adler 03a7d2f70b chore: fix servertailnet test flake (#10110)
https://github.com/coder/coder/actions/runs/6424100765/job/17444018788?pr=10083#step:5:771
2023-10-06 11:31:53 -05:00
Kyle Carberry 2d2bea79a7 fix: convert the new dashboard theme to be an experiment (#10108) 2023-10-06 09:46:52 -05:00
Kyle Carberry 69b65693c9 fix: make the dark mode a bit less dark (#10107)
We got feedback from our Discord that it was a bit too dark,
so this brightens it up a little!
2023-10-06 14:09:21 +00:00
Kyle Carberry 23425d36a1 fix: invert the favicon on dark mode (#10097) 2023-10-06 07:55:11 -05:00
Spike Curtis 983e8c3ae8 feat: add API support for workspace automatic updates (#10099)
* Added automatic_updates to workspaces table

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

* Queries and API updates

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

* Golden files

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

* Enable automatic updates on autostart

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

* db migration number

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

* fix imports and ts mock

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

* code review updates

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-10-06 13:27:12 +04:00
Kyle Carberry d24d2d2c8d fix: use proper react hook for favicon theme (#10094)
I was using `useState` before, which didn't re-render on load.
2023-10-05 18:23:56 -05:00
Bruno Quaresma 127f65c98b fix(site): fix logo width on sign in (#10091) 2023-10-05 17:44:53 -05:00
Kyle Carberry 4ad080c3b9 fix: apply the same border for button groups (#10092) 2023-10-05 17:44:45 -05:00
Kyle Carberry 14c8824c83 fix: use proper state in system theme (#10090) 2023-10-05 17:32:52 -05:00
Kyle Carberry fa0a597530 fix: add build status favicons based on system theme (#10089) 2023-10-05 22:23:11 +00:00
Kyle Carberry f270d9d351 feat: make the dashboard darker (#10084)
* feat: make the dashboard darker

Coder is a the internal software development platform. It is not
designed to be opinionated on colors, but it should look great.

Focusing on neutrality for our default dashboard theme is great
for our ICP. Some organizations may lean towards colors more or
less, and that shouldn't influence their decision when exploring
Coder.

* Make it a lil more dark

* Improve button outline

* Lower the red brightness

* Improve the divider contrast
2023-10-05 16:46:38 -05:00
Bruno Quaresma 04e7748a9b feat(site): load previous builds (#10076) 2023-10-05 18:30:11 -03:00
Kyle Carberry 1eb21d247b feat: adjust favicon based on system color-scheme (#10087)
This will still default to light-theme, but support showing
a white favicon when the system-theme is dark.
2023-10-05 16:11:15 -05:00
Bruno Quaresma a5f8300c76 refactor(site): do not block the screen when disconnected (#10057)
Close https://github.com/coder/coder/issues/9858

Demo:
https://github.com/coder/coder/assets/3165839/d6d4d2d7-487a-4739-86c9-042242d9820f
2023-10-05 18:06:26 -03:00
Michael Smith 2d6c4fe90a feat(site): add WorkspacesButton component (#10011)
* chore: Add OverflowY component

* chore: Add PopoverContainer component

* chore: Add SearchBox

* feat: add WorkspacesButton

* chore: Install MUI utils package

* chore: integrate WorkspacesButton

* chore: reorganize files

* fix: resolve hover state visual glitch

* chore: Add story for OverflowY

* fix: remove dynamic name from OverflowY story

* chore: update stories again

* fix: remove all references to icons (for now)

* refactor: move flex shrink to be OverflowY concern

* fix: remove needless render key

* fix: make sure popover closes before navigation

* refactor: clean up WorkspacesButton to use more native MUI

* fix: update integration into rest of view

* fix: remove JS security concern

* refactor: parameterize button language

* revert: undo sql/go file change

* fix: remove permissions dependency

* fix: simplify button prop types

* fix: lift data dependencies to page component

* refactor: clean up props

* fix: update dependencies again for Storybook
2023-10-05 20:46:42 +00:00
Kyle Carberry ad47ef17e8 feat: allow reading the agent token from a file (#10080)
Adds `CODER_AGENT_TOKEN_FILE` which will read the agent token from
a file if `CODER_AGENT_TOKEN` is not provided. Using a Kubernetes
Secret with a volume-mounted file is a more secure way to provide
the agent token instead of an environment variable.
2023-10-05 15:41:05 -05:00
Jon Ayers eb4826a11f chore: remove workspace_actions experiment (#10030) 2023-10-05 14:18:35 -05:00
Kayla Washburn 3c87c4df1b feat: show descriptions for parameter options (#10068) 2023-10-05 13:14:28 -06:00
Jon Ayers b32d79ef0b fix: fix failed workspaces continuously auto-deleting (#10069)
- Fixes an issue where workspaces that are eligible for auto-deletion
  are retried every tick (1 minute) even if the previous deletion
  transition failed.

  The updated logic only attempts to delete workspaces that previously
  failed once a day (24 hours since last attempt).
2023-10-05 14:11:39 -05:00
Jon Ayers 91265678ad chore: add auditing to workspace dormancy (#10070)
- Adds an audit log for workspaces automatically transitioned to the dormant
  state.
- Imposes a mininum of 1 minute on cleanup-related fields. This is to
  prevent accidental API misuse from resulting in catastrophe.
2023-10-05 13:41:07 -05:00
Kyle Carberry 888b97fd86 chore: use JobStatus computed from the db (#10079)
Fixes `main`
2023-10-05 18:25:06 +00:00
Kayla Washburn 246dae0e1a chore: use emotion for styling (pt. 3) (#10026) 2023-10-05 10:49:44 -06:00
Kayla Washburn f001a57614 fix: only allow promoting successful template versions (#9998) 2023-10-05 10:49:25 -06:00
Mathias Fredriksson 48ee80a559 fix(cli): prevent sqlDB leaks in ConnectToPostgres (#10072) 2023-10-05 17:57:48 +03:00
Mathias Fredriksson 5d5a7da67f fix(scaletest): output error and trace instead of {} for json output (#10075) 2023-10-05 13:31:54 +00:00
Marcin Tojek ab9276bd08 feat: modify workspace_agent_stats index (#10073) 2023-10-05 14:49:08 +02:00
Colin Adler 30440915bc chore: add mod replace wireguard-go to fix race condition (#10071)
Fixes https://github.com/coder/coder/issues/10045
2023-10-04 23:47:01 -05:00
Steven Masley 5021e23105 chore: compute job status as column (#10024)
* chore: provisioner job status as column
* use provisioner job status for workspace searching
2023-10-04 20:57:46 -05:00
Colin Adler d5040441aa fix(site): change utils/delay import path (#10065) 2023-10-04 17:15:20 -05:00
Kyle Carberry df8e10cc4c chore: increase ForceCancelInterval for test flakes (#10066)
See https://github.com/coder/coder/actions/runs/6411239320/job/17406394658
2023-10-04 21:16:39 +00:00
Kyle Carberry bca7416069 fix: add --version flag to the root to support migrating customers (#10063) 2023-10-04 15:37:15 -05:00
Jon Ayers 1cd4405caf fix: change alpha badge color to violet (#10029)
- Makes it less scary.
2023-10-04 15:20:13 -05:00
Bruno Quaresma 03c377b754 fix(site): fix users page for template admins (#10060)
Fix https://github.com/coder/coder/issues/10053
2023-10-04 17:05:49 -03:00
Bruno Quaresma a8ed88b22e fix(site): fix chart label depending on interval (#10059)
Close https://github.com/coder/coder/issues/10056
2023-10-04 17:05:29 -03:00
Eric Paulsen 1076d16456 docs: update offline tf provider config (#10062) 2023-10-04 15:40:09 -04:00
Kyle Carberry fd06b7f7a0 fix: allow all environment variables to fallback prefix to HOMEBREW_ (#10050)
See the customer use-case in the code docs.
2023-10-04 18:57:49 +00:00
Kyle Carberry 252ec14556 fix: update the validation url for github enterprise (#10061)
This was wrong... not sure why.
2023-10-04 18:49:40 +00:00
Kyle Carberry 7f9b4ad9a8 fix: allow auditors to query deployment stats and insights (#10058)
This is a customer request.
2023-10-04 18:37:25 +00:00
Bruno Quaresma 516b88dc25 fix(site): disable auto fields when they are disabled in the template settings (#10022)
- Disable form inputs 
- Add disable badge + tooltip with more info

<img width="1679" alt="Screen Shot 2023-10-03 at 14 20 26" src="https://github.com/coder/coder/assets/3165839/7555eb77-19d9-4a13-965e-6d40c3b852dd">

Fix https://github.com/coder/coder/issues/9820
2023-10-04 15:00:09 -03:00
Ben Potter 46551e619d docs: add v2.2.1 changelog (#10055)
* fix external auth link

* chore v2.2.1 changelog
2023-10-04 17:02:14 +00:00
Kyle Carberry 64692f0b69 chore: update docs for external-auth (#10046) 2023-10-04 11:38:41 -05:00
Kyle Carberry defef4671c fix: silence bash deprecation warning on macOS runners (#10051)
See https://github.com/coder/coder/actions/runs/6407839577/job/17395535790?pr=10050
2023-10-04 15:36:27 +00:00
Cian Johnston 2c2e98cc39 fix(coderd): fetch workspace agent scripts and log sources using system auth ctx (#10043)
* add failing unit test
* fetch log sources and agent scripts using system auth ctx
2023-10-04 15:50:51 +01:00
Kyle Carberry 8e44dce5b3 chore: fix external-auth.mp4 docs video (#10048) 2023-10-04 14:39:13 +00:00
Bruno Quaresma 6651aff57b feat(site): add user activity on template insights (#10013)
Close https://github.com/coder/coder/issues/9497


https://github.com/coder/coder/assets/3165839/941e268c-6c06-47ae-b57d-665e01e804b0
2023-10-04 11:37:47 -03:00
Kyle Carberry b468415a81 chore: revert docs for git-auth until release (#10044) 2023-10-04 14:33:28 +00:00
Bruno Quaresma 5e0cb372b4 fix(site): fix orphan values on insights (#10036) 2023-10-04 10:16:59 -03:00
Cian Johnston 2405bbe1b9 fix(coderd/database/dbtestutil): fix typo in pgDump (#10033) 2023-10-04 13:21:45 +01:00
Michael Smith c7218b40c9 docs: update frontend contribution docs (#10028)
* docs: update frontend contribution docs

* fix: update docs formatting
2023-10-04 08:11:07 -04:00
Colin Adler 4ab52766d2 feat: add --net-admin option to install script (#9953) 2023-10-03 23:01:46 +00:00
Colin Adler 39846d69d3 feat: modify agent install script to give CAP_NET_ADMIN if available (#9908) 2023-10-03 17:34:29 -05:00
timquinlan 2a19b46ab7 update to gateway offline install docs, 2 typos, 2 command additions (#10027) 2023-10-03 17:58:26 -04:00
dependabot[bot] 6322e13046 chore: bump postcss from 8.4.27 to 8.4.31 in /site (#10019)
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.27 to 8.4.31.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.27...8.4.31)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-04 00:28:54 +03:00
Bruno Quaresma efdbb6f9e6 fix(site): remove 48 week option (#10025) 2023-10-03 18:25:03 -03:00
Kayla Washburn e6aeee2ba2 feat: warn users when renaming workspaces (#10023) 2023-10-03 15:04:34 -06:00
dependabot[bot] 4df5c1ddec chore: bump the alpine image from 3.18.3 to 3.18.4 in Dockerfile.base (#9993)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-03 14:09:50 -05:00
Bruno Quaresma bdb9954f87 feat(site): add support for weekly data on template insights (#9997)
Close https://github.com/coder/coder/issues/9495
2023-10-03 15:14:11 -03:00
Bruno Quaresma e7042e601c fix(site): fix navbar hover (#10021) 2023-10-03 14:10:51 -03:00
Kayla Washburn c194119689 chore: rename AwaitTemplateVersionJobCompleted and AwaitWorkspaceBuildJobCompleted (#10003) 2023-10-03 11:02:56 -06:00
Kayla Washburn 4b97ac269b chore: refactor Pill styles (#10004) 2023-10-03 10:47:28 -06:00
Kyle Carberry 5e3bf275da chore: check for valid regex in git auth configs (#10020) 2023-10-03 16:45:07 +00:00
Ammar Bandukwala 70a4e56c01 ci: switch to M1 runners
ci: switch to M1 runners
    
 Per
    https://github.blog/2023-10-02-introducing-the-new-apple-silicon-powered-m1-macos-larger-runner-for-github-actions/,
    we should see large performance improvements.
2023-10-03 10:54:20 -05:00
Kyle Carberry f16eb1331f chore: update multiple git providers docs (#10017)
* chore: update multiple git providers docs

* Improve var name

* Fix fmt
2023-10-03 15:27:02 +00:00
Kyle Carberry eeab33b1c3 fix: do not require client_secret for external auth providers (#10016)
Device-based auth does not need a client secret.
2023-10-03 14:29:34 +00:00
Cian Johnston 9aac15212b fix(cli): remove exp scaletest from slim binary (#9934)
- Removes the `exp scaletest` command from the slim binary 
- Updates scaletest-runner template to fetch the full binary from the running Coder instance
2023-10-03 15:13:04 +01:00
Kyle Carberry 45b53c285f feat: allow external services to be authable (#9996)
* feat: allow external services to be authable

* Refactor external auth config structure for defaults

* Add support for new config properties

* Change the name of external auth

* Move externalauth -> external-auth

* Run gen

* Fix tests

* Fix MW tests

* Fix git auth redirect

* Fix lint

* Fix name

* Allow any ID

* Fix invalid type test

* Fix e2e tests

* Fix comments

* Fix colors

* Allow accepting any type as string

* Run gen

* Fix href
2023-10-03 14:04:39 +00:00
Marcin Tojek f62f45a303 feat!: add sections parameter to template insights (#10010) 2023-10-03 15:44:50 +02:00
Marcin Tojek cb60409a8c feat: add index to workspace_agent_stats (#10009) 2023-10-03 11:13:44 +00:00
dependabot[bot] cc2772c646 chore: bump github.com/prometheus/client_golang from 1.16.0 to 1.17.0 (#9989)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-03 13:12:06 +03:00
Cian Johnston e55c25e037 chore: enable exhaustruct linter for database param structs (#9995) 2023-10-03 09:23:45 +01:00
Muhammad Atif Ali 352ec7bc4f chore(dogfood): rename project_directory to folder (#10008) 2023-10-03 07:11:58 +00:00
Monika Pawluczuk 4966ef02cf feat(cli): add reverse tunnelling SSH support for unix sockets (#9976) 2023-10-03 16:39:39 +10:00
Muhammad Atif Ali 465546eefd chore(dogfood): open vscode desktop in coder repo directory (#9999) 2023-10-03 08:55:25 +03:00
Eric Paulsen 3980dbd029 docs: add support to enterprise features (#10005) 2023-10-02 21:25:05 -05:00
Bruno Quaresma 9e1e365b32 chore(site): remove user search service (#9939) 2023-10-02 15:24:28 -03:00
Bruno Quaresma 42e25740eb chore(site): remove users and pagination services (#9932) 2023-10-02 15:10:51 -03:00
Kayla Washburn 885b2502ed chore: replace <ChooseOne> with alternatives when appropriate (#9907) 2023-10-02 10:51:35 -06:00
Kayla Washburn 148fa819ae chore: use emotion for styling (pt. 2) (#9951) 2023-10-02 10:48:11 -06:00
dependabot[bot] fabcc41a6b chore: bump @types/node from 18.17.0 to 18.18.1 in /offlinedocs (#9971)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 18.17.0 to 18.18.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  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>
2023-10-02 10:06:59 -05:00
dependabot[bot] 6452008e32 chore: bump github.com/sqlc-dev/pqtype from 0.2.0 to 0.3.0 (#9991)
Bumps [github.com/sqlc-dev/pqtype](https://github.com/sqlc-dev/pqtype) from 0.2.0 to 0.3.0.
- [Release notes](https://github.com/sqlc-dev/pqtype/releases)
- [Commits](https://github.com/sqlc-dev/pqtype/compare/v0.2.0...v0.3.0)

---
updated-dependencies:
- dependency-name: github.com/sqlc-dev/pqtype
  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>
2023-10-02 10:06:50 -05:00
dependabot[bot] f694204773 chore: bump @types/node from 18.17.0 to 18.18.1 in /site (#9967)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 18.17.0 to 18.18.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  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>
2023-10-02 10:38:26 -04:00
dependabot[bot] 0a54506940 chore: bump the otel group with 3 updates (#9988)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-02 14:27:21 +00:00
dependabot[bot] f802fba89e chore: bump github.com/open-policy-agent/opa from 0.56.0 to 0.57.0 (#9992)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-02 14:15:58 +00:00
dependabot[bot] 89c2938b20 chore: bump google.golang.org/api from 0.142.0 to 0.143.0 (#9990)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-02 17:02:51 +03:00
dependabot[bot] 06411b8b17 ci: bump the github-actions group with 2 updates (#9994)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-02 17:02:41 +03:00
Marcin Tojek 57909e0c72 site(e2e): wait for empty workspaces (#9987) 2023-10-02 11:53:44 +00:00
Mathias Fredriksson 4b0565c895 feat(scaletest): add service banner status for runner (#9945) 2023-10-02 14:37:38 +03:00
Mathias Fredriksson e6d2ddb54b feat(scaletest): add apps to scaletest template (#9944) 2023-10-02 14:37:16 +03:00
Muhammad Atif Ali 1a07ee0b16 chore(site): make name bold in delete dialog
This makes the name bold in delete dialog and makes it readable.
<img width="439" alt="Screenshot 2023-09-24 at 03 30 55" src="https://github.com/coder/coder/assets/10648092/b97e634b-c7c8-4300-b78c-8091b3f2c9f0">

Edit: Tests are passed. thanks @aslilac
2023-10-02 13:57:21 +03:00
Cian Johnston 1c48610d56 feat(scaletest/dashboard): integrate chromedp (#9927)
* Adds a set of actions to automatically interact with a Coder instance using chromedp
* Integrates the chromedp actions into the scaletest dashboard command,
* Re-enables the previously disabled unit tests for scaletest/dashboard
* Removes previous dashboard actions based around codersdk
2023-10-02 10:40:17 +01:00
Muhammad Atif Ali 1906cc4806 Revert "chore: bump remark-gfm from 3.0.1 to 4.0.0 in /site (#9965)" (#9981)
This reverts commit 81a046e0a9.
2023-10-02 09:56:35 +02:00
dependabot[bot] 81a046e0a9 chore: bump remark-gfm from 3.0.1 to 4.0.0 in /site (#9965)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-02 10:31:56 +03:00
dependabot[bot] 247eeab3c8 chore: bump next from 13.4.12 to 13.5.3 in /offlinedocs (#9972)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-02 09:22:15 +03:00
dependabot[bot] 52b16f0622 chore: bump the eslint group in /site with 1 update
Bumps the eslint group in /site with 1 update: [eslint](https://github.com/eslint/eslint).

<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/eslint/eslint/releases">eslint's releases</a>.</em></p>
<blockquote>
<h2>v8.50.0</h2>
<h2>Features</h2>
<ul>
<li><a href="https://github.com/eslint/eslint/commit/27d5a9e57ad347982a68fcd0e75eafee42d344f0"><code>27d5a9e</code></a> feat: add suggestions to array-callback-return (<a href="https://redirect.github.com/eslint/eslint/issues/17590">#17590</a>) (Tanuj Kanti)</li>
<li><a href="https://github.com/eslint/eslint/commit/f9082ff3f3956a0a5a7d7659de63640a21c4de0f"><code>f9082ff</code></a> feat: flat-rule-tester make sure default config always matches (<a href="https://redirect.github.com/eslint/eslint/issues/17585">#17585</a>) (fnx)</li>
<li><a href="https://github.com/eslint/eslint/commit/83914adbfd5fce7d11b33d095ba6d6a39be0dbbc"><code>83914ad</code></a> feat: Implement SourceCode#applyInlineConfig() (<a href="https://redirect.github.com/eslint/eslint/issues/17351">#17351</a>) (Nicholas C. Zakas)</li>
<li><a href="https://github.com/eslint/eslint/commit/22a558228ff98f478fa308c9ecde361acc4caf20"><code>22a5582</code></a> feat: add rule <code>no-object-constructor</code>, deprecate <code>no-new-object</code> (<a href="https://redirect.github.com/eslint/eslint/issues/17576">#17576</a>) (Francesco Trotta)</li>
<li><a href="https://github.com/eslint/eslint/commit/85a3d9e967b19cb4a0189746499d81ef2f93e14e"><code>85a3d9e</code></a> feat: allowVoid option in array-callback-return (<a href="https://redirect.github.com/eslint/eslint/issues/17564">#17564</a>) (Tanuj Kanti)</li>
</ul>
<h2>Bug Fixes</h2>
<ul>
<li><a href="https://github.com/eslint/eslint/commit/cc4d26b5a59d510f2c878e973fd245e8eff27c2a"><code>cc4d26b</code></a> fix: Ensure deprecated context.parserServices warns (<a href="https://redirect.github.com/eslint/eslint/issues/17593">#17593</a>) (Nicholas C. Zakas)</li>
<li><a href="https://github.com/eslint/eslint/commit/1ea4cfb585dcb52ac3cb1522a32f25cfe507121b"><code>1ea4cfb</code></a> fix: Ensure all RuleTester tests all deprecated context methods (<a href="https://redirect.github.com/eslint/eslint/issues/17587">#17587</a>) (Nicholas C. Zakas)</li>
<li><a href="https://github.com/eslint/eslint/commit/aa1b657a9febcd03e9298c03ae2888762795e322"><code>aa1b657</code></a> fix: wrong suggestion and message in <code>no-misleading-character-class</code> (<a href="https://redirect.github.com/eslint/eslint/issues/17571">#17571</a>) (Yosuke Ota)</li>
</ul>
<h2>Documentation</h2>
<ul>
<li><a href="https://github.com/eslint/eslint/commit/180053759c6cf05a326c710353b4717fbf289ee0"><code>1800537</code></a> docs: Fix and standardize JSX code examples (<a href="https://redirect.github.com/eslint/eslint/issues/17591">#17591</a>) (Francesco Trotta)</li>
<li><a href="https://github.com/eslint/eslint/commit/48a44a73ac456739bdee348bbaf1840d2b1e4830"><code>48a44a7</code></a> docs: Add correct/incorrect tags to <code>prefer-arrow-callback</code> (<a href="https://redirect.github.com/eslint/eslint/issues/17589">#17589</a>) (Francesco Trotta)</li>
<li><a href="https://github.com/eslint/eslint/commit/20893d48b9012f2b61bbbfeac8bee70d68d90e5e"><code>20893d4</code></a> docs: fix incorrect tag's place (<a href="https://redirect.github.com/eslint/eslint/issues/17575">#17575</a>) (Tanuj Kanti)</li>
<li><a href="https://github.com/eslint/eslint/commit/bd7a71fd6b7efb0445393304e2d48c5c06d46a45"><code>bd7a71f</code></a> docs: Update README (GitHub Actions Bot)</li>
</ul>
<h2>Chores</h2>
<ul>
<li><a href="https://github.com/eslint/eslint/commit/f8a8a2d6b45c82f94a574623759b6e3d2af193f3"><code>f8a8a2d</code></a> chore: upgrade <code>@​eslint/js</code><a href="https://github.com/8"><code>@​8</code></a>.50.0 (<a href="https://redirect.github.com/eslint/eslint/issues/17599">#17599</a>) (Milos Djermanovic)</li>
<li><a href="https://github.com/eslint/eslint/commit/38ada6df8f4a0313b7d0739b28f0af6b4897b8ce"><code>38ada6d</code></a> chore: package.json update for <code>@​eslint/js</code> release (ESLint Jenkins)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/eslint/eslint/blob/main/CHANGELOG.md">eslint's changelog</a>.</em></p>
<blockquote>
<p>v8.50.0 - September 22, 2023</p>
<ul>
<li><a href="https://github.com/eslint/eslint/commit/f8a8a2d6b45c82f94a574623759b6e3d2af193f3"><code>f8a8a2d</code></a> chore: upgrade <code>@​eslint/js</code><a href="https://github.com/8"><code>@​8</code></a>.50.0 (<a href="https://redirect.github.com/eslint/eslint/issues/17599">#17599</a>) (Milos Djermanovic)</li>
<li><a href="https://github.com/eslint/eslint/commit/38ada6df8f4a0313b7d0739b28f0af6b4897b8ce"><code>38ada6d</code></a> chore: package.json update for <code>@​eslint/js</code> release (ESLint Jenkins)</li>
<li><a href="https://github.com/eslint/eslint/commit/27d5a9e57ad347982a68fcd0e75eafee42d344f0"><code>27d5a9e</code></a> feat: add suggestions to array-callback-return (<a href="https://redirect.github.com/eslint/eslint/issues/17590">#17590</a>) (Tanuj Kanti)</li>
<li><a href="https://github.com/eslint/eslint/commit/f9082ff3f3956a0a5a7d7659de63640a21c4de0f"><code>f9082ff</code></a> feat: flat-rule-tester make sure default config always matches (<a href="https://redirect.github.com/eslint/eslint/issues/17585">#17585</a>) (fnx)</li>
<li><a href="https://github.com/eslint/eslint/commit/83914adbfd5fce7d11b33d095ba6d6a39be0dbbc"><code>83914ad</code></a> feat: Implement SourceCode#applyInlineConfig() (<a href="https://redirect.github.com/eslint/eslint/issues/17351">#17351</a>) (Nicholas C. Zakas)</li>
<li><a href="https://github.com/eslint/eslint/commit/cc4d26b5a59d510f2c878e973fd245e8eff27c2a"><code>cc4d26b</code></a> fix: Ensure deprecated context.parserServices warns (<a href="https://redirect.github.com/eslint/eslint/issues/17593">#17593</a>) (Nicholas C. Zakas)</li>
<li><a href="https://github.com/eslint/eslint/commit/1ea4cfb585dcb52ac3cb1522a32f25cfe507121b"><code>1ea4cfb</code></a> fix: Ensure all RuleTester tests all deprecated context methods (<a href="https://redirect.github.com/eslint/eslint/issues/17587">#17587</a>) (Nicholas C. Zakas)</li>
<li><a href="https://github.com/eslint/eslint/commit/180053759c6cf05a326c710353b4717fbf289ee0"><code>1800537</code></a> docs: Fix and standardize JSX code examples (<a href="https://redirect.github.com/eslint/eslint/issues/17591">#17591</a>) (Francesco Trotta)</li>
<li><a href="https://github.com/eslint/eslint/commit/22a558228ff98f478fa308c9ecde361acc4caf20"><code>22a5582</code></a> feat: add rule <code>no-object-constructor</code>, deprecate <code>no-new-object</code> (<a href="https://redirect.github.com/eslint/eslint/issues/17576">#17576</a>) (Francesco Trotta)</li>
<li><a href="https://github.com/eslint/eslint/commit/48a44a73ac456739bdee348bbaf1840d2b1e4830"><code>48a44a7</code></a> docs: Add correct/incorrect tags to <code>prefer-arrow-callback</code> (<a href="https://redirect.github.com/eslint/eslint/issues/17589">#17589</a>) (Francesco Trotta)</li>
<li><a href="https://github.com/eslint/eslint/commit/aa1b657a9febcd03e9298c03ae2888762795e322"><code>aa1b657</code></a> fix: wrong suggestion and message in <code>no-misleading-character-class</code> (<a href="https://redirect.github.com/eslint/eslint/issues/17571">#17571</a>) (Yosuke Ota)</li>
<li><a href="https://github.com/eslint/eslint/commit/20893d48b9012f2b61bbbfeac8bee70d68d90e5e"><code>20893d4</code></a> docs: fix incorrect tag's place (<a href="https://redirect.github.com/eslint/eslint/issues/17575">#17575</a>) (Tanuj Kanti)</li>
<li><a href="https://github.com/eslint/eslint/commit/85a3d9e967b19cb4a0189746499d81ef2f93e14e"><code>85a3d9e</code></a> feat: allowVoid option in array-callback-return (<a href="https://redirect.github.com/eslint/eslint/issues/17564">#17564</a>) (Tanuj Kanti)</li>
<li><a href="https://github.com/eslint/eslint/commit/bd7a71fd6b7efb0445393304e2d48c5c06d46a45"><code>bd7a71f</code></a> docs: Update README (GitHub Actions Bot)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/eslint/eslint/commit/299bfae1254f22bd4b3bccd897d6355f63e0d3b6"><code>299bfae</code></a> 8.50.0</li>
<li><a href="https://github.com/eslint/eslint/commit/212687c36a9e2682b84f5de2b683b025182d9777"><code>212687c</code></a> Build: changelog update for 8.50.0</li>
<li><a href="https://github.com/eslint/eslint/commit/f8a8a2d6b45c82f94a574623759b6e3d2af193f3"><code>f8a8a2d</code></a> chore: upgrade <code>@​eslint/js</code><a href="https://github.com/8"><code>@​8</code></a>.50.0 (<a href="https://redirect.github.com/eslint/eslint/issues/17599">#17599</a>)</li>
<li><a href="https://github.com/eslint/eslint/commit/38ada6df8f4a0313b7d0739b28f0af6b4897b8ce"><code>38ada6d</code></a> chore: package.json update for <code>@​eslint/js</code> release</li>
<li><a href="https://github.com/eslint/eslint/commit/27d5a9e57ad347982a68fcd0e75eafee42d344f0"><code>27d5a9e</code></a> feat: add suggestions to array-callback-return (<a href="https://redirect.github.com/eslint/eslint/issues/17590">#17590</a>)</li>
<li><a href="https://github.com/eslint/eslint/commit/f9082ff3f3956a0a5a7d7659de63640a21c4de0f"><code>f9082ff</code></a> feat: flat-rule-tester make sure default config always matches (<a href="https://redirect.github.com/eslint/eslint/issues/17585">#17585</a>)</li>
<li><a href="https://github.com/eslint/eslint/commit/83914adbfd5fce7d11b33d095ba6d6a39be0dbbc"><code>83914ad</code></a> feat: Implement SourceCode#applyInlineConfig() (<a href="https://redirect.github.com/eslint/eslint/issues/17351">#17351</a>)</li>
<li><a href="https://github.com/eslint/eslint/commit/cc4d26b5a59d510f2c878e973fd245e8eff27c2a"><code>cc4d26b</code></a> fix: Ensure deprecated context.parserServices warns (<a href="https://redirect.github.com/eslint/eslint/issues/17593">#17593</a>)</li>
<li><a href="https://github.com/eslint/eslint/commit/1ea4cfb585dcb52ac3cb1522a32f25cfe507121b"><code>1ea4cfb</code></a> fix: Ensure all RuleTester tests all deprecated context methods (<a href="https://redirect.github.com/eslint/eslint/issues/17587">#17587</a>)</li>
<li><a href="https://github.com/eslint/eslint/commit/180053759c6cf05a326c710353b4717fbf289ee0"><code>1800537</code></a> docs: Fix and standardize JSX code examples (<a href="https://redirect.github.com/eslint/eslint/issues/17591">#17591</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/eslint/eslint/compare/v8.49.0...v8.50.0">compare view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions


</details>
2023-10-02 08:53:55 +03:00
dependabot[bot] 8af28717a3 chore: bump eslint from 8.49.0 to 8.50.0 in /offlinedocs (#9970)
Bumps [eslint](https://github.com/eslint/eslint) from 8.49.0 to 8.50.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.49.0...v8.50.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  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>
2023-10-02 05:46:22 +00:00
dependabot[bot] d650cf9b8c chore: bump eslint-config-next from 13.4.10 to 13.5.3 in /offlinedocs (#9973)
Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 13.4.10 to 13.5.3.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v13.5.3/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-type: direct:development
  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>
2023-10-02 08:42:11 +03:00
dependabot[bot] cac677b4ba chore: bump @octokit/types from 11.1.0 to 12.0.0 in /site (#9963)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-01 11:50:36 +00:00
dependabot[bot] 3dc478ad6b chore: bump ts-proto from 1.158.0 to 1.159.1 in /site (#9964)
Bumps [ts-proto](https://github.com/stephenh/ts-proto) from 1.158.0 to 1.159.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/stephenh/ts-proto/releases">ts-proto's releases</a>.</em></p>
<blockquote>
<h2>v1.159.1</h2>
<h2><a href="https://github.com/stephenh/ts-proto/compare/v1.159.0...v1.159.1">1.159.1</a> (2023-09-30)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Use a Map when map keys are boolean. (<a href="https://redirect.github.com/stephenh/ts-proto/issues/933">#933</a>) (<a href="https://github.com/stephenh/ts-proto/commit/c1253a3761405d7a2ffe4d15f4c3ffb364697a02">c1253a3</a>), closes <a href="https://redirect.github.com/stephenh/ts-proto/issues/926">#926</a></li>
</ul>
<h2>v1.159.0</h2>
<h1><a href="https://github.com/stephenh/ts-proto/compare/v1.158.1...v1.159.0">1.159.0</a> (2023-09-30)</h1>
<h3>Features</h3>
<ul>
<li>Add globalThisPolyfill, defaults false. (<a href="https://redirect.github.com/stephenh/ts-proto/issues/931">#931</a>) (<a href="https://github.com/stephenh/ts-proto/commit/085fa21603a74544af192f404289c2e62ecfd8f6">085fa21</a>)</li>
</ul>
<h2>v1.158.1</h2>
<h2><a href="https://github.com/stephenh/ts-proto/compare/v1.158.0...v1.158.1">1.158.1</a> (2023-09-30)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Use globalThis for Array/String/Boolean (<a href="https://redirect.github.com/stephenh/ts-proto/issues/930">#930</a>) (<a href="https://github.com/stephenh/ts-proto/commit/9a252c3d4cf988496f6de17cc378dbb09a1baf92">9a252c3</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/stephenh/ts-proto/blob/main/CHANGELOG.md">ts-proto's changelog</a>.</em></p>
<blockquote>
<h2><a href="https://github.com/stephenh/ts-proto/compare/v1.159.0...v1.159.1">1.159.1</a> (2023-09-30)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Use a Map when map keys are boolean. (<a href="https://redirect.github.com/stephenh/ts-proto/issues/933">#933</a>) (<a href="https://github.com/stephenh/ts-proto/commit/c1253a3761405d7a2ffe4d15f4c3ffb364697a02">c1253a3</a>), closes <a href="https://redirect.github.com/stephenh/ts-proto/issues/926">#926</a></li>
</ul>
<h1><a href="https://github.com/stephenh/ts-proto/compare/v1.158.1...v1.159.0">1.159.0</a> (2023-09-30)</h1>
<h3>Features</h3>
<ul>
<li>Add globalThisPolyfill, defaults false. (<a href="https://redirect.github.com/stephenh/ts-proto/issues/931">#931</a>) (<a href="https://github.com/stephenh/ts-proto/commit/085fa21603a74544af192f404289c2e62ecfd8f6">085fa21</a>)</li>
</ul>
<h2><a href="https://github.com/stephenh/ts-proto/compare/v1.158.0...v1.158.1">1.158.1</a> (2023-09-30)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>Use globalThis for Array/String/Boolean (<a href="https://redirect.github.com/stephenh/ts-proto/issues/930">#930</a>) (<a href="https://github.com/stephenh/ts-proto/commit/9a252c3d4cf988496f6de17cc378dbb09a1baf92">9a252c3</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/stephenh/ts-proto/commit/09e9fa2594431785253a2fc15e66d85e721003e9"><code>09e9fa2</code></a> chore(release): 1.159.1 [skip ci]</li>
<li><a href="https://github.com/stephenh/ts-proto/commit/c1253a3761405d7a2ffe4d15f4c3ffb364697a02"><code>c1253a3</code></a> fix: Use a Map when map keys are boolean. (<a href="https://redirect.github.com/stephenh/ts-proto/issues/933">#933</a>)</li>
<li><a href="https://github.com/stephenh/ts-proto/commit/ccf54be52d181c341ff310c68665a65fef3ed86d"><code>ccf54be</code></a> chore(release): 1.159.0 [skip ci]</li>
<li><a href="https://github.com/stephenh/ts-proto/commit/085fa21603a74544af192f404289c2e62ecfd8f6"><code>085fa21</code></a> feat: Add globalThisPolyfill, defaults false. (<a href="https://redirect.github.com/stephenh/ts-proto/issues/931">#931</a>)</li>
<li><a href="https://github.com/stephenh/ts-proto/commit/6f856376b103284e2694d3bb7d4471c6b43429f9"><code>6f85637</code></a> chore(release): 1.158.1 [skip ci]</li>
<li><a href="https://github.com/stephenh/ts-proto/commit/9a252c3d4cf988496f6de17cc378dbb09a1baf92"><code>9a252c3</code></a> fix: Use globalThis for Array/String/Boolean (<a href="https://redirect.github.com/stephenh/ts-proto/issues/930">#930</a>)</li>
<li>See full diff in <a href="https://github.com/stephenh/ts-proto/compare/v1.158.0...v1.159.1">compare view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions


</details>
2023-10-01 14:49:04 +03:00
dependabot[bot] b035c4d88a chore: bump chromatic from 7.1.0 to 7.2.0 in /site (#9966)
Bumps [chromatic](https://github.com/chromaui/chromatic-cli) from 7.1.0 to 7.2.0.
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/chromaui/chromatic-cli/commit/5366e6b8a73f606cdf48bec69176c090bdaf58c4"><code>5366e6b</code></a> 7.2.0</li>
<li><a href="https://github.com/chromaui/chromatic-cli/commit/0749afb68e1da025de47af3204e6ea5062c9c571"><code>0749afb</code></a> Step package.json back</li>
<li><a href="https://github.com/chromaui/chromatic-cli/commit/788d8e96f72a95fc69f831fbceab862fd404a6ce"><code>788d8e9</code></a> Merge pull request <a href="https://redirect.github.com/chromaui/chromatic-cli/issues/819">#819</a> from chromaui/tom/drop-loggly</li>
<li><a href="https://github.com/chromaui/chromatic-cli/commit/0967fa81cc4358e3709e4468c5599e1797922c58"><code>0967fa8</code></a> 7.2.1-canary.0</li>
<li><a href="https://github.com/chromaui/chromatic-cli/commit/2f9484e733bd09300de76707d24815e08adbaca7"><code>2f9484e</code></a> No longer log to loggly</li>
<li><a href="https://github.com/chromaui/chromatic-cli/commit/6f614e64036e02bbb0cec3d9252718de7277acb0"><code>6f614e6</code></a> 7.2.0-next.1</li>
<li><a href="https://github.com/chromaui/chromatic-cli/commit/10bc2a32a1db2e5039e117e233f3aba3c6cfe51b"><code>10bc2a3</code></a> 7.2.0-next.0</li>
<li><a href="https://github.com/chromaui/chromatic-cli/commit/a0b14d8c45f8fc2c50cbb7b944bcddee3a9cf294"><code>a0b14d8</code></a> Merge pull request <a href="https://redirect.github.com/chromaui/chromatic-cli/issues/814">#814</a> from chromaui/tom/ap-3623-add-cli-support-for-a-chrom...</li>
<li><a href="https://github.com/chromaui/chromatic-cli/commit/a8acc0e8d33180a35846feb8c314d0fe0edfae97"><code>a8acc0e</code></a> Trying <code>xlarge</code></li>
<li><a href="https://github.com/chromaui/chromatic-cli/commit/5af4e277207621d0b379bc272a60972cf1c29eeb"><code>5af4e27</code></a> Try medium resource class</li>
<li>Additional commits viewable in <a href="https://github.com/chromaui/chromatic-cli/compare/v7.1.0...v7.2.0">compare view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions


</details>
2023-10-01 14:42:35 +03:00
dependabot[bot] 3d71173e74 chore: bump yup from 1.2.0 to 1.3.2 in /site (#9968)
Bumps [yup](https://github.com/jquense/yup) from 1.2.0 to 1.3.2.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/jquense/yup/blob/master/CHANGELOG.md">yup's changelog</a>.</em></p>
<blockquote>
<h2><a href="https://github.com/jquense/yup/compare/v1.3.1...v1.3.2">1.3.2</a> (2023-09-29)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>pick and omit with excluded edges (<a href="https://github.com/jquense/yup/commit/6956ee788369dff00e5ecadb506726af3598a87e">6956ee7</a>), closes <a href="https://redirect.github.com/jquense/yup/issues/2097">#2097</a></li>
</ul>
<h2><a href="https://github.com/jquense/yup/compare/v1.3.0...v1.3.1">1.3.1</a> (2023-09-26)</h2>
<h3>Bug Fixes</h3>
<ul>
<li>ValidationError extends Error (<a href="https://github.com/jquense/yup/commit/bc5121b92d8e16baf8fe9b83f0247a4e90e169b8">bc5121b</a>)</li>
</ul>
<h1><a href="https://github.com/jquense/yup/compare/v1.2.0...v1.3.0">1.3.0</a> (2023-09-23)</h1>
<h3>Bug Fixes</h3>
<ul>
<li>add tuple to locale object (<a href="https://redirect.github.com/jquense/yup/issues/2100">#2100</a>) (<a href="https://github.com/jquense/yup/commit/809b55a9c16e0cd567f4eced9b9ab02ad8b0bffa">809b55a</a>)</li>
<li>performance improvement (<a href="https://redirect.github.com/jquense/yup/issues/2043">#2043</a>) (<a href="https://redirect.github.com/jquense/yup/issues/2044">#2044</a>) (<a href="https://github.com/jquense/yup/commit/ee1b7317b0a9fc0e16a7d33064c3e5584bd7f2d5">ee1b731</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li>Allow schema metadata to be strongly typed (<a href="https://redirect.github.com/jquense/yup/issues/2021">#2021</a>) (<a href="https://github.com/jquense/yup/commit/e593f8f72e7195cf0ac48fa8e1cd82d95c1e6bb5">e593f8f</a>)</li>
</ul>
<h3>Reverts</h3>
<ul>
<li>Revert &quot;fix: performance improvement (<a href="https://redirect.github.com/jquense/yup/issues/2043">#2043</a>) (<a href="https://redirect.github.com/jquense/yup/issues/2044">#2044</a>)&quot; (<a href="https://redirect.github.com/jquense/yup/issues/2071">#2071</a>) (<a href="https://github.com/jquense/yup/commit/b940eef48eb7456622ae384d0ffa7363d4fbad25">b940eef</a>), closes <a href="https://redirect.github.com/jquense/yup/issues/2043">#2043</a> <a href="https://redirect.github.com/jquense/yup/issues/2044">#2044</a> <a href="https://redirect.github.com/jquense/yup/issues/2071">#2071</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/jquense/yup/commit/a58f02d2f6164c46a9757a818eebac582f7a441c"><code>a58f02d</code></a> Publish v1.3.2</li>
<li><a href="https://github.com/jquense/yup/commit/2f2c0aa9be6258b763f95e7de076c2dcfbe0cae3"><code>2f2c0aa</code></a> fix type</li>
<li><a href="https://github.com/jquense/yup/commit/020901fcf3cfb12a38afd90e4fe56e2e37c312a1"><code>020901f</code></a> Allow parent resolve before describe. (<a href="https://redirect.github.com/jquense/yup/issues/2051">#2051</a>)</li>
<li><a href="https://github.com/jquense/yup/commit/6956ee788369dff00e5ecadb506726af3598a87e"><code>6956ee7</code></a> fix: pick and omit with excluded edges</li>
<li><a href="https://github.com/jquense/yup/commit/e15297bb747552e4742bbaa2ecb95aad1c37f918"><code>e15297b</code></a> Publish v1.3.1</li>
<li><a href="https://github.com/jquense/yup/commit/bc5121b92d8e16baf8fe9b83f0247a4e90e169b8"><code>bc5121b</code></a> fix: ValidationError extends Error</li>
<li><a href="https://github.com/jquense/yup/commit/9c1208c0685a5a7571ef74c39a08d248100f793f"><code>9c1208c</code></a> Publish v1.3.0</li>
<li><a href="https://github.com/jquense/yup/commit/809b55a9c16e0cd567f4eced9b9ab02ad8b0bffa"><code>809b55a</code></a> fix: add tuple to locale object (<a href="https://redirect.github.com/jquense/yup/issues/2100">#2100</a>)</li>
<li><a href="https://github.com/jquense/yup/commit/13ce359c20be195794f22f16a64d633bc28db7f5"><code>13ce359</code></a> Update README.md (<a href="https://redirect.github.com/jquense/yup/issues/2101">#2101</a>)</li>
<li><a href="https://github.com/jquense/yup/commit/1ee9b21c994b4293f3ab338119dc17ab2f4e284c"><code>1ee9b21</code></a> Export Lazy and Reference types (<a href="https://redirect.github.com/jquense/yup/issues/2093">#2093</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/jquense/yup/compare/v1.2.0...v1.3.2">compare view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions


</details>
2023-10-01 14:41:38 +03:00
dependabot[bot] 5b18007311 chore: bump @vitejs/plugin-react from 4.0.1 to 4.1.0 in /site (#9969)
Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 4.0.1 to 4.1.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/vitejs/vite-plugin-react/releases"><code>@​vitejs/plugin-react</code>'s releases</a>.</em></p>
<blockquote>
<h2>v4.1.0</h2>
<ul>
<li>Add <code>@types/babel__cores</code> to dependencies (fix <a href="https://redirect.github.com/vitejs/vite-plugin-react/issues/211">#211</a>)</li>
<li>Improve build perf when not using Babel plugins by lazy loading <code>@babel/core</code> <a href="https://redirect.github.com/vitejs/vite-plugin-react/pull/212">#212</a></li>
<li>Better invalidation message when an export is added &amp; fix HMR for export of nullish values <a href="https://redirect.github.com/vitejs/vite-plugin-react/pull/215">#215</a></li>
<li>Include non-dev jsx runtime in optimizeDeps &amp; support HMR for JS files using the non dev runtime <a href="https://redirect.github.com/vitejs/vite-plugin-react/pull/224">#224</a></li>
<li>The build output now contains a <code>index.d.cts</code> file so you don't get types errors when setting <code>moduleResolution</code> to <code>node16</code> or <code>nodenext</code> in your tsconfig (we recommend using <code>bundler</code> which is more close to how Vite works)</li>
</ul>
<h2>v4.0.4</h2>
<ul>
<li>Fix <a href="https://github.com/vitejs/vite-plugin-react/discussions/198">#198</a>: Enable Babel if presets list is not empty</li>
</ul>
<h2>v4.0.3</h2>
<ul>
<li>Revert <a href="https://redirect.github.com/vitejs/vite-plugin-react/pull/108">#108</a>: Remove throw when refresh runtime is loaded twice to enable usage in micro frontend apps. This was added to help fix setup usage, and this is not worth an annoying warning for others or a config parameter.</li>
</ul>
<h2>v4.0.2</h2>
<ul>
<li>Fix fast-refresh for files that are transformed into jsx (<a href="https://redirect.github.com/vitejs/vite-plugin-react/pull/188">#188</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md"><code>@​vitejs/plugin-react</code>'s changelog</a>.</em></p>
<blockquote>
<h2>4.1.0 (2023-09-24)</h2>
<ul>
<li>Add <code>@types/babel__cores</code> to dependencies (fix <a href="https://redirect.github.com/vitejs/vite-plugin-react/issues/211">#211</a>)</li>
<li>Improve build perf when not using Babel plugins by lazy loading <code>@babel/core</code> <a href="https://redirect.github.com/vitejs/vite-plugin-react/pull/212">#212</a></li>
<li>Better invalidation message when an export is added &amp; fix HMR for export of nullish values <a href="https://redirect.github.com/vitejs/vite-plugin-react/pull/215">#215</a></li>
<li>Include non-dev jsx runtime in optimizeDeps &amp; support HMR for JS files using the non dev runtime <a href="https://redirect.github.com/vitejs/vite-plugin-react/pull/224">#224</a></li>
<li>The build output now contains a <code>index.d.cts</code> file so you don't get types errors when setting <code>moduleResolution</code> to <code>node16</code> or <code>nodenext</code> in your tsconfig (we recommend using <code>bundler</code> which is more close to how Vite works)</li>
</ul>
<h2>4.0.4 (2023-07-31)</h2>
<ul>
<li>Fix <a href="https://github.com/vitejs/vite-plugin-react/discussions/198">#198</a>: Enable Babel if presets list is not empty</li>
</ul>
<h2>4.0.3 (2023-07-10)</h2>
<ul>
<li>Revert <a href="https://redirect.github.com/vitejs/vite-plugin-react/pull/108">#108</a>: Remove throw when refresh runtime is loaded twice to enable usage in micro frontend apps. This was added to help fix setup usage, and this is not worth an annoying warning for others or a config parameter.</li>
</ul>
<h2>4.0.2 (2023-07-06)</h2>
<ul>
<li>Fix fast-refresh for files that are transformed into jsx (<a href="https://redirect.github.com/vitejs/vite-plugin-react/pull/188">#188</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/vitejs/vite-plugin-react/commit/bf1453edac2d21f7fc06680aa66d178082b770df"><code>bf1453e</code></a> release: plugin-react@4.1.0</li>
<li><a href="https://github.com/vitejs/vite-plugin-react/commit/25fe88a02d3a718b81a3b1290ff4e46bfab427f9"><code>25fe88a</code></a> feat: add support for non-dev jsx runtime (<a href="https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react/issues/224">#224</a>)</li>
<li><a href="https://github.com/vitejs/vite-plugin-react/commit/272c338a7c2927d6e673901bc52f25791d466a98"><code>272c338</code></a> fix(deps): update all non-major dependencies (<a href="https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react/issues/206">#206</a>)</li>
<li><a href="https://github.com/vitejs/vite-plugin-react/commit/286360281992c425bf75cb0a18846f65fcdc5ef3"><code>2863602</code></a> fix: better invalidation message when an export is added &amp; fix HMR for export...</li>
<li><a href="https://github.com/vitejs/vite-plugin-react/commit/ccd25ddf1fe9d70d9486e8f07da95bcf3fd0b5d6"><code>ccd25dd</code></a> perf: lazy load <code>@babel/core</code> (<a href="https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react/issues/212">#212</a>)</li>
<li><a href="https://github.com/vitejs/vite-plugin-react/commit/8413a1835e78a9d923bcc5a5723cb1e903c8fc0f"><code>8413a18</code></a> fix: add <code>@types/babel__cores</code> to dependencies (fix <a href="https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react/issues/211">#211</a>)</li>
<li><a href="https://github.com/vitejs/vite-plugin-react/commit/90328ce92ecfdc9797aa58af67e46f9872b29118"><code>90328ce</code></a> release: plugin-react@4.0.4</li>
<li><a href="https://github.com/vitejs/vite-plugin-react/commit/9f9e2f66d699c47e8bf7b88175fdd55908a2bf35"><code>9f9e2f6</code></a> fix(deps): update all non-major dependencies (<a href="https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react/issues/199">#199</a>)</li>
<li><a href="https://github.com/vitejs/vite-plugin-react/commit/6e2e0a406e8478002dc0ab4a5104d370d4afc7b3"><code>6e2e0a4</code></a> fix <a href="https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react/issues/198">#198</a>: enable Babel if presets list is not empty</li>
<li><a href="https://github.com/vitejs/vite-plugin-react/commit/cbdf19f8f973fe78495336ca14247ddfc276f998"><code>cbdf19f</code></a> release: plugin-react@4.0.3</li>
<li>Additional commits viewable in <a href="https://github.com/vitejs/vite-plugin-react/commits/v4.1.0/packages/plugin-react">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@vitejs/plugin-react&package-manager=npm_and_yarn&previous-version=4.0.1&new-version=4.1.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

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

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

---

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

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions


</details>
2023-10-01 14:41:02 +03:00
Kyle Carberry 5596fb20b5 chore: move /gitauth to /externalauth on the frontend (#9954)
* chore: move `/gitauth` to `/externalauth` on the frontend

This actually took a lot more jank than anticipated,
so I wanted to split this up before adding the ability
to embed new providers.

* Rename FE

* Fix em' up

* Fix linting error

* Fix e2e tests

* chore: update helm golden files
2023-09-30 14:30:01 -05:00
Muhammad Atif Ali 16a2d4d733 chore(dogfood): use coder modules in dogfood (#9890)
updates dogfood to use coder modules

---

<details open="true"><summary>Generated summary (powered by <a href="https://app.graphite.dev">Graphite</a>)</summary>

> # TL;DR
> This pull request removes the `dotfiles_url` and `jetbrains_ide` parameters, and the related code from the `main.tf` file. It also adds several new modules for `dotfiles`, `git-clone`, `personalize`, `code-server`, `jetbrains_gateway`, `vscode`, and `filebrowser`. The `startup_script` has been updated to remove the installation and launch of `filebrowser` and the `dotfiles` related code. 
> 
> # What changed
> - Removed `dotfiles_url` and `jetbrains_ide` parameters.
> - Removed related code for `dotfiles_url` and `jetbrains_ide`.
> - Added new modules for `dotfiles`, `git-clone`, `personalize`, `code-server`, `jetbrains_gateway`, `vscode`, and `filebrowser`.
> - Updated `startup_script` to remove the installation and launch of `filebrowser` and the `dotfiles` related code.
> 
> # How to test
> 1. Go to https://dev.coder.com/templates/coder/versions/dazzling_hugle0 and create a new workspace or change your dogfood workspace version to `dazzling_hugle0` .
> 2. Verify that the new modules are working as expected.
> 
> # Why make this change
> The `dotfiles_url` and `jetbrains_ide` parameters and related code were removed to simplify the configuration and make it easier to manage. The new modules were added to provide additional functionality and improve the overall user experience. The `startup_script` was updated to reflect these changes.
</details>
2023-09-30 22:22:14 +03:00
Kyle Carberry c1cd93da30 chore: update helm golden files (#9959) 2023-09-30 18:36:34 +00:00
Ammar Bandukwala 3d21872230 chore: set sessionAffinity to None in helm chart (#9918) 2023-09-30 12:33:20 -05:00
Steven Masley e9ccb8dc78 chore: change cli error message handling (#9952)
* chore: add command to easily visualize different errors

* chore: add verbose error printing in cli
2023-09-29 16:50:23 -05:00
Stephen Kirby fa297e1096 feat(site): add white coder icon (#9955)
* Added a white coder svg

* renamed svg

* rename icon
2023-09-29 15:25:44 -05:00
Mathias Fredriksson e6cb2c5a8e chore(go.mod): update cdr.dev/slog to include additional stackdriver field (#9938) 2023-09-29 20:22:37 +00:00
Eric Paulsen 710b170adf site(fix): add space characters to group display name (#9950) 2023-09-29 15:25:59 -04:00
Kyle Carberry 8abca9bea7 chore: rename git_auth to external_auth in our schema (#9935)
* chore: rename `git_auth` to `external_auth` in our schema

We're changing Git auth to be external auth. It will support
any OAuth2 or OIDC provider.

To split up the larger change I want to contribute the schema
changes first, and I'll add the feature itself in another PR.

* Fix names

* Fix outdated view

* Rename some additional places

* Fix sort order

* Fix template versions auth route

* Fix types

* Fix dbauthz
2023-09-29 19:13:20 +00:00
Kayla Washburn 2b5428e95f refactor: start using emotion for styling (#9909) 2023-09-29 13:08:17 -06:00
Colin Adler 4da1223a80 fix: pass OnSubscribe to HA MultiAgent (#9947)
Fixes https://github.com/coder/coder/issues/9929
2023-09-29 13:37:17 -05:00
Kayla Washburn 61154a6bb5 chore: update datadog (#9925) 2023-09-29 12:06:24 -06:00
Steven Masley 92308bec3b chore: color value_source for deployment values (#9922)
* chore: Return populated options vs a blank
* Strip secret values
2023-09-29 12:04:28 -05:00
Marcin Tojek 87ebe6c2c2 test: fix flaky TestPostWorkspacesByOrganization/Create (#9931) 2023-09-29 13:06:21 +00:00
Marcin Tojek c0705ec40e fix: flaky TestCreateValidateRichParameters/ValidateString (#9928) 2023-09-29 12:54:51 +02:00
Marcin Tojek 2dd49cc0a7 fix(site): double input box for logo url (#9926) 2023-09-29 07:27:47 +00:00
Steven Masley beac36027f chore: generate any interface as Deployment Option in TypeScript (#9917)
* chore: handle interfaces as "any" in typescript

Use generated Deployment Option
2023-09-28 16:14:28 -05:00
Colin Adler 885041a65b revert: downgrade alpine to v3.18.3 (#9916) 2023-09-28 20:27:47 +00:00
Steven Masley 8cff6237ba chore: show yaml option in deployment page (#9912) 2023-09-28 15:10:06 -05:00
Colin Adler 92c0237899 chore: upgrade alpine to v3.18.4 2023-09-28 19:47:48 +00:00
Colin Adler ed8ae2f123 chore: upgrade go to 1.20.8 2023-09-28 14:17:15 -05:00
Steven Masley 9e845213f5 chore: include organization id in workspace build audit log (#9905)
* chore: include organization id in workspace build audit log
* rename 'BuildAudit' to 'WorkspaceBuildAudit'
2023-09-28 11:29:12 -05:00
Marcin Tojek 305556f655 feat(site): use custom application name (#9902) 2023-09-28 18:23:27 +02:00
Kayla Washburn 0f946669c1 chore: simplify imports (#9896) 2023-09-28 09:21:36 -06:00
Marcin Tojek dae528f5e7 feat: expose application name and logo url via meta properties (#9900) 2023-09-28 10:50:40 +02:00
Steven Masley 2d1b35390e chore: relax wsproxy version checking (#9796) 2023-09-27 19:34:55 +00:00
Michael Smith dcad8fdc2f fix: add checks for preventing HSL colors from entering React state (#9893)
* fix: remove hsl color from initial form state

* chore: add extra color helpers

* chore: add extra runtime validation for colors

* chore: clean up comments for clarity

* chore: add tests for colors
2023-09-27 19:27:11 +00:00
Ben Potter 20a681af8d chore: add v2.2.0 changelog (#9889)
* chore: add v2.2.0 changelog

* add actual changelog
2023-09-27 17:38:43 +00:00
Kayla Washburn 066b25f710 chore: remove Maybe (#9880) 2023-09-27 10:45:12 -06:00
Cian Johnston 72e8f88af3 feat(scaletest/terraform): add cert-manager, otel, and TLS (#9894) 2023-09-27 17:41:07 +01:00
Mathias Fredriksson 0878381d0b fix(cli/cliui): avoid nil pointer deref when agent disconnects (#9892)
Fixes #9884
2023-09-27 19:10:15 +03:00
Kyle Carberry de6d0b9a1a fix: only specify vscode proxy uri if app subdomains enabled (#9891)
Otherwise this generates an invalid URI that breaks code-server!
2023-09-27 15:13:47 +00:00
Marcin Tojek cb5f8df4c2 feat: expose application name via Appearance API (#9886) 2023-09-27 17:02:18 +02:00
Mathias Fredriksson 68738771b9 feat(scaletest): create automated pprof dumps during scaletest (#9887) 2023-09-27 15:58:43 +03:00
Cian Johnston fad02081fc fix: avoid logging env in unit tests (#9885) 2023-09-27 13:34:40 +01:00
Mathias Fredriksson d8515f02af feat(scaletest): add grafana annotations and slack reporting (#9852)
Fixes #9575
Fixes #9576
2023-09-27 11:44:11 +00:00
Mathias Fredriksson 4e442040f7 feat(coderd/httpmw): log start timestamp for http requests (#9776) 2023-09-27 14:39:22 +03:00
Spike Curtis c67db6efb0 fix: wait for bash prompt before commands (#9882)
Signed-off-by: Spike Curtis <spike@coder.com>
2023-09-27 12:26:24 +04:00
Cian Johnston 399b428149 chore(scaletest): update scaletest terraform to match big.cdr.dev (#9860)
- Removes usage of null_resources as no longer required
- Updates terraform in both infra/ and k8s/ to match
- Updates provisionerd deployment to use helm chart
2023-09-27 09:02:35 +01:00
Kyle Carberry 726a4dadf2 feat: allow magic string to generate session token for external apps (#9878)
* fix: make non-http external app links open in the current window

* Allow magic string to be replaced with a session token for external apps

* Improve readibility
2023-09-26 16:04:24 -05:00
Kira Pilot 75366ec6b5 more renames (#9876) 2023-09-26 14:45:29 -04:00
Kayla Washburn 6f0e2a7968 refactor: poll for git auth updates when creating a workspace (#9804) 2023-09-26 11:39:07 -06:00
Marcin Tojek 4c3b579f58 feat: expose insights into user activity (#9807) 2023-09-26 18:42:16 +02:00
Cian Johnston 1f4335733c fix(coderd/workspaceapps/apptest): bump sleep in testReconnectingPTY to reduce test flakes (#9875) 2023-09-26 14:58:18 +01:00
Bruno Quaresma b0b9d32a2a chore(site): remove dead code (#9830) 2023-09-26 10:05:24 -03:00
Cian Johnston 93ef696b57 refactor(agent): add agenttest.New helper function (#9812)
* Adds agenttest.New() helper function
* Makes sure agent gets closed on test cleanup
* Makes sure you don't forget to set session token
* Sets the agent and client logger automatically
2023-09-26 12:05:19 +01:00
Cian Johnston 96c5076c69 fix(coderd): remove troublesome test case (#9874) 2023-09-26 11:33:21 +01:00
Kyle Carberry c4718fd747 fix: remove pinned version for dogfood (#9872) 2023-09-26 02:23:42 +00:00
Muhammad Atif Ali 04c85c3833 chore: add icons (#9871) 2023-09-26 00:46:07 +00:00
Kyle Carberry f7c89082d2 chore: swap dogfood to use modules (#9870) 2023-09-25 19:41:45 -05:00
Kyle Carberry 442fb105c9 chore: add git svg icon (#9869) 2023-09-26 00:15:55 +00:00
Kyle Carberry 20bfe6e9e5 fix: allow expansion from log_path for coder_script (#9868) 2023-09-25 23:45:55 +00:00
Colin Adler 89292264be feat(coderd): add simple healthcheck formatting option (#9864) 2023-09-25 22:55:50 +00:00
Kyle Carberry 6e6b808143 fix: add case for logs without a source (#9866)
This is to support legacy logs!
2023-09-25 22:19:51 +00:00
Kyle Carberry d3220c5db9 fix: resolve flake in log sender by checking context (#9865)
See: https://github.com/coder/coder/actions/runs/6305051172/job/17117693579
2023-09-25 22:19:27 +00:00
Kyle Carberry 1262eef2c0 feat: add support for coder_script (#9584)
* Add basic migrations

* Improve schema

* Refactor agent scripts into it's own package

* Support legacy start and stop script format

* Pipe the scripts!

* Finish the piping

* Fix context usage

* It works!

* Fix sql query

* Fix SQL query

* Rename `LogSourceID` -> `SourceID`

* Fix the FE

* fmt

* Rename migrations

* Fix log tests

* Fix lint err

* Fix gen

* Fix story type

* Rename source to script

* Fix schema jank

* Uncomment test

* Rename proto to TimeoutSeconds

* Fix comments

* Fix comments

* Fix legacy endpoint without specified log_source

* Fix non-blocking by default in agent

* Fix resources tests

* Fix dbfake

* Fix resources

* Fix linting I think

* Add fixtures

* fmt

* Fix startup script behavior

* Fix comments

* Fix context

* Fix cancel

* Fix SQL tests

* Fix e2e tests

* Interrupt on Windows

* Fix agent leaking script process

* Fix migrations

* Fix stories

* Fix duplicate logs appearing

* Gen

* Fix log location

* Fix tests

* Fix tests

* Fix log output

* Show display name in output

* Fix print

* Return timeout on start context

* Gen

* Fix fixture

* Fix the agent status

* Fix startup timeout msg

* Fix command using shared context

* Fix timeout draining

* Change signal type

* Add deterministic colors to startup script logs

---------

Co-authored-by: Muhammad Atif Ali <atif@coder.com>
2023-09-25 16:47:17 -05:00
Kayla Washburn dac1375880 feat: add &hellip; to actions that require confirmation (#9862) 2023-09-25 15:25:02 -06:00
Kayla Washburn ff3fc0971c feat: create a workspace from any template version (#9861) 2023-09-25 14:54:19 -06:00
Ammar Bandukwala 4adbf24a08 docs: fix typo (#9859) 2023-09-25 19:52:53 +00:00
Steven Masley b6c5e94ffa fix: consider all 'devel' builds as 'dev' builds (#9794)
* fix: all 'devel' builds should be considered 'dev' builds.

If CI needs to be distinguished from a dev build, we should add
a different pre-release tag for those builds.

* change CI version checking to be more strict
2023-09-25 13:59:49 -05:00
Kayla Washburn 47d3161b0b fix: give more room to lonely resource metadata items (#9832) 2023-09-25 09:40:51 -06:00
Michael Smith 3757005e82 feat: add middle click support for workspace rows (#9834)
* chore: add generic ref support for useClickable

* chore: update useClickable call sites to use type parameter

* chore: update useClickableTableRow implementation

* chore: update other components using useClickableTableRow

* feat: add middle-click and cmd-click support for rows

* refactor: rename variable for clarity

* docs: add comment for clarity

* chore: add more click logic and comments

* refactor: clean up useClickableTableRow

* docs: rewrite comments for clarity

* fix: update TimelineEntry to accept forwarded ref

* fix: fix keyboard event logic to respond to spaces properly
2023-09-25 11:32:33 -04:00
Marcin Tojek 415818035c test(site): e2e: use IPv4 address for web server (#9843) 2023-09-25 14:44:15 +02:00
Cian Johnston 4622ea2c10 chore: remove filebrowser.db (#9854)
* rm and gitignore filebrowser.db
2023-09-25 12:41:27 +00:00
Bruno Quaresma cb545bcc30 chore(site): move a few pages to lazy load (#9831) 2023-09-25 09:38:24 -03:00
Muhammad Atif Ali 63ea12e74c chore: remove terraform provider versions for pr-deployment template (#9853) 2023-09-25 12:33:01 +00:00
dependabot[bot] 7da3180036 chore: bump github.com/spf13/afero from 1.9.5 to 1.10.0 (#9850)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-25 15:32:21 +03:00
dependabot[bot] 9d5af5b483 chore: bump github.com/coder/terraform-provider-coder from 0.11.1 to 0.12.0 (#9849)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-25 15:23:19 +03:00
dependabot[bot] e649b7cefe chore: bump google.golang.org/api from 0.141.0 to 0.142.0 (#9848)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-25 15:22:54 +03:00
dependabot[bot] 5c1ee6990e chore: bump github.com/gohugoio/hugo from 0.118.2 to 0.119.0 (#9847)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-25 15:22:43 +03:00
dependabot[bot] 9358b3bd84 ci: bump the github-actions group with 2 updates (#9846)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-25 15:22:19 +03:00
Muhammad Atif Ali 26e0d7580c chore: avoid pushing .terraform.lock.hcl while creating the template (#9851) 2023-09-25 15:13:32 +03:00
Cian Johnston 382843dc5f chore(docs): add notice re enabling encryption (#9844) 2023-09-25 11:07:38 +01:00
Bruno Quaresma 85ab9c2d48 chore(site): replace agent log service (#9814) 2023-09-22 16:13:25 -03:00
Bruno Quaresma 7bb0061804 chore(site): remove workspace build machine (#9813) 2023-09-22 16:12:25 -03:00
Muhammad Atif Ali df0c597843 docs: update VS Code Web docs (#9811)
* docs: update VS Code Web docs

* add back folder
2023-09-22 18:49:20 +00:00
Colin Adler cc009fe121 ci: increase timeout for template build jobs (#9829) 2023-09-22 13:13:50 -05:00
Ammar Bandukwala 733171a93b revert: "ci: cache datadog-ci (#9702)" (#9828)
Original commit changeset: b104e0ec0c
2023-09-22 17:31:25 +00:00
Michael Smith c201fc2538 revert: remove localStorage sync for search params (#9827) 2023-09-22 12:43:08 -04:00
Bruno Quaresma f6ee08d100 chore(site): replace create user machine by react-query (#9810) 2023-09-22 13:28:56 -03:00
Jon Ayers 9216725698 chore: add alpha badges to workspace cleanup fields (#9802) 2023-09-22 10:41:17 -05:00
Cian Johnston 8d8402da00 fix(coderd/database): avoid clobbering workspace build state (#9826)
Fixes #9823.

- Decomposes UpdateWorkspaceBuildByID into UpdateWorkspaceBuildProvisionerStateByID and UpdateWorkspaceBuildDeadlineByID.
- Replaces existing invocations of UpdateWorkspaceBuildByID with the newer queries where applicable.
- Modifies GetActiveWorkspaceBuildsByTemplateID to not return incomplete workspace builds.
2023-09-22 16:22:07 +01:00
Cian Johnston a1f3a6b606 refactor(scaletest/terraform): break up infra creation and k8s resource provisioning (#9824)
Breaks up scaletest/terraform into {infra, k8s}.
2023-09-22 15:55:47 +01:00
Michael Smith 84999cb33d feat: add empty state for SSO auth methods (#9818)
* fix: remove needless undefined checks

* refactor: clean up button markup

* refactor: restrict access to full auth in oidc functions

* feat: add empty SSO state
2023-09-22 09:38:49 -04:00
Spike Curtis e9077f3bd2 fix: use AlwaysEnable for licenses with all features (#9808)
* fix: use AlwaysEnable for licenses with all features

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

* use dbtime.Now() intead of time.Now()

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-09-22 05:02:28 +00:00
Dean Sheather 94a0612cd2 chore: update tailscale (#9817) 2023-09-22 07:21:38 +10:00
Colin Adler c900b5f8df feat: add single tailnet support to pgcoord (#9351) 2023-09-21 14:30:48 -05:00
Colin Adler fbad06f406 chore: bump wgtunnel (#9805) 2023-09-21 13:33:13 -05:00
Michael Smith 91a04c0132 chore: enhance tests for TemplateSchedulePage (#9801)
* chore: Add benchmark logs to test

* chore: Remove benchmark logic

* chore: add hard cutoff for waitFor calls

* refactor: clean up waitFor cut-off logic

* chore: add assertion that submit button is not disabled

* chore: Remove disabled check at the start of the test

* fix: extend cutoff for waitFor config
2023-09-21 14:25:26 -04:00
Muhammad Atif Ali 201a6c0c79 chore: remove extra condition from pr-deploy.yaml (#9788) 2023-09-21 17:58:46 +03:00
Bruno Quaresma 801c6c994b chore(site): replace setup machine by react-query (#9809) 2023-09-21 11:21:49 -03:00
Spike Curtis 866ba8ede5 fix: call agent directly in cli tests (#9789)
* fix: call agent directly in cli tests

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

* log verbose in TestDERPHeaders

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-09-21 13:30:24 +04:00
Muhammad Atif Ali fa858531a8 feat: add nomad template (#9786) 2023-09-21 08:54:56 +00:00
Michael Smith b742661abd feat: make workspace search bar remember text (#9759)
* minor: Add useEffectEvent polyfill

* chore: update filter to have better callback support

* docs: Clean up comments

* fix: add localStorage to useWorkspacesFilter

* refactor: Centralize stable useSearchParams

* refactor: clean up filter to be fully pure on mount

* chore: add tests for useEffectEvent

* wip: commit progress for searchbar fix

* chore: clean up WorkspacesPage

* fix: add logic for syncing queries with search params

* chore: Rename initialValue to fallbackFilter

* chore: Remove todo comment

* refactor: update code to use useEffectEvent

* docs: clean up comments for clarity

* fix: update url check to use regex
2023-09-20 16:44:19 -04:00
Bruno Quaresma 92a90eb9ae test(site): fix terminal page test flakiness (#9798) 2023-09-20 15:19:27 -03:00
Bruno Quaresma 1cd0bea86e chore: make workspace batch actions enabled by default (#9797) 2023-09-20 14:10:01 -03:00
Eric Paulsen fae8a470df fix(cli): display pasted session token (#9710) 2023-09-20 12:41:59 -04:00
Steven Masley f89b68056d chore: add debug information to wsproxy errors (#9683)
* chore: add debug information to wsproxy errors
* Use codersdk.ReadBodyAsError
2023-09-20 10:05:14 -05:00
Cian Johnston ae1896f2dc chore(docs): replace numbering with bullets in admin/encryption.md (#9792) 2023-09-20 15:15:39 +01:00
Marcin Tojek 8e012e4e1a test(site): workaround for reattached xterm-rows (#9790) 2023-09-20 14:25:30 +02:00
Steven Masley a18bf73131 chore: display warnings on app share failure (#9783)
* chore: add warnings to app share failure
Warnings only appear if the app is misconfigured to the deployment
2023-09-19 21:54:51 +00:00
Kayla Washburn 1fd1c654a9 fix: hide empty update message box (#9784) 2023-09-19 13:11:22 -06:00
Dean Sheather eda32659a8 chore: fix flake in tunnel (#9746) 2023-09-19 18:54:56 +00:00
Spike Curtis 70e481e7a5 fix: use terminal emulator that keeps state in ReconnectingPTY tests (#9765)
* Add more pty diagnostics for terminal parsing

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

* print escaped strings

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

* Only log on failure - heisenbug?

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

* use the terminal across matches to keep cursor & contents state

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

* Only log bytes if we're not expecting EOF

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-09-19 17:57:30 +00:00
Kayla Washburn 269b1c59f1 refactor: get rid of templateVariablesXService (#9763) 2023-09-19 11:54:14 -06:00
Mathias Fredriksson 530dd9d247 fix(coderd): subscribe to workspace when streaming agent logs to detect outdated build (#9729)
Fixes #9721
2023-09-19 20:02:27 +03:00
Bruno Quaresma 87d50f17a2 chore(site): refactor groups to use react-query (#9701) 2023-09-19 13:37:17 -03:00
Mathias Fredriksson 161a3cfa26 fix(coderd/batchstats): use debug log on query cancellation in flush (#9778)
Fixes #9772
2023-09-19 18:16:07 +03:00
Marcin Tojek ceb52ac24a fix: user should always belong to an organization (#9781) 2023-09-19 16:22:37 +02:00
Mathias Fredriksson d6089ae0ad fix(coderd/batchstats): use debug log on context cancellation in flush (#9777) 2023-09-19 13:19:39 +00:00
Bruno Quaresma 7bc98c296b refactor(site): do not display error details as message (#9755) 2023-09-19 09:54:59 -03:00
Bruno Quaresma e26bb2d91b chore(site): rollback remark-gfm version (#9774) 2023-09-19 09:43:11 -03:00
Bruno Quaresma ffa77ba6ff feat(site): show CLI flags and env variables for the options (#9757) 2023-09-19 09:34:04 -03:00
Dr. Pi3cH 94cccd0a01 fix: make sure fly_app name is lower case (#9771)
fly_app does not support uppercase. Coder allows uppercase for username/owner.
2023-09-19 12:30:52 +00:00
Eric Paulsen e9c183d0dc feat: add envFrom value to Helm chart (#9587)
* feat: add envFrom value to Helm chart

* add envUseClusterAccessURL

* add tests

---------

Co-authored-by: Dean Sheather <dean@deansheather.com>
2023-09-19 16:03:13 +04:00
Marcin Tojek ceeb9987a5 test(cli): ensure first option selected with is expected (#9770) 2023-09-19 13:55:51 +02:00
Marcin Tojek b0e3daa120 feat(coderd): support weekly aggregated insights (#9684) 2023-09-19 13:06:19 +02:00
Muhammad Atif Ali b358e3d558 chore: fix permission for kubeconfig (#9768) 2023-09-19 08:36:00 +00:00
Spike Curtis 375c70d141 feat: integrate Acquirer for provisioner jobs (#9717)
* chore: add Acquirer to provisionerdserver pkg

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

* code review improvements & fixes

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

* feat: integrate Acquirer for provisioner jobs

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

* Fix imports, whitespace

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

* provisionerdserver always closes; remove poll interval from playwright

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

* post jobs outside transactions

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

* graceful shutdown in test

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

* Mark AcquireJob deprecated

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

* Graceful shutdown on all provisionerd tests

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

* Deprecate, not remove CLI flags

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-09-19 10:25:57 +04:00
dependabot[bot] 6cf531bfef chore: bump typescript from 5.1.6 to 5.2.2 in /site (#9743)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-19 08:32:14 +03:00
dependabot[bot] 8fe4401e23 chore: bump github.com/klauspost/compress from 1.16.5 to 1.17.0 (#9735)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-19 05:30:04 +00:00
Cian Johnston aa8652c928 fix(dbtestutil): avoid truncating inserts that span multiple lines (#9756) 2023-09-18 19:46:22 +00:00
Kayla Washburn ed25f1449d refactor: use a line graph for DAUChart (#9758) 2023-09-18 12:28:30 -06:00
Kayla Washburn f85aa443dd feat: show version messages in version lists (#9708) 2023-09-18 11:17:07 -06:00
Jon Ayers 622442203d chore: fix test flake in TestProvisionerd (#9709) 2023-09-18 11:23:22 -05:00
Cian Johnston 45eadfc136 fix(cli): specify IgnoreErrors in slogtest options for scaletest cli tests (#9751)
* fix(cli): specify IgnoreErrors in slogtest options for scaletest cli tests
* bump test timeout
2023-09-18 16:39:28 +01:00
Asher 17f9991118 fix: reconnect terminal on non-modified key presses (#9686)
* Listen to web terminal keydown on capture

Instead of bubbling.  I think maybe what happens here is that xterm is
capturing key presses and preventing the event from bubbling?  So
setting the listener on the capture phase instead works around this.
Probably would also work to dipsose the terminal.

* Prevent issuing terminal reload when already reloading

I am not sure this actually causes any issues, but might as well.

* Ignore modifier keys for reconnecting terminal
2023-09-18 07:24:01 -08:00
Michael Smith 2caf7a7ceb fix: remove just-debounce-it (#9707)
* fix: Switch UserAutocomplete to useDebouncedFunction

* fix: switch UserOrGroupAutocomplete to useDebouncedFunction

* chore: remove just-debounce-it dependency

* docs: Clean up comments
2023-09-18 14:20:11 +00:00
Bruno Quaresma a2aff1f527 chore(site): replace xstate by react-query on update password (#9696) 2023-09-18 11:03:35 -03:00
Bruno Quaresma e74d8a7b21 chore(site): refactor starter templates to use react-query (#9697)
* Remove starter templates service

* Remove starter template service

* Remove template x service
2023-09-18 11:02:55 -03:00
dependabot[bot] e1bd6ddc25 chore: bump the otel group with 3 updates (#9731)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 16:55:38 +03:00
dependabot[bot] a989e8363b chore: bump google.golang.org/api from 0.140.0 to 0.141.0 (#9732)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 16:54:32 +03:00
dependabot[bot] 24bff1098d chore: bump gopkg.in/DataDog/dd-trace-go.v1 from 1.54.0 to 1.55.0 (#9734)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 16:53:40 +03:00
dependabot[bot] 1d8a4ed201 chore: bump jest-location-mock from 1.0.9 to 2.0.0 in /site (#9727)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 13:46:08 +00:00
Bruno Quaresma 2319486806 chore(site): replace deployment stats service by react-query (#9698) 2023-09-18 10:44:52 -03:00
dependabot[bot] 5a22f08f3f chore: bump @tanstack/react-query from 4.33.0 to 4.35.3 in /site (#9738)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 16:34:56 +03:00
dependabot[bot] d326f1b10c chore: bump remark-gfm from 3.0.1 to 4.0.0 in /site (#9726)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 16:32:57 +03:00
dependabot[bot] 972425e3d4 chore: bump @playwright/test from 1.37.1 to 1.38.0 in /site (#9739)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 16:32:42 +03:00
dependabot[bot] b76a430d22 chore: bump github.com/valyala/fasthttp from 1.49.0 to 1.50.0 (#9736)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 16:31:54 +03:00
dependabot[bot] ea93b4bbe4 chore: bump react-icons from 4.10.1 to 4.11.0 in /offlinedocs (#9740)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 16:31:48 +03:00
dependabot[bot] bd3f2f8c10 chore: bump eslint from 8.48.0 to 8.49.0 in /offlinedocs (#9742)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 16:31:16 +03:00
dependabot[bot] 010a13c654 chore: bump @testing-library/user-event from 14.4.3 to 14.5.1 in /site (#9730)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 16:24:05 +03:00
dependabot[bot] 9c8140270a chore: bump the react group in /site with 1 update (#9723)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 16:23:35 +03:00
dependabot[bot] c58e5bf09a chore: bump the eslint group in /site with 1 update (#9725)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 11:41:20 +00:00
Muhammad Atif Ali 3ed65de82e chore: increase dependabot open pr limit for site (#9728) 2023-09-18 11:24:49 +00:00
dependabot[bot] eb72866a29 ci: bump the github-actions group with 2 updates (#9722)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 11:15:56 +00:00
Cian Johnston 1df7589105 feat(coderd/database/dbtestutil): add ability to dump database on failure (#9704)
Adds dbtestutil.DumpOnFailure() to allow dumping the entire test database contents upon test failure.
This does nothing for dbfake currently.
2023-09-18 11:50:15 +01:00
Muhammad Atif Ali e6865e0df5 ci: remove example templates readme from docs filter (#9720) 2023-09-18 10:38:57 +00:00
Cian Johnston 9bcff30dee fix(coderd/database): migrate workspaces.last_used_at to timestamptz (#9699) 2023-09-18 11:07:54 +01:00
Cian Johnston 1f5eb088b5 fix(examples): fix typo in examples.gen.json (#9718) 2023-09-18 09:09:30 +00:00
Cian Johnston 653488e8ee fix(coderd): emit CollectedAt as UTC in convertWorkspaceAgentMetadata (#9700) 2023-09-18 09:17:18 +01:00
Ben Potter 5de5d20808 fix: remove broken fly.io template from starter templates (#9711)
Co-authored-by: Atif Ali <atif@coder.com>
2023-09-17 20:13:36 +03:00
sharkymark b2f84668c8 docs: document display_apps feature to hide/show built-in coder_app resources (#9714)
* docs: display_apps feature to hide/show terminal, vscode desktop, port-forwarding, etc.

* Update docs/templates/index.md

Co-authored-by: Muhammad Atif Ali <atif@coder.com>

* Update docs/templates/index.md

Co-authored-by: Muhammad Atif Ali <atif@coder.com>

---------

Co-authored-by: Muhammad Atif Ali <atif@coder.com>
2023-09-17 15:13:56 +00:00
kpoxo6op ab08b2c3e4 fix typo in README.md (#9715)
typo
2023-09-17 07:07:13 -05:00
Michael Smith fcc8b9ec92 fix: prevent workspace search bar text from getting garbled (#9703)
* chore: Reorganize hook calls for useWorkspacesFilter

* refactor: Clean up some filter logic

* refactor: Create debounce utility hooks

* docs: Clean up comments for clarity

* fix: Update focus logic to apply for any inner focus

* fix: Add onBlur behavior for state syncs

* chore: Add progress for debounce test

* chore: Finish tests for debounce hooks

* docs: Add file description and warning
2023-09-15 15:55:59 -04:00
Ammar Bandukwala b104e0ec0c ci: cache datadog-ci (#9702) 2023-09-15 19:22:38 +00:00
Kayla Washburn 9e053ce220 feat: show update messages on workspace page (#9705) 2023-09-15 13:09:29 -06:00
Kayla Washburn efe804498b feat: add quiet hours settings page (#9676) 2023-09-15 11:14:33 -06:00
Cian Johnston 72dff7f188 fix(enterprise/dbcrypt): do not skip deleted users when encrypting or deleting (#9694)
- Broadens scope of data generation in TestServerDBCrypt over all user login types, statuses, and deletion status.
- Adds support for specifying user status / user deletion status in dbgen
- Adds more comprehensive logging in TestServerDBCrypt upon test failure (to be generalized and expanded upon in a follow-up)
- Adds AllUserIDs query, updates dbcrypt to use this instead of GetUsers.
2023-09-15 15:09:40 +01:00
Mathias Fredriksson bc97eaa41b feat(scaletest): add scaletest-runner template (#9662)
Closes #9571
2023-09-15 13:23:37 +00:00
Marcin Tojek d0d64bbdca refactor: define insights interval (#9693) 2023-09-15 12:01:00 +00:00
Cian Johnston 65db7a71b7 feat(coderd/database/dbtestutil): set default database timezone to non-UTC in unit tests (#9672)
- Adds dbtestutil.WithTimezone(tz) to allow setting the timezone for a test database.
- Modifies our test database setup code to pick a consistently weird timezone for the database.
- Adds the facility randtz.Name() to pick a random timezone which is consistent across subtests (via sync.Once).
- Adds a linter rule to warn against setting the test database timezone to UTC.
2023-09-15 09:01:32 +01:00
Ammar Bandukwala 281faf9ccd ci: upgrade runners (#9689)
Per DataDog, I've doubled the resources of our slowest jobs.
2023-09-14 20:20:39 -05:00
Ammar Bandukwala b63dfe7b75 perf(cli): optimize CPU consumption of help pages (#9607)
This change reduces the CPU consumption of --help by ~50%.

Also, this change removes ANSI escape codes from our golden files. I
don't think those were worth the inability to parallelize golden file tests and
global state fragility.
2023-09-14 19:48:29 -05:00
Jon Ayers 7311ffbd9d feat: implement agent process management (#9461)
- An opt-in feature has been added to the agent to allow
   deprioritizing non coder-related processes for CPU by setting their
   niceness level to 10.
- Opting in to the feature requires setting CODER_PROC_PRIO_MGMT to a non-empty value.
2023-09-14 19:45:05 -05:00
Bruno Quaresma 79d4179123 chore(site): migrate a few services to react-query used in the DashboardProvider (#9667) 2023-09-14 19:35:03 -03:00
Bruno Quaresma 3b088a5cb8 chore(site): refactor deployment values service to react-query (#9669) 2023-09-14 18:49:23 -03:00
Marcin Tojek 225cf8acec chore: print page content on failed promise (#9681) 2023-09-14 13:30:46 +02:00
Marcin Tojek dcad0a437c fix: wait for build job (#9680) 2023-09-14 11:20:09 +02:00
Cian Johnston 8b6e2862fd refactor(coderd): collapse activityBumpWorkspace into a single query (#9652)
* Adds unit-style tests for activityBumpWorkspace
* Ports logic of activityBumpWorkspace to a SQL query
* Updates activityBumpWorkspace to call above query
2023-09-14 09:09:51 +01:00
Colin Adler 38560dd922 chore: remove coder trace telemetry (#9677) 2023-09-14 02:20:28 -04:00
Bruno Quaresma e7b0181519 feat(site): add support to .sh and .tpl files (#9674) 2023-09-13 16:27:02 -03:00
Steven Masley e0e6d7c9a6 fix: case insensitve agent ssh session env var (#9675) 2023-09-13 14:18:00 -05:00
Steven Masley 0e4d6896e3 test: fix cleanup order on provisioner daemon work dir (#9668)
* test: fix cleanup order on provisioner daemon work dir

* Reduce the test race condition
2023-09-13 13:13:08 -05:00
Kayla Washburn 53a985ff11 refactor: clean up workspace and template settings (#9654) 2023-09-13 11:55:27 -06:00
dependabot[bot] 6c409b8872 chore: bump chromatic from 6.24.1 to 7.1.0 in /site (#9629)
Bumps [chromatic](https://github.com/chromaui/chromatic-cli) from 6.24.1 to 7.1.0.
- [Changelog](https://github.com/chromaui/chromatic-cli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chromaui/chromatic-cli/compare/v6.24.1...v7.1.0)

---
updated-dependencies:
- dependency-name: chromatic
  dependency-type: direct:development
  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>
2023-09-13 14:49:54 -03:00
Jon Ayers 3dc1e22d56 feat: add template_id and template_name to workspace data source (#9655) 2023-09-13 11:09:08 -05:00
Marcin Tojek d171b3611b fix: flaky: TestDeleteTemplate/NoWorkspaces (#9666) 2023-09-13 17:05:08 +02:00
Cian Johnston 3be783b319 fix(scaletest/workspacetraffic): wait for non-zero metrics before cancelling in TestRun (#9663) 2023-09-13 15:54:56 +01:00
Muhammad Atif Ali 254f459d69 chore(dogfood): add a coder_app for JetBrains gateway IDEs (#9644) 2023-09-13 17:49:36 +03:00
Marcin Tojek a229855e71 test(site): e2e: improve webTerminal test (#9659) 2023-09-13 16:18:10 +02:00
Spike Curtis 4ebf490d97 feat: add Acquirer to provisionerdserver pkg (#9658)
* chore: add Acquirer to provisionerdserver pkg

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

* code review improvements & fixes

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-09-13 16:36:43 +04:00
Cian Johnston 2ac532982d feat(coderd/database): generate foreign key constraints and add database.IsForeignKeyViolation (#9657)
* feat(coderd/database): generate foreign key constraints, add database.IsForeignKeyViolation

* address PR comments
2023-09-13 13:28:46 +01:00
Cian Johnston a6f7f71808 chore(docs/admin): update encryption.md (#9660) 2023-09-13 12:56:20 +01:00
Marcin Tojek e2579e9440 test: fix flaky TestPatchTemplateMeta/Modified (#9656) 2023-09-13 11:34:53 +02:00
Steven Masley 18c34ee456 chore: support external types in typescript codegen (#9633)
* chore: support external types in typescript codegen
* fix enums on external packages
* Support clibase.struct
* Add regexp
* Make gen with updated generator
2023-09-12 18:21:09 -05:00
Bruno Quaresma 641bf272ed fix(site): fix renderer error (#9653) 2023-09-12 19:59:09 -03:00
Kayla Washburn 0726eb56bb fix: update autostart/autostop text (#9650) 2023-09-12 15:29:39 -06:00
Bruno Quaresma b3c98395ab chore(site): use react-query to fetch roles (#9630) 2023-09-12 15:07:56 -03:00
Bruno Quaresma b33cb0ef97 chore(site): refactor workspace quota to use react-query instead of XState (#9626) 2023-09-12 14:58:35 -03:00
Julio 64bc317cd4 Doc - Remove sysbox 16 pods max. (#9646) 2023-09-12 17:49:51 +00:00
Bruno Quaresma 5ca8c4287f chore(site): move ssh keys management to react-query (#9625) 2023-09-12 14:40:07 -03:00
Bruno Quaresma 6db89b0372 chore(site): move templates fetching to react-query (#9622) 2023-09-12 14:09:16 -03:00
Bruno Quaresma b7550bfda5 chore(site): remove unecessary types and move types to where they are used (#9621) 2023-09-12 13:54:05 -03:00
Marcin Tojek 0e28397c82 fix: use CRC32 to shorten app subdomain (#9645) 2023-09-12 17:11:45 +02:00
Marcin Tojek 898971b329 refactor: generate application URL on backend side (#9618) 2023-09-12 15:25:10 +02:00
dependabot[bot] 228d1cf361 chore: bump the eslint group in /site with 3 updates (#9639)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-12 10:25:04 +00:00
dependabot[bot] 531e1334af chore: bump monaco-editor from 0.41.0 to 0.43.0 in /site (#9642)
Bumps [monaco-editor](https://github.com/microsoft/monaco-editor) from 0.41.0 to 0.43.0.
- [Changelog](https://github.com/microsoft/monaco-editor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/microsoft/monaco-editor/compare/v0.41.0...v0.43.0)

---
updated-dependencies:
- dependency-name: monaco-editor
  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>
2023-09-12 12:56:43 +03:00
dependabot[bot] 7f126758a5 chore: bump jest-websocket-mock from 2.4.0 to 2.5.0 in /site (#9641)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-12 09:24:10 +00:00
dependabot[bot] d49bc2003b chore: bump msw from 1.2.2 to 1.3.0 in /site (#9640)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-12 12:16:16 +03:00
dependabot[bot] 8b08a78168 chore: bump google.golang.org/api from 0.138.0 to 0.140.0 (#9636)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-12 08:45:29 +00:00
Muhammad Atif Ali f1f522a9a7 revert: "chore: bump the eslint group in /site with 3 updates" (#9638) 2023-09-12 11:26:51 +03:00
dependabot[bot] 2c19995712 ci: bump the github-actions group with 2 updates (#9620)
Bumps the github-actions group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [crate-ci/typos](https://github.com/crate-ci/typos).


Updates `actions/checkout` from 3 to 4
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

Updates `crate-ci/typos` from 1.16.10 to 1.16.11
- [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.16.10...v1.16.11)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- 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>
2023-09-12 10:22:22 +03:00
dependabot[bot] e85981713d chore: bump the xterm group in /site with 6 updates (#9627)
Bumps the xterm group in /site with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [xterm](https://github.com/xtermjs/xterm.js) | `5.2.1` | `5.3.0` |
| [xterm-addon-canvas](https://github.com/xtermjs/xterm.js) | `0.4.0` | `0.5.0` |
| [xterm-addon-fit](https://github.com/xtermjs/xterm.js) | `0.7.0` | `0.8.0` |
| [xterm-addon-unicode11](https://github.com/xtermjs/xterm.js) | `0.5.0` | `0.6.0` |
| [xterm-addon-web-links](https://github.com/xtermjs/xterm.js) | `0.8.0` | `0.9.0` |
| [xterm-addon-webgl](https://github.com/xtermjs/xterm.js) | `0.15.0` | `0.16.0` |


Updates `xterm` from 5.2.1 to 5.3.0
- [Release notes](https://github.com/xtermjs/xterm.js/releases)
- [Commits](https://github.com/xtermjs/xterm.js/compare/5.2.1...5.3.0)

Updates `xterm-addon-canvas` from 0.4.0 to 0.5.0
- [Release notes](https://github.com/xtermjs/xterm.js/releases)
- [Commits](https://github.com/xtermjs/xterm.js/compare/0.4...0.5)

Updates `xterm-addon-fit` from 0.7.0 to 0.8.0
- [Release notes](https://github.com/xtermjs/xterm.js/releases)
- [Commits](https://github.com/xtermjs/xterm.js/compare/0.7...0.8)

Updates `xterm-addon-unicode11` from 0.5.0 to 0.6.0
- [Release notes](https://github.com/xtermjs/xterm.js/releases)
- [Commits](https://github.com/xtermjs/xterm.js/compare/0.5...0.6)

Updates `xterm-addon-web-links` from 0.8.0 to 0.9.0
- [Release notes](https://github.com/xtermjs/xterm.js/releases)
- [Commits](https://github.com/xtermjs/xterm.js/compare/0.8...0.9)

Updates `xterm-addon-webgl` from 0.15.0 to 0.16.0
- [Release notes](https://github.com/xtermjs/xterm.js/releases)
- [Commits](https://github.com/xtermjs/xterm.js/compare/0.15...0.16)

---
updated-dependencies:
- dependency-name: xterm
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: xterm
- dependency-name: xterm-addon-canvas
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: xterm
- dependency-name: xterm-addon-fit
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: xterm
- dependency-name: xterm-addon-unicode11
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: xterm
- dependency-name: xterm-addon-web-links
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: xterm
- dependency-name: xterm-addon-webgl
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: xterm
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-12 10:21:59 +03:00
dependabot[bot] 140a7d2de2 chore: bump the eslint group in /site with 3 updates (#9628)
Bumps the eslint group in /site with 3 updates: [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin), [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) and [eslint](https://github.com/eslint/eslint).


Updates `@typescript-eslint/eslint-plugin` from 6.5.0 to 6.7.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.7.0/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 6.5.0 to 6.7.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.7.0/packages/parser)

Updates `eslint` from 8.48.0 to 8.49.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.48.0...v8.49.0)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: eslint
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: eslint
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: eslint
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-12 10:21:47 +03:00
dependabot[bot] 3e0969004d chore: bump ts-proto from 1.156.0 to 1.157.0 in /site (#9631)
Bumps [ts-proto](https://github.com/stephenh/ts-proto) from 1.156.0 to 1.157.0.
- [Release notes](https://github.com/stephenh/ts-proto/releases)
- [Changelog](https://github.com/stephenh/ts-proto/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stephenh/ts-proto/compare/v1.156.0...v1.157.0)

---
updated-dependencies:
- dependency-name: ts-proto
  dependency-type: direct:development
  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>
2023-09-12 10:21:09 +03:00
dependabot[bot] abbd780373 chore: bump cronstrue from 2.31.0 to 2.32.0 in /site (#9632)
Bumps [cronstrue](https://github.com/bradymholt/cronstrue) from 2.31.0 to 2.32.0.
- [Release notes](https://github.com/bradymholt/cronstrue/releases)
- [Changelog](https://github.com/bradymholt/cRonstrue/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bradymholt/cronstrue/compare/v2.31.0...v2.32.0)

---
updated-dependencies:
- dependency-name: cronstrue
  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>
2023-09-12 10:20:54 +03:00
dependabot[bot] d4bdb96883 chore: bump google.golang.org/grpc from 1.57.0 to 1.58.0 (#9563)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.57.0 to 1.58.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.57.0...v1.58.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>
2023-09-12 10:18:21 +03:00
Kayla Washburn 13acf5976c chore: remove i18next (#9608) 2023-09-11 11:17:58 -06:00
Marcin Tojek 1a1c230534 fix(docs): skip >> character (#9616) 2023-09-11 12:23:40 +02:00
Marcin Tojek 67fe3ae8d6 feat: clean stale provisioner files (#9545) 2023-09-11 07:37:14 +00:00
sharkymark d055f93706 Docs coder apps web ides (#9613)
* chore: document external URLs, refresh web IDE images

* chore: coder_app external URL, updated images, file browser

* Update docs/ides/web-ides.md

---------

Co-authored-by: Muhammad Atif Ali <atif@coder.com>
2023-09-10 20:26:59 +00:00
Muhammad Atif Ali 84ede326e8 docs: add VS Code Web to web-ides (#9591) 2023-09-10 02:32:41 +00:00
Ammar Bandukwala f703a5b34e chore(cli): lighten flags on help page (#9606) 2023-09-08 13:43:56 -05:00
Ammar Bandukwala e361f1107b feat(cli): colorize help page (#9589) 2023-09-08 18:21:33 +00:00
Bruno Quaresma 11404af9ca chore(site): update files to use a more standard naming (#9605) 2023-09-08 15:14:41 -03:00
Bruno Quaresma 554ddb11cd chore(site): refactor stories and test from page components (#9603)
* Refactor AuditPage

* Refactor CliAuthPageView stories

* Refactor CreateTemplateForm stories

* Refactor CreateUserPage test

* Refactor CreateWorkspacePage tests

* Fix stories name

* Refactor AppereancePageView stories

* Refactor GitAuthSettingsPageView stories

* Refactor NetworkSettingsPageView stories

* Refactor SecuritySettingsPageView stories

* Refactor UserAuthSettingsPageView stories

* Refactor GroupsPage stories

* Refactor LoginPage tests

* Refactor SetupPage stories

* Refactor StarterTemplatePageView stories

* Refactor StarterTemplatesPage tests

* Refactor TemplatePage tests

* RefactorTemplateSettingsPage tests

* Refactor TemplatesPage tests

* Flat TemplateVersionEditorPage

* Refactor TemplateVersionPage stories

* Refactor UserSettingsPage stories

* Refactor UsersPage stories

* Simplify IndexPage

* Refactor WorkspaceSettingsPage stories

* Refactor WorkspacePage stories

* Refactor Conditionals stories

* Fix typo

* Fix imports

* Fix ChooseOne story

* Fix UserAuthSettingsPageView stories
2023-09-08 15:14:13 -03:00
David Morcillo 9e5a59e222 Update upgrade.md (#9569)
Fix wrong docker-compose update instructions.
2023-09-08 10:38:35 -04:00
Marcin Tojek ad23075e1b refactor: build application URL (#9601)
* refactor: build application URL

* fix
2023-09-08 14:01:57 +00:00
Cian Johnston 392b11272b fixup! fix(scripts): modify logic for determining terraform arch (#9595) (#9596) 2023-09-08 11:07:57 +00:00
Cian Johnston a3a16a1586 fix(scripts): modify logic for determining terraform arch (#9595)
Commit fb3616c used a build-arg to set the desired terrafomr
architeture to fetch. This is set in build_docker.sh but not
when building using depot.dev. Baking the logic into the
Dockerfile instead.
2023-09-08 10:47:51 +00:00
Spike Curtis 8d7eb1728c fix: stop inserting provisioner daemons into the database (#9108)
Signed-off-by: Spike Curtis <spike@coder.com>
2023-09-08 10:37:36 +00:00
Cian Johnston fb3616c37e fix(scripts): wget terraform directly from releases.hashicorp.com (#9594) 2023-09-08 10:02:18 +00:00
Spike Curtis 11b6068112 feat: add support for networked provisioners (#9593)
* Refactor provisionerd to use interface to connect to provisioners

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

* feat: add support for networked provisioners

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

* fix token length and linting

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-09-08 09:53:48 +00:00
Kayla Washburn 8b51a2f3c5 docs: mention the need for permission to read email addresses (#9588) 2023-09-07 16:40:22 -06:00
Ammar Bandukwala dd97fe2bce chore(cli): replace lipgloss with coder/pretty (#9564)
This change will improve over CLI performance and "snappiness" as well as
substantially reduce our test times. Preliminary benchmarks show
`coder server --help` times cut from 300ms to 120ms on my dogfood
instance.

The inefficiency of lipgloss disproportionately impacts our system, as all help
text for every command is generated whenever any command is invoked.

The `pretty` API could clean up a lot of the code (e.g., by replacing
complex string concatenations with Printf), but this commit is too
expansive as is so that work will be done in a follow up.
2023-09-07 16:28:22 -05:00
Kayla Washburn 8421f56137 refactor: bring back updated install.sh with patches (#9583) 2023-09-07 21:17:16 +00:00
Eric Paulsen ccda1c5c7d fix: rm tf provider versions in examples/ (#9586) 2023-09-07 18:53:23 +00:00
Cian Johnston 0306631518 fix(docs): fix broken link (#9585) 2023-09-07 18:36:26 +00:00
Bruno Quaresma 869d040cc6 chore(site): refactor stories and tests from components directory (#9578)
* Refactor Alert

* Refactor Avatar and its stories

* Refactor AvatarData and its stories

* Refactor CodeExample and its tests

* Refactor ServiceBanner stories

* Refactor Navbar and its tests

* Refactor ServiceBanner stories

* Refactor LicenseBannerView stories

* Refactor DeploymentBannerView stories

* Extract optionValue into a module

* Refactor DeleteDialog stories

* Refactor ConfirmDialog tests

* Refactor EmptyState tests

* Flat ErrorBoundaryState and refactor stories

* Refactor Expander stories

* Refactor FormFooter stories

* Refactor FullPageForm stories

* Refactor EnterpriseSnackbar stories

* Refactor GroupAvatar stories

* Refactor HelpTooltip stories and remove index

* Remove unecessary types module from IconField

* Refactor LoadingButton stories

* Refactor Margins stories

* Refactor Markdown stories

* Refactor PageHeader stories

* Refactor PageButton tests

* Refactor Pill stories

* Refactor Resources stories

* Refactor RichParameterInput stories and flat MultiTextField

* Remove unecessary Stack story

* Refactor TableRowMenu stories

* Refactor TemplateLayout stories

* Refactor Typography props

* Refactor UserAutocomplete

* Refactor WorkspaceBuildLogs components and tests

* Refactor WorkspaceStatusBadge stories

* Fix wrong imports

* Remove Example.args pattern

* Fix wrong import

* Refactor EmptyState stories

* Refactor HelpTooltip stories

* Remove not valid ErrorAlert story

* Fix AvatarData story

* Add border back to CodeExample

* Fix Navbar story

* Fix AgentRow proxy in the stories
2023-09-07 18:38:28 +02:00
Kayla Washburn 4f142fa959 feat: add minimal frontend for creating workspaces from specific template versions (#9475) 2023-09-07 10:33:42 -06:00
Mathias Fredriksson 40fcabfa0e chore(go.mod): update github.com/hashicorp/terraform-json to main (#9582) 2023-09-07 16:06:28 +00:00
Cian Johnston 7d7c84bb4d feat(coderd): connect dbcrypt package implementation (#9523)
See also: https://github.com/coder/coder/pull/9522

- Adds commands `server dbcrypt {rotate,decrypt,delete}` to re-encrypt, decrypt, or delete encrypted data, respectively.
- Plumbs through dbcrypt in enterprise/coderd (including unit tests).
- Adds documentation in admin/encryption.md.

This enables dbcrypt by default, but the feature is soft-enforced on supplying external token encryption keys. Without specifying any keys, encryption/decryption is a no-op.
2023-09-07 15:49:49 +01:00
Mathias Fredriksson ed7f682fd1 chore(go.mod): update github.com/hashicorp/terraform-json to v0.17.1 (#9570) 2023-09-07 14:55:39 +03:00
dependabot[bot] 5a6f6e5679 chore: bump the golang-x group with 4 updates (#9562)
Bumps the golang-x group with 4 updates: [golang.org/x/crypto](https://github.com/golang/crypto), [golang.org/x/net](https://github.com/golang/net), [golang.org/x/oauth2](https://github.com/golang/oauth2) and [golang.org/x/tools](https://github.com/golang/tools).


Updates `golang.org/x/crypto` from 0.12.0 to 0.13.0
- [Commits](https://github.com/golang/crypto/compare/v0.12.0...v0.13.0)

Updates `golang.org/x/net` from 0.14.0 to 0.15.0
- [Commits](https://github.com/golang/net/compare/v0.14.0...v0.15.0)

Updates `golang.org/x/oauth2` from 0.11.0 to 0.12.0
- [Commits](https://github.com/golang/oauth2/compare/v0.11.0...v0.12.0)

Updates `golang.org/x/tools` from 0.12.0 to 0.13.0
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.12.0...v0.13.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-07 08:26:40 +00:00
Kayla Washburn fd565e0e0b chore: enable test-2e (#9567) 2023-09-07 05:49:40 +00:00
Kayla Washburn bfff88d2d3 revert: improve flag interpretation for install.sh (#9566) 2023-09-07 05:29:52 +00:00
Muhammad Atif Ali 2dae60038a fix(docs): fix manifest of gcp docs (#9559) 2023-09-06 22:21:00 +03:00
Kayla Washburn fd8a86808f chore: add #9555 to .git-blame-ignore-revs (#9557) 2023-09-06 13:10:58 -06:00
Kayla Washburn 988c9af015 chore: format code with semicolons when using prettier (#9555) 2023-09-06 12:59:26 -06:00
Muhammad Atif Ali bef38b8413 docs: update google-cloud-platform docs to use our marketplace image (#9549) 2023-09-06 21:58:14 +03:00
Kayla Washburn 4ed8dd0d6c refactor: improve flag interpretation for install.sh (#9554) 2023-09-06 12:57:28 -06:00
Bruno Quaresma b15bfa41c2 chore(site): move components close to where they are used (#9552)
* Move AppLink

* Move AuditLogRow

* Move UserDropdown

* Move BuildsTable

* Remove CodeBlock

* Move CreateUserForm

* Move DeploymentBanner

* Move ResetPassworDialog

* Move EditRolesButton

* Move EnterpriseSnackbar

* Move GitAuth

* Move LicenseBanner

* Move Logs

* Move MultiTextField

* Move Navbar

* Remove PasswordField

* Move RuntimeErrorState

* Remove Section

* Move SectionAction

* Move ServiceBanner

* Move SettingsAccountForm

* Move LicenseCard

* Move SettingsSecurityForm

* Move SignInForm

* Remove TabPanel and TabSidebar

* Move TemplateStats and TemplateVariableField

* Move TemplateEditor

* Move TerminalLink

* Move SSH Button

* Move many tooltips

* Move UsersTable

* Move VersionsTable

* Move VSCodeDesktopButton

* Remove WarningAlert

* Move Workspace

* Move WorkspaceActions

* Move WorkspaceBuildProgress

* Move WorkspaceDeletedBanner

* Move WorkspaceScheduleForm

* Move WorkspaceSection

* Move WorkspaceStats

* Fix imports

* Flat CreateUserForm

* Flat GitAuth

* Transform WorkspaceSection into ChartSection

* Flat AccountForm

* Flat ConfirmDeleteDialog

* Flat ResetPasswordDialog

* Flat BuildsTable
2023-09-06 18:06:08 +00:00
Kayla Washburn 75139d1d06 refactor(site): update versions table design (#9540) 2023-09-06 08:31:07 -06:00
Ben Potter 1bcc4152af chore: avoid formatting changelogs (#9548) 2023-09-06 14:26:50 +00:00
Eric Paulsen 4ec2fea66b docs: fix psk secret cmd (#9524) 2023-09-06 09:04:44 -04:00
Cian Johnston 7918e65510 feat(coderd): add dbcrypt package (#9522)
- Adds package enterprise/dbcrypt to implement database encryption/decryption
- Adds table dbcrypt_keys and associated queries
- Adds columns oauth_access_token_key_id and oauth_refresh_token_key_id
  to tables git_auth_links and user_links

Co-authored-by: Kyle Carberry <kyle@coder.com>
2023-09-06 12:06:26 +01:00
Spike Curtis 3bd0fd396c feat: add boringcrypto builds for linux (#9543)
This reverts commit da0ef92f77.
2023-09-06 12:48:27 +04:00
Kayla Washburn 2849895832 fix: use $coder_version instead of hardcoded version in release script (#9539) 2023-09-05 17:30:54 -06:00
1771 changed files with 88129 additions and 53622 deletions
+5
View File
@@ -0,0 +1,5 @@
# If you would like `git blame` to ignore commits from this file, run...
# git config blame.ignoreRevsFile .git-blame-ignore-revs
# chore: format code with semicolons when using prettier (#9555)
988c9af0153561397686c119da9d1336d2433fdd
+1 -1
View File
@@ -4,7 +4,7 @@ description: |
inputs:
version:
description: "The Go version to use."
default: "1.20.7"
default: "1.20.10"
runs:
using: "composite"
steps:
+1 -1
View File
@@ -5,6 +5,6 @@ runs:
using: "composite"
steps:
- name: Setup sqlc
uses: sqlc-dev/setup-sqlc@v3
uses: sqlc-dev/setup-sqlc@v4
with:
sqlc-version: "1.20.0"
+1 -1
View File
@@ -20,7 +20,7 @@ runs:
echo "No API key provided, skipping..."
exit 0
fi
npm install -g @datadog/datadog-ci@2.10.0
npm install -g @datadog/datadog-ci@2.21.0
datadog-ci junit upload --service coder ./gotests.xml \
--tags os:${{runner.os}} --tags runner_name:${{runner.name}}
env:
+1
View File
@@ -92,6 +92,7 @@ updates:
- dependency-name: "@types/node"
update-types:
- version-update:semver-major
open-pull-requests-limit: 15
groups:
react:
patterns:
+5 -4
View File
@@ -1,12 +1,10 @@
terraform {
required_providers {
coder = {
source = "coder/coder"
version = "~> 0.11.0"
source = "coder/coder"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.22"
source = "hashicorp/kubernetes"
}
}
}
@@ -237,6 +235,9 @@ resource "kubernetes_deployment" "main" {
"app.kubernetes.io/name" = "coder-workspace"
}
}
strategy {
type = "Recreate"
}
template {
metadata {
+39 -36
View File
@@ -39,7 +39,7 @@ jobs:
offlinedocs: ${{ steps.filter.outputs.offlinedocs }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 1
# For pull requests it's not necessary to checkout the code
@@ -53,7 +53,6 @@ jobs:
docs:
- "docs/**"
- "README.md"
- "examples/templates/**"
- "examples/web-server/**"
- "examples/monitoring/**"
- "examples/lima/**"
@@ -110,7 +109,7 @@ jobs:
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 1
@@ -137,7 +136,7 @@ jobs:
# Check for any typos
- name: Check for typos
uses: crate-ci/typos@v1.16.10
uses: crate-ci/typos@v1.16.21
with:
config: .github/workflows/typos.toml
@@ -165,7 +164,7 @@ jobs:
if: needs.changes.outputs.docs-only == 'false' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 1
@@ -209,7 +208,7 @@ jobs:
timeout-minutes: 7
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 1
@@ -221,10 +220,10 @@ jobs:
with:
# This doesn't need caching. It's super fast anyways!
cache: false
go-version: 1.20.7
go-version: 1.20.10
- name: Install shfmt
run: go install mvdan.cc/sh/v3/cmd/shfmt@v3.5.0
run: go install mvdan.cc/sh/v3/cmd/shfmt@v3.7.0
- name: make fmt
run: |
@@ -235,7 +234,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-xl' || matrix.os == 'windows-2019' && github.repository_owner == 'coder' && 'windows-latest-8-cores' || matrix.os }}
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 }}
needs: changes
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
timeout-minutes: 20
@@ -245,10 +244,10 @@ jobs:
os:
- ubuntu-latest
- macos-latest
- windows-2019
- windows-2022
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 1
@@ -272,22 +271,29 @@ jobs:
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?
if [ "${{ matrix.os }}" == "macos-latest" ]; then
brew install google-chrome
fi
# By default Go will use the number of logical CPUs, which
# is a fine default.
PARALLEL_FLAG=""
# macOS will output "The default interactive shell is now zsh"
# intermittently in CI...
if [ "${{ matrix.os }}" == "macos-latest" ]; then
touch ~/.bash_profile && echo "export BASH_SILENCE_DEPRECATION_WARNING=1" >> ~/.bash_profile
fi
export TS_DEBUG_DISCO=true
gotestsum --junitfile="gotests.xml" --jsonfile="gotests.json" \
--packages="./..." -- $PARALLEL_FLAG -short -failfast $COVERAGE_FLAGS
- name: Print test stats
if: success() || failure()
run: |
# Artifacts are not available after rerunning a job,
# so we need to print the test stats to the log.
go run ./scripts/ci-report/main.go gotests.json | tee gotests_stats.json
- name: Upload test stats to Datadog
timeout-minutes: 1
continue-on-error: true
uses: ./.github/actions/upload-datadog
if: success() || failure()
with:
@@ -317,7 +323,7 @@ jobs:
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 1
@@ -332,14 +338,9 @@ jobs:
export TS_DEBUG_DISCO=true
make test-postgres
- name: Print test stats
if: success() || failure()
run: |
# Artifacts are not available after rerunning a job,
# so we need to print the test stats to the log.
go run ./scripts/ci-report/main.go gotests.json | tee gotests_stats.json
- name: Upload test stats to Datadog
timeout-minutes: 1
continue-on-error: true
uses: ./.github/actions/upload-datadog
if: success() || failure()
with:
@@ -365,7 +366,7 @@ jobs:
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 1
@@ -380,6 +381,8 @@ jobs:
gotestsum --junitfile="gotests.xml" -- -race ./...
- name: Upload test stats to Datadog
timeout-minutes: 1
continue-on-error: true
uses: ./.github/actions/upload-datadog
if: always()
with:
@@ -387,7 +390,7 @@ jobs:
deploy:
name: "deploy"
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-16vcpu-ubuntu-2204' || 'ubuntu-latest' }}
timeout-minutes: 30
needs: changes
if: |
@@ -398,7 +401,7 @@ jobs:
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -486,7 +489,7 @@ jobs:
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 1
@@ -510,13 +513,13 @@ jobs:
flags: unittest-js
test-e2e:
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-16vcpu-ubuntu-2204' || 'ubuntu-latest' }}
needs: changes
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ts == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 1
@@ -582,7 +585,7 @@ jobs:
if: needs.changes.outputs.ts == 'true' || needs.changes.outputs.ci == 'true'
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
# Required by Chromatic for build-over-build history, otherwise we
# only get 1 commit on shallow checkout.
@@ -647,7 +650,7 @@ jobs:
if: needs.changes.outputs.offlinedocs == 'true' || needs.changes.outputs.ci == 'true'
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
# 0 is required here for version.sh to work.
fetch-depth: 0
@@ -727,7 +730,7 @@ jobs:
DOCKER_CLI_EXPERIMENTAL: "enabled"
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -741,7 +744,7 @@ jobs:
uses: ./.github/actions/setup-sqlc
- name: GHCR Login
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
+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.0
uses: contributor-assistant/github-action@v2.3.1
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
+2 -2
View File
@@ -32,10 +32,10 @@ jobs:
if: github.repository_owner == 'coder'
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Docker login
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
+4 -4
View File
@@ -23,7 +23,7 @@ jobs:
runs-on: buildjet-4vcpu-ubuntu-2204
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Get branch name
id: branch-name
@@ -38,7 +38,7 @@ jobs:
echo "tag=${tag}" >> $GITHUB_OUTPUT
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v4
uses: DeterminateSystems/nix-installer-action@v6
- name: Run the Magic Nix Cache
uses: DeterminateSystems/magic-nix-cache-action@v2
@@ -46,7 +46,7 @@ jobs:
- run: nix build .#devEnvImage && ./result | docker load
- name: Login to DockerHub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
@@ -61,7 +61,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Get short commit SHA
id: vars
+2 -2
View File
@@ -17,7 +17,7 @@ jobs:
timeout-minutes: 240
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup Go
uses: ./.github/actions/setup-go
@@ -44,7 +44,7 @@ jobs:
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup Go
uses: ./.github/actions/setup-go
+1 -1
View File
@@ -14,4 +14,4 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Assign author
uses: toshimaru/auto-author-assign@v1.6.2
uses: toshimaru/auto-author-assign@v2.0.1
+13 -12
View File
@@ -37,10 +37,6 @@ permissions:
packages: write
pull-requests: write # needed for commenting on PRs
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
check_pr:
runs-on: ubuntu-latest
@@ -48,7 +44,7 @@ jobs:
PR_OPEN: ${{ steps.check_pr.outputs.pr_open }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Check if PR is open
id: check_pr
@@ -73,12 +69,12 @@ jobs:
CODER_BASE_IMAGE_TAG: ${{ steps.set_tags.outputs.CODER_BASE_IMAGE_TAG }}
CODER_IMAGE_TAG: ${{ steps.set_tags.outputs.CODER_IMAGE_TAG }}
NEW: ${{ steps.check_deployment.outputs.NEW }}
BUILD: ${{ steps.build_conditionals.outputs.first_or_force_build || steps.build_conditionals.outputs.automatic_rebuild }}
BUILD: ${{ steps.build_conditionals.outputs.first_or_force_build == 'true' || steps.build_conditionals.outputs.automatic_rebuild == 'true' }}
runs-on: "ubuntu-latest"
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -110,6 +106,7 @@ jobs:
set -euo pipefail
mkdir -p ~/.kube
echo "${{ secrets.PR_DEPLOYMENTS_KUBECONFIG }}" > ~/.kube/config
chmod 644 ~/.kube/config
export KUBECONFIG=~/.kube/config
- name: Check if the helm deployment already exists
@@ -165,7 +162,7 @@ jobs:
echo "automatic_rebuild=${{ steps.check_deployment.outputs.NEW == 'false' && steps.filter.outputs.all_count > steps.filter.outputs.ignored_count }}" >> $GITHUB_OUTPUT
comment-pr:
needs: [check_pr, get_info]
needs: get_info
if: needs.get_info.outputs.BUILD == 'true' || github.event.inputs.deploy == 'true'
runs-on: "ubuntu-latest"
steps:
@@ -197,12 +194,16 @@ jobs:
# 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' }}
# 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 }}
cancel-in-progress: true
env:
DOCKER_CLI_EXPERIMENTAL: "enabled"
CODER_IMAGE_TAG: ${{ needs.get_info.outputs.CODER_IMAGE_TAG }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -216,7 +217,7 @@ jobs:
uses: ./.github/actions/setup-sqlc
- name: GHCR Login
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -257,6 +258,7 @@ jobs:
set -euo pipefail
mkdir -p ~/.kube
echo "${{ secrets.PR_DEPLOYMENTS_KUBECONFIG }}" > ~/.kube/config
chmod 644 ~/.kube/config
export KUBECONFIG=~/.kube/config
- name: Check if image exists
@@ -296,7 +298,7 @@ jobs:
kubectl create namespace "pr${{ env.PR_NUMBER }}"
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Check and Create Certificate
if: needs.get_info.outputs.NEW == 'true' || github.event.inputs.deploy == 'true'
@@ -417,7 +419,6 @@ jobs:
# Create template
cd ./.github/pr-deployments/template
terraform init
coder templates create -y --variable namespace=pr${{ env.PR_NUMBER }} kubernetes
# Create workspace
+5 -5
View File
@@ -40,7 +40,7 @@ jobs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -85,7 +85,7 @@ jobs:
cat "$CODER_RELEASE_NOTES_FILE"
- name: Docker Login
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -357,7 +357,7 @@ jobs:
cd "$temp_dir"
# Download checksums
checksums_url="$(gh release view --repo coder/coder v2.1.4 --json assets \
checksums_url="$(gh release view --repo coder/coder "v$coder_version" --json assets \
| jq -r ".assets | map(.url) | .[]" \
| grep -e ".checksums.txt\$")"
wget "$checksums_url" -O checksums.txt
@@ -409,7 +409,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -489,7 +489,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
+3 -3
View File
@@ -26,7 +26,7 @@ jobs:
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
@@ -59,7 +59,7 @@ jobs:
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -122,7 +122,7 @@ jobs:
image_name: ${{ steps.build.outputs.image }}
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@fbd16365eb88e12433951383f5e99bd901fc618f
uses: aquasecurity/trivy-action@b77b85c0254bba6789e787844f0585cde1e56320
with:
image-ref: ${{ steps.build.outputs.image }}
format: sarif
+5 -6
View File
@@ -34,7 +34,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Run delete-old-branches-action
uses: beatlabs/delete-old-branches-action@v0.0.10
with:
@@ -52,8 +52,8 @@ jobs:
with:
token: ${{ github.token }}
repository: ${{ github.repository }}
retain_days: 1
keep_minimum_runs: 1
retain_days: 30
keep_minimum_runs: 30
delete_workflow_pattern: pr-cleanup.yaml
- name: Delete PR Deploy workflow skipped runs
@@ -61,7 +61,6 @@ jobs:
with:
token: ${{ github.token }}
repository: ${{ github.repository }}
retain_days: 0
keep_minimum_runs: 0
delete_run_by_conclusion_pattern: skipped
retain_days: 30
keep_minimum_runs: 30
delete_workflow_pattern: pr-deploy.yaml
+9 -4
View File
@@ -30,15 +30,14 @@ site/e2e/states/*.json
site/e2e/.auth.json
site/playwright-report/*
site/.swc
site/dist/
# Make target for updating golden files (any dir).
.gen-golden
# Build
/build/
/dist/
site/out/
build/
dist/
out/
# Bundle analysis
site/stats/
@@ -64,3 +63,9 @@ scaletest/terraform/secrets.tfvars
# Nix
result
# Data dumps from unit tests
**/*.test.sql
# Filebrowser.db
**/filebrowser.db
+3
View File
@@ -10,6 +10,9 @@ linters-settings:
include:
# Gradually extend to cover more of the codebase.
- 'httpmw\.\w+'
# We want to enforce all values are specified when inserting or updating
# a database row. Ref: #9936
- 'github.com/coder/coder/v2/coderd/database\.[^G][^e][^t]\w+Params'
gocognit:
min-complexity: 300
+9 -4
View File
@@ -33,15 +33,14 @@ site/e2e/states/*.json
site/e2e/.auth.json
site/playwright-report/*
site/.swc
site/dist/
# Make target for updating golden files (any dir).
.gen-golden
# Build
/build/
/dist/
site/out/
build/
dist/
out/
# Bundle analysis
site/stats/
@@ -67,6 +66,12 @@ scaletest/terraform/secrets.tfvars
# Nix
result
# Data dumps from unit tests
**/*.test.sql
# Filebrowser.db
**/filebrowser.db
# .prettierignore.include:
# Helm templates contain variables that are invalid YAML and can't be formatted
# by Prettier.
+2 -2
View File
@@ -1,9 +1,8 @@
# This config file is used in conjunction with `.editorconfig` to specify
# formatting for prettier-supported files. See `.editorconfig` and
# `site/.editorconfig`for whitespace formatting options.
# `site/.editorconfig` for whitespace formatting options.
printWidth: 80
proseWrap: always
semi: false
trailingComma: all
useTabs: false
tabWidth: 2
@@ -12,6 +11,7 @@ overrides:
- README.md
- docs/api/**/*.md
- docs/cli/**/*.md
- docs/changelogs/*.md
- .github/**/*.{yaml,yml,toml}
- scripts/**/*.{yaml,yml,toml}
options:
+7 -5
View File
@@ -20,7 +20,7 @@
"codersdk",
"cronstrue",
"databasefake",
"dbfake",
"dbmem",
"dbgen",
"dbtype",
"DERP",
@@ -39,6 +39,7 @@
"enterprisemeta",
"errgroup",
"eventsourcemock",
"externalauth",
"Failf",
"fatih",
"Formik",
@@ -186,9 +187,6 @@
]
},
"eslint.workingDirectories": ["./site"],
"files.exclude": {
"**/node_modules": true
},
"search.exclude": {
"**.pb.go": true,
"**/*.gen.json": true,
@@ -198,7 +196,11 @@
"docs/api/*.md": true,
"docs/templates/*.md": true,
"LICENSE": true,
"scripts/metricsdocgen/metrics": true
"scripts/metricsdocgen/metrics": true,
"site/out/**": true,
"site/storybook-static/**": true,
"**.map": true,
"pnpm-lock.yaml": true
},
// Ensure files always have a newline.
"files.insertFinalNewline": true,
+41 -12
View File
@@ -107,9 +107,9 @@ endif
clean:
rm -rf build site/out
mkdir -p build site/out/bin
git restore site/out
rm -rf build/ site/build/ site/out/
mkdir -p build/ site/out/bin/
git restore site/out/
.PHONY: clean
build-slim: $(CODER_SLIM_BINARIES)
@@ -419,7 +419,6 @@ lint: lint/shellcheck lint/go lint/ts lint/helm lint/site-icons
lint/site-icons:
./scripts/check_site_icons.sh
.PHONY: lint/site-icons
lint/ts:
@@ -449,13 +448,15 @@ lint/helm:
DB_GEN_FILES := \
coderd/database/querier.go \
coderd/database/unique_constraint.go \
coderd/database/dbfake/dbfake.go \
coderd/database/dbmem/dbmem.go \
coderd/database/dbmetrics/dbmetrics.go \
coderd/database/dbauthz/dbauthz.go \
coderd/database/dbmock/dbmock.go
# all gen targets should be added here and to gen/mark-fresh
gen: \
tailnet/proto/tailnet.pb.go \
agent/proto/agent.pb.go \
provisionersdk/proto/provisioner.pb.go \
provisionerd/proto/provisionerd.pb.go \
coderd/database/dump.sql \
@@ -472,6 +473,7 @@ gen: \
site/.prettierignore \
site/.eslintignore \
site/e2e/provisionerGenerated.ts \
site/src/theme/icons.json \
examples/examples.gen.json
.PHONY: gen
@@ -479,6 +481,8 @@ gen: \
# used during releases so we don't run generation scripts.
gen/mark-fresh:
files="\
tailnet/proto/tailnet.pb.go \
agent/proto/agent.pb.go \
provisionersdk/proto/provisioner.pb.go \
provisionerd/proto/provisionerd.pb.go \
coderd/database/dump.sql \
@@ -495,6 +499,7 @@ gen/mark-fresh:
site/.prettierignore \
site/.eslintignore \
site/e2e/provisionerGenerated.ts \
site/src/theme/icons.json \
examples/examples.gen.json \
"
for file in $$files; do
@@ -515,12 +520,30 @@ coderd/database/dump.sql: coderd/database/gen/dump/main.go $(wildcard coderd/dat
go run ./coderd/database/gen/dump/main.go
# Generates Go code for querying the database.
# coderd/database/queries.sql.go
# coderd/database/models.go
coderd/database/querier.go: coderd/database/sqlc.yaml coderd/database/dump.sql $(wildcard coderd/database/queries/*.sql)
./coderd/database/generate.sh
coderd/database/dbmock/dbmock.go: coderd/database/db.go coderd/database/querier.go
go generate ./coderd/database/dbmock/
tailnet/proto/tailnet.pb.go: tailnet/proto/tailnet.proto
protoc \
--go_out=. \
--go_opt=paths=source_relative \
--go-drpc_out=. \
--go-drpc_opt=paths=source_relative \
./tailnet/proto/tailnet.proto
agent/proto/agent.pb.go: agent/proto/agent.proto
protoc \
--go_out=. \
--go_opt=paths=source_relative \
--go-drpc_out=. \
--go-drpc_opt=paths=source_relative \
./agent/proto/agent.proto
provisionersdk/proto/provisioner.pb.go: provisionersdk/proto/provisioner.proto
protoc \
--go_out=. \
@@ -537,16 +560,18 @@ provisionerd/proto/provisionerd.pb.go: provisionerd/proto/provisionerd.proto
--go-drpc_opt=paths=source_relative \
./provisionerd/proto/provisionerd.proto
site/src/api/typesGenerated.ts: scripts/apitypings/main.go $(shell find ./codersdk $(FIND_EXCLUSIONS) -type f -name '*.go')
go run scripts/apitypings/main.go > site/src/api/typesGenerated.ts
cd site
pnpm run format:types ./src/api/typesGenerated.ts
site/src/api/typesGenerated.ts: $(wildcard scripts/apitypings/*) $(shell find ./codersdk $(FIND_EXCLUSIONS) -type f -name '*.go')
go run ./scripts/apitypings/ > $@
pnpm run format:write:only "$@"
site/e2e/provisionerGenerated.ts:
site/e2e/provisionerGenerated.ts: provisionerd/proto/provisionerd.pb.go provisionersdk/proto/provisioner.pb.go
cd site
../scripts/pnpm_install.sh
pnpm run gen:provisioner
site/src/theme/icons.json: $(wildcard scripts/gensite/*) $(wildcard site/static/icon/*)
go run ./scripts/gensite/ -icons "$@"
pnpm run format:write:only "$@"
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
@@ -559,7 +584,7 @@ docs/admin/prometheus.md: scripts/metricsdocgen/main.go scripts/metricsdocgen/me
pnpm run format:write:only ./docs/admin/prometheus.md
docs/cli.md: scripts/clidocgen/main.go examples/examples.gen.json $(GO_SRC_FILES)
BASE_PATH="." go run ./scripts/clidocgen
CI=true BASE_PATH="." go run ./scripts/clidocgen
pnpm run format:write:only ./docs/cli.md ./docs/cli/*.md ./docs/manifest.json
docs/admin/audit-logs.md: scripts/auditdocgen/main.go enterprise/audit/table.go coderd/rbac/object_gen.go
@@ -570,7 +595,7 @@ coderd/apidoc/swagger.json: $(shell find ./scripts/apidocgen $(FIND_EXCLUSIONS)
./scripts/apidocgen/generate.sh
pnpm run format:write:only ./docs/api ./docs/manifest.json ./coderd/apidoc/swagger.json
update-golden-files: cli/testdata/.gen-golden helm/coder/tests/testdata/.gen-golden helm/provisioner/tests/testdata/.gen-golden scripts/ci-report/testdata/.gen-golden enterprise/cli/testdata/.gen-golden coderd/.gen-golden
update-golden-files: cli/testdata/.gen-golden helm/coder/tests/testdata/.gen-golden helm/provisioner/tests/testdata/.gen-golden scripts/ci-report/testdata/.gen-golden enterprise/cli/testdata/.gen-golden coderd/.gen-golden provisioner/terraform/testdata/.gen-golden
.PHONY: update-golden-files
cli/testdata/.gen-golden: $(wildcard cli/testdata/*.golden) $(wildcard cli/*.tpl) $(GO_SRC_FILES) $(wildcard cli/*_test.go)
@@ -593,6 +618,10 @@ coderd/.gen-golden: $(wildcard coderd/testdata/*/*.golden) $(GO_SRC_FILES) $(wil
go test ./coderd -run="Test.*Golden$$" -update
touch "$@"
provisioner/terraform/testdata/.gen-golden: $(wildcard provisioner/terraform/testdata/*/*.golden) $(GO_SRC_FILES) $(wildcard provisioner/terraform/*_test.go)
go test ./provisioner/terraform -run="Test.*Golden$$" -update
touch "$@"
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 "$@"
+1 -1
View File
@@ -70,7 +70,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. You can modify the installation process by including flags. Run the install script with `--help` for reference.
> See [install](docs/install) for additional methods.
> See [install](https://coder.com/docs/v2/latest/install) for additional methods.
Once installed, you can start a production deployment<sup>1</sup> with a single command:
+384 -274
View File
@@ -12,9 +12,10 @@ import (
"net/http"
"net/netip"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"runtime/debug"
"sort"
"strconv"
"strings"
@@ -34,6 +35,8 @@ import (
"tailscale.com/types/netlogtype"
"cdr.dev/slog"
"github.com/coder/coder/v2/agent/agentproc"
"github.com/coder/coder/v2/agent/agentscripts"
"github.com/coder/coder/v2/agent/agentssh"
"github.com/coder/coder/v2/agent/reconnectingpty"
"github.com/coder/coder/v2/buildinfo"
@@ -51,6 +54,10 @@ const (
ProtocolDial = "dial"
)
// EnvProcPrioMgmt determines whether we attempt to manage
// process CPU and OOM Killer priority.
const EnvProcPrioMgmt = "CODER_PROC_PRIO_MGMT"
type Options struct {
Filesystem afero.Fs
LogDir string
@@ -68,6 +75,11 @@ type Options struct {
PrometheusRegistry *prometheus.Registry
ReportMetadataInterval time.Duration
ServiceBannerRefreshInterval time.Duration
Syscaller agentproc.Syscaller
// ModifiedProcesses is used for testing process priority management.
ModifiedProcesses chan []*agentproc.Process
// ProcessManagementTick is used for testing process priority management.
ProcessManagementTick <-chan time.Time
}
type Client interface {
@@ -78,7 +90,7 @@ type Client interface {
PostLifecycle(ctx context.Context, state agentsdk.PostLifecycleRequest) error
PostAppHealth(ctx context.Context, req agentsdk.PostAppHealthsRequest) error
PostStartup(ctx context.Context, req agentsdk.PostStartupRequest) error
PostMetadata(ctx context.Context, key string, req agentsdk.PostMetadataRequest) error
PostMetadata(ctx context.Context, req agentsdk.PostMetadataRequest) error
PatchLogs(ctx context.Context, req agentsdk.PatchLogs) error
GetServiceBanner(ctx context.Context) (codersdk.ServiceBannerConfig, error)
}
@@ -120,6 +132,10 @@ func New(options Options) Agent {
prometheusRegistry = prometheus.NewRegistry()
}
if options.Syscaller == nil {
options.Syscaller = agentproc.NewSyscaller()
}
ctx, cancelFunc := context.WithCancel(context.Background())
a := &agent{
tailnetListenPort: options.TailnetListenPort,
@@ -143,6 +159,9 @@ func New(options Options) Agent {
sshMaxTimeout: options.SSHMaxTimeout,
subsystems: options.Subsystems,
addresses: options.Addresses,
syscaller: options.Syscaller,
modifiedProcs: options.ModifiedProcesses,
processManagementTick: options.ProcessManagementTick,
prometheusRegistry: prometheusRegistry,
metrics: newAgentMetrics(prometheusRegistry),
@@ -177,6 +196,7 @@ type agent struct {
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]
@@ -197,6 +217,12 @@ type agent struct {
prometheusRegistry *prometheus.Registry
metrics *agentMetrics
syscaller agentproc.Syscaller
// modifiedProcs is used for testing process priority management.
modifiedProcs chan []*agentproc.Process
// processManagementTick is used for testing process priority management.
processManagementTick <-chan time.Time
}
func (a *agent) TailnetConn() *tailnet.Conn {
@@ -213,7 +239,13 @@ func (a *agent) init(ctx context.Context) {
sshSrv.Manifest = &a.manifest
sshSrv.ServiceBanner = &a.serviceBanner
a.sshServer = sshSrv
a.scriptRunner = agentscripts.New(agentscripts.Options{
LogDir: a.logDir,
Logger: a.logger,
SSHServer: sshSrv,
Filesystem: a.filesystem,
PatchLogs: a.client.PatchLogs,
})
go a.runLoop(ctx)
}
@@ -225,6 +257,7 @@ func (a *agent) runLoop(ctx context.Context) {
go a.reportLifecycleLoop(ctx)
go a.reportMetadataLoop(ctx)
go a.fetchServiceBannerLoop(ctx)
go a.manageProcessPriorityLoop(ctx)
for retrier := retry.New(100*time.Millisecond, 10*time.Second); retrier.Wait(ctx); {
a.logger.Info(ctx, "connecting to coderd")
@@ -329,140 +362,210 @@ func (t *trySingleflight) Do(key string, fn func()) {
}
func (a *agent) reportMetadataLoop(ctx context.Context) {
const metadataLimit = 128
tickerDone := make(chan struct{})
collectDone := make(chan struct{})
ctx, cancel := context.WithCancel(ctx)
defer func() {
cancel()
<-collectDone
<-tickerDone
}()
var (
baseTicker = time.NewTicker(a.reportMetadataInterval)
lastCollectedAtMu sync.RWMutex
lastCollectedAts = make(map[string]time.Time)
metadataResults = make(chan metadataResultAndKey, metadataLimit)
logger = a.logger.Named("metadata")
logger = a.logger.Named("metadata")
report = make(chan struct{}, 1)
collect = make(chan struct{}, 1)
metadataResults = make(chan metadataResultAndKey, 1)
)
defer baseTicker.Stop()
// We use a custom singleflight that immediately returns if there is already
// a goroutine running for a given key. This is to prevent a build-up of
// goroutines waiting on Do when the script takes many multiples of
// baseInterval to run.
flight := trySingleflight{m: map[string]struct{}{}}
postMetadata := func(mr metadataResultAndKey) {
err := a.client.PostMetadata(ctx, mr.key, *mr.result)
if err != nil {
a.logger.Error(ctx, "agent failed to report metadata", slog.Error(err))
// Set up collect and report as a single ticker with two channels,
// this is to allow collection and reporting to be triggered
// independently of each other.
go func() {
t := time.NewTicker(a.reportMetadataInterval)
defer func() {
t.Stop()
close(report)
close(collect)
close(tickerDone)
}()
wake := func(c chan<- struct{}) {
select {
case c <- struct{}{}:
default:
}
}
}
wake(collect) // Start immediately.
for {
select {
case <-ctx.Done():
return
case <-t.C:
wake(report)
wake(collect)
}
}
}()
go func() {
defer close(collectDone)
var (
// We use a custom singleflight that immediately returns if there is already
// a goroutine running for a given key. This is to prevent a build-up of
// goroutines waiting on Do when the script takes many multiples of
// baseInterval to run.
flight = trySingleflight{m: map[string]struct{}{}}
lastCollectedAtMu sync.RWMutex
lastCollectedAts = make(map[string]time.Time)
)
for {
select {
case <-ctx.Done():
return
case <-collect:
}
manifest := a.manifest.Load()
if manifest == nil {
continue
}
// If the manifest changes (e.g. on agent reconnect) we need to
// purge old cache values to prevent lastCollectedAt from growing
// boundlessly.
lastCollectedAtMu.Lock()
for key := range lastCollectedAts {
if slices.IndexFunc(manifest.Metadata, func(md codersdk.WorkspaceAgentMetadataDescription) bool {
return md.Key == key
}) < 0 {
logger.Debug(ctx, "deleting lastCollected key, missing from manifest",
slog.F("key", key),
)
delete(lastCollectedAts, key)
}
}
lastCollectedAtMu.Unlock()
// Spawn a goroutine for each metadata collection, and use a
// channel to synchronize the results and avoid both messy
// mutex logic and overloading the API.
for _, md := range manifest.Metadata {
md := md
// We send the result to the channel in the goroutine to avoid
// sending the same result multiple times. So, we don't care about
// the return values.
go flight.Do(md.Key, func() {
ctx := slog.With(ctx, slog.F("key", md.Key))
lastCollectedAtMu.RLock()
collectedAt, ok := lastCollectedAts[md.Key]
lastCollectedAtMu.RUnlock()
if ok {
// If the interval is zero, we assume the user just wants
// a single collection at startup, not a spinning loop.
if md.Interval == 0 {
return
}
intervalUnit := time.Second
// reportMetadataInterval is only less than a second in tests,
// so adjust the interval unit for them.
if a.reportMetadataInterval < time.Second {
intervalUnit = 100 * time.Millisecond
}
// The last collected value isn't quite stale yet, so we skip it.
if collectedAt.Add(time.Duration(md.Interval) * intervalUnit).After(time.Now()) {
return
}
}
timeout := md.Timeout
if timeout == 0 {
if md.Interval != 0 {
timeout = md.Interval
} else if interval := int64(a.reportMetadataInterval.Seconds()); interval != 0 {
// Fallback to the report interval
timeout = interval * 3
} else {
// If the interval is still 0 (possible if the interval
// is less than a second), default to 5. This was
// randomly picked.
timeout = 5
}
}
ctxTimeout := time.Duration(timeout) * time.Second
ctx, cancel := context.WithTimeout(ctx, ctxTimeout)
defer cancel()
now := time.Now()
select {
case <-ctx.Done():
logger.Warn(ctx, "metadata collection timed out", slog.F("timeout", ctxTimeout))
case metadataResults <- metadataResultAndKey{
key: md.Key,
result: a.collectMetadata(ctx, md, now),
}:
lastCollectedAtMu.Lock()
lastCollectedAts[md.Key] = now
lastCollectedAtMu.Unlock()
}
})
}
}
}()
// Gather metadata updates and report them once every interval. If a
// previous report is in flight, wait for it to complete before
// sending a new one. If the network conditions are bad, we won't
// benefit from canceling the previous send and starting a new one.
var (
updatedMetadata = make(map[string]*codersdk.WorkspaceAgentMetadataResult)
reportTimeout = 30 * time.Second
reportSemaphore = make(chan struct{}, 1)
)
reportSemaphore <- struct{}{}
for {
select {
case <-ctx.Done():
return
case mr := <-metadataResults:
postMetadata(mr)
// This can overwrite unsent values, but that's fine because
// we're only interested about up-to-date values.
updatedMetadata[mr.key] = mr.result
continue
case <-baseTicker.C:
}
if len(metadataResults) > 0 {
// The inner collection loop expects the channel is empty before spinning up
// all the collection goroutines.
logger.Debug(ctx, "metadata collection backpressured",
slog.F("queue_len", len(metadataResults)),
)
continue
}
manifest := a.manifest.Load()
if manifest == nil {
continue
}
if len(manifest.Metadata) > metadataLimit {
logger.Error(
ctx, "metadata limit exceeded",
slog.F("limit", metadataLimit), slog.F("got", len(manifest.Metadata)),
)
continue
}
// If the manifest changes (e.g. on agent reconnect) we need to
// purge old cache values to prevent lastCollectedAt from growing
// boundlessly.
lastCollectedAtMu.Lock()
for key := range lastCollectedAts {
if slices.IndexFunc(manifest.Metadata, func(md codersdk.WorkspaceAgentMetadataDescription) bool {
return md.Key == key
}) < 0 {
logger.Debug(ctx, "deleting lastCollected key, missing from manifest",
slog.F("key", key),
)
delete(lastCollectedAts, key)
}
}
lastCollectedAtMu.Unlock()
// Spawn a goroutine for each metadata collection, and use a
// channel to synchronize the results and avoid both messy
// mutex logic and overloading the API.
for _, md := range manifest.Metadata {
md := md
// We send the result to the channel in the goroutine to avoid
// sending the same result multiple times. So, we don't care about
// the return values.
go flight.Do(md.Key, func() {
ctx := slog.With(ctx, slog.F("key", md.Key))
lastCollectedAtMu.RLock()
collectedAt, ok := lastCollectedAts[md.Key]
lastCollectedAtMu.RUnlock()
if ok {
// If the interval is zero, we assume the user just wants
// a single collection at startup, not a spinning loop.
if md.Interval == 0 {
return
}
intervalUnit := time.Second
// reportMetadataInterval is only less than a second in tests,
// so adjust the interval unit for them.
if a.reportMetadataInterval < time.Second {
intervalUnit = 100 * time.Millisecond
}
// The last collected value isn't quite stale yet, so we skip it.
if collectedAt.Add(time.Duration(md.Interval) * intervalUnit).After(time.Now()) {
return
}
}
timeout := md.Timeout
if timeout == 0 {
if md.Interval != 0 {
timeout = md.Interval
} else if interval := int64(a.reportMetadataInterval.Seconds()); interval != 0 {
// Fallback to the report interval
timeout = interval * 3
} else {
// If the interval is still 0 (possible if the interval
// is less than a second), default to 5. This was
// randomly picked.
timeout = 5
}
}
ctxTimeout := time.Duration(timeout) * time.Second
ctx, cancel := context.WithTimeout(ctx, ctxTimeout)
defer cancel()
now := time.Now()
case <-report:
if len(updatedMetadata) > 0 {
select {
case <-ctx.Done():
logger.Warn(ctx, "metadata collection timed out", slog.F("timeout", ctxTimeout))
case metadataResults <- metadataResultAndKey{
key: md.Key,
result: a.collectMetadata(ctx, md, now),
}:
lastCollectedAtMu.Lock()
lastCollectedAts[md.Key] = now
lastCollectedAtMu.Unlock()
case <-reportSemaphore:
default:
// If there's already a report in flight, don't send
// another one, wait for next tick instead.
continue
}
})
metadata := make([]agentsdk.Metadata, 0, len(updatedMetadata))
for key, result := range updatedMetadata {
metadata = append(metadata, agentsdk.Metadata{
Key: key,
WorkspaceAgentMetadataResult: *result,
})
delete(updatedMetadata, key)
}
go func() {
ctx, cancel := context.WithTimeout(ctx, reportTimeout)
defer func() {
cancel()
reportSemaphore <- struct{}{}
}()
err := a.client.PostMetadata(ctx, agentsdk.PostMetadataRequest{Metadata: metadata})
if err != nil {
a.logger.Error(ctx, "agent failed to report metadata", slog.Error(err))
}
}()
}
}
}
}
@@ -631,41 +734,29 @@ func (a *agent) run(ctx context.Context) error {
}
}
lifecycleState := codersdk.WorkspaceAgentLifecycleReady
scriptDone := make(chan error, 1)
err = a.scriptRunner.Init(manifest.Scripts)
if err != nil {
return xerrors.Errorf("init script runner: %w", err)
}
err = a.trackConnGoroutine(func() {
defer close(scriptDone)
scriptDone <- a.runStartupScript(ctx, manifest.StartupScript)
err := a.scriptRunner.Execute(ctx, func(script codersdk.WorkspaceAgentScript) bool {
return script.RunOnStart
})
if err != nil {
a.logger.Warn(ctx, "startup script(s) failed", slog.Error(err))
if errors.Is(err, agentscripts.ErrTimeout) {
a.setLifecycle(ctx, codersdk.WorkspaceAgentLifecycleStartTimeout)
} else {
a.setLifecycle(ctx, codersdk.WorkspaceAgentLifecycleStartError)
}
} else {
a.setLifecycle(ctx, codersdk.WorkspaceAgentLifecycleReady)
}
a.scriptRunner.StartCron()
})
if err != nil {
return xerrors.Errorf("track startup script: %w", err)
return xerrors.Errorf("track conn goroutine: %w", err)
}
go func() {
var timeout <-chan time.Time
// If timeout is zero, an older version of the coder
// provider was used. Otherwise a timeout is always > 0.
if manifest.StartupScriptTimeout > 0 {
t := time.NewTimer(manifest.StartupScriptTimeout)
defer t.Stop()
timeout = t.C
}
var err error
select {
case err = <-scriptDone:
case <-timeout:
a.logger.Warn(ctx, "script timed out", slog.F("lifecycle", "startup"), slog.F("timeout", manifest.StartupScriptTimeout))
a.setLifecycle(ctx, codersdk.WorkspaceAgentLifecycleStartTimeout)
err = <-scriptDone // The script can still complete after a timeout.
}
if err != nil {
if errors.Is(err, context.Canceled) {
return
}
lifecycleState = codersdk.WorkspaceAgentLifecycleStartError
}
a.setLifecycle(ctx, lifecycleState)
}()
}
// This automatically closes when the context ends!
@@ -812,7 +903,10 @@ func (a *agent) createTailnet(ctx context.Context, agentID uuid.UUID, derpMap *t
}
break
}
logger.Debug(ctx, "accepted conn", slog.F("remote", conn.RemoteAddr().String()))
clog := logger.With(
slog.F("remote", conn.RemoteAddr().String()),
slog.F("local", conn.LocalAddr().String()))
clog.Info(ctx, "accepted conn")
wg.Add(1)
closed := make(chan struct{})
go func() {
@@ -844,7 +938,7 @@ func (a *agent) createTailnet(ctx context.Context, agentID uuid.UUID, derpMap *t
logger.Warn(ctx, "failed to unmarshal init", slog.F("raw", data))
return
}
_ = a.handleReconnectingPTY(ctx, logger, msg, conn)
_ = a.handleReconnectingPTY(ctx, clog, msg, conn)
}()
}
wg.Wait()
@@ -871,6 +965,10 @@ func (a *agent) createTailnet(ctx context.Context, agentID uuid.UUID, derpMap *t
}
break
}
clog := a.logger.Named("speedtest").With(
slog.F("remote", conn.RemoteAddr().String()),
slog.F("local", conn.LocalAddr().String()))
clog.Info(ctx, "accepted conn")
wg.Add(1)
closed := make(chan struct{})
go func() {
@@ -883,7 +981,12 @@ func (a *agent) createTailnet(ctx context.Context, agentID uuid.UUID, derpMap *t
}()
go func() {
defer close(closed)
_ = speedtest.ServeConn(conn)
sErr := speedtest.ServeConn(conn)
if sErr != nil {
clog.Error(ctx, "test ended with error", slog.Error(sErr))
return
}
clog.Info(ctx, "test ended")
}()
}
wg.Wait()
@@ -980,93 +1083,6 @@ func (a *agent) runDERPMapSubscriber(ctx context.Context, network *tailnet.Conn)
}
}
func (a *agent) runStartupScript(ctx context.Context, script string) error {
return a.runScript(ctx, "startup", script)
}
func (a *agent) runShutdownScript(ctx context.Context, script string) error {
return a.runScript(ctx, "shutdown", script)
}
func (a *agent) runScript(ctx context.Context, lifecycle, script string) (err error) {
if script == "" {
return nil
}
logger := a.logger.With(slog.F("lifecycle", lifecycle))
logger.Info(ctx, fmt.Sprintf("running %s script", lifecycle), slog.F("script", script))
fileWriter, err := a.filesystem.OpenFile(filepath.Join(a.logDir, fmt.Sprintf("coder-%s-script.log", lifecycle)), os.O_CREATE|os.O_RDWR, 0o600)
if err != nil {
return xerrors.Errorf("open %s script log file: %w", lifecycle, err)
}
defer func() {
err := fileWriter.Close()
if err != nil {
logger.Warn(ctx, fmt.Sprintf("close %s script log file", lifecycle), slog.Error(err))
}
}()
cmdPty, err := a.sshServer.CreateCommand(ctx, script, nil)
if err != nil {
return xerrors.Errorf("%s script: create command: %w", lifecycle, err)
}
cmd := cmdPty.AsExec()
var stdout, stderr io.Writer = fileWriter, fileWriter
if lifecycle == "startup" {
send, flushAndClose := agentsdk.LogsSender(a.client.PatchLogs, logger)
// If ctx is canceled here (or in a writer below), we may be
// discarding logs, but that's okay because we're shutting down
// anyway. We could consider creating a new context here if we
// want better control over flush during shutdown.
defer func() {
if err := flushAndClose(ctx); err != nil {
logger.Warn(ctx, "flush startup logs failed", slog.Error(err))
}
}()
infoW := agentsdk.StartupLogsWriter(ctx, send, codersdk.WorkspaceAgentLogSourceStartupScript, codersdk.LogLevelInfo)
defer infoW.Close()
errW := agentsdk.StartupLogsWriter(ctx, send, codersdk.WorkspaceAgentLogSourceStartupScript, codersdk.LogLevelError)
defer errW.Close()
stdout = io.MultiWriter(fileWriter, infoW)
stderr = io.MultiWriter(fileWriter, errW)
}
cmd.Stdout = stdout
cmd.Stderr = stderr
start := time.Now()
defer func() {
end := time.Now()
execTime := end.Sub(start)
exitCode := 0
if err != nil {
exitCode = 255 // Unknown status.
var exitError *exec.ExitError
if xerrors.As(err, &exitError) {
exitCode = exitError.ExitCode()
}
logger.Warn(ctx, fmt.Sprintf("%s script failed", lifecycle), slog.F("execution_time", execTime), slog.F("exit_code", exitCode), slog.Error(err))
} else {
logger.Info(ctx, fmt.Sprintf("%s script completed", lifecycle), slog.F("execution_time", execTime), slog.F("exit_code", exitCode))
}
}()
err = cmd.Run()
if err != nil {
// cmd.Run does not return a context canceled error, it returns "signal: killed".
if ctx.Err() != nil {
return ctx.Err()
}
return xerrors.Errorf("%s script: run: %w", lifecycle, err)
}
return nil
}
func (a *agent) handleReconnectingPTY(ctx context.Context, logger slog.Logger, msg codersdk.WorkspaceAgentReconnectingPTYInit, conn net.Conn) (retErr error) {
defer conn.Close()
a.metrics.connectionsTotal.Add(1)
@@ -1087,12 +1103,12 @@ func (a *agent) handleReconnectingPTY(ctx context.Context, logger slog.Logger, m
// If the agent is closed, we don't want to
// log this as an error since it's expected.
if closed {
connLogger.Debug(ctx, "reconnecting pty failed with attach error (agent closed)", slog.Error(err))
connLogger.Info(ctx, "reconnecting pty failed with attach error (agent closed)", slog.Error(err))
} else {
connLogger.Error(ctx, "reconnecting pty failed with attach error", slog.Error(err))
}
}
connLogger.Debug(ctx, "reconnecting pty connection closed")
connLogger.Info(ctx, "reconnecting pty connection closed")
}()
var rpty reconnectingpty.ReconnectingPTY
@@ -1253,6 +1269,115 @@ func (a *agent) startReportingConnectionStats(ctx context.Context) {
}
}
var prioritizedProcs = []string{"coder agent"}
func (a *agent) manageProcessPriorityLoop(ctx context.Context) {
defer func() {
if r := recover(); r != nil {
a.logger.Critical(ctx, "recovered from panic",
slog.F("panic", r),
slog.F("stack", string(debug.Stack())),
)
}
}()
if val := a.envVars[EnvProcPrioMgmt]; val == "" || runtime.GOOS != "linux" {
a.logger.Debug(ctx, "process priority not enabled, agent will not manage process niceness/oom_score_adj ",
slog.F("env_var", EnvProcPrioMgmt),
slog.F("value", val),
slog.F("goos", runtime.GOOS),
)
return
}
if a.processManagementTick == nil {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
a.processManagementTick = ticker.C
}
for {
procs, err := a.manageProcessPriority(ctx)
if err != nil {
a.logger.Error(ctx, "manage process priority",
slog.Error(err),
)
}
if a.modifiedProcs != nil {
a.modifiedProcs <- procs
}
select {
case <-a.processManagementTick:
case <-ctx.Done():
return
}
}
}
func (a *agent) manageProcessPriority(ctx context.Context) ([]*agentproc.Process, error) {
const (
niceness = 10
)
procs, err := agentproc.List(a.filesystem, a.syscaller)
if err != nil {
return nil, xerrors.Errorf("list: %w", err)
}
var (
modProcs = []*agentproc.Process{}
logger slog.Logger
)
for _, proc := range procs {
logger = a.logger.With(
slog.F("cmd", proc.Cmd()),
slog.F("pid", proc.PID),
)
containsFn := func(e string) bool {
contains := strings.Contains(proc.Cmd(), e)
return contains
}
// If the process is prioritized we should adjust
// it's oom_score_adj and avoid lowering its niceness.
if slices.ContainsFunc[[]string, string](prioritizedProcs, containsFn) {
continue
}
score, err := proc.Niceness(a.syscaller)
if err != nil {
logger.Warn(ctx, "unable to get proc niceness",
slog.Error(err),
)
continue
}
// We only want processes that don't have a nice value set
// so we don't override user nice values.
// Getpriority actually returns priority for the nice value
// which is niceness + 20, so here 20 = a niceness of 0 (aka unset).
if score != 20 {
// We don't log here since it can get spammy
continue
}
err = proc.SetNiceness(a.syscaller, niceness)
if err != nil {
logger.Warn(ctx, "unable to set proc niceness",
slog.F("niceness", niceness),
slog.Error(err),
)
continue
}
modProcs = append(modProcs, proc)
}
return modProcs, nil
}
// isClosed returns whether the API is closed or not.
func (a *agent) isClosed() bool {
select {
@@ -1336,39 +1461,24 @@ func (a *agent) Close() error {
}
lifecycleState := codersdk.WorkspaceAgentLifecycleOff
if manifest := a.manifest.Load(); manifest != nil && manifest.ShutdownScript != "" {
scriptDone := make(chan error, 1)
go func() {
defer close(scriptDone)
scriptDone <- a.runShutdownScript(ctx, manifest.ShutdownScript)
}()
var timeout <-chan time.Time
// If timeout is zero, an older version of the coder
// provider was used. Otherwise a timeout is always > 0.
if manifest.ShutdownScriptTimeout > 0 {
t := time.NewTimer(manifest.ShutdownScriptTimeout)
defer t.Stop()
timeout = t.C
}
var err error
select {
case err = <-scriptDone:
case <-timeout:
a.logger.Warn(ctx, "script timed out", slog.F("lifecycle", "shutdown"), slog.F("timeout", manifest.ShutdownScriptTimeout))
a.setLifecycle(ctx, codersdk.WorkspaceAgentLifecycleShutdownTimeout)
err = <-scriptDone // The script can still complete after a timeout.
}
if err != nil {
err = a.scriptRunner.Execute(ctx, func(script codersdk.WorkspaceAgentScript) bool {
return script.RunOnStop
})
if err != nil {
a.logger.Warn(ctx, "shutdown script(s) failed", slog.Error(err))
if errors.Is(err, agentscripts.ErrTimeout) {
lifecycleState = codersdk.WorkspaceAgentLifecycleShutdownTimeout
} else {
lifecycleState = codersdk.WorkspaceAgentLifecycleShutdownError
}
}
// Set final state and wait for it to be reported because context
// cancellation will stop the report loop.
a.setLifecycle(ctx, lifecycleState)
err = a.scriptRunner.Close()
if err != nil {
a.logger.Error(ctx, "script runner close", slog.Error(err))
}
// Wait for the lifecycle to be reported, but don't wait forever so
// that we don't break user expectations.
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
+338 -174
View File
@@ -21,10 +21,12 @@ import (
"strings"
"sync"
"sync/atomic"
"syscall"
"testing"
"time"
scp "github.com/bramvdbogaerde/go-scp"
"github.com/golang/mock/gomock"
"github.com/google/uuid"
"github.com/pion/udp"
"github.com/pkg/sftp"
@@ -41,11 +43,13 @@ import (
"tailscale.com/tailcfg"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/agent"
"github.com/coder/coder/v2/agent/agentproc"
"github.com/coder/coder/v2/agent/agentproc/agentproctest"
"github.com/coder/coder/v2/agent/agentssh"
"github.com/coder/coder/v2/agent/agenttest"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/coder/v2/pty"
@@ -346,13 +350,18 @@ func TestAgent_Session_TTY_MOTD(t *testing.T) {
unexpected: []string{},
},
{
name: "Trim",
manifest: agentsdk.Manifest{},
name: "Trim",
// Enable motd since it will be printed after the banner,
// this ensures that we can test for an exact mount of
// newlines.
manifest: agentsdk.Manifest{
MOTDFile: name,
},
banner: codersdk.ServiceBannerConfig{
Enabled: true,
Message: "\n\n\n\n\n\nbanner\n\n\n\n\n\n",
},
expectedRe: regexp.MustCompile("([^\n\r]|^)banner\r\n\r\n[^\r\n]"),
expectedRe: regexp.MustCompile(`([^\n\r]|^)banner\r\n\r\n[^\r\n]`),
},
}
@@ -371,6 +380,7 @@ func TestAgent_Session_TTY_MOTD(t *testing.T) {
}
}
//nolint:tparallel // Sub tests need to run sequentially.
func TestAgent_Session_TTY_MOTD_Update(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
@@ -430,33 +440,38 @@ func TestAgent_Session_TTY_MOTD_Update(t *testing.T) {
}
//nolint:dogsled // Allow the blank identifiers.
conn, client, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0, setSBInterval)
for _, test := range tests {
sshClient, err := conn.SSHClient(ctx)
require.NoError(t, err)
t.Cleanup(func() {
_ = sshClient.Close()
})
//nolint:paralleltest // These tests need to swap the banner func.
for i, test := range tests {
test := test
// 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) {
select {
case ready <- struct{}{}:
default:
}
return test.banner, nil
})
<-ready
<-ready // Wait for two updates to ensure the value has propagated.
t.Run(fmt.Sprintf("%d", i), func(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) {
select {
case ready <- struct{}{}:
default:
}
return test.banner, nil
})
<-ready
<-ready // Wait for two updates to ensure the value has propagated.
sshClient, err := conn.SSHClient(ctx)
require.NoError(t, err)
t.Cleanup(func() {
_ = sshClient.Close()
})
session, err := sshClient.NewSession()
require.NoError(t, err)
t.Cleanup(func() {
_ = session.Close()
})
session, err := sshClient.NewSession()
require.NoError(t, err)
t.Cleanup(func() {
_ = session.Close()
})
testSessionOutput(t, session, test.expected, test.unexpected, nil)
testSessionOutput(t, session, test.expected, test.unexpected, nil)
})
}
}
@@ -1055,84 +1070,6 @@ func TestAgent_SSHConnectionEnvVars(t *testing.T) {
}
}
func TestAgent_StartupScript(t *testing.T) {
t.Parallel()
output := "something"
command := "sh -c 'echo " + output + "'"
if runtime.GOOS == "windows" {
command = "cmd.exe /c echo " + output
}
t.Run("Success", func(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
client := agenttest.NewClient(t,
logger,
uuid.New(),
agentsdk.Manifest{
StartupScript: command,
DERPMap: &tailcfg.DERPMap{},
},
make(chan *agentsdk.Stats),
tailnet.NewCoordinator(logger),
)
closer := agent.New(agent.Options{
Client: client,
Filesystem: afero.NewMemMapFs(),
Logger: logger.Named("agent"),
ReconnectingPTYTimeout: 0,
})
t.Cleanup(func() {
_ = closer.Close()
})
assert.Eventually(t, func() bool {
got := client.GetLifecycleStates()
return len(got) > 0 && got[len(got)-1] == codersdk.WorkspaceAgentLifecycleReady
}, testutil.WaitShort, testutil.IntervalMedium)
require.Len(t, client.GetStartupLogs(), 1)
require.Equal(t, output, client.GetStartupLogs()[0].Output)
})
// This ensures that even when coderd sends back that the startup
// script has written too many lines it will still succeed!
t.Run("OverflowsAndSkips", func(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
client := agenttest.NewClient(t,
logger,
uuid.New(),
agentsdk.Manifest{
StartupScript: command,
DERPMap: &tailcfg.DERPMap{},
},
make(chan *agentsdk.Stats, 50),
tailnet.NewCoordinator(logger),
)
client.PatchWorkspaceLogs = func() error {
resp := httptest.NewRecorder()
httpapi.Write(context.Background(), resp, http.StatusRequestEntityTooLarge, codersdk.Response{
Message: "Too many lines!",
})
res := resp.Result()
defer res.Body.Close()
return codersdk.ReadBodyAsError(res)
}
closer := agent.New(agent.Options{
Client: client,
Filesystem: afero.NewMemMapFs(),
Logger: logger.Named("agent"),
ReconnectingPTYTimeout: 0,
})
t.Cleanup(func() {
_ = closer.Close()
})
assert.Eventually(t, func() bool {
got := client.GetLifecycleStates()
return len(got) > 0 && got[len(got)-1] == codersdk.WorkspaceAgentLifecycleReady
}, testutil.WaitShort, testutil.IntervalMedium)
require.Len(t, client.GetStartupLogs(), 0)
})
}
func TestAgent_Metadata(t *testing.T) {
t.Parallel()
@@ -1140,34 +1077,43 @@ func TestAgent_Metadata(t *testing.T) {
t.Run("Once", func(t *testing.T) {
t.Parallel()
//nolint:dogsled
_, client, _, _, _ := setupAgent(t, agentsdk.Manifest{
Metadata: []codersdk.WorkspaceAgentMetadataDescription{
{
Key: "greeting",
Key: "greeting1",
Interval: 0,
Script: echoHello,
},
{
Key: "greeting2",
Interval: 1,
Script: echoHello,
},
},
}, 0, func(_ *agenttest.Client, opts *agent.Options) {
opts.ReportMetadataInterval = 100 * time.Millisecond
opts.ReportMetadataInterval = testutil.IntervalFast
})
var gotMd map[string]agentsdk.PostMetadataRequest
var gotMd map[string]agentsdk.Metadata
require.Eventually(t, func() bool {
gotMd = client.GetMetadata()
return len(gotMd) == 1
}, testutil.WaitShort, testutil.IntervalMedium)
return len(gotMd) == 2
}, testutil.WaitShort, testutil.IntervalFast/2)
collectedAt := gotMd["greeting"].CollectedAt
collectedAt1 := gotMd["greeting1"].CollectedAt
collectedAt2 := gotMd["greeting2"].CollectedAt
require.Never(t, func() bool {
require.Eventually(t, func() bool {
gotMd = client.GetMetadata()
if len(gotMd) != 1 {
if len(gotMd) != 2 {
panic("unexpected number of metadata")
}
return !gotMd["greeting"].CollectedAt.Equal(collectedAt)
}, testutil.WaitShort, testutil.IntervalMedium)
return !gotMd["greeting2"].CollectedAt.Equal(collectedAt2)
}, testutil.WaitShort, testutil.IntervalFast/2)
require.Equal(t, gotMd["greeting1"].CollectedAt, collectedAt1, "metadata should not be collected again")
})
t.Run("Many", func(t *testing.T) {
@@ -1186,7 +1132,7 @@ func TestAgent_Metadata(t *testing.T) {
opts.ReportMetadataInterval = testutil.IntervalFast
})
var gotMd map[string]agentsdk.PostMetadataRequest
var gotMd map[string]agentsdk.Metadata
require.Eventually(t, func() bool {
gotMd = client.GetMetadata()
return len(gotMd) == 1
@@ -1287,8 +1233,11 @@ func TestAgent_Lifecycle(t *testing.T) {
t.Parallel()
_, client, _, _, _ := setupAgent(t, agentsdk.Manifest{
StartupScript: "sleep 3",
StartupScriptTimeout: time.Nanosecond,
Scripts: []codersdk.WorkspaceAgentScript{{
Script: "sleep 3",
Timeout: time.Millisecond,
RunOnStart: true,
}},
}, 0)
want := []codersdk.WorkspaceAgentLifecycle{
@@ -1309,8 +1258,11 @@ func TestAgent_Lifecycle(t *testing.T) {
t.Parallel()
_, client, _, _, _ := setupAgent(t, agentsdk.Manifest{
StartupScript: "false",
StartupScriptTimeout: 30 * time.Second,
Scripts: []codersdk.WorkspaceAgentScript{{
Script: "false",
Timeout: 30 * time.Second,
RunOnStart: true,
}},
}, 0)
want := []codersdk.WorkspaceAgentLifecycle{
@@ -1331,8 +1283,11 @@ func TestAgent_Lifecycle(t *testing.T) {
t.Parallel()
_, client, _, _, _ := setupAgent(t, agentsdk.Manifest{
StartupScript: "true",
StartupScriptTimeout: 30 * time.Second,
Scripts: []codersdk.WorkspaceAgentScript{{
Script: "true",
Timeout: 30 * time.Second,
RunOnStart: true,
}},
}, 0)
want := []codersdk.WorkspaceAgentLifecycle{
@@ -1353,8 +1308,11 @@ func TestAgent_Lifecycle(t *testing.T) {
t.Parallel()
_, client, _, _, closer := setupAgent(t, agentsdk.Manifest{
ShutdownScript: "sleep 3",
StartupScriptTimeout: 30 * time.Second,
Scripts: []codersdk.WorkspaceAgentScript{{
Script: "sleep 3",
Timeout: 30 * time.Second,
RunOnStop: true,
}},
}, 0)
assert.Eventually(t, func() bool {
@@ -1391,8 +1349,11 @@ func TestAgent_Lifecycle(t *testing.T) {
t.Parallel()
_, client, _, _, closer := setupAgent(t, agentsdk.Manifest{
ShutdownScript: "sleep 3",
ShutdownScriptTimeout: time.Nanosecond,
Scripts: []codersdk.WorkspaceAgentScript{{
Script: "sleep 3",
Timeout: time.Millisecond,
RunOnStop: true,
}},
}, 0)
assert.Eventually(t, func() bool {
@@ -1430,8 +1391,11 @@ func TestAgent_Lifecycle(t *testing.T) {
t.Parallel()
_, client, _, _, closer := setupAgent(t, agentsdk.Manifest{
ShutdownScript: "false",
ShutdownScriptTimeout: 30 * time.Second,
Scripts: []codersdk.WorkspaceAgentScript{{
Script: "false",
Timeout: 30 * time.Second,
RunOnStop: true,
}},
}, 0)
assert.Eventually(t, func() bool {
@@ -1475,9 +1439,16 @@ func TestAgent_Lifecycle(t *testing.T) {
logger,
uuid.New(),
agentsdk.Manifest{
DERPMap: derpMap,
StartupScript: "echo 1",
ShutdownScript: "echo " + expected,
DERPMap: derpMap,
Scripts: []codersdk.WorkspaceAgentScript{{
LogPath: "coder-startup-script.log",
Script: "echo 1",
RunOnStart: true,
}, {
LogPath: "coder-shutdown-script.log",
Script: "echo " + expected,
RunOnStop: true,
}},
},
make(chan *agentsdk.Stats, 50),
tailnet.NewCoordinator(logger),
@@ -1528,9 +1499,7 @@ func TestAgent_Startup(t *testing.T) {
t.Parallel()
_, client, _, _, _ := setupAgent(t, agentsdk.Manifest{
StartupScript: "true",
StartupScriptTimeout: 30 * time.Second,
Directory: "",
Directory: "",
}, 0)
assert.Eventually(t, func() bool {
return client.GetStartup().Version != ""
@@ -1542,9 +1511,7 @@ func TestAgent_Startup(t *testing.T) {
t.Parallel()
_, client, _, _, _ := setupAgent(t, agentsdk.Manifest{
StartupScript: "true",
StartupScriptTimeout: 30 * time.Second,
Directory: "~",
Directory: "~",
}, 0)
assert.Eventually(t, func() bool {
return client.GetStartup().Version != ""
@@ -1558,9 +1525,7 @@ func TestAgent_Startup(t *testing.T) {
t.Parallel()
_, client, _, _, _ := setupAgent(t, agentsdk.Manifest{
StartupScript: "true",
StartupScriptTimeout: 30 * time.Second,
Directory: "coder/coder",
Directory: "coder/coder",
}, 0)
assert.Eventually(t, func() bool {
return client.GetStartup().Version != ""
@@ -1574,9 +1539,7 @@ func TestAgent_Startup(t *testing.T) {
t.Parallel()
_, client, _, _, _ := setupAgent(t, agentsdk.Manifest{
StartupScript: "true",
StartupScriptTimeout: 30 * time.Second,
Directory: "$HOME",
Directory: "$HOME",
}, 0)
assert.Eventually(t, func() bool {
return client.GetStartup().Version != ""
@@ -1601,11 +1564,13 @@ func TestAgent_ReconnectingPTY(t *testing.T) {
_, err := exec.LookPath("screen")
hasScreen := err == nil
// Make sure UTF-8 works even with LANG set to something like C.
t.Setenv("LANG", "C")
for _, backendType := range backends {
backendType := backendType
t.Run(backendType, func(t *testing.T) {
if backendType == "Screen" {
t.Parallel()
if runtime.GOOS != "linux" {
t.Skipf("`screen` is not supported on %s", runtime.GOOS)
} else if !hasScreen {
@@ -1620,8 +1585,6 @@ func TestAgent_ReconnectingPTY(t *testing.T) {
err = os.Symlink(bashPath, filepath.Join(dir, "bash"))
require.NoError(t, err, "symlink bash into reconnecting pty PATH")
t.Setenv("PATH", dir)
} else {
t.Parallel()
}
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
@@ -1630,26 +1593,21 @@ func TestAgent_ReconnectingPTY(t *testing.T) {
//nolint:dogsled
conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{}, 0)
id := uuid.New()
netConn1, err := conn.ReconnectingPTY(ctx, id, 80, 80, "bash")
// --norc disables executing .bashrc, which is often used to customize the bash prompt
netConn1, err := conn.ReconnectingPTY(ctx, id, 80, 80, "bash --norc")
require.NoError(t, err)
defer netConn1.Close()
tr1 := testutil.NewTerminalReader(t, netConn1)
// A second simultaneous connection.
netConn2, err := conn.ReconnectingPTY(ctx, id, 80, 80, "bash")
netConn2, err := conn.ReconnectingPTY(ctx, id, 80, 80, "bash --norc")
require.NoError(t, err)
defer netConn2.Close()
tr2 := testutil.NewTerminalReader(t, netConn2)
// Brief pause to reduce the likelihood that we send keystrokes while
// the shell is simultaneously sending a prompt.
time.Sleep(100 * time.Millisecond)
data, err := json.Marshal(codersdk.ReconnectingPTYRequest{
Data: "echo test\r\n",
})
require.NoError(t, err)
_, err = netConn1.Write(data)
require.NoError(t, err)
matchPrompt := func(line string) bool {
return strings.Contains(line, "$ ") || strings.Contains(line, "# ")
}
matchEchoCommand := func(line string) bool {
return strings.Contains(line, "echo test")
}
@@ -1663,47 +1621,72 @@ func TestAgent_ReconnectingPTY(t *testing.T) {
return strings.Contains(line, "exit") || strings.Contains(line, "logout")
}
// Wait for the prompt before writing commands. If the command arrives before the prompt is written, screen
// will sometimes put the command output on the same line as the command and the test will flake
require.NoError(t, tr1.ReadUntil(ctx, matchPrompt), "find prompt")
require.NoError(t, tr2.ReadUntil(ctx, matchPrompt), "find prompt")
data, err := json.Marshal(codersdk.ReconnectingPTYRequest{
Data: "echo test\r",
})
require.NoError(t, err)
_, err = netConn1.Write(data)
require.NoError(t, err)
// Once for typing the command...
require.NoError(t, testutil.ReadUntil(ctx, t, netConn1, matchEchoCommand), "find echo command")
require.NoError(t, tr1.ReadUntil(ctx, matchEchoCommand), "find echo command")
// And another time for the actual output.
require.NoError(t, testutil.ReadUntil(ctx, t, netConn1, matchEchoOutput), "find echo output")
require.NoError(t, tr1.ReadUntil(ctx, matchEchoOutput), "find echo output")
// Same for the other connection.
require.NoError(t, testutil.ReadUntil(ctx, t, netConn2, matchEchoCommand), "find echo command")
require.NoError(t, testutil.ReadUntil(ctx, t, netConn2, matchEchoOutput), "find echo output")
require.NoError(t, tr2.ReadUntil(ctx, matchEchoCommand), "find echo command")
require.NoError(t, tr2.ReadUntil(ctx, matchEchoOutput), "find echo output")
_ = netConn1.Close()
_ = netConn2.Close()
netConn3, err := conn.ReconnectingPTY(ctx, id, 80, 80, "bash")
netConn3, err := conn.ReconnectingPTY(ctx, id, 80, 80, "bash --norc")
require.NoError(t, err)
defer netConn3.Close()
tr3 := testutil.NewTerminalReader(t, netConn3)
// Same output again!
require.NoError(t, testutil.ReadUntil(ctx, t, netConn3, matchEchoCommand), "find echo command")
require.NoError(t, testutil.ReadUntil(ctx, t, netConn3, matchEchoOutput), "find echo output")
require.NoError(t, tr3.ReadUntil(ctx, matchEchoCommand), "find echo command")
require.NoError(t, tr3.ReadUntil(ctx, matchEchoOutput), "find echo output")
// Exit should cause the connection to close.
data, err = json.Marshal(codersdk.ReconnectingPTYRequest{
Data: "exit\r\n",
Data: "exit\r",
})
require.NoError(t, err)
_, err = netConn3.Write(data)
require.NoError(t, err)
// Once for the input and again for the output.
require.NoError(t, testutil.ReadUntil(ctx, t, netConn3, matchExitCommand), "find exit command")
require.NoError(t, testutil.ReadUntil(ctx, t, netConn3, matchExitOutput), "find exit output")
require.NoError(t, tr3.ReadUntil(ctx, matchExitCommand), "find exit command")
require.NoError(t, tr3.ReadUntil(ctx, matchExitOutput), "find exit output")
// Wait for the connection to close.
require.ErrorIs(t, testutil.ReadUntil(ctx, t, netConn3, nil), io.EOF)
require.ErrorIs(t, tr3.ReadUntil(ctx, nil), io.EOF)
// Try a non-shell command. It should output then immediately exit.
netConn4, err := conn.ReconnectingPTY(ctx, uuid.New(), 80, 80, "echo test")
require.NoError(t, err)
defer netConn4.Close()
require.NoError(t, testutil.ReadUntil(ctx, t, netConn4, matchEchoOutput), "find echo output")
require.ErrorIs(t, testutil.ReadUntil(ctx, t, netConn3, nil), io.EOF)
tr4 := testutil.NewTerminalReader(t, netConn4)
require.NoError(t, tr4.ReadUntil(ctx, matchEchoOutput), "find echo output")
require.ErrorIs(t, tr4.ReadUntil(ctx, nil), io.EOF)
// Ensure that UTF-8 is supported. Avoid the terminal emulator because it
// does not appear to support UTF-8, just make sure the bytes that come
// back have the character in it.
netConn5, err := conn.ReconnectingPTY(ctx, uuid.New(), 80, 80, "echo ")
require.NoError(t, err)
defer netConn5.Close()
bytes, err := io.ReadAll(netConn5)
require.NoError(t, err)
require.Contains(t, string(bytes), "")
})
}
}
@@ -1898,13 +1881,16 @@ func TestAgent_UpdatedDERP(t *testing.T) {
func TestAgent_Speedtest(t *testing.T) {
t.Parallel()
t.Skip("This test is relatively flakey because of Tailscale's speedtest code...")
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
derpMap, _ := tailnettest.RunDERPAndSTUN(t)
//nolint:dogsled
conn, _, _, _, _ := setupAgent(t, agentsdk.Manifest{
DERPMap: derpMap,
}, 0)
}, 0, func(client *agenttest.Client, options *agent.Options) {
options.Logger = logger.Named("agent")
})
defer conn.Close()
res, err := conn.Speedtest(ctx, speedtest.Upload, 250*time.Millisecond)
require.NoError(t, err)
@@ -2395,6 +2381,173 @@ func TestAgent_Metrics_SSH(t *testing.T) {
require.NoError(t, err)
}
func TestAgent_ManageProcessPriority(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
t.Parallel()
if runtime.GOOS != "linux" {
t.Skip("Skipping non-linux environment")
}
var (
expectedProcs = map[int32]agentproc.Process{}
fs = afero.NewMemMapFs()
syscaller = agentproctest.NewMockSyscaller(gomock.NewController(t))
ticker = make(chan time.Time)
modProcs = make(chan []*agentproc.Process)
logger = slog.Make(sloghuman.Sink(io.Discard))
)
// Create some processes.
for i := 0; i < 4; i++ {
// Create a prioritized process. This process should
// have it's oom_score_adj set to -500 and its nice
// score should be untouched.
var proc agentproc.Process
if i == 0 {
proc = agentproctest.GenerateProcess(t, fs,
func(p *agentproc.Process) {
p.CmdLine = "./coder\x00agent\x00--no-reap"
p.PID = int32(i)
},
)
} else {
proc = agentproctest.GenerateProcess(t, fs,
func(p *agentproc.Process) {
// Make the cmd something similar to a prioritized
// process but differentiate the arguments.
p.CmdLine = "./coder\x00stat"
},
)
syscaller.EXPECT().SetPriority(proc.PID, 10).Return(nil)
syscaller.EXPECT().GetPriority(proc.PID).Return(20, nil)
}
syscaller.EXPECT().
Kill(proc.PID, syscall.Signal(0)).
Return(nil)
expectedProcs[proc.PID] = proc
}
_, _, _, _, _ = setupAgent(t, agentsdk.Manifest{}, 0, func(c *agenttest.Client, o *agent.Options) {
o.Syscaller = syscaller
o.ModifiedProcesses = modProcs
o.EnvironmentVariables = map[string]string{agent.EnvProcPrioMgmt: "1"}
o.Filesystem = fs
o.Logger = logger
o.ProcessManagementTick = ticker
})
actualProcs := <-modProcs
require.Len(t, actualProcs, len(expectedProcs)-1)
})
t.Run("IgnoreCustomNice", func(t *testing.T) {
t.Parallel()
if runtime.GOOS != "linux" {
t.Skip("Skipping non-linux environment")
}
var (
expectedProcs = map[int32]agentproc.Process{}
fs = afero.NewMemMapFs()
ticker = make(chan time.Time)
syscaller = agentproctest.NewMockSyscaller(gomock.NewController(t))
modProcs = make(chan []*agentproc.Process)
logger = slog.Make(sloghuman.Sink(io.Discard))
)
// Create some processes.
for i := 0; i < 2; i++ {
proc := agentproctest.GenerateProcess(t, fs)
syscaller.EXPECT().
Kill(proc.PID, syscall.Signal(0)).
Return(nil)
if i == 0 {
// Set a random nice score. This one should not be adjusted by
// our management loop.
syscaller.EXPECT().GetPriority(proc.PID).Return(25, nil)
} else {
syscaller.EXPECT().GetPriority(proc.PID).Return(20, nil)
syscaller.EXPECT().SetPriority(proc.PID, 10).Return(nil)
}
expectedProcs[proc.PID] = proc
}
_, _, _, _, _ = setupAgent(t, agentsdk.Manifest{}, 0, func(c *agenttest.Client, o *agent.Options) {
o.Syscaller = syscaller
o.ModifiedProcesses = modProcs
o.EnvironmentVariables = map[string]string{agent.EnvProcPrioMgmt: "1"}
o.Filesystem = fs
o.Logger = logger
o.ProcessManagementTick = ticker
})
actualProcs := <-modProcs
// We should ignore the process with a custom nice score.
require.Len(t, actualProcs, 1)
})
t.Run("DisabledByDefault", func(t *testing.T) {
t.Parallel()
if runtime.GOOS != "linux" {
t.Skip("Skipping non-linux environment")
}
var (
buf bytes.Buffer
wr = &syncWriter{
w: &buf,
}
)
log := slog.Make(sloghuman.Sink(wr)).Leveled(slog.LevelDebug)
_, _, _, _, _ = setupAgent(t, agentsdk.Manifest{}, 0, func(c *agenttest.Client, o *agent.Options) {
o.Logger = log
})
require.Eventually(t, func() bool {
wr.mu.Lock()
defer wr.mu.Unlock()
return strings.Contains(buf.String(), "process priority not enabled")
}, testutil.WaitLong, testutil.IntervalFast)
})
t.Run("DisabledForNonLinux", func(t *testing.T) {
t.Parallel()
if runtime.GOOS == "linux" {
t.Skip("Skipping linux environment")
}
var (
buf bytes.Buffer
wr = &syncWriter{
w: &buf,
}
)
log := slog.Make(sloghuman.Sink(wr)).Leveled(slog.LevelDebug)
_, _, _, _, _ = setupAgent(t, agentsdk.Manifest{}, 0, func(c *agenttest.Client, o *agent.Options) {
o.Logger = log
// Try to enable it so that we can assert that non-linux
// environments are truly disabled.
o.EnvironmentVariables = map[string]string{agent.EnvProcPrioMgmt: "1"}
})
require.Eventually(t, func() bool {
wr.mu.Lock()
defer wr.mu.Unlock()
return strings.Contains(buf.String(), "process priority not enabled")
}, testutil.WaitLong, testutil.IntervalFast)
})
}
func verifyCollectedMetrics(t *testing.T, expected []agentsdk.AgentMetric, actual []*promgo.MetricFamily) bool {
t.Helper()
@@ -2416,3 +2569,14 @@ func verifyCollectedMetrics(t *testing.T, expected []agentsdk.AgentMetric, actua
}
return true
}
type syncWriter struct {
mu sync.Mutex
w io.Writer
}
func (s *syncWriter) Write(p []byte) (int, error) {
s.mu.Lock()
defer s.mu.Unlock()
return s.w.Write(p)
}
+5
View File
@@ -0,0 +1,5 @@
// Package agentproctest contains utility functions
// for testing process management in the agent.
package agentproctest
//go:generate mockgen -destination ./syscallermock.go -package agentproctest github.com/coder/coder/v2/agent/agentproc Syscaller
+49
View File
@@ -0,0 +1,49 @@
package agentproctest
import (
"fmt"
"testing"
"github.com/spf13/afero"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/agent/agentproc"
"github.com/coder/coder/v2/cryptorand"
)
func GenerateProcess(t *testing.T, fs afero.Fs, muts ...func(*agentproc.Process)) agentproc.Process {
t.Helper()
pid, err := cryptorand.Intn(1<<31 - 1)
require.NoError(t, err)
arg1, err := cryptorand.String(5)
require.NoError(t, err)
arg2, err := cryptorand.String(5)
require.NoError(t, err)
arg3, err := cryptorand.String(5)
require.NoError(t, err)
cmdline := fmt.Sprintf("%s\x00%s\x00%s", arg1, arg2, arg3)
process := agentproc.Process{
CmdLine: cmdline,
PID: int32(pid),
}
for _, mut := range muts {
mut(&process)
}
process.Dir = fmt.Sprintf("%s/%d", "/proc", process.PID)
err = fs.MkdirAll(process.Dir, 0o555)
require.NoError(t, err)
err = afero.WriteFile(fs, fmt.Sprintf("%s/cmdline", process.Dir), []byte(process.CmdLine), 0o444)
require.NoError(t, err)
return process
}
@@ -0,0 +1,78 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/coder/coder/v2/agent/agentproc (interfaces: Syscaller)
// Package agentproctest is a generated GoMock package.
package agentproctest
import (
reflect "reflect"
syscall "syscall"
gomock "github.com/golang/mock/gomock"
)
// MockSyscaller is a mock of Syscaller interface.
type MockSyscaller struct {
ctrl *gomock.Controller
recorder *MockSyscallerMockRecorder
}
// MockSyscallerMockRecorder is the mock recorder for MockSyscaller.
type MockSyscallerMockRecorder struct {
mock *MockSyscaller
}
// NewMockSyscaller creates a new mock instance.
func NewMockSyscaller(ctrl *gomock.Controller) *MockSyscaller {
mock := &MockSyscaller{ctrl: ctrl}
mock.recorder = &MockSyscallerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockSyscaller) EXPECT() *MockSyscallerMockRecorder {
return m.recorder
}
// GetPriority mocks base method.
func (m *MockSyscaller) GetPriority(arg0 int32) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPriority", arg0)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetPriority indicates an expected call of GetPriority.
func (mr *MockSyscallerMockRecorder) GetPriority(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPriority", reflect.TypeOf((*MockSyscaller)(nil).GetPriority), arg0)
}
// Kill mocks base method.
func (m *MockSyscaller) Kill(arg0 int32, arg1 syscall.Signal) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Kill", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// Kill indicates an expected call of Kill.
func (mr *MockSyscallerMockRecorder) Kill(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Kill", reflect.TypeOf((*MockSyscaller)(nil).Kill), arg0, arg1)
}
// SetPriority mocks base method.
func (m *MockSyscaller) SetPriority(arg0 int32, arg1 int) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetPriority", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// SetPriority indicates an expected call of SetPriority.
func (mr *MockSyscallerMockRecorder) SetPriority(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPriority", reflect.TypeOf((*MockSyscaller)(nil).SetPriority), arg0, arg1)
}
+3
View File
@@ -0,0 +1,3 @@
// Package agentproc contains logic for interfacing with local
// processes running in the same context as the agent.
package agentproc
+24
View File
@@ -0,0 +1,24 @@
//go:build !linux
// +build !linux
package agentproc
import (
"github.com/spf13/afero"
)
func (p *Process) Niceness(sc Syscaller) (int, error) {
return 0, errUnimplemented
}
func (p *Process) SetNiceness(sc Syscaller, score int) error {
return errUnimplemented
}
func (p *Process) Cmd() string {
return ""
}
func List(fs afero.Fs, syscaller Syscaller) ([]*Process, error) {
return nil, errUnimplemented
}
+166
View File
@@ -0,0 +1,166 @@
package agentproc_test
import (
"runtime"
"syscall"
"testing"
"github.com/golang/mock/gomock"
"github.com/spf13/afero"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/agent/agentproc"
"github.com/coder/coder/v2/agent/agentproc/agentproctest"
)
func TestList(t *testing.T) {
t.Parallel()
if runtime.GOOS != "linux" {
t.Skipf("skipping non-linux environment")
}
t.Run("OK", func(t *testing.T) {
t.Parallel()
var (
fs = afero.NewMemMapFs()
sc = agentproctest.NewMockSyscaller(gomock.NewController(t))
expectedProcs = make(map[int32]agentproc.Process)
)
for i := 0; i < 4; i++ {
proc := agentproctest.GenerateProcess(t, fs)
expectedProcs[proc.PID] = proc
sc.EXPECT().
Kill(proc.PID, syscall.Signal(0)).
Return(nil)
}
actualProcs, err := agentproc.List(fs, sc)
require.NoError(t, err)
require.Len(t, actualProcs, len(expectedProcs))
for _, proc := range actualProcs {
expected, ok := expectedProcs[proc.PID]
require.True(t, ok)
require.Equal(t, expected.PID, proc.PID)
require.Equal(t, expected.CmdLine, proc.CmdLine)
require.Equal(t, expected.Dir, proc.Dir)
}
})
t.Run("FinishedProcess", func(t *testing.T) {
t.Parallel()
var (
fs = afero.NewMemMapFs()
sc = agentproctest.NewMockSyscaller(gomock.NewController(t))
expectedProcs = make(map[int32]agentproc.Process)
)
for i := 0; i < 3; i++ {
proc := agentproctest.GenerateProcess(t, fs)
expectedProcs[proc.PID] = proc
sc.EXPECT().
Kill(proc.PID, syscall.Signal(0)).
Return(nil)
}
// Create a process that's already finished. We're not adding
// it to the map because it should be skipped over.
proc := agentproctest.GenerateProcess(t, fs)
sc.EXPECT().
Kill(proc.PID, syscall.Signal(0)).
Return(xerrors.New("os: process already finished"))
actualProcs, err := agentproc.List(fs, sc)
require.NoError(t, err)
require.Len(t, actualProcs, len(expectedProcs))
for _, proc := range actualProcs {
expected, ok := expectedProcs[proc.PID]
require.True(t, ok)
require.Equal(t, expected.PID, proc.PID)
require.Equal(t, expected.CmdLine, proc.CmdLine)
require.Equal(t, expected.Dir, proc.Dir)
}
})
t.Run("NoSuchProcess", func(t *testing.T) {
t.Parallel()
var (
fs = afero.NewMemMapFs()
sc = agentproctest.NewMockSyscaller(gomock.NewController(t))
expectedProcs = make(map[int32]agentproc.Process)
)
for i := 0; i < 3; i++ {
proc := agentproctest.GenerateProcess(t, fs)
expectedProcs[proc.PID] = proc
sc.EXPECT().
Kill(proc.PID, syscall.Signal(0)).
Return(nil)
}
// Create a process that doesn't exist. We're not adding
// it to the map because it should be skipped over.
proc := agentproctest.GenerateProcess(t, fs)
sc.EXPECT().
Kill(proc.PID, syscall.Signal(0)).
Return(syscall.ESRCH)
actualProcs, err := agentproc.List(fs, sc)
require.NoError(t, err)
require.Len(t, actualProcs, len(expectedProcs))
for _, proc := range actualProcs {
expected, ok := expectedProcs[proc.PID]
require.True(t, ok)
require.Equal(t, expected.PID, proc.PID)
require.Equal(t, expected.CmdLine, proc.CmdLine)
require.Equal(t, expected.Dir, proc.Dir)
}
})
}
// These tests are not very interesting but they provide some modicum of
// confidence.
func TestProcess(t *testing.T) {
t.Parallel()
if runtime.GOOS != "linux" {
t.Skipf("skipping non-linux environment")
}
t.Run("SetNiceness", func(t *testing.T) {
t.Parallel()
var (
sc = agentproctest.NewMockSyscaller(gomock.NewController(t))
proc = &agentproc.Process{
PID: 32,
}
score = 20
)
sc.EXPECT().SetPriority(proc.PID, score).Return(nil)
err := proc.SetNiceness(sc, score)
require.NoError(t, err)
})
t.Run("Cmd", func(t *testing.T) {
t.Parallel()
var (
proc = &agentproc.Process{
CmdLine: "helloworld\x00--arg1\x00--arg2",
}
expectedName = "helloworld --arg1 --arg2"
)
require.Equal(t, expectedName, proc.Cmd())
})
}
+109
View File
@@ -0,0 +1,109 @@
//go:build linux
// +build linux
package agentproc
import (
"errors"
"path/filepath"
"strconv"
"strings"
"syscall"
"github.com/spf13/afero"
"golang.org/x/xerrors"
)
func List(fs afero.Fs, syscaller Syscaller) ([]*Process, error) {
d, err := fs.Open(defaultProcDir)
if err != nil {
return nil, xerrors.Errorf("open dir %q: %w", defaultProcDir, err)
}
defer d.Close()
entries, err := d.Readdirnames(0)
if err != nil {
return nil, xerrors.Errorf("readdirnames: %w", err)
}
processes := make([]*Process, 0, len(entries))
for _, entry := range entries {
pid, err := strconv.ParseInt(entry, 10, 32)
if err != nil {
continue
}
// Check that the process still exists.
exists, err := isProcessExist(syscaller, int32(pid))
if err != nil {
return nil, xerrors.Errorf("check process exists: %w", err)
}
if !exists {
continue
}
cmdline, err := afero.ReadFile(fs, filepath.Join(defaultProcDir, entry, "cmdline"))
if err != nil {
var errNo syscall.Errno
if xerrors.As(err, &errNo) && errNo == syscall.EPERM {
continue
}
return nil, xerrors.Errorf("read cmdline: %w", err)
}
processes = append(processes, &Process{
PID: int32(pid),
CmdLine: string(cmdline),
Dir: filepath.Join(defaultProcDir, entry),
})
}
return processes, nil
}
func isProcessExist(syscaller Syscaller, pid int32) (bool, error) {
err := syscaller.Kill(pid, syscall.Signal(0))
if err == nil {
return true, nil
}
if err.Error() == "os: process already finished" {
return false, nil
}
var errno syscall.Errno
if !errors.As(err, &errno) {
return false, err
}
switch errno {
case syscall.ESRCH:
return false, nil
case syscall.EPERM:
return true, nil
}
return false, xerrors.Errorf("kill: %w", err)
}
func (p *Process) Niceness(sc Syscaller) (int, error) {
nice, err := sc.GetPriority(p.PID)
if err != nil {
return 0, xerrors.Errorf("get priority for %q: %w", p.CmdLine, err)
}
return nice, nil
}
func (p *Process) SetNiceness(sc Syscaller, score int) error {
err := sc.SetPriority(p.PID, score)
if err != nil {
return xerrors.Errorf("set priority for %q: %w", p.CmdLine, err)
}
return nil
}
func (p *Process) Cmd() string {
return strings.Join(p.cmdLine(), " ")
}
func (p *Process) cmdLine() []string {
return strings.Split(p.CmdLine, "\x00")
}
+19
View File
@@ -0,0 +1,19 @@
package agentproc
import (
"syscall"
)
type Syscaller interface {
SetPriority(pid int32, priority int) error
GetPriority(pid int32) (int, error)
Kill(pid int32, sig syscall.Signal) error
}
const defaultProcDir = "/proc"
type Process struct {
Dir string
CmdLine string
PID int32
}
+30
View File
@@ -0,0 +1,30 @@
//go:build !linux
// +build !linux
package agentproc
import (
"syscall"
"golang.org/x/xerrors"
)
func NewSyscaller() Syscaller {
return nopSyscaller{}
}
var errUnimplemented = xerrors.New("unimplemented")
type nopSyscaller struct{}
func (nopSyscaller) SetPriority(pid int32, priority int) error {
return errUnimplemented
}
func (nopSyscaller) GetPriority(pid int32) (int, error) {
return 0, errUnimplemented
}
func (nopSyscaller) Kill(pid int32, sig syscall.Signal) error {
return errUnimplemented
}
+42
View File
@@ -0,0 +1,42 @@
//go:build linux
// +build linux
package agentproc
import (
"syscall"
"golang.org/x/sys/unix"
"golang.org/x/xerrors"
)
func NewSyscaller() Syscaller {
return UnixSyscaller{}
}
type UnixSyscaller struct{}
func (UnixSyscaller) SetPriority(pid int32, nice int) error {
err := unix.Setpriority(unix.PRIO_PROCESS, int(pid), nice)
if err != nil {
return xerrors.Errorf("set priority: %w", err)
}
return nil
}
func (UnixSyscaller) GetPriority(pid int32) (int, error) {
nice, err := unix.Getpriority(0, int(pid))
if err != nil {
return 0, xerrors.Errorf("get priority: %w", err)
}
return nice, nil
}
func (UnixSyscaller) Kill(pid int32, sig syscall.Signal) error {
err := syscall.Kill(int(pid), sig)
if err != nil {
return xerrors.Errorf("kill: %w", err)
}
return nil
}
+314
View File
@@ -0,0 +1,314 @@
package agentscripts
import (
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"os/user"
"path/filepath"
"sync"
"sync/atomic"
"time"
"github.com/robfig/cron/v3"
"github.com/spf13/afero"
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
"cdr.dev/slog"
"github.com/coder/coder/v2/agent/agentssh"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
)
var (
// ErrTimeout is returned when a script times out.
ErrTimeout = xerrors.New("script timed out")
// ErrOutputPipesOpen is returned when a script exits leaving the output
// pipe(s) (stdout, stderr) open. This happens because we set WaitDelay on
// the command, which gives us two things:
//
// 1. The ability to ensure that a script exits (this is important for e.g.
// blocking login, and avoiding doing so indefinitely)
// 2. Improved command cancellation on timeout
ErrOutputPipesOpen = xerrors.New("script exited without closing output pipes")
parser = cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.DowOptional)
)
// Options are a set of options for the runner.
type Options struct {
LogDir string
Logger slog.Logger
SSHServer *agentssh.Server
Filesystem afero.Fs
PatchLogs func(ctx context.Context, req agentsdk.PatchLogs) error
}
// New creates a runner for the provided scripts.
func New(opts Options) *Runner {
cronCtx, cronCtxCancel := context.WithCancel(context.Background())
return &Runner{
Options: opts,
cronCtx: cronCtx,
cronCtxCancel: cronCtxCancel,
cron: cron.New(cron.WithParser(parser)),
closed: make(chan struct{}),
}
}
type Runner struct {
Options
cronCtx context.Context
cronCtxCancel context.CancelFunc
cmdCloseWait sync.WaitGroup
closed chan struct{}
closeMutex sync.Mutex
cron *cron.Cron
initialized atomic.Bool
scripts []codersdk.WorkspaceAgentScript
}
// Init initializes the runner with the provided scripts.
// It also schedules any scripts that have a schedule.
// This function must be called before Execute.
func (r *Runner) Init(scripts []codersdk.WorkspaceAgentScript) error {
if r.initialized.Load() {
return xerrors.New("init: already initialized")
}
r.initialized.Store(true)
r.scripts = scripts
r.Logger.Info(r.cronCtx, "initializing agent scripts", slog.F("script_count", len(scripts)), slog.F("log_dir", r.LogDir))
for _, script := range scripts {
if script.Cron == "" {
continue
}
script := script
_, err := r.cron.AddFunc(script.Cron, func() {
err := r.run(r.cronCtx, script)
if err != nil {
r.Logger.Warn(context.Background(), "run agent script on schedule", slog.Error(err))
}
})
if err != nil {
return xerrors.Errorf("add schedule: %w", err)
}
}
return nil
}
// StartCron starts the cron scheduler.
// This is done async to allow for the caller to execute scripts prior.
func (r *Runner) StartCron() {
// cron.Start() and cron.Stop() does not guarantee that the cron goroutine
// has exited by the time the `cron.Stop()` context returns, so we need to
// track it manually.
err := r.trackCommandGoroutine(func() {
r.cron.Run()
})
if err != nil {
r.Logger.Warn(context.Background(), "start cron failed", slog.Error(err))
}
}
// Execute runs a set of scripts according to a filter.
func (r *Runner) Execute(ctx context.Context, filter func(script codersdk.WorkspaceAgentScript) bool) error {
if filter == nil {
// Execute em' all!
filter = func(script codersdk.WorkspaceAgentScript) bool {
return true
}
}
var eg errgroup.Group
for _, script := range r.scripts {
if !filter(script) {
continue
}
script := script
eg.Go(func() error {
err := r.run(ctx, script)
if err != nil {
return xerrors.Errorf("run agent script %q: %w", script.LogSourceID, err)
}
return nil
})
}
return eg.Wait()
}
// run executes the provided script with the timeout.
// If the timeout is exceeded, the process is sent an interrupt signal.
// If the process does not exit after a few seconds, it is forcefully killed.
// This function immediately returns after a timeout, and does not wait for the process to exit.
func (r *Runner) run(ctx context.Context, script codersdk.WorkspaceAgentScript) error {
logPath := script.LogPath
if logPath == "" {
logPath = fmt.Sprintf("coder-script-%s.log", script.LogSourceID)
}
if logPath[0] == '~' {
// First we check the environment.
homeDir, err := os.UserHomeDir()
if err != nil {
u, err := user.Current()
if err != nil {
return xerrors.Errorf("current user: %w", err)
}
homeDir = u.HomeDir
}
logPath = filepath.Join(homeDir, logPath[1:])
}
logPath = os.ExpandEnv(logPath)
if !filepath.IsAbs(logPath) {
logPath = filepath.Join(r.LogDir, logPath)
}
logger := r.Logger.With(slog.F("log_path", logPath))
logger.Info(ctx, "running agent script", slog.F("script", script.Script))
fileWriter, err := r.Filesystem.OpenFile(logPath, os.O_CREATE|os.O_RDWR, 0o600)
if err != nil {
return xerrors.Errorf("open %s script log file: %w", logPath, err)
}
defer func() {
err := fileWriter.Close()
if err != nil {
logger.Warn(ctx, fmt.Sprintf("close %s script log file", logPath), slog.Error(err))
}
}()
var cmd *exec.Cmd
cmdCtx := ctx
if script.Timeout > 0 {
var ctxCancel context.CancelFunc
cmdCtx, ctxCancel = context.WithTimeout(ctx, script.Timeout)
defer ctxCancel()
}
cmdPty, err := r.SSHServer.CreateCommand(cmdCtx, script.Script, nil)
if err != nil {
return xerrors.Errorf("%s script: create command: %w", logPath, err)
}
cmd = cmdPty.AsExec()
cmd.SysProcAttr = cmdSysProcAttr()
cmd.WaitDelay = 10 * time.Second
cmd.Cancel = cmdCancel(cmd)
send, flushAndClose := agentsdk.LogsSender(script.LogSourceID, r.PatchLogs, logger)
// If ctx is canceled here (or in a writer below), we may be
// discarding logs, but that's okay because we're shutting down
// anyway. We could consider creating a new context here if we
// want better control over flush during shutdown.
defer func() {
if err := flushAndClose(ctx); err != nil {
logger.Warn(ctx, "flush startup logs failed", slog.Error(err))
}
}()
infoW := agentsdk.LogsWriter(ctx, send, script.LogSourceID, codersdk.LogLevelInfo)
defer infoW.Close()
errW := agentsdk.LogsWriter(ctx, send, script.LogSourceID, codersdk.LogLevelError)
defer errW.Close()
cmd.Stdout = io.MultiWriter(fileWriter, infoW)
cmd.Stderr = io.MultiWriter(fileWriter, errW)
start := time.Now()
defer func() {
end := time.Now()
execTime := end.Sub(start)
exitCode := 0
if err != nil {
exitCode = 255 // Unknown status.
var exitError *exec.ExitError
if xerrors.As(err, &exitError) {
exitCode = exitError.ExitCode()
}
logger.Warn(ctx, fmt.Sprintf("%s script failed", logPath), slog.F("execution_time", execTime), slog.F("exit_code", exitCode), slog.Error(err))
} else {
logger.Info(ctx, fmt.Sprintf("%s script completed", logPath), slog.F("execution_time", execTime), slog.F("exit_code", exitCode))
}
}()
err = cmd.Start()
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return ErrTimeout
}
return xerrors.Errorf("%s script: start command: %w", logPath, err)
}
cmdDone := make(chan error, 1)
err = r.trackCommandGoroutine(func() {
cmdDone <- cmd.Wait()
})
if err != nil {
return xerrors.Errorf("%s script: track command goroutine: %w", logPath, err)
}
select {
case <-cmdCtx.Done():
// Wait for the command to drain!
select {
case <-cmdDone:
case <-time.After(10 * time.Second):
}
err = cmdCtx.Err()
case err = <-cmdDone:
}
switch {
case errors.Is(err, exec.ErrWaitDelay):
err = ErrOutputPipesOpen
message := fmt.Sprintf("script exited successfully, but output pipes were not closed after %s", cmd.WaitDelay)
details := fmt.Sprint(
"This usually means a child process was started with references to stdout or stderr. As a result, this " +
"process may now have been terminated. Consider redirecting the output or using a separate " +
"\"coder_script\" for the process, see " +
"https://coder.com/docs/v2/latest/templates/troubleshooting#startup-script-issues for more information.",
)
// Inform the user by propagating the message via log writers.
_, _ = fmt.Fprintf(cmd.Stderr, "WARNING: %s. %s\n", message, details)
// Also log to agent logs for ease of debugging.
r.Logger.Warn(ctx, message, slog.F("details", details), slog.Error(err))
case errors.Is(err, context.DeadlineExceeded):
err = ErrTimeout
}
return err
}
func (r *Runner) Close() error {
r.closeMutex.Lock()
defer r.closeMutex.Unlock()
if r.isClosed() {
return nil
}
close(r.closed)
r.cronCtxCancel()
<-r.cron.Stop().Done()
r.cmdCloseWait.Wait()
return nil
}
func (r *Runner) trackCommandGoroutine(fn func()) error {
r.closeMutex.Lock()
defer r.closeMutex.Unlock()
if r.isClosed() {
return xerrors.New("track command goroutine: closed")
}
r.cmdCloseWait.Add(1)
go func() {
defer r.cmdCloseWait.Done()
fn()
}()
return nil
}
func (r *Runner) isClosed() bool {
select {
case <-r.closed:
return true
default:
return false
}
}
+20
View File
@@ -0,0 +1,20 @@
//go:build !windows
package agentscripts
import (
"os/exec"
"syscall"
)
func cmdSysProcAttr() *syscall.SysProcAttr {
return &syscall.SysProcAttr{
Setsid: true,
}
}
func cmdCancel(cmd *exec.Cmd) func() error {
return func() error {
return syscall.Kill(-cmd.Process.Pid, syscall.SIGHUP)
}
}
+80
View File
@@ -0,0 +1,80 @@
package agentscripts_test
import (
"context"
"testing"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/spf13/afero"
"github.com/stretchr/testify/require"
"go.uber.org/atomic"
"go.uber.org/goleak"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/agent/agentscripts"
"github.com/coder/coder/v2/agent/agentssh"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
)
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
func TestExecuteBasic(t *testing.T) {
t.Parallel()
logs := make(chan agentsdk.PatchLogs, 1)
runner := setup(t, func(ctx context.Context, req agentsdk.PatchLogs) error {
logs <- req
return nil
})
defer runner.Close()
err := runner.Init([]codersdk.WorkspaceAgentScript{{
Script: "echo hello",
}})
require.NoError(t, err)
require.NoError(t, runner.Execute(context.Background(), func(script codersdk.WorkspaceAgentScript) bool {
return true
}))
log := <-logs
require.Equal(t, "hello", log.Logs[0].Output)
}
func TestTimeout(t *testing.T) {
t.Parallel()
runner := setup(t, nil)
defer runner.Close()
err := runner.Init([]codersdk.WorkspaceAgentScript{{
Script: "sleep infinity",
Timeout: time.Millisecond,
}})
require.NoError(t, err)
require.ErrorIs(t, runner.Execute(context.Background(), nil), agentscripts.ErrTimeout)
}
func setup(t *testing.T, patchLogs func(ctx context.Context, req agentsdk.PatchLogs) error) *agentscripts.Runner {
t.Helper()
if patchLogs == nil {
// noop
patchLogs = func(ctx context.Context, req agentsdk.PatchLogs) error {
return nil
}
}
fs := afero.NewMemMapFs()
logger := slogtest.Make(t, nil)
s, err := agentssh.NewServer(context.Background(), logger, prometheus.NewRegistry(), fs, 0, "")
require.NoError(t, err)
s.AgentToken = func() string { return "" }
s.Manifest = atomic.NewPointer(&agentsdk.Manifest{})
t.Cleanup(func() {
_ = s.Close()
})
return agentscripts.New(agentscripts.Options{
LogDir: t.TempDir(),
Logger: logger,
SSHServer: s,
Filesystem: fs,
PatchLogs: patchLogs,
})
}
@@ -0,0 +1,17 @@
package agentscripts
import (
"os"
"os/exec"
"syscall"
)
func cmdSysProcAttr() *syscall.SysProcAttr {
return &syscall.SysProcAttr{}
}
func cmdCancel(cmd *exec.Cmd) func() error {
return func() error {
return cmd.Process.Signal(os.Interrupt)
}
}
+31 -4
View File
@@ -19,6 +19,7 @@ import (
"time"
"github.com/gliderlabs/ssh"
"github.com/kballard/go-shellquote"
"github.com/pkg/sftp"
"github.com/prometheus/client_golang/prometheus"
"github.com/spf13/afero"
@@ -254,11 +255,13 @@ func (s *Server) sessionStart(session ssh.Session, extraEnv []string) (retErr er
magicType = strings.TrimPrefix(kv, MagicSessionTypeEnvironmentVariable+"=")
env = append(env[:index], env[index+1:]...)
}
switch magicType {
case MagicSessionTypeVSCode:
// Always force lowercase checking to be case-insensitive.
switch strings.ToLower(magicType) {
case strings.ToLower(MagicSessionTypeVSCode):
s.connCountVSCode.Add(1)
defer s.connCountVSCode.Add(-1)
case MagicSessionTypeJetBrains:
case strings.ToLower(MagicSessionTypeJetBrains):
s.connCountJetBrains.Add(1)
defer s.connCountJetBrains.Add(-1)
case "":
@@ -513,8 +516,32 @@ func (s *Server) CreateCommand(ctx context.Context, script string, env []string)
if runtime.GOOS == "windows" {
caller = "/c"
}
name := shell
args := []string{caller, script}
// A preceding space is generally not idiomatic for a shebang,
// but in Terraform it's quite standard to use <<EOF for a multi-line
// string which would indent with spaces, so we accept it for user-ease.
if strings.HasPrefix(strings.TrimSpace(script), "#!") {
// If the script starts with a shebang, we should
// execute it directly. This is useful for running
// scripts that aren't executable.
shebang := strings.SplitN(strings.TrimSpace(script), "\n", 2)[0]
shebang = strings.TrimSpace(shebang)
shebang = strings.TrimPrefix(shebang, "#!")
words, err := shellquote.Split(shebang)
if err != nil {
return nil, xerrors.Errorf("split shebang: %w", err)
}
name = words[0]
if len(words) > 1 {
args = words[1:]
} else {
args = []string{}
}
args = append(args, caller, script)
}
// gliderlabs/ssh returns a command slice of zero
// when a shell is requested.
if len(script) == 0 {
@@ -526,7 +553,7 @@ func (s *Server) CreateCommand(ctx context.Context, script string, env []string)
}
}
cmd := pty.CommandContext(ctx, shell, args...)
cmd := pty.CommandContext(ctx, name, args...)
cmd.Dir = manifest.Directory
// If the metadata directory doesn't exist, we run the command
+37
View File
@@ -6,6 +6,7 @@ import (
"bytes"
"context"
"net"
"runtime"
"strings"
"sync"
"testing"
@@ -71,6 +72,42 @@ func TestNewServer_ServeClient(t *testing.T) {
<-done
}
func TestNewServer_ExecuteShebang(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("bash doesn't exist on Windows")
}
ctx := context.Background()
logger := slogtest.Make(t, nil)
s, err := agentssh.NewServer(ctx, logger, prometheus.NewRegistry(), afero.NewMemMapFs(), 0, "")
require.NoError(t, err)
t.Cleanup(func() {
_ = s.Close()
})
s.AgentToken = func() string { return "" }
s.Manifest = atomic.NewPointer(&agentsdk.Manifest{})
t.Run("Basic", func(t *testing.T) {
t.Parallel()
cmd, err := s.CreateCommand(ctx, `#!/bin/bash
echo test`, nil)
require.NoError(t, err)
output, err := cmd.AsExec().CombinedOutput()
require.NoError(t, err)
require.Equal(t, "test\n", string(output))
})
t.Run("Args", func(t *testing.T) {
t.Parallel()
cmd, err := s.CreateCommand(ctx, `#!/usr/bin/env bash
echo test`, nil)
require.NoError(t, err)
output, err := cmd.AsExec().CombinedOutput()
require.NoError(t, err)
require.Equal(t, "test\n", string(output))
})
}
func TestNewServer_CloseActiveConnections(t *testing.T) {
t.Parallel()
+4 -1
View File
@@ -1,6 +1,8 @@
package agentssh
import (
"strings"
"github.com/prometheus/client_golang/prometheus"
)
@@ -78,5 +80,6 @@ func magicTypeMetricLabel(magicType string) string {
default:
magicType = "unknown"
}
return magicType
// Always be case insensitive
return strings.ToLower(magicType)
}
+57
View File
@@ -0,0 +1,57 @@
package agenttest
import (
"context"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/agent"
"github.com/coder/coder/v2/codersdk/agentsdk"
)
// New starts a new agent for use in tests.
// The agent will use the provided coder URL and session token.
// The options passed to agent.New() can be modified by passing an optional
// variadic func(*agent.Options).
// Returns the agent. Closing the agent is handled by the test cleanup.
// It is the responsibility of the caller to call coderdtest.AwaitWorkspaceAgents
// to ensure agent is connected.
func New(t testing.TB, coderURL *url.URL, agentToken string, opts ...func(*agent.Options)) agent.Agent {
t.Helper()
var o agent.Options
log := slogtest.Make(t, nil).Leveled(slog.LevelDebug).Named("agent")
o.Logger = log
for _, opt := range opts {
opt(&o)
}
if o.Client == nil {
agentClient := agentsdk.New(coderURL)
agentClient.SetSessionToken(agentToken)
agentClient.SDK.SetLogger(log)
o.Client = agentClient
}
if o.ExchangeToken == nil {
o.ExchangeToken = func(_ context.Context) (string, error) {
return agentToken, nil
}
}
if o.LogDir == "" {
o.LogDir = t.TempDir()
}
agt := agent.New(o)
t.Cleanup(func() {
assert.NoError(t, agt.Close(), "failed to close agent during cleanup")
})
return agt
}
+8 -6
View File
@@ -45,7 +45,7 @@ type Client struct {
logger slog.Logger
agentID uuid.UUID
manifest agentsdk.Manifest
metadata map[string]agentsdk.PostMetadataRequest
metadata map[string]agentsdk.Metadata
statsChan chan *agentsdk.Stats
coordinator tailnet.Coordinator
LastWorkspaceAgent func()
@@ -136,20 +136,22 @@ func (c *Client) GetStartup() agentsdk.PostStartupRequest {
return c.startup
}
func (c *Client) GetMetadata() map[string]agentsdk.PostMetadataRequest {
func (c *Client) GetMetadata() map[string]agentsdk.Metadata {
c.mu.Lock()
defer c.mu.Unlock()
return maps.Clone(c.metadata)
}
func (c *Client) PostMetadata(ctx context.Context, key string, req agentsdk.PostMetadataRequest) error {
func (c *Client) PostMetadata(ctx context.Context, req agentsdk.PostMetadataRequest) error {
c.mu.Lock()
defer c.mu.Unlock()
if c.metadata == nil {
c.metadata = make(map[string]agentsdk.PostMetadataRequest)
c.metadata = make(map[string]agentsdk.Metadata)
}
for _, md := range req.Metadata {
c.metadata[md.Key] = md
c.logger.Debug(ctx, "post metadata", slog.F("key", md.Key), slog.F("md", md))
}
c.metadata[key] = req
c.logger.Debug(ctx, "post metadata", slog.F("key", key), slog.F("req", req))
return nil
}
File diff suppressed because it is too large Load Diff
+211
View File
@@ -0,0 +1,211 @@
syntax = "proto3";
option go_package = "github.com/coder/coder/v2/agent/proto";
package coder.agent.v2;
import "tailnet/proto/tailnet.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";
message WorkspaceApp {
bytes uuid = 1;
string url = 2;
bool external = 3;
string slug = 4;
string display_name = 5;
string command = 6;
string icon = 7;
bool subdomain = 8;
string subdomain_name = 9;
enum SharingLevel {
SHARING_LEVEL_UNSPECIFIED = 0;
OWNER = 1;
AUTHENTICATED = 2;
PUBLIC = 3;
}
SharingLevel sharing_level = 10;
message HealthCheck {
string url = 1;
int32 interval = 2;
int32 threshold = 3;
}
HealthCheck healthcheck = 11;
enum Health {
HEALTH_UNSPECIFIED = 0;
DISABLED = 1;
INITIALIZING = 2;
HEALTHY = 3;
UNHEALTHY = 4;
}
Health health = 12;
}
message Manifest {
uint32 git_auth_configs = 1;
string vs_code_port_proxy_uri = 2;
repeated WorkspaceApp apps = 3;
coder.tailnet.v2.DERPMap derp_map = 4;
}
message GetManifestRequest {}
message ServiceBanner {
bool enabled = 1;
string message = 2;
string background_color = 3;
}
message GetServiceBannerRequest {}
message Stats {
// ConnectionsByProto is a count of connections by protocol.
map<string, int64> connections_by_proto = 1;
// ConnectionCount is the number of connections received by an agent.
int64 connection_count = 2;
// ConnectionMedianLatencyMS is the median latency of all connections in milliseconds.
double connection_median_latency_ms = 3;
// RxPackets is the number of received packets.
int64 rx_packets = 4;
// RxBytes is the number of received bytes.
int64 rx_bytes = 5;
// TxPackets is the number of transmitted bytes.
int64 tx_packets = 6;
// TxBytes is the number of transmitted bytes.
int64 tx_bytes = 7;
// SessionCountVSCode is the number of connections received by an agent
// that are from our VS Code extension.
int64 session_count_vscode = 8;
// SessionCountJetBrains is the number of connections received by an agent
// that are from our JetBrains extension.
int64 session_count_jetbrains = 9;
// SessionCountReconnectingPTY is the number of connections received by an agent
// that are from the reconnecting web terminal.
int64 session_count_reconnecting_pty = 10;
// SessionCountSSH is the number of connections received by an agent
// that are normal, non-tagged SSH sessions.
int64 session_count_ssh = 11;
message Metric {
string name = 1;
enum Type {
TYPE_UNSPECIFIED = 0;
COUNTER = 1;
GAUGE = 2;
}
Type type = 2;
double value = 3;
map<string, string> labels = 4;
}
}
message UpdateStatsRequest{
Stats stats = 1;
}
message UpdateStatsResponse {
google.protobuf.Duration report_interval_nanoseconds = 1;
}
message Lifecycle {
enum State {
STATE_UNSPECIFIED = 0;
CREATED = 1;
STARTED = 2;
START_TIMEOUT = 3;
START_ERROR = 4;
READY = 5;
SHUTTING_DOWN = 6;
SHUTDOWN_TIMEOUT = 7;
SHUTDOWN_ERROR = 8;
OFF = 9;
}
State state = 1;
}
message UpdateLifecycleRequest {
Lifecycle lifecycle = 1;
}
enum AppHealth {
APP_HEALTH_UNSPECIFIED = 0;
DISABLED = 1;
INITIALIZING = 2;
HEALTHY = 3;
UNHEALTHY = 4;
}
message BatchUpdateAppHealthRequest {
message HealthUpdate {
bytes uuid = 1;
AppHealth health = 2;
}
repeated HealthUpdate updates = 1;
}
message BatchUpdateAppHealthResponse {}
message Startup {
string version = 1;
string expanded_directory = 2;
repeated string subsystems = 3;
}
message UpdateStartupRequest{
Startup startup = 1;
}
message Metadata {
string key = 1;
google.protobuf.Timestamp collected_at = 2;
int64 age = 3;
string value = 4;
string error = 5;
}
message BatchUpdateMetadataRequest {
repeated Metadata metadata = 2;
}
message BatchUpdateMetadataResponse {}
message Log {
google.protobuf.Timestamp created_at = 1;
string output = 2;
enum Level {
LEVEL_UNSPECIFIED = 0;
TRACE = 1;
DEBUG = 2;
INFO = 3;
WARN = 4;
ERROR = 5;
}
Level level = 3;
}
message BatchCreateLogsRequest {
bytes source_id = 1;
repeated Log logs = 2;
}
message BatchCreateLogsResponse {}
service Agent {
rpc GetManifest(GetManifestRequest) returns (Manifest);
rpc GetServiceBanner(GetServiceBannerRequest) returns (ServiceBanner);
rpc UpdateStats(UpdateStatsRequest) returns (UpdateStatsResponse);
rpc UpdateLifecycle(UpdateLifecycleRequest) returns (Lifecycle);
rpc BatchUpdateAppHealths(BatchUpdateAppHealthRequest) returns (BatchUpdateAppHealthResponse);
rpc UpdateStartup(UpdateStartupRequest) returns (Startup);
rpc BatchUpdateMetadata(BatchUpdateMetadataRequest) returns (BatchUpdateMetadataResponse);
rpc BatchCreateLogs(BatchCreateLogsRequest) returns (BatchCreateLogsResponse);
rpc StreamDERPMaps(tailnet.v2.StreamDERPMapsRequest) returns (stream tailnet.v2.DERPMap);
rpc CoordinateTailnet(stream tailnet.v2.CoordinateRequest) returns (stream tailnet.v2.CoordinateResponse);
}
+539
View File
@@ -0,0 +1,539 @@
// Code generated by protoc-gen-go-drpc. DO NOT EDIT.
// protoc-gen-go-drpc version: v0.0.33
// source: agent/proto/agent.proto
package proto
import (
context "context"
errors "errors"
proto1 "github.com/coder/coder/v2/tailnet/proto"
protojson "google.golang.org/protobuf/encoding/protojson"
proto "google.golang.org/protobuf/proto"
drpc "storj.io/drpc"
drpcerr "storj.io/drpc/drpcerr"
)
type drpcEncoding_File_agent_proto_agent_proto struct{}
func (drpcEncoding_File_agent_proto_agent_proto) Marshal(msg drpc.Message) ([]byte, error) {
return proto.Marshal(msg.(proto.Message))
}
func (drpcEncoding_File_agent_proto_agent_proto) MarshalAppend(buf []byte, msg drpc.Message) ([]byte, error) {
return proto.MarshalOptions{}.MarshalAppend(buf, msg.(proto.Message))
}
func (drpcEncoding_File_agent_proto_agent_proto) Unmarshal(buf []byte, msg drpc.Message) error {
return proto.Unmarshal(buf, msg.(proto.Message))
}
func (drpcEncoding_File_agent_proto_agent_proto) JSONMarshal(msg drpc.Message) ([]byte, error) {
return protojson.Marshal(msg.(proto.Message))
}
func (drpcEncoding_File_agent_proto_agent_proto) JSONUnmarshal(buf []byte, msg drpc.Message) error {
return protojson.Unmarshal(buf, msg.(proto.Message))
}
type DRPCAgentClient interface {
DRPCConn() drpc.Conn
GetManifest(ctx context.Context, in *GetManifestRequest) (*Manifest, error)
GetServiceBanner(ctx context.Context, in *GetServiceBannerRequest) (*ServiceBanner, error)
UpdateStats(ctx context.Context, in *UpdateStatsRequest) (*UpdateStatsResponse, error)
UpdateLifecycle(ctx context.Context, in *UpdateLifecycleRequest) (*Lifecycle, error)
BatchUpdateAppHealths(ctx context.Context, in *BatchUpdateAppHealthRequest) (*BatchUpdateAppHealthResponse, error)
UpdateStartup(ctx context.Context, in *UpdateStartupRequest) (*Startup, error)
BatchUpdateMetadata(ctx context.Context, in *BatchUpdateMetadataRequest) (*BatchUpdateMetadataResponse, error)
BatchCreateLogs(ctx context.Context, in *BatchCreateLogsRequest) (*BatchCreateLogsResponse, error)
StreamDERPMaps(ctx context.Context, in *proto1.StreamDERPMapsRequest) (DRPCAgent_StreamDERPMapsClient, error)
CoordinateTailnet(ctx context.Context) (DRPCAgent_CoordinateTailnetClient, error)
}
type drpcAgentClient struct {
cc drpc.Conn
}
func NewDRPCAgentClient(cc drpc.Conn) DRPCAgentClient {
return &drpcAgentClient{cc}
}
func (c *drpcAgentClient) DRPCConn() drpc.Conn { return c.cc }
func (c *drpcAgentClient) GetManifest(ctx context.Context, in *GetManifestRequest) (*Manifest, error) {
out := new(Manifest)
err := c.cc.Invoke(ctx, "/coder.agent.v2.Agent/GetManifest", drpcEncoding_File_agent_proto_agent_proto{}, in, out)
if err != nil {
return nil, err
}
return out, nil
}
func (c *drpcAgentClient) GetServiceBanner(ctx context.Context, in *GetServiceBannerRequest) (*ServiceBanner, error) {
out := new(ServiceBanner)
err := c.cc.Invoke(ctx, "/coder.agent.v2.Agent/GetServiceBanner", drpcEncoding_File_agent_proto_agent_proto{}, in, out)
if err != nil {
return nil, err
}
return out, nil
}
func (c *drpcAgentClient) UpdateStats(ctx context.Context, in *UpdateStatsRequest) (*UpdateStatsResponse, error) {
out := new(UpdateStatsResponse)
err := c.cc.Invoke(ctx, "/coder.agent.v2.Agent/UpdateStats", drpcEncoding_File_agent_proto_agent_proto{}, in, out)
if err != nil {
return nil, err
}
return out, nil
}
func (c *drpcAgentClient) UpdateLifecycle(ctx context.Context, in *UpdateLifecycleRequest) (*Lifecycle, error) {
out := new(Lifecycle)
err := c.cc.Invoke(ctx, "/coder.agent.v2.Agent/UpdateLifecycle", drpcEncoding_File_agent_proto_agent_proto{}, in, out)
if err != nil {
return nil, err
}
return out, nil
}
func (c *drpcAgentClient) BatchUpdateAppHealths(ctx context.Context, in *BatchUpdateAppHealthRequest) (*BatchUpdateAppHealthResponse, error) {
out := new(BatchUpdateAppHealthResponse)
err := c.cc.Invoke(ctx, "/coder.agent.v2.Agent/BatchUpdateAppHealths", drpcEncoding_File_agent_proto_agent_proto{}, in, out)
if err != nil {
return nil, err
}
return out, nil
}
func (c *drpcAgentClient) UpdateStartup(ctx context.Context, in *UpdateStartupRequest) (*Startup, error) {
out := new(Startup)
err := c.cc.Invoke(ctx, "/coder.agent.v2.Agent/UpdateStartup", drpcEncoding_File_agent_proto_agent_proto{}, in, out)
if err != nil {
return nil, err
}
return out, nil
}
func (c *drpcAgentClient) BatchUpdateMetadata(ctx context.Context, in *BatchUpdateMetadataRequest) (*BatchUpdateMetadataResponse, error) {
out := new(BatchUpdateMetadataResponse)
err := c.cc.Invoke(ctx, "/coder.agent.v2.Agent/BatchUpdateMetadata", drpcEncoding_File_agent_proto_agent_proto{}, in, out)
if err != nil {
return nil, err
}
return out, nil
}
func (c *drpcAgentClient) BatchCreateLogs(ctx context.Context, in *BatchCreateLogsRequest) (*BatchCreateLogsResponse, error) {
out := new(BatchCreateLogsResponse)
err := c.cc.Invoke(ctx, "/coder.agent.v2.Agent/BatchCreateLogs", drpcEncoding_File_agent_proto_agent_proto{}, in, out)
if err != nil {
return nil, err
}
return out, nil
}
func (c *drpcAgentClient) StreamDERPMaps(ctx context.Context, in *proto1.StreamDERPMapsRequest) (DRPCAgent_StreamDERPMapsClient, error) {
stream, err := c.cc.NewStream(ctx, "/coder.agent.v2.Agent/StreamDERPMaps", drpcEncoding_File_agent_proto_agent_proto{})
if err != nil {
return nil, err
}
x := &drpcAgent_StreamDERPMapsClient{stream}
if err := x.MsgSend(in, drpcEncoding_File_agent_proto_agent_proto{}); err != nil {
return nil, err
}
if err := x.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
type DRPCAgent_StreamDERPMapsClient interface {
drpc.Stream
Recv() (*proto1.DERPMap, error)
}
type drpcAgent_StreamDERPMapsClient struct {
drpc.Stream
}
func (x *drpcAgent_StreamDERPMapsClient) GetStream() drpc.Stream {
return x.Stream
}
func (x *drpcAgent_StreamDERPMapsClient) Recv() (*proto1.DERPMap, error) {
m := new(proto1.DERPMap)
if err := x.MsgRecv(m, drpcEncoding_File_agent_proto_agent_proto{}); err != nil {
return nil, err
}
return m, nil
}
func (x *drpcAgent_StreamDERPMapsClient) RecvMsg(m *proto1.DERPMap) error {
return x.MsgRecv(m, drpcEncoding_File_agent_proto_agent_proto{})
}
func (c *drpcAgentClient) CoordinateTailnet(ctx context.Context) (DRPCAgent_CoordinateTailnetClient, error) {
stream, err := c.cc.NewStream(ctx, "/coder.agent.v2.Agent/CoordinateTailnet", drpcEncoding_File_agent_proto_agent_proto{})
if err != nil {
return nil, err
}
x := &drpcAgent_CoordinateTailnetClient{stream}
return x, nil
}
type DRPCAgent_CoordinateTailnetClient interface {
drpc.Stream
Send(*proto1.CoordinateRequest) error
Recv() (*proto1.CoordinateResponse, error)
}
type drpcAgent_CoordinateTailnetClient struct {
drpc.Stream
}
func (x *drpcAgent_CoordinateTailnetClient) GetStream() drpc.Stream {
return x.Stream
}
func (x *drpcAgent_CoordinateTailnetClient) Send(m *proto1.CoordinateRequest) error {
return x.MsgSend(m, drpcEncoding_File_agent_proto_agent_proto{})
}
func (x *drpcAgent_CoordinateTailnetClient) Recv() (*proto1.CoordinateResponse, error) {
m := new(proto1.CoordinateResponse)
if err := x.MsgRecv(m, drpcEncoding_File_agent_proto_agent_proto{}); err != nil {
return nil, err
}
return m, nil
}
func (x *drpcAgent_CoordinateTailnetClient) RecvMsg(m *proto1.CoordinateResponse) error {
return x.MsgRecv(m, drpcEncoding_File_agent_proto_agent_proto{})
}
type DRPCAgentServer interface {
GetManifest(context.Context, *GetManifestRequest) (*Manifest, error)
GetServiceBanner(context.Context, *GetServiceBannerRequest) (*ServiceBanner, error)
UpdateStats(context.Context, *UpdateStatsRequest) (*UpdateStatsResponse, error)
UpdateLifecycle(context.Context, *UpdateLifecycleRequest) (*Lifecycle, error)
BatchUpdateAppHealths(context.Context, *BatchUpdateAppHealthRequest) (*BatchUpdateAppHealthResponse, error)
UpdateStartup(context.Context, *UpdateStartupRequest) (*Startup, error)
BatchUpdateMetadata(context.Context, *BatchUpdateMetadataRequest) (*BatchUpdateMetadataResponse, error)
BatchCreateLogs(context.Context, *BatchCreateLogsRequest) (*BatchCreateLogsResponse, error)
StreamDERPMaps(*proto1.StreamDERPMapsRequest, DRPCAgent_StreamDERPMapsStream) error
CoordinateTailnet(DRPCAgent_CoordinateTailnetStream) error
}
type DRPCAgentUnimplementedServer struct{}
func (s *DRPCAgentUnimplementedServer) GetManifest(context.Context, *GetManifestRequest) (*Manifest, error) {
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
}
func (s *DRPCAgentUnimplementedServer) GetServiceBanner(context.Context, *GetServiceBannerRequest) (*ServiceBanner, error) {
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
}
func (s *DRPCAgentUnimplementedServer) UpdateStats(context.Context, *UpdateStatsRequest) (*UpdateStatsResponse, error) {
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
}
func (s *DRPCAgentUnimplementedServer) UpdateLifecycle(context.Context, *UpdateLifecycleRequest) (*Lifecycle, error) {
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
}
func (s *DRPCAgentUnimplementedServer) BatchUpdateAppHealths(context.Context, *BatchUpdateAppHealthRequest) (*BatchUpdateAppHealthResponse, error) {
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
}
func (s *DRPCAgentUnimplementedServer) UpdateStartup(context.Context, *UpdateStartupRequest) (*Startup, error) {
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
}
func (s *DRPCAgentUnimplementedServer) BatchUpdateMetadata(context.Context, *BatchUpdateMetadataRequest) (*BatchUpdateMetadataResponse, error) {
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
}
func (s *DRPCAgentUnimplementedServer) BatchCreateLogs(context.Context, *BatchCreateLogsRequest) (*BatchCreateLogsResponse, error) {
return nil, drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
}
func (s *DRPCAgentUnimplementedServer) StreamDERPMaps(*proto1.StreamDERPMapsRequest, DRPCAgent_StreamDERPMapsStream) error {
return drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
}
func (s *DRPCAgentUnimplementedServer) CoordinateTailnet(DRPCAgent_CoordinateTailnetStream) error {
return drpcerr.WithCode(errors.New("Unimplemented"), drpcerr.Unimplemented)
}
type DRPCAgentDescription struct{}
func (DRPCAgentDescription) NumMethods() int { return 10 }
func (DRPCAgentDescription) Method(n int) (string, drpc.Encoding, drpc.Receiver, interface{}, bool) {
switch n {
case 0:
return "/coder.agent.v2.Agent/GetManifest", drpcEncoding_File_agent_proto_agent_proto{},
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
return srv.(DRPCAgentServer).
GetManifest(
ctx,
in1.(*GetManifestRequest),
)
}, DRPCAgentServer.GetManifest, true
case 1:
return "/coder.agent.v2.Agent/GetServiceBanner", drpcEncoding_File_agent_proto_agent_proto{},
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
return srv.(DRPCAgentServer).
GetServiceBanner(
ctx,
in1.(*GetServiceBannerRequest),
)
}, DRPCAgentServer.GetServiceBanner, true
case 2:
return "/coder.agent.v2.Agent/UpdateStats", drpcEncoding_File_agent_proto_agent_proto{},
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
return srv.(DRPCAgentServer).
UpdateStats(
ctx,
in1.(*UpdateStatsRequest),
)
}, DRPCAgentServer.UpdateStats, true
case 3:
return "/coder.agent.v2.Agent/UpdateLifecycle", drpcEncoding_File_agent_proto_agent_proto{},
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
return srv.(DRPCAgentServer).
UpdateLifecycle(
ctx,
in1.(*UpdateLifecycleRequest),
)
}, DRPCAgentServer.UpdateLifecycle, true
case 4:
return "/coder.agent.v2.Agent/BatchUpdateAppHealths", drpcEncoding_File_agent_proto_agent_proto{},
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
return srv.(DRPCAgentServer).
BatchUpdateAppHealths(
ctx,
in1.(*BatchUpdateAppHealthRequest),
)
}, DRPCAgentServer.BatchUpdateAppHealths, true
case 5:
return "/coder.agent.v2.Agent/UpdateStartup", drpcEncoding_File_agent_proto_agent_proto{},
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
return srv.(DRPCAgentServer).
UpdateStartup(
ctx,
in1.(*UpdateStartupRequest),
)
}, DRPCAgentServer.UpdateStartup, true
case 6:
return "/coder.agent.v2.Agent/BatchUpdateMetadata", drpcEncoding_File_agent_proto_agent_proto{},
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
return srv.(DRPCAgentServer).
BatchUpdateMetadata(
ctx,
in1.(*BatchUpdateMetadataRequest),
)
}, DRPCAgentServer.BatchUpdateMetadata, true
case 7:
return "/coder.agent.v2.Agent/BatchCreateLogs", drpcEncoding_File_agent_proto_agent_proto{},
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
return srv.(DRPCAgentServer).
BatchCreateLogs(
ctx,
in1.(*BatchCreateLogsRequest),
)
}, DRPCAgentServer.BatchCreateLogs, true
case 8:
return "/coder.agent.v2.Agent/StreamDERPMaps", drpcEncoding_File_agent_proto_agent_proto{},
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
return nil, srv.(DRPCAgentServer).
StreamDERPMaps(
in1.(*proto1.StreamDERPMapsRequest),
&drpcAgent_StreamDERPMapsStream{in2.(drpc.Stream)},
)
}, DRPCAgentServer.StreamDERPMaps, true
case 9:
return "/coder.agent.v2.Agent/CoordinateTailnet", drpcEncoding_File_agent_proto_agent_proto{},
func(srv interface{}, ctx context.Context, in1, in2 interface{}) (drpc.Message, error) {
return nil, srv.(DRPCAgentServer).
CoordinateTailnet(
&drpcAgent_CoordinateTailnetStream{in1.(drpc.Stream)},
)
}, DRPCAgentServer.CoordinateTailnet, true
default:
return "", nil, nil, nil, false
}
}
func DRPCRegisterAgent(mux drpc.Mux, impl DRPCAgentServer) error {
return mux.Register(impl, DRPCAgentDescription{})
}
type DRPCAgent_GetManifestStream interface {
drpc.Stream
SendAndClose(*Manifest) error
}
type drpcAgent_GetManifestStream struct {
drpc.Stream
}
func (x *drpcAgent_GetManifestStream) SendAndClose(m *Manifest) error {
if err := x.MsgSend(m, drpcEncoding_File_agent_proto_agent_proto{}); err != nil {
return err
}
return x.CloseSend()
}
type DRPCAgent_GetServiceBannerStream interface {
drpc.Stream
SendAndClose(*ServiceBanner) error
}
type drpcAgent_GetServiceBannerStream struct {
drpc.Stream
}
func (x *drpcAgent_GetServiceBannerStream) SendAndClose(m *ServiceBanner) error {
if err := x.MsgSend(m, drpcEncoding_File_agent_proto_agent_proto{}); err != nil {
return err
}
return x.CloseSend()
}
type DRPCAgent_UpdateStatsStream interface {
drpc.Stream
SendAndClose(*UpdateStatsResponse) error
}
type drpcAgent_UpdateStatsStream struct {
drpc.Stream
}
func (x *drpcAgent_UpdateStatsStream) SendAndClose(m *UpdateStatsResponse) error {
if err := x.MsgSend(m, drpcEncoding_File_agent_proto_agent_proto{}); err != nil {
return err
}
return x.CloseSend()
}
type DRPCAgent_UpdateLifecycleStream interface {
drpc.Stream
SendAndClose(*Lifecycle) error
}
type drpcAgent_UpdateLifecycleStream struct {
drpc.Stream
}
func (x *drpcAgent_UpdateLifecycleStream) SendAndClose(m *Lifecycle) error {
if err := x.MsgSend(m, drpcEncoding_File_agent_proto_agent_proto{}); err != nil {
return err
}
return x.CloseSend()
}
type DRPCAgent_BatchUpdateAppHealthsStream interface {
drpc.Stream
SendAndClose(*BatchUpdateAppHealthResponse) error
}
type drpcAgent_BatchUpdateAppHealthsStream struct {
drpc.Stream
}
func (x *drpcAgent_BatchUpdateAppHealthsStream) SendAndClose(m *BatchUpdateAppHealthResponse) error {
if err := x.MsgSend(m, drpcEncoding_File_agent_proto_agent_proto{}); err != nil {
return err
}
return x.CloseSend()
}
type DRPCAgent_UpdateStartupStream interface {
drpc.Stream
SendAndClose(*Startup) error
}
type drpcAgent_UpdateStartupStream struct {
drpc.Stream
}
func (x *drpcAgent_UpdateStartupStream) SendAndClose(m *Startup) error {
if err := x.MsgSend(m, drpcEncoding_File_agent_proto_agent_proto{}); err != nil {
return err
}
return x.CloseSend()
}
type DRPCAgent_BatchUpdateMetadataStream interface {
drpc.Stream
SendAndClose(*BatchUpdateMetadataResponse) error
}
type drpcAgent_BatchUpdateMetadataStream struct {
drpc.Stream
}
func (x *drpcAgent_BatchUpdateMetadataStream) SendAndClose(m *BatchUpdateMetadataResponse) error {
if err := x.MsgSend(m, drpcEncoding_File_agent_proto_agent_proto{}); err != nil {
return err
}
return x.CloseSend()
}
type DRPCAgent_BatchCreateLogsStream interface {
drpc.Stream
SendAndClose(*BatchCreateLogsResponse) error
}
type drpcAgent_BatchCreateLogsStream struct {
drpc.Stream
}
func (x *drpcAgent_BatchCreateLogsStream) SendAndClose(m *BatchCreateLogsResponse) error {
if err := x.MsgSend(m, drpcEncoding_File_agent_proto_agent_proto{}); err != nil {
return err
}
return x.CloseSend()
}
type DRPCAgent_StreamDERPMapsStream interface {
drpc.Stream
Send(*proto1.DERPMap) error
}
type drpcAgent_StreamDERPMapsStream struct {
drpc.Stream
}
func (x *drpcAgent_StreamDERPMapsStream) Send(m *proto1.DERPMap) error {
return x.MsgSend(m, drpcEncoding_File_agent_proto_agent_proto{})
}
type DRPCAgent_CoordinateTailnetStream interface {
drpc.Stream
Send(*proto1.CoordinateResponse) error
Recv() (*proto1.CoordinateRequest, error)
}
type drpcAgent_CoordinateTailnetStream struct {
drpc.Stream
}
func (x *drpcAgent_CoordinateTailnetStream) Send(m *proto1.CoordinateResponse) error {
return x.MsgSend(m, drpcEncoding_File_agent_proto_agent_proto{})
}
func (x *drpcAgent_CoordinateTailnetStream) Recv() (*proto1.CoordinateRequest, error) {
m := new(proto1.CoordinateRequest)
if err := x.MsgRecv(m, drpcEncoding_File_agent_proto_agent_proto{}); err != nil {
return nil, err
}
return m, nil
}
func (x *drpcAgent_CoordinateTailnetStream) RecvMsg(m *proto1.CoordinateRequest) error {
return x.MsgRecv(m, drpcEncoding_File_agent_proto_agent_proto{})
}
+1 -1
View File
@@ -196,8 +196,8 @@ func (s *ptyState) waitForStateOrContext(ctx context.Context, state State) (Stat
// until EOF or an error writing to ptty or reading from conn.
func readConnLoop(ctx context.Context, conn net.Conn, ptty pty.PTYCmd, metrics *prometheus.CounterVec, logger slog.Logger) {
decoder := json.NewDecoder(conn)
var req codersdk.ReconnectingPTYRequest
for {
var req codersdk.ReconnectingPTYRequest
err := decoder.Decode(&req)
if xerrors.Is(err, io.EOF) {
return
+2 -1
View File
@@ -206,12 +206,13 @@ func (rpty *screenReconnectingPTY) doAttach(ctx context.Context, conn net.Conn,
cmd := pty.CommandContext(ctx, "screen", append([]string{
// -S is for setting the session's name.
"-S", rpty.id,
// -U tells screen to use UTF-8 encoding.
// -x allows attaching to an already attached session.
// -RR reattaches to the daemon or creates the session daemon if missing.
// -q disables the "New screen..." message that appears for five seconds
// when creating a new session with -RR.
// -c is the flag for the config file.
"-xRRqc", rpty.configFile,
"-UxRRqc", rpty.configFile,
rpty.command.Path,
// pty.Cmd duplicates Path as the first argument so remove it.
}, rpty.command.Args[1:]...)...)
+7
View File
@@ -0,0 +1,7 @@
//go:build boringcrypto
package buildinfo
import "crypto/boring"
var boringcrypto = boring.Enabled()
+24 -7
View File
@@ -30,8 +30,15 @@ var (
)
const (
// develPrefix is prefixed to developer versions of the application.
develPrefix = "v0.0.0-devel"
// noVersion is the reported version when the version cannot be determined.
// Usually because `go build` is run instead of `make build`.
noVersion = "v0.0.0"
// develPreRelease is the pre-release tag for developer versions of the
// application. This includes CI builds. The pre-release tag should be appended
// to the version with a "-".
// Example: v0.0.0-devel
develPreRelease = "devel"
)
// Version returns the semantic version of the build.
@@ -45,7 +52,8 @@ func Version() string {
if tag == "" {
// This occurs when the tag hasn't been injected,
// like when using "go run".
version = develPrefix + revision
// <version>-<pre-release>+<revision>
version = fmt.Sprintf("%s-%s%s", noVersion, develPreRelease, revision)
return
}
version = "v" + tag
@@ -63,18 +71,23 @@ func Version() string {
// disregarded. If it detects that either version is a developer build it
// returns true.
func VersionsMatch(v1, v2 string) bool {
// Developer versions are disregarded...hopefully they know what they are
// doing.
if strings.HasPrefix(v1, develPrefix) || strings.HasPrefix(v2, develPrefix) {
// If no version is attached, then it is a dev build outside of CI. The version
// will be disregarded... hopefully they know what they are doing.
if strings.Contains(v1, noVersion) || strings.Contains(v2, noVersion) {
return true
}
return semver.MajorMinor(v1) == semver.MajorMinor(v2)
}
func IsDevVersion(v string) bool {
return strings.Contains(v, "-"+develPreRelease)
}
// IsDev returns true if this is a development build.
// CI builds are also considered development builds.
func IsDev() bool {
return strings.HasPrefix(Version(), develPrefix)
return IsDevVersion(Version())
}
// IsSlim returns true if this is a slim build.
@@ -87,6 +100,10 @@ func IsAGPL() bool {
return strings.Contains(agpl, "t")
}
func IsBoringCrypto() bool {
return boringcrypto
}
// ExternalURL returns a URL referencing the current Coder version.
// For production builds, this will link directly to a release.
// For development builds, this will link to a commit.
+8 -2
View File
@@ -57,13 +57,19 @@ func TestBuildInfo(t *testing.T) {
expectMatch: true,
},
// Our CI instance uses a "-devel" prerelease
// flag. This is not the same as a developer WIP build.
// flag.
{
name: "DevelPreleaseNotIgnored",
name: "DevelPreleaseMajor",
v1: "v1.1.1-devel+123abac",
v2: "v1.2.3",
expectMatch: false,
},
{
name: "DevelPreleaseSame",
v1: "v1.1.1-devel+123abac",
v2: "v1.1.9",
expectMatch: true,
},
{
name: "MajorMismatch",
v1: "v1.2.3",
+5
View File
@@ -0,0 +1,5 @@
//go:build !boringcrypto
package buildinfo
var boringcrypto = false
+22 -4
View File
@@ -29,6 +29,7 @@ import (
"cdr.dev/slog/sloggers/slogjson"
"cdr.dev/slog/sloggers/slogstackdriver"
"github.com/coder/coder/v2/agent"
"github.com/coder/coder/v2/agent/agentproc"
"github.com/coder/coder/v2/agent/reaper"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/cli/clibase"
@@ -198,9 +199,19 @@ func (r *RootCmd) workspaceAgent() *clibase.Cmd {
var exchangeToken func(context.Context) (agentsdk.AuthenticateResponse, error)
switch auth {
case "token":
token, err := inv.ParsedFlags().GetString(varAgentToken)
if err != nil {
return xerrors.Errorf("CODER_AGENT_TOKEN must be set for token auth: %w", err)
token, _ := inv.ParsedFlags().GetString(varAgentToken)
if token == "" {
tokenFile, _ := inv.ParsedFlags().GetString(varAgentTokenFile)
if tokenFile != "" {
tokenBytes, err := os.ReadFile(tokenFile)
if err != nil {
return xerrors.Errorf("read token file %q: %w", tokenFile, err)
}
token = strings.TrimSpace(string(tokenBytes))
}
}
if token == "" {
return xerrors.Errorf("CODER_AGENT_TOKEN or CODER_AGENT_TOKEN_FILE must be set for token auth")
}
client.SetSessionToken(token)
case "google-instance-identity":
@@ -267,6 +278,8 @@ func (r *RootCmd) workspaceAgent() *clibase.Cmd {
subsystems = append(subsystems, subsystem)
}
procTicker := time.NewTicker(time.Second)
defer procTicker.Stop()
agnt := agent.New(agent.Options{
Client: client,
Logger: logger,
@@ -284,13 +297,18 @@ func (r *RootCmd) workspaceAgent() *clibase.Cmd {
return resp.SessionToken, nil
},
EnvironmentVariables: map[string]string{
"GIT_ASKPASS": executablePath,
"GIT_ASKPASS": executablePath,
agent.EnvProcPrioMgmt: os.Getenv(agent.EnvProcPrioMgmt),
},
IgnorePorts: ignorePorts,
SSHMaxTimeout: sshMaxTimeout,
Subsystems: subsystems,
PrometheusRegistry: prometheusRegistry,
Syscaller: agentproc.NewSyscaller(),
// Intentionally set this to nil. It's mainly used
// for testing.
ModifiedProcesses: nil,
})
prometheusSrvClose := ServeHandler(ctx, logger, prometheusMetricsHandler(prometheusRegistry, logger), prometheusAddress, "prometheus")
+16 -15
View File
@@ -38,9 +38,9 @@ func TestWorkspaceAgent(t *testing.T) {
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
logDir := t.TempDir()
inv, _ := clitest.New(t,
@@ -92,9 +92,9 @@ func TestWorkspaceAgent(t *testing.T) {
}},
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
inv, _ := clitest.New(t, "agent", "--auth", "azure-instance-identity", "--agent-url", client.URL.String())
inv = inv.WithContext(
@@ -144,9 +144,9 @@ func TestWorkspaceAgent(t *testing.T) {
}},
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
inv, _ := clitest.New(t, "agent", "--auth", "aws-instance-identity", "--agent-url", client.URL.String())
inv = inv.WithContext(
@@ -176,8 +176,9 @@ func TestWorkspaceAgent(t *testing.T) {
GoogleTokenValidator: validator,
IncludeProvisionerDaemon: true,
})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Response{{
Type: &proto.Response_Apply{
@@ -195,14 +196,14 @@ func TestWorkspaceAgent(t *testing.T) {
},
}},
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, member, owner.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
inv, cfg := clitest.New(t, "agent", "--auth", "google-instance-identity", "--agent-url", client.URL.String())
ptytest.New(t).Attach(inv)
clitest.SetupConfig(t, client, cfg)
clitest.SetupConfig(t, member, cfg)
clitest.Start(t,
inv.WithContext(
//nolint:revive,staticcheck
@@ -253,9 +254,9 @@ func TestWorkspaceAgent(t *testing.T) {
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
logDir := t.TempDir()
inv, _ := clitest.New(t,
+129 -18
View File
@@ -1,6 +1,8 @@
package clibase
import (
"bytes"
"encoding/json"
"os"
"strings"
@@ -65,6 +67,20 @@ type Option struct {
ValueSource ValueSource `json:"value_source,omitempty"`
}
// optionNoMethods is just a wrapper around Option so we can defer to the
// default json.Unmarshaler behavior.
type optionNoMethods Option
func (o *Option) UnmarshalJSON(data []byte) error {
// If an option has no values, we have no idea how to unmarshal it.
// So just discard the json data.
if o.Value == nil {
o.Value = &DiscardValue
}
return json.Unmarshal(data, (*optionNoMethods)(o))
}
func (o Option) YAMLPath() string {
if o.YAML == "" {
return ""
@@ -79,15 +95,101 @@ func (o Option) YAMLPath() string {
// OptionSet is a group of options that can be applied to a command.
type OptionSet []Option
// UnmarshalJSON implements json.Unmarshaler for OptionSets. Options have an
// interface Value type that cannot handle unmarshalling because the types cannot
// be inferred. Since it is a slice, instantiating the Options first does not
// help.
//
// However, we typically do instantiate the slice to have the correct types.
// So this unmarshaller will attempt to find the named option in the existing
// set, if it cannot, the value is discarded. If the option exists, the value
// is unmarshalled into the existing option, and replaces the existing option.
//
// The value is discarded if it's type cannot be inferred. This behavior just
// feels "safer", although it should never happen if the correct option set
// is passed in. The situation where this could occur is if a client and server
// are on different versions with different options.
func (optSet *OptionSet) UnmarshalJSON(data []byte) error {
dec := json.NewDecoder(bytes.NewBuffer(data))
// Should be a json array, so consume the starting open bracket.
t, err := dec.Token()
if err != nil {
return xerrors.Errorf("read array open bracket: %w", err)
}
if t != json.Delim('[') {
return xerrors.Errorf("expected array open bracket, got %q", t)
}
// As long as json elements exist, consume them. The counter is used for
// better errors.
var i int
OptionSetDecodeLoop:
for dec.More() {
var opt Option
// jValue is a placeholder value that allows us to capture the
// raw json for the value to attempt to unmarshal later.
var jValue jsonValue
opt.Value = &jValue
err := dec.Decode(&opt)
if err != nil {
return xerrors.Errorf("decode %d option: %w", i, err)
}
// This counter is used to contextualize errors to show which element of
// the array we failed to decode. It is only used in the error above, as
// if the above works, we can instead use the Option.Name which is more
// descriptive and useful. So increment here for the next decode.
i++
// Try to see if the option already exists in the option set.
// If it does, just update the existing option.
for optIndex, have := range *optSet {
if have.Name == opt.Name {
if jValue != nil {
err := json.Unmarshal(jValue, &(*optSet)[optIndex].Value)
if err != nil {
return xerrors.Errorf("decode option %q value: %w", have.Name, err)
}
// Set the opt's value
opt.Value = (*optSet)[optIndex].Value
} else {
// Hopefully the user passed empty values in the option set. There is no easy way
// to tell, and if we do not do this, it breaks json.Marshal if we do it again on
// this new option set.
opt.Value = (*optSet)[optIndex].Value
}
// Override the existing.
(*optSet)[optIndex] = opt
// Go to the next option to decode.
continue OptionSetDecodeLoop
}
}
// If the option doesn't exist, the value will be discarded.
// We do this because we cannot infer the type of the value.
opt.Value = DiscardValue
*optSet = append(*optSet, opt)
}
t, err = dec.Token()
if err != nil {
return xerrors.Errorf("read array close bracket: %w", err)
}
if t != json.Delim(']') {
return xerrors.Errorf("expected array close bracket, got %q", t)
}
return nil
}
// Add adds the given Options to the OptionSet.
func (s *OptionSet) Add(opts ...Option) {
*s = append(*s, opts...)
func (optSet *OptionSet) Add(opts ...Option) {
*optSet = append(*optSet, opts...)
}
// Filter will only return options that match the given filter. (return true)
func (s OptionSet) Filter(filter func(opt Option) bool) OptionSet {
func (optSet OptionSet) Filter(filter func(opt Option) bool) OptionSet {
cpy := make(OptionSet, 0)
for _, opt := range s {
for _, opt := range optSet {
if filter(opt) {
cpy = append(cpy, opt)
}
@@ -96,13 +198,13 @@ func (s OptionSet) Filter(filter func(opt Option) bool) OptionSet {
}
// FlagSet returns a pflag.FlagSet for the OptionSet.
func (s *OptionSet) FlagSet() *pflag.FlagSet {
if s == nil {
func (optSet *OptionSet) FlagSet() *pflag.FlagSet {
if optSet == nil {
return &pflag.FlagSet{}
}
fs := pflag.NewFlagSet("", pflag.ContinueOnError)
for _, opt := range *s {
for _, opt := range *optSet {
if opt.Flag == "" {
continue
}
@@ -139,8 +241,8 @@ func (s *OptionSet) FlagSet() *pflag.FlagSet {
// ParseEnv parses the given environment variables into the OptionSet.
// Use EnvsWithPrefix to filter out prefixes.
func (s *OptionSet) ParseEnv(vs []EnvVar) error {
if s == nil {
func (optSet *OptionSet) ParseEnv(vs []EnvVar) error {
if optSet == nil {
return nil
}
@@ -154,12 +256,21 @@ func (s *OptionSet) ParseEnv(vs []EnvVar) error {
envs[v.Name] = v.Value
}
for i, opt := range *s {
for i, opt := range *optSet {
if opt.Env == "" {
continue
}
envVal, ok := envs[opt.Env]
if !ok {
// Homebrew strips all environment variables that do not start with `HOMEBREW_`.
// This prevented using brew to invoke the Coder agent, because the environment
// variables to not get passed down.
//
// A customer wanted to use their custom tap inside a workspace, which was failing
// because the agent lacked the environment variables to authenticate with Git.
envVal, ok = envs[`HOMEBREW_`+opt.Env]
}
// Currently, empty values are treated as if the environment variable is
// unset. This behavior is technically not correct as there is now no
// way for a user to change a Default value to an empty string from
@@ -172,7 +283,7 @@ func (s *OptionSet) ParseEnv(vs []EnvVar) error {
continue
}
(*s)[i].ValueSource = ValueSourceEnv
(*optSet)[i].ValueSource = ValueSourceEnv
if err := opt.Value.Set(envVal); err != nil {
merr = multierror.Append(
merr, xerrors.Errorf("parse %q: %w", opt.Name, err),
@@ -185,14 +296,14 @@ func (s *OptionSet) ParseEnv(vs []EnvVar) error {
// SetDefaults sets the default values for each Option, skipping values
// that already have a value source.
func (s *OptionSet) SetDefaults() error {
if s == nil {
func (optSet *OptionSet) SetDefaults() error {
if optSet == nil {
return nil
}
var merr *multierror.Error
for i, opt := range *s {
for i, opt := range *optSet {
// Skip values that may have already been set by the user.
if opt.ValueSource != ValueSourceNone {
continue
@@ -212,7 +323,7 @@ func (s *OptionSet) SetDefaults() error {
)
continue
}
(*s)[i].ValueSource = ValueSourceDefault
(*optSet)[i].ValueSource = ValueSourceDefault
if err := opt.Value.Set(opt.Default); err != nil {
merr = multierror.Append(
merr, xerrors.Errorf("parse %q: %w", opt.Name, err),
@@ -224,9 +335,9 @@ func (s *OptionSet) SetDefaults() error {
// ByName returns the Option with the given name, or nil if no such option
// exists.
func (s *OptionSet) ByName(name string) *Option {
for i := range *s {
opt := &(*s)[i]
func (optSet *OptionSet) ByName(name string) *Option {
for i := range *optSet {
opt := &(*optSet)[i]
if opt.Name == name {
return opt
}
+188
View File
@@ -1,11 +1,17 @@
package clibase_test
import (
"bytes"
"encoding/json"
"regexp"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/codersdk"
)
func TestOptionSet_ParseFlags(t *testing.T) {
@@ -200,4 +206,186 @@ func TestOptionSet_ParseEnv(t *testing.T) {
require.NoError(t, err)
require.EqualValues(t, expected, actual.Value)
})
t.Run("Homebrew", func(t *testing.T) {
t.Parallel()
var agentToken clibase.String
os := clibase.OptionSet{
clibase.Option{
Name: "Agent Token",
Value: &agentToken,
Env: "AGENT_TOKEN",
},
}
err := os.ParseEnv([]clibase.EnvVar{
{Name: "HOMEBREW_AGENT_TOKEN", Value: "foo"},
})
require.NoError(t, err)
require.EqualValues(t, "foo", agentToken)
})
}
func TestOptionSet_JsonMarshal(t *testing.T) {
t.Parallel()
// This unit test ensures if the source optionset is missing the option
// and cannot determine the type, it will not panic. The unmarshal will
// succeed with a best effort.
t.Run("MissingSrcOption", func(t *testing.T) {
t.Parallel()
var str clibase.String = "something"
var arr clibase.StringArray = []string{"foo", "bar"}
opts := clibase.OptionSet{
clibase.Option{
Name: "StringOpt",
Value: &str,
},
clibase.Option{
Name: "ArrayOpt",
Value: &arr,
},
}
data, err := json.Marshal(opts)
require.NoError(t, err, "marshal option set")
tgt := clibase.OptionSet{}
err = json.Unmarshal(data, &tgt)
require.NoError(t, err, "unmarshal option set")
for i := range opts {
compareOptionsExceptValues(t, opts[i], tgt[i])
require.Empty(t, tgt[i].Value.String(), "unknown value types are empty")
}
})
t.Run("RegexCase", func(t *testing.T) {
t.Parallel()
val := clibase.Regexp(*regexp.MustCompile(".*"))
opts := clibase.OptionSet{
clibase.Option{
Name: "Regex",
Value: &val,
Default: ".*",
},
}
data, err := json.Marshal(opts)
require.NoError(t, err, "marshal option set")
var foundVal clibase.Regexp
newOpts := clibase.OptionSet{
clibase.Option{
Name: "Regex",
Value: &foundVal,
},
}
err = json.Unmarshal(data, &newOpts)
require.NoError(t, err, "unmarshal option set")
require.EqualValues(t, opts[0].Value.String(), newOpts[0].Value.String())
})
t.Run("AllValues", func(t *testing.T) {
t.Parallel()
vals := coderdtest.DeploymentValues(t)
opts := vals.Options()
sources := []clibase.ValueSource{
clibase.ValueSourceNone,
clibase.ValueSourceFlag,
clibase.ValueSourceEnv,
clibase.ValueSourceYAML,
clibase.ValueSourceDefault,
}
for i := range opts {
opts[i].ValueSource = sources[i%len(sources)]
}
data, err := json.Marshal(opts)
require.NoError(t, err, "marshal option set")
newOpts := (&codersdk.DeploymentValues{}).Options()
err = json.Unmarshal(data, &newOpts)
require.NoError(t, err, "unmarshal option set")
for i := range opts {
exp := opts[i]
found := newOpts[i]
compareOptionsExceptValues(t, exp, found)
compareValues(t, exp, found)
}
thirdOpts := (&codersdk.DeploymentValues{}).Options()
data, err = json.Marshal(newOpts)
require.NoError(t, err, "marshal option set")
err = json.Unmarshal(data, &thirdOpts)
require.NoError(t, err, "unmarshal option set")
// Compare to the original opts again
for i := range opts {
exp := opts[i]
found := thirdOpts[i]
compareOptionsExceptValues(t, exp, found)
compareValues(t, exp, found)
}
})
}
func compareOptionsExceptValues(t *testing.T, exp, found clibase.Option) {
t.Helper()
require.Equalf(t, exp.Name, found.Name, "option name %q", exp.Name)
require.Equalf(t, exp.Description, found.Description, "option description %q", exp.Name)
require.Equalf(t, exp.Required, found.Required, "option required %q", exp.Name)
require.Equalf(t, exp.Flag, found.Flag, "option flag %q", exp.Name)
require.Equalf(t, exp.FlagShorthand, found.FlagShorthand, "option flag shorthand %q", exp.Name)
require.Equalf(t, exp.Env, found.Env, "option env %q", exp.Name)
require.Equalf(t, exp.YAML, found.YAML, "option yaml %q", exp.Name)
require.Equalf(t, exp.Default, found.Default, "option default %q", exp.Name)
require.Equalf(t, exp.ValueSource, found.ValueSource, "option value source %q", exp.Name)
require.Equalf(t, exp.Hidden, found.Hidden, "option hidden %q", exp.Name)
require.Equalf(t, exp.Annotations, found.Annotations, "option annotations %q", exp.Name)
require.Equalf(t, exp.Group, found.Group, "option group %q", exp.Name)
// UseInstead is the same comparison problem, just check the length
require.Equalf(t, len(exp.UseInstead), len(found.UseInstead), "option use instead %q", exp.Name)
}
func compareValues(t *testing.T, exp, found clibase.Option) {
t.Helper()
if (exp.Value == nil || found.Value == nil) || (exp.Value.String() != found.Value.String() && found.Value.String() == "") {
// If the string values are different, this can be a "nil" issue.
// So only run this case if the found string is the empty string.
// We use MarshalYAML for struct strings, and it will return an
// empty string '""' for nil slices/maps/etc.
// So use json to compare.
expJSON, err := json.Marshal(exp.Value)
require.NoError(t, err, "marshal")
foundJSON, err := json.Marshal(found.Value)
require.NoError(t, err, "marshal")
expJSON = normalizeJSON(expJSON)
foundJSON = normalizeJSON(foundJSON)
assert.Equalf(t, string(expJSON), string(foundJSON), "option value %q", exp.Name)
} else {
assert.Equal(t,
exp.Value.String(),
found.Value.String(),
"option value %q", exp.Name)
}
}
// normalizeJSON handles the fact that an empty map/slice is not the same
// as a nil empty/slice. For our purposes, they are the same.
func normalizeJSON(data []byte) []byte {
if bytes.Equal(data, []byte("[]")) || bytes.Equal(data, []byte("{}")) {
return []byte("null")
}
return data
}
+48
View File
@@ -430,6 +430,35 @@ func (discardValue) Type() string {
return "discard"
}
func (discardValue) UnmarshalJSON([]byte) error {
return nil
}
// jsonValue is intentionally not exported. It is just used to store the raw JSON
// data for a value to defer it's unmarshal. It implements the pflag.Value to be
// usable in an Option.
type jsonValue json.RawMessage
func (jsonValue) Set(string) error {
return xerrors.Errorf("json value is read-only")
}
func (jsonValue) String() string {
return ""
}
func (jsonValue) Type() string {
return "json"
}
func (j *jsonValue) UnmarshalJSON(data []byte) error {
if j == nil {
return xerrors.New("json.RawMessage: UnmarshalJSON on nil pointer")
}
*j = append((*j)[0:0], data...)
return nil
}
var _ pflag.Value = (*Enum)(nil)
type Enum struct {
@@ -464,6 +493,25 @@ func (e *Enum) String() string {
type Regexp regexp.Regexp
func (r *Regexp) MarshalJSON() ([]byte, error) {
return json.Marshal(r.String())
}
func (r *Regexp) UnmarshalJSON(data []byte) error {
var source string
err := json.Unmarshal(data, &source)
if err != nil {
return err
}
exp, err := regexp.Compile(source)
if err != nil {
return xerrors.Errorf("invalid regex expression: %w", err)
}
*r = Regexp(*exp)
return nil
}
func (r *Regexp) MarshalYAML() (interface{}, error) {
return yaml.Node{
Kind: yaml.ScalarNode,
+5 -5
View File
@@ -51,12 +51,12 @@ func deepMapNode(n *yaml.Node, path []string, headComment string) *yaml.Node {
// the stack.
//
// It is isomorphic with FromYAML.
func (s *OptionSet) MarshalYAML() (any, error) {
func (optSet *OptionSet) MarshalYAML() (any, error) {
root := yaml.Node{
Kind: yaml.MappingNode,
}
for _, opt := range *s {
for _, opt := range *optSet {
if opt.YAML == "" {
continue
}
@@ -222,7 +222,7 @@ func (o *Option) setFromYAMLNode(n *yaml.Node) error {
// UnmarshalYAML converts the given YAML node into the option set.
// It is isomorphic with ToYAML.
func (s *OptionSet) UnmarshalYAML(rootNode *yaml.Node) error {
func (optSet *OptionSet) UnmarshalYAML(rootNode *yaml.Node) error {
// The rootNode will be a DocumentNode if it's read from a file. We do
// not support multiple documents in a single file.
if rootNode.Kind == yaml.DocumentNode {
@@ -240,8 +240,8 @@ func (s *OptionSet) UnmarshalYAML(rootNode *yaml.Node) error {
matchedNodes := make(map[string]*yaml.Node, len(yamlNodes))
var merr error
for i := range *s {
opt := &(*s)[i]
for i := range *optSet {
opt := &(*optSet)[i]
if opt.YAML == "" {
continue
}
+2 -2
View File
@@ -29,7 +29,7 @@ import (
// New creates a CLI instance with a configuration pointed to a
// temporary testing directory.
func New(t *testing.T, args ...string) (*clibase.Invocation, config.Root) {
func New(t testing.TB, args ...string) (*clibase.Invocation, config.Root) {
var root cli.RootCmd
cmd, err := root.Command(root.AGPL())
@@ -56,7 +56,7 @@ func (l *logWriter) Write(p []byte) (n int, err error) {
}
func NewWithCommand(
t *testing.T, cmd *clibase.Cmd, args ...string,
t testing.TB, cmd *clibase.Cmd, args ...string,
) (*clibase.Invocation, config.Root) {
configDir := config.Root(t.TempDir())
logger := slogtest.Make(t, nil)
+4 -14
View File
@@ -11,8 +11,6 @@ import (
"strings"
"testing"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/cli/clibase"
@@ -28,7 +26,7 @@ import (
// make update-golden-files
var UpdateGoldenFiles = flag.Bool("update", false, "update .golden files")
var timestampRegex = regexp.MustCompile(`(?i)\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(.\d+)?Z`)
var timestampRegex = regexp.MustCompile(`(?i)\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(.\d+)?(Z|[+-]\d+:\d+)`)
type CommandHelpCase struct {
Name string
@@ -50,16 +48,8 @@ func DefaultCases() []CommandHelpCase {
// TestCommandHelp will test the help output of the given commands
// using golden files.
//
//nolint:tparallel,paralleltest
func TestCommandHelp(t *testing.T, getRoot func(t *testing.T) *clibase.Cmd, cases []CommandHelpCase) {
ogColorProfile := lipgloss.ColorProfile()
// ANSI256 escape codes are far easier for humans to parse in a diff,
// but TrueColor is probably more popular with modern terminals.
lipgloss.SetColorProfile(termenv.ANSI)
t.Cleanup(func() {
lipgloss.SetColorProfile(ogColorProfile)
})
t.Parallel()
rootClient, replacements := prepareTestData(t)
root := getRoot(t)
@@ -192,14 +182,14 @@ func prepareTestData(t *testing.T) (*codersdk.Client, map[string]string) {
})
require.NoError(t, err)
version := coderdtest.CreateTemplateVersion(t, rootClient, firstUser.OrganizationID, nil)
version = coderdtest.AwaitTemplateVersionJob(t, rootClient, version.ID)
version = coderdtest.AwaitTemplateVersionJobCompleted(t, rootClient, version.ID)
template := coderdtest.CreateTemplate(t, rootClient, firstUser.OrganizationID, version.ID, func(req *codersdk.CreateTemplateRequest) {
req.Name = "test-template"
})
workspace := coderdtest.CreateWorkspace(t, rootClient, firstUser.OrganizationID, template.ID, func(req *codersdk.CreateWorkspaceRequest) {
req.Name = "test-workspace"
})
workspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, rootClient, workspace.LatestBuild.ID)
workspaceBuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, rootClient, workspace.LatestBuild.ID)
replacements := map[string]string{
firstUser.UserID.String(): "[first user ID]",
+19 -5
View File
@@ -80,6 +80,10 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO
if err != nil {
return xerrors.Errorf("fetch: %w", err)
}
logSources := map[uuid.UUID]codersdk.WorkspaceAgentLogSource{}
for _, source := range agent.LogSources {
logSources[source.ID] = source
}
sw := &stageWriter{w: writer}
@@ -123,7 +127,7 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO
return nil
}
stage := "Running workspace agent startup script"
stage := "Running workspace agent startup scripts"
follow := opts.Wait
if !follow {
stage += " (non-blocking)"
@@ -173,7 +177,12 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO
return nil
}
for _, log := range logs {
sw.Log(log.CreatedAt, log.Level, log.Output)
source, hasSource := logSources[log.SourceID]
output := log.Output
if hasSource && source.DisplayName != "" {
output = source.DisplayName + ": " + output
}
sw.Log(log.CreatedAt, log.Level, output)
lastLog = log
}
}
@@ -192,16 +201,19 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO
switch agent.LifecycleState {
case codersdk.WorkspaceAgentLifecycleReady:
sw.Complete(stage, agent.ReadyAt.Sub(*agent.StartedAt))
case codersdk.WorkspaceAgentLifecycleStartTimeout:
sw.Fail(stage, 0)
sw.Log(time.Time{}, codersdk.LogLevelWarn, "Warning: A startup script timed out and your workspace may be incomplete.")
case codersdk.WorkspaceAgentLifecycleStartError:
sw.Fail(stage, agent.ReadyAt.Sub(*agent.StartedAt))
// Use zero time (omitted) to separate these from the startup logs.
sw.Log(time.Time{}, codersdk.LogLevelWarn, "Warning: The startup script exited with an error and your workspace may be incomplete.")
sw.Log(time.Time{}, codersdk.LogLevelWarn, "Warning: A startup script exited with an error and your workspace may be incomplete.")
sw.Log(time.Time{}, codersdk.LogLevelWarn, troubleshootingMessage(agent, "https://coder.com/docs/v2/latest/templates#startup-script-exited-with-an-error"))
default:
switch {
case agent.LifecycleState.Starting():
// Use zero time (omitted) to separate these from the startup logs.
sw.Log(time.Time{}, codersdk.LogLevelWarn, "Notice: The startup script is still running and your workspace may be incomplete.")
sw.Log(time.Time{}, codersdk.LogLevelWarn, "Notice: The startup scripts are still running and your workspace may be incomplete.")
sw.Log(time.Time{}, codersdk.LogLevelWarn, troubleshootingMessage(agent, "https://coder.com/docs/v2/latest/templates#your-workspace-may-be-incomplete"))
// Note: We don't complete or fail the stage here, it's
// intentionally left open to indicate this stage didn't
@@ -225,12 +237,14 @@ func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentO
sw.Start(stage)
sw.Log(time.Now(), codersdk.LogLevelWarn, "Wait for it to reconnect or restart your workspace.")
sw.Log(time.Now(), codersdk.LogLevelWarn, troubleshootingMessage(agent, "https://coder.com/docs/v2/latest/templates#agent-connection-issues"))
disconnectedAt := *agent.DisconnectedAt
for agent.Status == codersdk.WorkspaceAgentDisconnected {
if agent, err = fetch(); err != nil {
return xerrors.Errorf("fetch: %w", err)
}
}
sw.Complete(stage, agent.LastConnectedAt.Sub(*agent.DisconnectedAt))
sw.Complete(stage, agent.LastConnectedAt.Sub(disconnectedAt))
}
}
}
+147 -56
View File
@@ -5,12 +5,14 @@ import (
"bytes"
"context"
"io"
"os"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
@@ -25,9 +27,31 @@ import (
func TestAgent(t *testing.T) {
t.Parallel()
waitLines := func(t *testing.T, output <-chan string, lines ...string) error {
t.Helper()
var got []string
outerLoop:
for _, want := range lines {
for {
select {
case line := <-output:
got = append(got, line)
if strings.Contains(line, want) {
continue outerLoop
}
case <-time.After(testutil.WaitShort):
assert.Failf(t, "timed out waiting for line", "want: %q; got: %q", want, got)
return xerrors.Errorf("timed out waiting for line: %q; got: %q", want, got)
}
}
}
return nil
}
for _, tc := range []struct {
name string
iter []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error
iter []func(context.Context, *testing.T, *codersdk.WorkspaceAgent, <-chan string, chan []codersdk.WorkspaceAgentLog) error
logs chan []codersdk.WorkspaceAgentLog
opts cliui.AgentOptions
want []string
@@ -38,12 +62,15 @@ func TestAgent(t *testing.T) {
opts: cliui.AgentOptions{
FetchInterval: time.Millisecond,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
iter: []func(context.Context, *testing.T, *codersdk.WorkspaceAgent, <-chan string, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnecting
return nil
},
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentLog) error {
func(_ context.Context, t *testing.T, agent *codersdk.WorkspaceAgent, output <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
return waitLines(t, output, "⧗ Waiting for the workspace agent to connect")
},
func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnected
agent.FirstConnectedAt = ptr.Ref(time.Now())
return nil
@@ -52,28 +79,62 @@ func TestAgent(t *testing.T) {
want: []string{
"⧗ Waiting for the workspace agent to connect",
"✔ Waiting for the workspace agent to connect",
"⧗ Running workspace agent startup script (non-blocking)",
"Notice: The startup script is still running and your workspace may be incomplete.",
"⧗ Running workspace agent startup scripts (non-blocking)",
"Notice: The startup scripts are still running and your workspace may be incomplete.",
"For more information and troubleshooting, see",
},
},
{
name: "Start timeout",
opts: cliui.AgentOptions{
FetchInterval: time.Millisecond,
},
iter: []func(context.Context, *testing.T, *codersdk.WorkspaceAgent, <-chan string, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnecting
return nil
},
func(_ context.Context, t *testing.T, agent *codersdk.WorkspaceAgent, output <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
return waitLines(t, output, "⧗ Waiting for the workspace agent to connect")
},
func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnected
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleStartTimeout
agent.FirstConnectedAt = ptr.Ref(time.Now())
return nil
},
},
want: []string{
"⧗ Waiting for the workspace agent to connect",
"✔ Waiting for the workspace agent to connect",
"⧗ Running workspace agent startup scripts (non-blocking)",
"✘ Running workspace agent startup scripts (non-blocking)",
"Warning: A startup script timed out and your workspace may be incomplete.",
},
},
{
name: "Initial connection timeout",
opts: cliui.AgentOptions{
FetchInterval: 1 * time.Millisecond,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
iter: []func(context.Context, *testing.T, *codersdk.WorkspaceAgent, <-chan string, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnecting
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleStarting
agent.StartedAt = ptr.Ref(time.Now())
return nil
},
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
func(_ context.Context, t *testing.T, agent *codersdk.WorkspaceAgent, output <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
return waitLines(t, output, "⧗ Waiting for the workspace agent to connect")
},
func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentTimeout
return nil
},
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentLog) error {
func(_ context.Context, t *testing.T, agent *codersdk.WorkspaceAgent, output <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
return waitLines(t, output, "The workspace agent is having trouble connecting, wait for it to connect or restart your workspace.")
},
func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnected
agent.FirstConnectedAt = ptr.Ref(time.Now())
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleReady
@@ -86,8 +147,8 @@ func TestAgent(t *testing.T) {
"The workspace agent is having trouble connecting, wait for it to connect or restart your workspace.",
"For more information and troubleshooting, see",
"✔ Waiting for the workspace agent to connect",
"⧗ Running workspace agent startup script (non-blocking)",
"✔ Running workspace agent startup script (non-blocking)",
"⧗ Running workspace agent startup scripts (non-blocking)",
"✔ Running workspace agent startup scripts (non-blocking)",
},
},
{
@@ -95,8 +156,8 @@ func TestAgent(t *testing.T) {
opts: cliui.AgentOptions{
FetchInterval: 1 * time.Millisecond,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
iter: []func(context.Context, *testing.T, *codersdk.WorkspaceAgent, <-chan string, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentDisconnected
agent.FirstConnectedAt = ptr.Ref(time.Now().Add(-1 * time.Minute))
agent.LastConnectedAt = ptr.Ref(time.Now().Add(-1 * time.Minute))
@@ -106,8 +167,12 @@ func TestAgent(t *testing.T) {
agent.ReadyAt = ptr.Ref(time.Now())
return nil
},
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
func(_ context.Context, t *testing.T, agent *codersdk.WorkspaceAgent, output <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
return waitLines(t, output, "⧗ The workspace agent lost connection")
},
func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnected
agent.DisconnectedAt = nil
agent.LastConnectedAt = ptr.Ref(time.Now())
return nil
},
@@ -120,26 +185,31 @@ func TestAgent(t *testing.T) {
},
},
{
name: "Startup script logs",
name: "Startup Logs",
opts: cliui.AgentOptions{
FetchInterval: time.Millisecond,
Wait: true,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentLog) error {
iter: []func(context.Context, *testing.T, *codersdk.WorkspaceAgent, <-chan string, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, logs chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnected
agent.FirstConnectedAt = ptr.Ref(time.Now())
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleStarting
agent.StartedAt = ptr.Ref(time.Now())
agent.LogSources = []codersdk.WorkspaceAgentLogSource{{
ID: uuid.Nil,
DisplayName: "testing",
}}
logs <- []codersdk.WorkspaceAgentLog{
{
CreatedAt: time.Now(),
Output: "Hello world",
SourceID: uuid.Nil,
},
}
return nil
},
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentLog) error {
func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, logs chan []codersdk.WorkspaceAgentLog) error {
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleReady
agent.ReadyAt = ptr.Ref(time.Now())
logs <- []codersdk.WorkspaceAgentLog{
@@ -152,10 +222,10 @@ func TestAgent(t *testing.T) {
},
},
want: []string{
"⧗ Running workspace agent startup script",
"Hello world",
"⧗ Running workspace agent startup scripts",
"testing: Hello world",
"Bye now",
"✔ Running workspace agent startup script",
"✔ Running workspace agent startup scripts",
},
},
{
@@ -164,8 +234,8 @@ func TestAgent(t *testing.T) {
FetchInterval: time.Millisecond,
Wait: true,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentLog) error {
iter: []func(context.Context, *testing.T, *codersdk.WorkspaceAgent, <-chan string, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, output <-chan string, logs chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnected
agent.FirstConnectedAt = ptr.Ref(time.Now())
agent.StartedAt = ptr.Ref(time.Now())
@@ -181,10 +251,10 @@ func TestAgent(t *testing.T) {
},
},
want: []string{
"⧗ Running workspace agent startup script",
"⧗ Running workspace agent startup scripts",
"Hello world",
"✘ Running workspace agent startup script",
"Warning: The startup script exited with an error and your workspace may be incomplete.",
"✘ Running workspace agent startup scripts",
"Warning: A startup script exited with an error and your workspace may be incomplete.",
"For more information and troubleshooting, see",
},
},
@@ -193,8 +263,8 @@ func TestAgent(t *testing.T) {
opts: cliui.AgentOptions{
FetchInterval: time.Millisecond,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentLog) error {
iter: []func(context.Context, *testing.T, *codersdk.WorkspaceAgent, <-chan string, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, output <-chan string, logs chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentDisconnected
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleOff
return nil
@@ -208,8 +278,8 @@ func TestAgent(t *testing.T) {
FetchInterval: time.Millisecond,
Wait: true,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentLog) error {
iter: []func(context.Context, *testing.T, *codersdk.WorkspaceAgent, <-chan string, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, output <-chan string, logs chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnected
agent.FirstConnectedAt = ptr.Ref(time.Now())
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleStarting
@@ -222,16 +292,19 @@ func TestAgent(t *testing.T) {
}
return nil
},
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentLog) error {
func(_ context.Context, t *testing.T, agent *codersdk.WorkspaceAgent, output <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
return waitLines(t, output, "Hello world")
},
func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
agent.ReadyAt = ptr.Ref(time.Now())
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleShuttingDown
return nil
},
},
want: []string{
"⧗ Running workspace agent startup script",
"⧗ Running workspace agent startup scripts",
"Hello world",
"✔ Running workspace agent startup script",
"✔ Running workspace agent startup scripts",
},
wantErr: true,
},
@@ -241,12 +314,15 @@ func TestAgent(t *testing.T) {
FetchInterval: time.Millisecond,
Wait: true,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
iter: []func(context.Context, *testing.T, *codersdk.WorkspaceAgent, <-chan string, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnecting
return nil
},
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
func(_ context.Context, t *testing.T, agent *codersdk.WorkspaceAgent, output <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
return waitLines(t, output, "⧗ Waiting for the workspace agent to connect")
},
func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
return xerrors.New("bad")
},
},
@@ -261,13 +337,16 @@ func TestAgent(t *testing.T) {
FetchInterval: time.Millisecond,
Wait: true,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
iter: []func(context.Context, *testing.T, *codersdk.WorkspaceAgent, <-chan string, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, _ <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentTimeout
agent.TroubleshootingURL = "https://troubleshoot"
return nil
},
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
func(_ context.Context, t *testing.T, agent *codersdk.WorkspaceAgent, output <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
return waitLines(t, output, "The workspace agent is having trouble connecting, wait for it to connect or restart your workspace.")
},
func(_ context.Context, _ *testing.T, agent *codersdk.WorkspaceAgent, output <-chan string, _ chan []codersdk.WorkspaceAgentLog) error {
return xerrors.New("bad")
},
},
@@ -286,22 +365,27 @@ func TestAgent(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
var buf bytes.Buffer
r, w, err := os.Pipe()
require.NoError(t, err, "create pipe failed")
defer r.Close()
defer w.Close()
agent := codersdk.WorkspaceAgent{
ID: uuid.New(),
Status: codersdk.WorkspaceAgentConnecting,
StartupScriptBehavior: codersdk.WorkspaceAgentStartupScriptBehaviorNonBlocking,
CreatedAt: time.Now(),
LifecycleState: codersdk.WorkspaceAgentLifecycleCreated,
ID: uuid.New(),
Status: codersdk.WorkspaceAgentConnecting,
CreatedAt: time.Now(),
LifecycleState: codersdk.WorkspaceAgentLifecycleCreated,
}
output := make(chan string, 100) // Buffered to avoid blocking, overflow is discarded.
logs := make(chan []codersdk.WorkspaceAgentLog, 1)
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
tc.opts.Fetch = func(_ context.Context, _ uuid.UUID) (codersdk.WorkspaceAgent, error) {
t.Log("iter", len(tc.iter))
var err error
if len(tc.iter) > 0 {
err = tc.iter[0](ctx, &agent, logs)
err = tc.iter[0](ctx, t, &agent, output, logs)
tc.iter = tc.iter[1:]
}
return agent, err
@@ -322,25 +406,26 @@ func TestAgent(t *testing.T) {
close(fetchLogs)
return fetchLogs, closeFunc(func() error { return nil }), nil
}
err := cliui.Agent(inv.Context(), &buf, uuid.Nil, tc.opts)
err := cliui.Agent(inv.Context(), w, uuid.Nil, tc.opts)
_ = w.Close()
return err
},
}
inv := cmd.Invoke()
w := clitest.StartWithWaiter(t, inv)
if tc.wantErr {
w.RequireError()
} else {
w.RequireSuccess()
}
waiter := clitest.StartWithWaiter(t, inv)
s := bufio.NewScanner(&buf)
s := bufio.NewScanner(r)
for s.Scan() {
line := s.Text()
t.Log(line)
select {
case output <- line:
default:
t.Logf("output overflow: %s", line)
}
if len(tc.want) == 0 {
require.Fail(t, "unexpected line: "+line)
require.Fail(t, "unexpected line", line)
}
require.Contains(t, line, tc.want[0])
tc.want = tc.want[1:]
@@ -349,6 +434,12 @@ func TestAgent(t *testing.T) {
if len(tc.want) > 0 {
require.Fail(t, "missing lines: "+strings.Join(tc.want, ", "))
}
if tc.wantErr {
waiter.RequireError()
} else {
waiter.RequireSuccess()
}
})
}
+131 -36
View File
@@ -1,12 +1,15 @@
package cliui
import (
"flag"
"os"
"sync"
"time"
"github.com/charmbracelet/charm/ui/common"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
"golang.org/x/xerrors"
"github.com/coder/pretty"
)
var Canceled = xerrors.New("canceled")
@@ -15,55 +18,147 @@ var Canceled = xerrors.New("canceled")
var DefaultStyles Styles
type Styles struct {
Bold,
Checkmark,
Code,
Crossmark,
DateTimeStamp,
Error,
Field,
Keyword,
Paragraph,
Placeholder,
Prompt,
FocusedPrompt,
Fuchsia,
Logo,
Warn,
Wrap lipgloss.Style
Wrap pretty.Style
}
var (
color termenv.Profile
colorOnce sync.Once
)
var (
Green = Color("#04B575")
Red = Color("#ED567A")
Fuchsia = Color("#EE6FF8")
Yellow = Color("#ECFD65")
Blue = Color("#5000ff")
)
// Color returns a color for the given string.
func Color(s string) termenv.Color {
colorOnce.Do(func() {
color = termenv.NewOutput(os.Stdout).ColorProfile()
if flag.Lookup("test.v") != nil {
// Use a consistent colorless profile in tests so that results
// are deterministic.
color = termenv.Ascii
}
})
return color.Color(s)
}
func isTerm() bool {
return color != termenv.Ascii
}
// Bold returns a formatter that renders text in bold
// if the terminal supports it.
func Bold(s string) string {
if !isTerm() {
return s
}
return pretty.Sprint(pretty.Bold(), s)
}
// BoldFmt returns a formatter that renders text in bold
// if the terminal supports it.
func BoldFmt() pretty.Formatter {
if !isTerm() {
return pretty.Style{}
}
return pretty.Bold()
}
// Timestamp formats a timestamp for display.
func Timestamp(t time.Time) string {
return pretty.Sprint(DefaultStyles.DateTimeStamp, t.Format(time.Stamp))
}
// Keyword formats a keyword for display.
func Keyword(s string) string {
return pretty.Sprint(DefaultStyles.Keyword, s)
}
// Placeholder formats a placeholder for display.
func Placeholder(s string) string {
return pretty.Sprint(DefaultStyles.Placeholder, s)
}
// Wrap prevents the text from overflowing the terminal.
func Wrap(s string) string {
return pretty.Sprint(DefaultStyles.Wrap, s)
}
// Code formats code for display.
func Code(s string) string {
return pretty.Sprint(DefaultStyles.Code, s)
}
// Field formats a field for display.
func Field(s string) string {
return pretty.Sprint(DefaultStyles.Field, s)
}
func ifTerm(fmt pretty.Formatter) pretty.Formatter {
if !isTerm() {
return pretty.Nop
}
return fmt
}
func init() {
lipgloss.SetDefaultRenderer(
lipgloss.NewRenderer(os.Stdout, termenv.WithColorCache(true)),
)
// All Styles are set after we change the DefaultRenderer so that the ColorCache
// is in effect, mitigating the severe performance issue seen here:
// https://github.com/coder/coder/issues/7884.
charmStyles := common.DefaultStyles()
// We do not adapt the color based on whether the terminal is light or dark.
// Doing so would require a round-trip between the program and the terminal
// due to the OSC query and response.
DefaultStyles = Styles{
Bold: lipgloss.NewStyle().Bold(true),
Checkmark: charmStyles.Checkmark,
Code: charmStyles.Code,
Crossmark: charmStyles.Error.Copy().SetString("✘"),
DateTimeStamp: charmStyles.LabelDim,
Error: charmStyles.Error,
Field: charmStyles.Code.Copy().Foreground(lipgloss.AdaptiveColor{Light: "#000000", Dark: "#FFFFFF"}),
Keyword: charmStyles.Keyword,
Paragraph: charmStyles.Paragraph,
Placeholder: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#585858", Dark: "#4d46b3"}),
Prompt: charmStyles.Prompt.Copy().Foreground(lipgloss.AdaptiveColor{Light: "#9B9B9B", Dark: "#5C5C5C"}),
FocusedPrompt: charmStyles.FocusedPrompt.Copy().Foreground(lipgloss.Color("#651fff")),
Fuchsia: charmStyles.SelectedMenuItem.Copy(),
Logo: charmStyles.Logo.Copy().SetString("Coder"),
Warn: lipgloss.NewStyle().Foreground(
lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#ECFD65"},
),
Wrap: lipgloss.NewStyle().Width(80),
Code: pretty.Style{
ifTerm(pretty.XPad(1, 1)),
pretty.FgColor(Red),
pretty.BgColor(color.Color("#2c2c2c")),
},
DateTimeStamp: pretty.Style{
pretty.FgColor(color.Color("#7571F9")),
},
Error: pretty.Style{
pretty.FgColor(Red),
},
Field: pretty.Style{
pretty.XPad(1, 1),
pretty.FgColor(color.Color("#FFFFFF")),
pretty.BgColor(color.Color("#2b2a2a")),
},
Keyword: pretty.Style{
pretty.FgColor(Green),
},
Placeholder: pretty.Style{
pretty.FgColor(color.Color("#4d46b3")),
},
Prompt: pretty.Style{
pretty.FgColor(color.Color("#5C5C5C")),
pretty.Wrap("> ", ""),
},
Warn: pretty.Style{
pretty.FgColor(Yellow),
},
Wrap: pretty.Style{
pretty.LineWrap(80),
},
}
DefaultStyles.FocusedPrompt = append(
DefaultStyles.Prompt,
pretty.FgColor(Blue),
)
}
// ValidateNotEmpty is a helper function to disallow empty inputs!
@@ -11,12 +11,12 @@ import (
"github.com/coder/coder/v2/codersdk"
)
type GitAuthOptions struct {
Fetch func(context.Context) ([]codersdk.TemplateVersionGitAuth, error)
type ExternalAuthOptions struct {
Fetch func(context.Context) ([]codersdk.TemplateVersionExternalAuth, error)
FetchInterval time.Duration
}
func GitAuth(ctx context.Context, writer io.Writer, opts GitAuthOptions) error {
func ExternalAuth(ctx context.Context, writer io.Writer, opts ExternalAuthOptions) error {
if opts.FetchInterval == 0 {
opts.FetchInterval = 500 * time.Millisecond
}
@@ -38,7 +38,7 @@ func GitAuth(ctx context.Context, writer io.Writer, opts GitAuthOptions) error {
return nil
}
_, _ = fmt.Fprintf(writer, "You must authenticate with %s to create a workspace with this template. Visit:\n\n\t%s\n\n", auth.Type.Pretty(), auth.AuthenticateURL)
_, _ = 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)
ticker.Reset(opts.FetchInterval)
spin.Start()
@@ -66,7 +66,7 @@ func GitAuth(ctx context.Context, writer io.Writer, opts GitAuthOptions) error {
}
}
spin.Stop()
_, _ = fmt.Fprintf(writer, "Successfully authenticated with %s!\n\n", auth.Type.Pretty())
_, _ = fmt.Fprintf(writer, "Successfully authenticated with %s!\n\n", auth.DisplayName)
}
return nil
}
@@ -15,7 +15,7 @@ import (
"github.com/coder/coder/v2/testutil"
)
func TestGitAuth(t *testing.T) {
func TestExternalAuth(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
@@ -25,12 +25,13 @@ func TestGitAuth(t *testing.T) {
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
var fetched atomic.Bool
return cliui.GitAuth(inv.Context(), inv.Stdout, cliui.GitAuthOptions{
Fetch: func(ctx context.Context) ([]codersdk.TemplateVersionGitAuth, error) {
return cliui.ExternalAuth(inv.Context(), inv.Stdout, cliui.ExternalAuthOptions{
Fetch: func(ctx context.Context) ([]codersdk.TemplateVersionExternalAuth, error) {
defer fetched.Store(true)
return []codersdk.TemplateVersionGitAuth{{
return []codersdk.TemplateVersionExternalAuth{{
ID: "github",
Type: codersdk.GitProviderGitHub,
DisplayName: "GitHub",
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
Authenticated: fetched.Load(),
AuthenticateURL: "https://example.com/gitauth/github",
}}, nil
+7 -7
View File
@@ -5,12 +5,12 @@ import (
"io"
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/coder/pretty"
)
// cliMessage provides a human-readable message for CLI errors and messages.
type cliMessage struct {
Style lipgloss.Style
Style pretty.Style
Header string
Prefix string
Lines []string
@@ -21,13 +21,13 @@ func (m cliMessage) String() string {
var str strings.Builder
if m.Prefix != "" {
_, _ = str.WriteString(m.Style.Bold(true).Render(m.Prefix))
_, _ = str.WriteString(Bold(m.Prefix))
}
_, _ = str.WriteString(m.Style.Bold(false).Render(m.Header))
pretty.Fprint(&str, m.Style, m.Header)
_, _ = str.WriteString("\r\n")
for _, line := range m.Lines {
_, _ = fmt.Fprintf(&str, " %s %s\r\n", m.Style.Render("|"), line)
_, _ = fmt.Fprintf(&str, " %s %s\r\n", pretty.Sprint(m.Style, "|"), line)
}
return str.String()
}
@@ -35,7 +35,7 @@ func (m cliMessage) String() string {
// Warn writes a log to the writer provided.
func Warn(wtr io.Writer, header string, lines ...string) {
_, _ = fmt.Fprint(wtr, cliMessage{
Style: DefaultStyles.Warn.Copy(),
Style: DefaultStyles.Warn,
Prefix: "WARN: ",
Header: header,
Lines: lines,
@@ -63,7 +63,7 @@ func Infof(wtr io.Writer, fmtStr string, args ...interface{}) {
// Error writes a log to the writer provided.
func Error(wtr io.Writer, header string, lines ...string) {
_, _ = fmt.Fprint(wtr, cliMessage{
Style: DefaultStyles.Error.Copy(),
Style: DefaultStyles.Error,
Prefix: "ERROR: ",
Header: header,
Lines: lines,
+9 -5
View File
@@ -7,6 +7,7 @@ import (
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/pretty"
)
func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.TemplateVersionParameter) (string, error) {
@@ -16,10 +17,10 @@ func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.Te
}
if templateVersionParameter.Ephemeral {
label += DefaultStyles.Warn.Render(" (build option)")
label += pretty.Sprint(DefaultStyles.Warn, " (build option)")
}
_, _ = fmt.Fprintln(inv.Stdout, DefaultStyles.Bold.Render(label))
_, _ = fmt.Fprintln(inv.Stdout, Bold(label))
if templateVersionParameter.DescriptionPlaintext != "" {
_, _ = fmt.Fprintln(inv.Stdout, " "+strings.TrimSpace(strings.Join(strings.Split(templateVersionParameter.DescriptionPlaintext, "\n"), "\n "))+"\n")
@@ -45,7 +46,10 @@ func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.Te
}
_, _ = fmt.Fprintln(inv.Stdout)
_, _ = fmt.Fprintln(inv.Stdout, " "+DefaultStyles.Prompt.String()+DefaultStyles.Field.Render(strings.Join(values, ", ")))
pretty.Fprintf(
inv.Stdout,
DefaultStyles.Prompt, "%s\n", strings.Join(values, ", "),
)
value = string(v)
}
} else if len(templateVersionParameter.Options) > 0 {
@@ -59,7 +63,7 @@ func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.Te
})
if err == nil {
_, _ = fmt.Fprintln(inv.Stdout)
_, _ = fmt.Fprintln(inv.Stdout, " "+DefaultStyles.Prompt.String()+DefaultStyles.Field.Render(richParameterOption.Name))
pretty.Fprintf(inv.Stdout, DefaultStyles.Prompt, "%s\n", richParameterOption.Name)
value = richParameterOption.Value
}
} else {
@@ -70,7 +74,7 @@ func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.Te
text += ":"
value, err = Prompt(inv, PromptOptions{
Text: DefaultStyles.Bold.Render(text),
Text: Bold(text),
Validate: func(value string) error {
return validateRichPrompt(value, templateVersionParameter)
},
+12 -8
View File
@@ -14,6 +14,7 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/pretty"
)
// PromptOptions supply a set of options to the prompt.
@@ -55,21 +56,24 @@ func Prompt(inv *clibase.Invocation, opts PromptOptions) (string, error) {
}
}
_, _ = fmt.Fprint(inv.Stdout, DefaultStyles.FocusedPrompt.String()+opts.Text+" ")
pretty.Fprintf(inv.Stdout, DefaultStyles.FocusedPrompt, "")
pretty.Fprintf(inv.Stdout, pretty.Nop, "%s ", opts.Text)
if opts.IsConfirm {
if len(opts.Default) == 0 {
opts.Default = ConfirmYes
}
renderedYes := DefaultStyles.Placeholder.Render(ConfirmYes)
renderedNo := DefaultStyles.Placeholder.Render(ConfirmNo)
var (
renderedYes = pretty.Sprint(DefaultStyles.Placeholder, ConfirmYes)
renderedNo = pretty.Sprint(DefaultStyles.Placeholder, ConfirmNo)
)
if opts.Default == ConfirmYes {
renderedYes = DefaultStyles.Bold.Render(ConfirmYes)
renderedYes = Bold(ConfirmYes)
} else {
renderedNo = DefaultStyles.Bold.Render(ConfirmNo)
renderedNo = Bold(ConfirmNo)
}
_, _ = fmt.Fprint(inv.Stdout, DefaultStyles.Placeholder.Render("("+renderedYes+DefaultStyles.Placeholder.Render("/"+renderedNo+DefaultStyles.Placeholder.Render(") "))))
pretty.Fprintf(inv.Stdout, DefaultStyles.Placeholder, "(%s/%s)", renderedYes, renderedNo)
} else if opts.Default != "" {
_, _ = fmt.Fprint(inv.Stdout, DefaultStyles.Placeholder.Render("("+opts.Default+") "))
_, _ = fmt.Fprint(inv.Stdout, pretty.Sprint(DefaultStyles.Placeholder, "("+opts.Default+") "))
}
interrupt := make(chan os.Signal, 1)
@@ -126,7 +130,7 @@ func Prompt(inv *clibase.Invocation, opts PromptOptions) (string, error) {
if opts.Validate != nil {
err := opts.Validate(line)
if err != nil {
_, _ = fmt.Fprintln(inv.Stdout, DefaultStyles.Error.Render(err.Error()))
_, _ = fmt.Fprintln(inv.Stdout, pretty.Sprint(DefaultStyles.Error, err.Error()))
return Prompt(inv, opts)
}
}
+13 -8
View File
@@ -15,6 +15,7 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/pretty"
)
func WorkspaceBuild(ctx context.Context, writer io.Writer, client *codersdk.Client, build uuid.UUID) error {
@@ -54,7 +55,7 @@ func (err *ProvisionerJobError) Error() string {
}
// ProvisionerJob renders a provisioner job with interactive cancellation.
func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOptions) error {
func ProvisionerJob(ctx context.Context, wr io.Writer, opts ProvisionerJobOptions) error {
if opts.FetchInterval == 0 {
opts.FetchInterval = time.Second
}
@@ -70,7 +71,7 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
jobMutex sync.Mutex
)
sw := &stageWriter{w: writer, verbose: opts.Verbose, silentLogs: opts.Silent}
sw := &stageWriter{w: wr, verbose: opts.Verbose, silentLogs: opts.Silent}
printStage := func() {
sw.Start(currentStage)
@@ -127,7 +128,11 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
return
}
}
_, _ = fmt.Fprintf(writer, DefaultStyles.FocusedPrompt.String()+DefaultStyles.Bold.Render("Gracefully canceling...")+"\n\n")
pretty.Fprintf(
wr,
DefaultStyles.FocusedPrompt.With(BoldFmt()),
"Gracefully canceling...\n\n",
)
err := opts.Cancel()
if err != nil {
errChan <- xerrors.Errorf("cancel: %w", err)
@@ -236,7 +241,7 @@ func (s *stageWriter) Log(createdAt time.Time, level codersdk.LogLevel, line str
w = &s.logBuf
}
render := func(s ...string) string { return strings.Join(s, " ") }
var style pretty.Style
var lines []string
if !createdAt.IsZero() {
@@ -249,14 +254,14 @@ func (s *stageWriter) Log(createdAt time.Time, level codersdk.LogLevel, line str
if !s.verbose {
return
}
render = DefaultStyles.Placeholder.Render
style = DefaultStyles.Placeholder
case codersdk.LogLevelError:
render = DefaultStyles.Error.Render
style = DefaultStyles.Error
case codersdk.LogLevelWarn:
render = DefaultStyles.Warn.Render
style = DefaultStyles.Warn
case codersdk.LogLevelInfo:
}
_, _ = fmt.Fprintf(w, "%s\n", render(lines...))
pretty.Fprintf(w, style, "%s\n", strings.Join(lines, " "))
}
func (s *stageWriter) flushLogs() {
+16 -15
View File
@@ -11,6 +11,7 @@ import (
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/pretty"
)
type WorkspaceResourcesOptions struct {
@@ -78,7 +79,7 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource
// Display a line for the resource.
tableWriter.AppendRow(table.Row{
DefaultStyles.Bold.Render(resourceAddress),
Bold(resourceAddress),
"",
"",
"",
@@ -107,7 +108,7 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource
if totalAgents > 1 {
sshCommand += "." + agent.Name
}
sshCommand = DefaultStyles.Code.Render(sshCommand)
sshCommand = pretty.Sprint(DefaultStyles.Code, sshCommand)
row = append(row, sshCommand)
}
tableWriter.AppendRow(row)
@@ -122,31 +123,31 @@ func renderAgentStatus(agent codersdk.WorkspaceAgent) string {
switch agent.Status {
case codersdk.WorkspaceAgentConnecting:
since := dbtime.Now().Sub(agent.CreatedAt)
return DefaultStyles.Warn.Render("⦾ connecting") + " " +
DefaultStyles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
return pretty.Sprint(DefaultStyles.Warn, "⦾ connecting") + " " +
pretty.Sprint(DefaultStyles.Placeholder, "["+strconv.Itoa(int(since.Seconds()))+"s]")
case codersdk.WorkspaceAgentDisconnected:
since := dbtime.Now().Sub(*agent.DisconnectedAt)
return DefaultStyles.Error.Render("⦾ disconnected") + " " +
DefaultStyles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
return pretty.Sprint(DefaultStyles.Error, "⦾ disconnected") + " " +
pretty.Sprint(DefaultStyles.Placeholder, "["+strconv.Itoa(int(since.Seconds()))+"s]")
case codersdk.WorkspaceAgentTimeout:
since := dbtime.Now().Sub(agent.CreatedAt)
return fmt.Sprintf(
"%s %s",
DefaultStyles.Warn.Render("⦾ timeout"),
DefaultStyles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]"),
pretty.Sprint(DefaultStyles.Warn, "⦾ timeout"),
pretty.Sprint(DefaultStyles.Placeholder, "["+strconv.Itoa(int(since.Seconds()))+"s]"),
)
case codersdk.WorkspaceAgentConnected:
return DefaultStyles.Keyword.Render("⦿ connected")
return pretty.Sprint(DefaultStyles.Keyword, "⦿ connected")
default:
return DefaultStyles.Warn.Render("○ unknown")
return pretty.Sprint(DefaultStyles.Warn, "○ unknown")
}
}
func renderAgentHealth(agent codersdk.WorkspaceAgent) string {
if agent.Health.Healthy {
return DefaultStyles.Keyword.Render("✔ healthy")
return pretty.Sprint(DefaultStyles.Keyword, "✔ healthy")
}
return DefaultStyles.Error.Render("✘ " + agent.Health.Reason)
return pretty.Sprint(DefaultStyles.Error, "✘ "+agent.Health.Reason)
}
func renderAgentVersion(agentVersion, serverVersion string) string {
@@ -154,11 +155,11 @@ func renderAgentVersion(agentVersion, serverVersion string) string {
agentVersion = "(unknown)"
}
if !semver.IsValid(serverVersion) || !semver.IsValid(agentVersion) {
return DefaultStyles.Placeholder.Render(agentVersion)
return pretty.Sprint(DefaultStyles.Placeholder, agentVersion)
}
outdated := semver.Compare(agentVersion, serverVersion) < 0
if outdated {
return DefaultStyles.Warn.Render(agentVersion + " (outdated)")
return pretty.Sprint(DefaultStyles.Warn, agentVersion+" (outdated)")
}
return DefaultStyles.Keyword.Render(agentVersion)
return pretty.Sprint(DefaultStyles.Keyword, agentVersion)
}
+1 -1
View File
@@ -44,7 +44,7 @@ func TestRenderAgentVersion(t *testing.T) {
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
actual := renderAgentVersion(testCase.agentVersion, testCase.serverVersion)
assert.Equal(t, testCase.expected, actual)
assert.Equal(t, testCase.expected, (actual))
})
}
}
+20 -29
View File
@@ -19,13 +19,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/agent"
"github.com/coder/coder/v2/agent/agenttest"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/coder/v2/provisioner/echo"
"github.com/coder/coder/v2/provisionersdk/proto"
"github.com/coder/coder/v2/pty/ptytest"
@@ -78,9 +75,10 @@ func TestConfigSSH(t *testing.T) {
},
},
})
user := coderdtest.CreateFirstUser(t, client)
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Response{{
Type: &proto.Response_Plan{
@@ -98,19 +96,11 @@ func TestConfigSSH(t *testing.T) {
}},
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
agentClient := agentsdk.New(client.URL)
agentClient.SetSessionToken(authToken)
agentCloser := agent.New(agent.Options{
Client: agentClient,
Logger: slogtest.Make(t, nil).Named("agent"),
})
defer func() {
_ = agentCloser.Close()
}()
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, member, owner.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
_ = agenttest.New(t, client.URL, authToken)
resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
agentConn, err := client.DialWorkspaceAgent(context.Background(), resources[0].Agents[0].ID, nil)
require.NoError(t, err)
@@ -156,7 +146,7 @@ func TestConfigSSH(t *testing.T) {
"--ssh-option", "Port "+strconv.Itoa(tcpAddr.Port),
"--ssh-config-file", sshConfigFile,
"--skip-proxy-command")
clitest.SetupConfig(t, client, root)
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t)
inv.Stdin = pty.Input()
inv.Stdout = pty.Output()
@@ -605,10 +595,10 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user = coderdtest.CreateFirstUser(t, client)
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, tt.echoResponse)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID)
_ = coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
)
// Prepare ssh config files.
@@ -721,19 +711,20 @@ func TestConfigSSH_Hostnames(t *testing.T) {
}
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
// authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID,
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID,
echo.WithResources(resources))
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, member, owner.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
sshConfigFile := sshConfigFileName(t)
inv, root := clitest.New(t, "config-ssh", "--ssh-config-file", sshConfigFile)
clitest.SetupConfig(t, client, root)
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t)
inv.Stdin = pty.Input()
+22 -6
View File
@@ -10,6 +10,8 @@ import (
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/util/ptr"
@@ -25,6 +27,7 @@ func (r *RootCmd) create() *clibase.Cmd {
workspaceName string
parameterFlags workspaceParameterFlags
autoUpdates string
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
@@ -75,7 +78,7 @@ func (r *RootCmd) create() *clibase.Cmd {
var template codersdk.Template
if templateName == "" {
_, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Wrap.Render("Select a template below to preview the provisioned infrastructure:"))
_, _ = fmt.Fprintln(inv.Stdout, pretty.Sprint(cliui.DefaultStyles.Wrap, "Select a template below to preview the provisioned infrastructure:"))
templates, err := client.TemplatesByOrganization(inv.Context(), organization.ID)
if err != nil {
@@ -93,7 +96,7 @@ func (r *RootCmd) create() *clibase.Cmd {
templateName := template.Name
if template.ActiveUserCount > 0 {
templateName += cliui.DefaultStyles.Placeholder.Render(
templateName += cliui.Placeholder(
fmt.Sprintf(
" (used by %s)",
formatActiveDevelopers(template.ActiveUserCount),
@@ -167,6 +170,7 @@ func (r *RootCmd) create() *clibase.Cmd {
AutostartSchedule: schedSpec,
TTLMillis: ttlMillis,
RichParameterValues: richParameters,
AutomaticUpdates: codersdk.AutomaticUpdates(autoUpdates),
})
if err != nil {
return xerrors.Errorf("create workspace: %w", err)
@@ -177,7 +181,12 @@ func (r *RootCmd) create() *clibase.Cmd {
return xerrors.Errorf("watch build: %w", err)
}
_, _ = fmt.Fprintf(inv.Stdout, "\nThe %s workspace has been created at %s!\n", cliui.DefaultStyles.Keyword.Render(workspace.Name), cliui.DefaultStyles.DateTimeStamp.Render(time.Now().Format(time.Stamp)))
_, _ = fmt.Fprintf(
inv.Stdout,
"\nThe %s workspace has been created at %s!\n",
cliui.Keyword(workspace.Name),
cliui.Timestamp(time.Now()),
)
return nil
},
}
@@ -201,6 +210,13 @@ func (r *RootCmd) create() *clibase.Cmd {
Description: "Specify a duration after which the workspace should shut down (e.g. 8h).",
Value: clibase.DurationOf(&stopAfter),
},
clibase.Option{
Flag: "automatic-updates",
Env: "CODER_WORKSPACE_AUTOMATIC_UPDATES",
Description: "Specify automatic updates setting for the workspace (accepts 'always' or 'never').",
Default: string(codersdk.AutomaticUpdatesNever),
Value: clibase.StringOf(&autoUpdates),
},
cliui.SkipPromptOption(),
)
cmd.Options = append(cmd.Options, parameterFlags.cliParameters()...)
@@ -258,9 +274,9 @@ func prepWorkspaceBuild(inv *clibase.Invocation, client *codersdk.Client, args p
return nil, err
}
err = cliui.GitAuth(ctx, inv.Stdout, cliui.GitAuthOptions{
Fetch: func(ctx context.Context) ([]codersdk.TemplateVersionGitAuth, error) {
return client.TemplateVersionGitAuth(ctx, templateVersion.ID)
err = cliui.ExternalAuth(ctx, inv.Stdout, cliui.ExternalAuthOptions{
Fetch: func(ctx context.Context) ([]codersdk.TemplateVersionExternalAuth, error) {
return client.TemplateVersionExternalAuth(ctx, templateVersion.ID)
},
})
if err != nil {
+92 -73
View File
@@ -14,7 +14,7 @@ import (
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/gitauth"
"github.com/coder/coder/v2/coderd/externalauth"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/provisioner/echo"
@@ -28,19 +28,21 @@ func TestCreate(t *testing.T) {
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, completeWithAgent())
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, completeWithAgent())
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
args := []string{
"create",
"my-workspace",
"--template", template.Name,
"--start-at", "9:30AM Mon-Fri US/Central",
"--stop-after", "8h",
"--automatic-updates", "always",
}
inv, root := clitest.New(t, args...)
clitest.SetupConfig(t, client, root)
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
go func() {
@@ -64,7 +66,7 @@ func TestCreate(t *testing.T) {
}
<-doneChan
ws, err := client.WorkspaceByOwnerAndName(context.Background(), "testuser", "my-workspace", codersdk.WorkspaceOptions{})
ws, err := member.WorkspaceByOwnerAndName(context.Background(), codersdk.Me, "my-workspace", codersdk.WorkspaceOptions{})
if assert.NoError(t, err, "expected workspace to be created") {
assert.Equal(t, ws.TemplateName, template.Name)
if assert.NotNil(t, ws.AutostartSchedule) {
@@ -73,6 +75,7 @@ func TestCreate(t *testing.T) {
if assert.NotNil(t, ws.TTLMillis) {
assert.Equal(t, *ws.TTLMillis, 8*time.Hour.Milliseconds())
}
assert.Equal(t, codersdk.AutomaticUpdatesAlways, ws.AutomaticUpdates)
}
})
@@ -81,7 +84,7 @@ func TestCreate(t *testing.T) {
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, completeWithAgent())
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
_, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
args := []string{
@@ -93,6 +96,7 @@ func TestCreate(t *testing.T) {
}
inv, root := clitest.New(t, args...)
//nolint:gocritic // Creating a workspace for another user requires owner permissions.
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
@@ -132,10 +136,11 @@ func TestCreate(t *testing.T) {
t.Run("InheritStopAfterFromTemplate", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, completeWithAgent())
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, completeWithAgent())
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
var defaultTTLMillis int64 = 2 * 60 * 60 * 1000 // 2 hours
ctr.DefaultTTLMillis = &defaultTTLMillis
})
@@ -145,7 +150,7 @@ func TestCreate(t *testing.T) {
"--template", template.Name,
}
inv, root := clitest.New(t, args...)
clitest.SetupConfig(t, client, root)
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t).Attach(inv)
waiter := clitest.StartWithWaiter(t, inv)
matches := []struct {
@@ -164,7 +169,7 @@ func TestCreate(t *testing.T) {
}
waiter.RequireSuccess()
ws, err := client.WorkspaceByOwnerAndName(context.Background(), "testuser", "my-workspace", codersdk.WorkspaceOptions{})
ws, err := member.WorkspaceByOwnerAndName(context.Background(), codersdk.Me, "my-workspace", codersdk.WorkspaceOptions{})
require.NoError(t, err, "expected workspace to be created")
assert.Equal(t, ws.TemplateName, template.Name)
assert.Equal(t, *ws.TTLMillis, template.DefaultTTLMillis)
@@ -175,7 +180,7 @@ func TestCreate(t *testing.T) {
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
_ = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
inv, root := clitest.New(t, "create", "my-workspace", "-y")
@@ -195,12 +200,13 @@ func TestCreate(t *testing.T) {
t.Run("FromNothing", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
inv, root := clitest.New(t, "create", "")
clitest.SetupConfig(t, client, root)
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
go func() {
@@ -220,7 +226,7 @@ func TestCreate(t *testing.T) {
}
<-doneChan
ws, err := client.WorkspaceByOwnerAndName(inv.Context(), "testuser", "my-workspace", codersdk.WorkspaceOptions{})
ws, err := member.WorkspaceByOwnerAndName(inv.Context(), codersdk.Me, "my-workspace", codersdk.WorkspaceOptions{})
if assert.NoError(t, err, "expected workspace to be created") {
assert.Equal(t, ws.TemplateName, template.Name)
assert.Nil(t, ws.AutostartSchedule, "expected workspace autostart schedule to be nil")
@@ -273,14 +279,15 @@ func TestCreateWithRichParameters(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
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, user.OrganizationID, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name)
clitest.SetupConfig(t, client, root)
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
go func() {
@@ -312,11 +319,12 @@ func TestCreateWithRichParameters(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
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, user.OrganizationID, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
tempDir := t.TempDir()
removeTmpDirUntilSuccessAfterTest(t, tempDir)
@@ -326,7 +334,7 @@ func TestCreateWithRichParameters(t *testing.T) {
secondParameterName + ": " + secondParameterValue + "\n" +
immutableParameterName + ": " + immutableParameterValue)
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name, "--rich-parameter-file", parameterFile.Name())
clitest.SetupConfig(t, client, root)
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
@@ -352,17 +360,18 @@ func TestCreateWithRichParameters(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
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, user.OrganizationID, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name,
"--parameter", fmt.Sprintf("%s=%s", firstParameterName, firstParameterValue),
"--parameter", fmt.Sprintf("%s=%s", secondParameterName, secondParameterValue),
"--parameter", fmt.Sprintf("%s=%s", immutableParameterName, immutableParameterValue))
clitest.SetupConfig(t, client, root)
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
go func() {
@@ -420,14 +429,15 @@ func TestCreateValidateRichParameters(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, prepareEchoResponses(stringRichParameters))
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, prepareEchoResponses(stringRichParameters))
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name)
clitest.SetupConfig(t, client, root)
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
go func() {
@@ -446,7 +456,9 @@ func TestCreateValidateRichParameters(t *testing.T) {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
pty.WriteLine(value)
if value != "" {
pty.WriteLine(value)
}
}
<-doneChan
})
@@ -455,14 +467,15 @@ func TestCreateValidateRichParameters(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, prepareEchoResponses(numberRichParameters))
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, prepareEchoResponses(numberRichParameters))
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name)
clitest.SetupConfig(t, client, root)
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
go func() {
@@ -481,7 +494,6 @@ func TestCreateValidateRichParameters(t *testing.T) {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
if value != "" {
pty.WriteLine(value)
}
@@ -493,14 +505,15 @@ func TestCreateValidateRichParameters(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, prepareEchoResponses(boolRichParameters))
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, prepareEchoResponses(boolRichParameters))
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name)
clitest.SetupConfig(t, client, root)
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
go func() {
@@ -519,7 +532,9 @@ func TestCreateValidateRichParameters(t *testing.T) {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
pty.WriteLine(value)
if value != "" {
pty.WriteLine(value)
}
}
<-doneChan
})
@@ -528,13 +543,14 @@ func TestCreateValidateRichParameters(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, prepareEchoResponses(listOfStringsRichParameters))
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, prepareEchoResponses(listOfStringsRichParameters))
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)
clitest.SetupConfig(t, client, root)
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t).Attach(inv)
clitest.Start(t, inv)
@@ -557,10 +573,11 @@ func TestCreateValidateRichParameters(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, prepareEchoResponses(listOfStringsRichParameters))
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, prepareEchoResponses(listOfStringsRichParameters))
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
tempDir := t.TempDir()
removeTmpDirUntilSuccessAfterTest(t, tempDir)
@@ -570,7 +587,7 @@ func TestCreateValidateRichParameters(t *testing.T) {
- eee
- fff`)
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name, "--rich-parameter-file", parameterFile.Name())
clitest.SetupConfig(t, client, root)
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t).Attach(inv)
clitest.Start(t, inv)
@@ -597,7 +614,7 @@ func TestCreateWithGitAuth(t *testing.T) {
{
Type: &proto.Response_Plan{
Plan: &proto.PlanComplete{
GitAuthProviders: []string{"github"},
ExternalAuthProviders: []string{"github"},
},
},
},
@@ -606,26 +623,28 @@ func TestCreateWithGitAuth(t *testing.T) {
}
client := coderdtest.New(t, &coderdtest.Options{
GitAuthConfigs: []*gitauth.Config{{
ExternalAuthConfigs: []*externalauth.Config{{
OAuth2Config: &testutil.OAuth2Config{},
ID: "github",
Regex: regexp.MustCompile(`github\.com`),
Type: codersdk.GitProviderGitHub,
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
DisplayName: "GitHub",
}},
IncludeProvisionerDaemon: true,
})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
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)
clitest.SetupConfig(t, client, root)
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t).Attach(inv)
clitest.Start(t, inv)
pty.ExpectMatch("You must authenticate with GitHub to create a workspace")
resp := coderdtest.RequestGitAuthCallback(t, "github", client)
resp := coderdtest.RequestExternalAuthCallback(t, "github", member)
_ = resp.Body.Close()
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
pty.ExpectMatch("Confirm create?")
+5 -1
View File
@@ -54,7 +54,11 @@ func (r *RootCmd) deleteWorkspace() *clibase.Cmd {
return err
}
_, _ = fmt.Fprintf(inv.Stdout, "\n%s has been deleted at %s!\n", cliui.DefaultStyles.Keyword.Render(workspace.FullName()), cliui.DefaultStyles.DateTimeStamp.Render(time.Now().Format(time.Stamp)))
_, _ = fmt.Fprintf(
inv.Stdout,
"\n%s has been deleted at %s!\n", cliui.Keyword(workspace.FullName()),
cliui.Timestamp(time.Now()),
)
return nil
},
}
+26 -22
View File
@@ -23,14 +23,15 @@ func TestDelete(t *testing.T) {
t.Run("WithParameter", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, member, owner.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
inv, root := clitest.New(t, "delete", workspace.Name, "-y")
clitest.SetupConfig(t, client, root)
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
go func() {
@@ -48,14 +49,15 @@ func TestDelete(t *testing.T) {
t.Run("Orphan", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
owner := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, owner.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
inv, root := clitest.New(t, "delete", workspace.Name, "-y", "--orphan")
//nolint:gocritic // Deleting orphaned workspaces requires an admin.
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
@@ -80,14 +82,14 @@ func TestDelete(t *testing.T) {
t.Run("OrphanDeletedUser", func(t *testing.T) {
t.Parallel()
client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
deleteMeClient, deleteMeUser := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
owner := coderdtest.CreateFirstUser(t, client)
deleteMeClient, deleteMeUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, deleteMeClient, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, deleteMeClient, workspace.LatestBuild.ID)
workspace := coderdtest.CreateWorkspace(t, deleteMeClient, owner.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, deleteMeClient, workspace.LatestBuild.ID)
// The API checks if the user has any workspaces, so we cannot delete a user
// this way.
@@ -101,6 +103,7 @@ func TestDelete(t *testing.T) {
inv, root := clitest.New(t, "delete", fmt.Sprintf("%s/%s", deleteMeUser.ID, workspace.Name), "-y", "--orphan")
//nolint:gocritic // Deleting orphaned workspaces requires an admin.
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
@@ -127,12 +130,13 @@ func TestDelete(t *testing.T) {
require.NoError(t, err)
version := coderdtest.CreateTemplateVersion(t, adminClient, orgID, nil)
coderdtest.AwaitTemplateVersionJob(t, adminClient, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, adminClient, version.ID)
template := coderdtest.CreateTemplate(t, adminClient, orgID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, orgID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
inv, root := clitest.New(t, "delete", user.Username+"/"+workspace.Name, "-y")
//nolint:gocritic // This requires an admin.
clitest.SetupConfig(t, adminClient, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
+16 -7
View File
@@ -13,6 +13,8 @@ import (
"golang.org/x/xerrors"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
)
@@ -20,6 +22,7 @@ import (
func (r *RootCmd) dotfiles() *clibase.Cmd {
var symlinkDir string
var gitbranch string
var dotfilesRepoDir string
cmd := &clibase.Cmd{
Use: "dotfiles <git_repo_url>",
@@ -33,11 +36,10 @@ func (r *RootCmd) dotfiles() *clibase.Cmd {
),
Handler: func(inv *clibase.Invocation) error {
var (
dotfilesRepoDir = "dotfiles"
gitRepo = inv.Args[0]
cfg = r.createConfig()
cfgDir = string(cfg)
dotfilesDir = filepath.Join(cfgDir, dotfilesRepoDir)
gitRepo = inv.Args[0]
cfg = r.createConfig()
cfgDir = string(cfg)
dotfilesDir = filepath.Join(cfgDir, dotfilesRepoDir)
// This follows the same pattern outlined by others in the market:
// https://github.com/coder/coder/pull/1696#issue-1245742312
installScriptSet = []string{
@@ -143,7 +145,7 @@ func (r *RootCmd) dotfiles() *clibase.Cmd {
return err
}
// if the repo exists we soft fail the update operation and try to continue
_, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Error.Render("Failed to update repo, continuing..."))
_, _ = fmt.Fprintln(inv.Stdout, pretty.Sprint(cliui.DefaultStyles.Error, "Failed to update repo, continuing..."))
}
if dotfilesExists && gitbranch != "" {
@@ -159,7 +161,7 @@ func (r *RootCmd) dotfiles() *clibase.Cmd {
if err != nil {
// Do not block on this error, just log it and continue
_, _ = fmt.Fprintln(inv.Stdout,
cliui.DefaultStyles.Error.Render(fmt.Sprintf("Failed to use branch %q (%s), continuing...", err.Error(), gitbranch)))
pretty.Sprint(cliui.DefaultStyles.Error, fmt.Sprintf("Failed to use branch %q (%s), continuing...", err.Error(), gitbranch)))
}
}
@@ -288,6 +290,13 @@ func (r *RootCmd) dotfiles() *clibase.Cmd {
"If empty, will default to cloning the default branch or using the existing branch in the cloned repo on disk.",
Value: clibase.StringOf(&gitbranch),
},
{
Flag: "repo-dir",
Default: "dotfiles",
Env: "CODER_DOTFILES_REPO_DIR",
Description: "Specifies the directory for the dotfiles repository, relative to global config directory.",
Value: clibase.StringOf(&dotfilesRepoDir),
},
cliui.SkipPromptOption(),
}
return cmd
+62
View File
@@ -50,6 +50,68 @@ func TestDotfiles(t *testing.T) {
require.NoError(t, err)
require.Equal(t, string(b), "wow")
})
t.Run("SwitchRepoDir", func(t *testing.T) {
t.Parallel()
_, root := clitest.New(t)
testRepo := testGitRepo(t, root)
// nolint:gosec
err := os.WriteFile(filepath.Join(testRepo, ".bashrc"), []byte("wow"), 0o750)
require.NoError(t, err)
c := exec.Command("git", "add", ".bashrc")
c.Dir = testRepo
err = c.Run()
require.NoError(t, err)
c = exec.Command("git", "commit", "-m", `"add .bashrc"`)
c.Dir = testRepo
out, err := c.CombinedOutput()
require.NoError(t, err, string(out))
inv, _ := clitest.New(t, "dotfiles", "--global-config", string(root), "--symlink-dir", string(root), "--repo-dir", "testrepo", "-y", testRepo)
err = inv.Run()
require.NoError(t, err)
b, err := os.ReadFile(filepath.Join(string(root), ".bashrc"))
require.NoError(t, err)
require.Equal(t, string(b), "wow")
stat, staterr := os.Stat(filepath.Join(string(root), "testrepo"))
require.NoError(t, staterr)
require.True(t, stat.IsDir())
})
t.Run("SwitchRepoDirRelative", func(t *testing.T) {
t.Parallel()
_, root := clitest.New(t)
testRepo := testGitRepo(t, root)
// nolint:gosec
err := os.WriteFile(filepath.Join(testRepo, ".bashrc"), []byte("wow"), 0o750)
require.NoError(t, err)
c := exec.Command("git", "add", ".bashrc")
c.Dir = testRepo
err = c.Run()
require.NoError(t, err)
c = exec.Command("git", "commit", "-m", `"add .bashrc"`)
c.Dir = testRepo
out, err := c.CombinedOutput()
require.NoError(t, err, string(out))
inv, _ := clitest.New(t, "dotfiles", "--global-config", string(root), "--symlink-dir", string(root), "--repo-dir", "./relrepo", "-y", testRepo)
err = inv.Run()
require.NoError(t, err)
b, err := os.ReadFile(filepath.Join(string(root), ".bashrc"))
require.NoError(t, err)
require.Equal(t, string(b), "wow")
stat, staterr := os.Stat(filepath.Join(string(root), "relrepo"))
require.NoError(t, staterr)
require.True(t, stat.IsDir())
})
t.Run("InstallScript", func(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
+106
View File
@@ -0,0 +1,106 @@
package cli
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/codersdk"
)
func (RootCmd) errorExample() *clibase.Cmd {
errorCmd := func(use string, err error) *clibase.Cmd {
return &clibase.Cmd{
Use: use,
Handler: func(inv *clibase.Invocation) error {
return err
},
}
}
// Make an api error
recorder := httptest.NewRecorder()
recorder.WriteHeader(http.StatusBadRequest)
resp := recorder.Result()
_ = resp.Body.Close()
resp.Request, _ = http.NewRequest(http.MethodPost, "http://example.com", nil)
apiError := codersdk.ReadBodyAsError(resp)
//nolint:errorlint,forcetypeassert
apiError.(*codersdk.Error).Response = codersdk.Response{
Message: "Top level sdk error message.",
Detail: "magic dust unavailable, please try again later",
Validations: []codersdk.ValidationError{
{
Field: "region",
Detail: "magic dust is not available in your region",
},
},
}
//nolint:errorlint,forcetypeassert
apiError.(*codersdk.Error).Helper = "Have you tried turning it off and on again?"
// Some flags
var magicWord clibase.String
cmd := &clibase.Cmd{
Use: "example-error",
Short: "Shows what different error messages look like",
Long: "This command is pretty pointless, but without it testing errors is" +
"difficult to visually inspect. Error message formatting is inherently" +
"visual, so we need a way to quickly see what they look like.",
Handler: func(inv *clibase.Invocation) error {
return inv.Command.HelpHandler(inv)
},
Children: []*clibase.Cmd{
// Typical codersdk api error
errorCmd("api", apiError),
// Typical cli error
errorCmd("cmd", xerrors.Errorf("some error: %w", errorWithStackTrace())),
// A multi-error
{
Use: "multi-error",
Handler: func(inv *clibase.Invocation) error {
// Closing the stdin file descriptor will cause the next close
// to fail. This is joined to the returned Command error.
if f, ok := inv.Stdin.(*os.File); ok {
_ = f.Close()
}
return xerrors.Errorf("some error: %w", errorWithStackTrace())
},
},
{
Use: "validation",
Options: clibase.OptionSet{
clibase.Option{
Name: "magic-word",
Description: "Take a good guess.",
Required: true,
Flag: "magic-word",
Default: "",
Value: clibase.Validate(&magicWord, func(value *clibase.String) error {
return xerrors.Errorf("magic word is incorrect")
}),
},
},
Handler: func(i *clibase.Invocation) error {
_, _ = fmt.Fprint(i.Stdout, "Try setting the --magic-word flag\n")
return nil
},
},
},
}
return cmd
}
func errorWithStackTrace() error {
return xerrors.Errorf("function decided not to work, and it never will")
}
+1
View File
@@ -12,6 +12,7 @@ func (r *RootCmd) expCmd() *clibase.Cmd {
Hidden: true,
Children: []*clibase.Cmd{
r.scaletestCmd(),
r.errorExample(),
},
}
return cmd
+96 -67
View File
@@ -1,3 +1,5 @@
//go:build !slim
package cli
import (
@@ -5,6 +7,7 @@ import (
"encoding/json"
"fmt"
"io"
"math/rand"
"net/http"
"os"
"strconv"
@@ -105,7 +108,6 @@ func (s *scaletestTracingFlags) provider(ctx context.Context) (trace.TracerProvi
tracerProvider, closeTracing, err := tracing.TracerProvider(ctx, scaletestTracerName, tracing.TracerOpts{
Default: s.traceEnable,
Coder: s.traceCoder,
Honeycomb: s.traceHoneycombAPIKey,
})
if err != nil {
@@ -501,7 +503,6 @@ func (r *RootCmd) scaletestCreateWorkspaces() *clibase.Cmd {
count int64
template string
noPlan bool
noCleanup bool
// TODO: implement this flag
// noCleanupFailures bool
@@ -523,6 +524,8 @@ func (r *RootCmd) scaletestCreateWorkspaces() *clibase.Cmd {
useHostUser bool
parameterFlags workspaceParameterFlags
tracingFlags = &scaletestTracingFlags{}
strategy = &scaletestStrategyFlags{}
cleanupStrategy = &scaletestStrategyFlags{cleanup: true}
@@ -590,37 +593,22 @@ func (r *RootCmd) scaletestCreateWorkspaces() *clibase.Cmd {
if tpl.ID == uuid.Nil {
return xerrors.Errorf("could not find template %q in any organization", template)
}
templateVersion, err := client.TemplateVersion(ctx, tpl.ActiveVersionID)
cliRichParameters, err := asWorkspaceBuildParameters(parameterFlags.richParameters)
if err != nil {
return xerrors.Errorf("get template version %q: %w", tpl.ActiveVersionID, err)
return xerrors.Errorf("can't parse given parameter values: %w", err)
}
// Do a dry-run to ensure the template and parameters are valid
// before we start creating users and workspaces.
if !noPlan {
dryRun, err := client.CreateTemplateVersionDryRun(ctx, templateVersion.ID, codersdk.CreateTemplateVersionDryRunRequest{
WorkspaceName: "scaletest",
})
if err != nil {
return xerrors.Errorf("start dry run workspace creation: %w", err)
}
_, _ = fmt.Fprintln(inv.Stdout, "Planning workspace...")
err = cliui.ProvisionerJob(inv.Context(), inv.Stdout, cliui.ProvisionerJobOptions{
Fetch: func() (codersdk.ProvisionerJob, error) {
return client.TemplateVersionDryRun(inv.Context(), templateVersion.ID, dryRun.ID)
},
Cancel: func() error {
return client.CancelTemplateVersionDryRun(inv.Context(), templateVersion.ID, dryRun.ID)
},
Logs: func() (<-chan codersdk.ProvisionerJobLog, io.Closer, error) {
return client.TemplateVersionDryRunLogsAfter(inv.Context(), templateVersion.ID, dryRun.ID, 0)
},
// Don't show log output for the dry-run unless there's an error.
Silent: true,
})
if err != nil {
return xerrors.Errorf("dry-run workspace: %w", err)
}
richParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
Action: WorkspaceCreate,
Template: tpl,
NewWorkspaceName: "scaletest-N", // TODO: the scaletest runner will pass in a different name here. Does this matter?
RichParameterFile: parameterFlags.richParameterFile,
RichParameters: cliRichParameters,
})
if err != nil {
return xerrors.Errorf("prepare build: %w", err)
}
tracerProvider, closeTracing, tracingEnabled, err := tracingFlags.provider(ctx)
@@ -651,7 +639,8 @@ func (r *RootCmd) scaletestCreateWorkspaces() *clibase.Cmd {
OrganizationID: me.OrganizationIDs[0],
// UserID is set by the test automatically.
Request: codersdk.CreateWorkspaceRequest{
TemplateID: tpl.ID,
TemplateID: tpl.ID,
RichParameterValues: richParameters,
},
NoWaitForAgents: noWaitForAgents,
},
@@ -770,12 +759,6 @@ func (r *RootCmd) scaletestCreateWorkspaces() *clibase.Cmd {
Description: "Required: Name or ID of the template to use for workspaces.",
Value: clibase.StringOf(&template),
},
{
Flag: "no-plan",
Env: "CODER_SCALETEST_NO_PLAN",
Description: `Skip the dry-run step to plan the workspace creation. This step ensures that the given parameters are valid for the given template.`,
Value: clibase.BoolOf(&noPlan),
},
{
Flag: "no-cleanup",
Env: "CODER_SCALETEST_NO_CLEANUP",
@@ -858,11 +841,12 @@ func (r *RootCmd) scaletestCreateWorkspaces() *clibase.Cmd {
Flag: "use-host-login",
Env: "CODER_SCALETEST_USE_HOST_LOGIN",
Default: "false",
Description: "Use the use logged in on the host machine, instead of creating users.",
Description: "Use the user logged in on the host machine, instead of creating users.",
Value: clibase.BoolOf(&useHostUser),
},
}
cmd.Options = append(cmd.Options, parameterFlags.cliParameters()...)
tracingFlags.attach(&cmd.Options)
strategy.attach(&cmd.Options)
cleanupStrategy.attach(&cmd.Options)
@@ -1047,9 +1031,10 @@ func (r *RootCmd) scaletestWorkspaceTraffic() *clibase.Cmd {
func (r *RootCmd) scaletestDashboard() *clibase.Cmd {
var (
count int64
minWait time.Duration
maxWait time.Duration
interval time.Duration
jitter time.Duration
headless bool
randSeed int64
client = &codersdk.Client{}
tracingFlags = &scaletestTracingFlags{}
@@ -1066,8 +1051,17 @@ func (r *RootCmd) scaletestDashboard() *clibase.Cmd {
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
if !(interval > 0) {
return xerrors.Errorf("--interval must be greater than zero")
}
if !(jitter < interval) {
return xerrors.Errorf("--jitter must be less than --interval")
}
ctx := inv.Context()
logger := slog.Make(sloghuman.Sink(inv.Stdout)).Leveled(slog.LevelInfo)
if r.verbose {
logger = logger.Leveled(slog.LevelDebug)
}
tracerProvider, closeTracing, tracingEnabled, err := tracingFlags.provider(ctx)
if err != nil {
return xerrors.Errorf("create tracer provider: %w", err)
@@ -1095,19 +1089,47 @@ func (r *RootCmd) scaletestDashboard() *clibase.Cmd {
th := harness.NewTestHarness(strategy.toStrategy(), cleanupStrategy.toStrategy())
for i := int64(0); i < count; i++ {
name := fmt.Sprintf("dashboard-%d", i)
config := dashboard.Config{
MinWait: minWait,
MaxWait: maxWait,
Trace: tracingEnabled,
Logger: logger.Named(name),
RollTable: dashboard.DefaultActions,
users, err := getScaletestUsers(ctx, client)
if err != nil {
return xerrors.Errorf("get scaletest users")
}
for _, usr := range users {
//nolint:gosec // not used for cryptographic purposes
rndGen := rand.New(rand.NewSource(randSeed))
name := fmt.Sprintf("dashboard-%s", usr.Username)
userTokResp, err := client.CreateToken(ctx, usr.ID.String(), codersdk.CreateTokenRequest{
Lifetime: 30 * 24 * time.Hour,
Scope: "",
TokenName: fmt.Sprintf("scaletest-%d", time.Now().Unix()),
})
if err != nil {
return xerrors.Errorf("create token for user: %w", err)
}
userClient := codersdk.New(client.URL)
userClient.SetSessionToken(userTokResp.Key)
config := dashboard.Config{
Interval: interval,
Jitter: jitter,
Trace: tracingEnabled,
Logger: logger.Named(name),
Headless: headless,
RandIntn: rndGen.Intn,
}
// Only take a screenshot if we're in verbose mode.
// This could be useful for debugging, but it will blow up the disk.
if r.verbose {
config.Screenshot = dashboard.Screenshot
}
//nolint:gocritic
logger.Info(ctx, "runner config", slog.F("interval", interval), slog.F("jitter", jitter), slog.F("headless", headless), slog.F("trace", tracingEnabled))
if err := config.Validate(); err != nil {
logger.Fatal(ctx, "validate config", slog.Error(err))
return err
}
var runner harness.Runnable = dashboard.NewRunner(client, metrics, config)
var runner harness.Runnable = dashboard.NewRunner(userClient, metrics, config)
if tracingEnabled {
runner = &runnableTraceWrapper{
tracer: tracer,
@@ -1144,25 +1166,32 @@ func (r *RootCmd) scaletestDashboard() *clibase.Cmd {
cmd.Options = []clibase.Option{
{
Flag: "count",
Env: "CODER_SCALETEST_DASHBOARD_COUNT",
Default: "1",
Description: "Number of concurrent workers.",
Value: clibase.Int64Of(&count),
Flag: "interval",
Env: "CODER_SCALETEST_DASHBOARD_INTERVAL",
Default: "10s",
Description: "Interval between actions.",
Value: clibase.DurationOf(&interval),
},
{
Flag: "min-wait",
Env: "CODER_SCALETEST_DASHBOARD_MIN_WAIT",
Default: "100ms",
Description: "Minimum wait between fetches.",
Value: clibase.DurationOf(&minWait),
Flag: "jitter",
Env: "CODER_SCALETEST_DASHBOARD_JITTER",
Default: "5s",
Description: "Jitter between actions.",
Value: clibase.DurationOf(&jitter),
},
{
Flag: "max-wait",
Env: "CODER_SCALETEST_DASHBOARD_MAX_WAIT",
Default: "1s",
Description: "Maximum wait between fetches.",
Value: clibase.DurationOf(&maxWait),
Flag: "headless",
Env: "CODER_SCALETEST_DASHBOARD_HEADLESS",
Default: "true",
Description: "Controls headless mode. Setting to false is useful for debugging.",
Value: clibase.BoolOf(&headless),
},
{
Flag: "rand-seed",
Env: "CODER_SCALETEST_DASHBOARD_RAND_SEED",
Default: "0",
Description: "Seed for the random number generator.",
Value: clibase.Int64Of(&randSeed),
},
}
@@ -1212,7 +1241,7 @@ func (r *runnableTraceWrapper) Run(ctx context.Context, id string, logs io.Write
return r.runner.Run(ctx2, id, logs)
}
func (r *runnableTraceWrapper) Cleanup(ctx context.Context, id string) error {
func (r *runnableTraceWrapper) Cleanup(ctx context.Context, id string, logs io.Writer) error {
c, ok := r.runner.(harness.Cleanable)
if !ok {
return nil
@@ -1224,7 +1253,7 @@ func (r *runnableTraceWrapper) Cleanup(ctx context.Context, id string) error {
ctx, span := r.tracer.Start(ctx, r.spanName+" cleanup")
defer span.End()
return c.Cleanup(ctx, id)
return c.Cleanup(ctx, id, logs)
}
// newScaleTestUser returns a random username and email address that can be used
+18
View File
@@ -0,0 +1,18 @@
//go:build slim
package cli
import "github.com/coder/coder/v2/cli/clibase"
func (r *RootCmd) scaletestCmd() *clibase.Cmd {
cmd := &clibase.Cmd{
Use: "scaletest",
Short: "Run a scale test against the Coder API",
Handler: func(inv *clibase.Invocation) error {
SlimUnsupported(inv.Stderr, "exp scaletest")
return nil
},
}
return cmd
}
+84 -23
View File
@@ -7,6 +7,8 @@ import (
"github.com/stretchr/testify/require"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/pty/ptytest"
@@ -21,7 +23,12 @@ func TestScaleTestCreateWorkspaces(t *testing.T) {
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancelFunc()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
log := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
client := coderdtest.New(t, &coderdtest.Options{
// We are not including any provisioner daemons because we do not actually
// build any workspaces here.
Logger: &log,
})
_ = coderdtest.CreateFirstUser(t, client)
// Write a parameters file.
@@ -41,6 +48,8 @@ func TestScaleTestCreateWorkspaces(t *testing.T) {
"--cleanup-job-timeout", "15s",
"--output", "text",
"--output", "json:"+outputFile,
"--parameter", "foo=baz",
"--rich-parameter-file", "/path/to/some/parameter/file.ext",
)
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
@@ -59,7 +68,10 @@ func TestScaleTestWorkspaceTraffic(t *testing.T) {
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitMedium)
defer cancelFunc()
client := coderdtest.New(t, nil)
log := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
client := coderdtest.New(t, &coderdtest.Options{
Logger: &log,
})
_ = coderdtest.CreateFirstUser(t, client)
inv, root := clitest.New(t, "exp", "scaletest", "workspace-traffic",
@@ -82,29 +94,78 @@ func TestScaleTestWorkspaceTraffic(t *testing.T) {
// This test just validates that the CLI command accepts its known arguments.
func TestScaleTestDashboard(t *testing.T) {
t.Parallel()
if testutil.RaceEnabled() {
t.Skip("Flakes under race detector, see https://github.com/coder/coder/issues/9168")
}
t.Run("MinWait", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancelFunc()
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitMedium)
defer cancelFunc()
log := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
client := coderdtest.New(t, &coderdtest.Options{
Logger: &log,
})
_ = coderdtest.CreateFirstUser(t, client)
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
inv, root := clitest.New(t, "exp", "scaletest", "dashboard",
"--interval", "0s",
)
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()
inv, root := clitest.New(t, "exp", "scaletest", "dashboard",
"--count", "1",
"--min-wait", "100ms",
"--max-wait", "1s",
"--timeout", "1s",
"--scaletest-prometheus-address", "127.0.0.1:0",
"--scaletest-prometheus-wait", "0s",
)
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()
err := inv.WithContext(ctx).Run()
require.ErrorContains(t, err, "--interval must be greater than zero")
})
err := inv.WithContext(ctx).Run()
require.NoError(t, err, "")
t.Run("MaxWait", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancelFunc()
log := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
client := coderdtest.New(t, &coderdtest.Options{
Logger: &log,
})
_ = coderdtest.CreateFirstUser(t, client)
inv, root := clitest.New(t, "exp", "scaletest", "dashboard",
"--interval", "1s",
"--jitter", "1s",
)
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()
err := inv.WithContext(ctx).Run()
require.ErrorContains(t, err, "--jitter must be less than --interval")
})
t.Run("OK", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitMedium)
defer cancelFunc()
log := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
client := coderdtest.New(t, &coderdtest.Options{
Logger: &log,
})
_ = coderdtest.CreateFirstUser(t, client)
inv, root := clitest.New(t, "exp", "scaletest", "dashboard",
"--interval", "1s",
"--jitter", "500ms",
"--timeout", "5s",
"--scaletest-prometheus-address", "127.0.0.1:0",
"--scaletest-prometheus-wait", "0s",
"--rand-seed", "1234567890",
)
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()
err := inv.WithContext(ctx).Run()
require.NoError(t, err, "")
})
}
+109
View File
@@ -0,0 +1,109 @@
package cli
import (
"encoding/json"
"os/signal"
"golang.org/x/xerrors"
"github.com/tidwall/gjson"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk/agentsdk"
)
func (r *RootCmd) externalAuth() *clibase.Cmd {
return &clibase.Cmd{
Use: "external-auth",
Short: "Manage external authentication",
Long: "Authenticate with external services inside of a workspace.",
Handler: func(i *clibase.Invocation) error {
return i.Command.HelpHandler(i)
},
Children: []*clibase.Cmd{
r.externalAuthAccessToken(),
},
}
}
func (r *RootCmd) externalAuthAccessToken() *clibase.Cmd {
var extra string
return &clibase.Cmd{
Use: "access-token <provider>",
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{
Description: "Ensure that the user is authenticated with GitHub before cloning.",
Command: `#!/usr/bin/env sh
OUTPUT=$(coder external-auth access-token github)
if [ $? -eq 0 ]; then
echo "Authenticated with GitHub"
else
echo "Please authenticate with GitHub:"
echo $OUTPUT
fi
`,
},
example{
Description: "Obtain an extra property of an access token for additional metadata.",
Command: "coder external-auth access-token slack --extra \"authed_user.id\"",
},
),
Options: clibase.OptionSet{{
Name: "Extra",
Flag: "extra",
Description: "Extract a field from the \"extra\" properties of the OAuth token.",
Value: clibase.StringOf(&extra),
}},
Handler: func(inv *clibase.Invocation) error {
ctx := inv.Context()
ctx, stop := signal.NotifyContext(ctx, InterruptSignals...)
defer stop()
client, err := r.createAgentClient()
if err != nil {
return xerrors.Errorf("create agent client: %w", err)
}
extAuth, err := client.ExternalAuth(ctx, agentsdk.ExternalAuthRequest{
ID: inv.Args[0],
})
if err != nil {
return xerrors.Errorf("get external auth token: %w", err)
}
if extAuth.URL != "" {
_, err = inv.Stdout.Write([]byte(extAuth.URL))
if err != nil {
return err
}
return cliui.Canceled
}
if extra != "" {
if extAuth.TokenExtra == nil {
return xerrors.Errorf("no extra properties found for token")
}
data, err := json.Marshal(extAuth.TokenExtra)
if err != nil {
return xerrors.Errorf("marshal extra properties: %w", err)
}
result := gjson.GetBytes(data, extra)
_, err = inv.Stdout.Write([]byte(result.String()))
if err != nil {
return err
}
return nil
}
_, err = inv.Stdout.Write([]byte(extAuth.AccessToken))
if err != nil {
return err
}
return nil
},
}
}
+67
View File
@@ -0,0 +1,67 @@
package cli_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/coder/v2/pty/ptytest"
)
func TestExternalAuth(t *testing.T) {
t.Parallel()
t.Run("CanceledWithURL", func(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
httpapi.Write(context.Background(), w, http.StatusOK, agentsdk.ExternalAuthResponse{
URL: "https://github.com",
})
}))
t.Cleanup(srv.Close)
url := srv.URL
inv, _ := clitest.New(t, "--agent-url", url, "external-auth", "access-token", "github")
pty := ptytest.New(t)
inv.Stdout = pty.Output()
waiter := clitest.StartWithWaiter(t, inv)
pty.ExpectMatch("https://github.com")
waiter.RequireIs(cliui.Canceled)
})
t.Run("SuccessWithToken", func(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
httpapi.Write(context.Background(), w, http.StatusOK, agentsdk.ExternalAuthResponse{
AccessToken: "bananas",
})
}))
t.Cleanup(srv.Close)
url := srv.URL
inv, _ := clitest.New(t, "--agent-url", url, "external-auth", "access-token", "github")
pty := ptytest.New(t)
inv.Stdout = pty.Output()
clitest.Start(t, inv)
pty.ExpectMatch("bananas")
})
t.Run("SuccessWithExtra", func(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
httpapi.Write(context.Background(), w, http.StatusOK, agentsdk.ExternalAuthResponse{
AccessToken: "bananas",
TokenExtra: map[string]interface{}{
"hey": "there",
},
})
}))
t.Cleanup(srv.Close)
url := srv.URL
inv, _ := clitest.New(t, "--agent-url", url, "external-auth", "access-token", "github", "--extra", "hey")
pty := ptytest.New(t)
inv.Stdout = pty.Output()
clitest.Start(t, inv)
pty.ExpectMatch("there")
})
}
+8 -2
View File
@@ -13,6 +13,7 @@ import (
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/cli/gitauth"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/retry"
)
@@ -38,7 +39,9 @@ func (r *RootCmd) gitAskpass() *clibase.Cmd {
return xerrors.Errorf("create agent client: %w", err)
}
token, err := client.GitAuth(ctx, host, false)
token, err := client.ExternalAuth(ctx, agentsdk.ExternalAuthRequest{
Match: host,
})
if err != nil {
var apiError *codersdk.Error
if errors.As(err, &apiError) && apiError.StatusCode() == http.StatusNotFound {
@@ -63,7 +66,10 @@ func (r *RootCmd) gitAskpass() *clibase.Cmd {
}
for r := retry.New(250*time.Millisecond, 10*time.Second); r.Wait(ctx); {
token, err = client.GitAuth(ctx, host, true)
token, err = client.ExternalAuth(ctx, agentsdk.ExternalAuthRequest{
Match: host,
Listen: true,
})
if err != nil {
continue
}
+4 -4
View File
@@ -23,7 +23,7 @@ func TestGitAskpass(t *testing.T) {
t.Run("UsernameAndPassword", func(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
httpapi.Write(context.Background(), w, http.StatusOK, agentsdk.GitAuthResponse{
httpapi.Write(context.Background(), w, http.StatusOK, agentsdk.ExternalAuthResponse{
Username: "something",
Password: "bananas",
})
@@ -65,8 +65,8 @@ func TestGitAskpass(t *testing.T) {
t.Run("Poll", func(t *testing.T) {
t.Parallel()
resp := atomic.Pointer[agentsdk.GitAuthResponse]{}
resp.Store(&agentsdk.GitAuthResponse{
resp := atomic.Pointer[agentsdk.ExternalAuthResponse]{}
resp.Store(&agentsdk.ExternalAuthResponse{
URL: "https://something.org",
})
poll := make(chan struct{}, 10)
@@ -96,7 +96,7 @@ func TestGitAskpass(t *testing.T) {
}()
<-poll
stderr.ExpectMatch("Open the following URL to authenticate")
resp.Store(&agentsdk.GitAuthResponse{
resp.Store(&agentsdk.ExternalAuthResponse{
Username: "username",
Password: "password",
})
+9 -5
View File
@@ -16,6 +16,7 @@ import (
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/pretty"
)
func (r *RootCmd) gitssh() *clibase.Cmd {
@@ -90,12 +91,15 @@ func (r *RootCmd) gitssh() *clibase.Cmd {
exitErr := &exec.ExitError{}
if xerrors.As(err, &exitErr) && exitErr.ExitCode() == 255 {
_, _ = fmt.Fprintln(inv.Stderr,
"\n"+cliui.DefaultStyles.Wrap.Render("Coder authenticates with "+cliui.DefaultStyles.Field.Render("git")+
" using the public key below. All clones with SSH are authenticated automatically 🪄.")+"\n")
_, _ = fmt.Fprintln(inv.Stderr, cliui.DefaultStyles.Code.Render(strings.TrimSpace(key.PublicKey))+"\n")
"\n"+pretty.Sprintf(
cliui.DefaultStyles.Wrap,
"Coder authenticates with "+pretty.Sprint(cliui.DefaultStyles.Field, "git")+
" using the public key below. All clones with SSH are authenticated automatically 🪄.")+"\n",
)
_, _ = fmt.Fprintln(inv.Stderr, pretty.Sprint(cliui.DefaultStyles.Code, strings.TrimSpace(key.PublicKey))+"\n")
_, _ = fmt.Fprintln(inv.Stderr, "Add to GitHub and GitLab:")
_, _ = fmt.Fprintln(inv.Stderr, cliui.DefaultStyles.Prompt.String()+"https://github.com/settings/ssh/new")
_, _ = fmt.Fprintln(inv.Stderr, cliui.DefaultStyles.Prompt.String()+"https://gitlab.com/-/profile/keys")
pretty.Fprintf(inv.Stderr, cliui.DefaultStyles.Prompt, "%s", "https://github.com/settings/ssh/new\n\n")
pretty.Fprintf(inv.Stderr, cliui.DefaultStyles.Prompt, "%s", "https://gitlab.com/-/profile/keys\n\n")
_, _ = fmt.Fprintln(inv.Stderr)
return err
}
+13 -11
View File
@@ -20,15 +20,18 @@ import (
"github.com/stretchr/testify/require"
gossh "golang.org/x/crypto/ssh"
"github.com/coder/coder/v2/agent"
"github.com/coder/coder/v2/agent/agenttest"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/coder/v2/provisioner/echo"
"github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/coder/v2/testutil"
)
func prepareTestGitSSH(ctx context.Context, t *testing.T) (*codersdk.Client, string, gossh.PublicKey) {
func prepareTestGitSSH(ctx context.Context, t *testing.T) (*agentsdk.Client, string, gossh.PublicKey) {
t.Helper()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
@@ -52,18 +55,17 @@ func prepareTestGitSSH(ctx context.Context, t *testing.T) (*codersdk.Client, str
ProvisionApply: echo.ProvisionApplyWithAgent(agentToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
// start workspace agent
inv, root := clitest.New(t, "agent", "--agent-token", agentToken, "--agent-url", client.URL.String())
agentClient := codersdk.New(client.URL)
agentClient := agentsdk.New(client.URL)
agentClient.SetSessionToken(agentToken)
clitest.SetupConfig(t, agentClient, root)
clitest.Start(t, inv)
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
_ = agenttest.New(t, client.URL, agentToken, func(o *agent.Options) {
o.Client = agentClient
})
_ = coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
return agentClient, agentToken, pubkey
}
@@ -140,7 +142,7 @@ func TestGitSSH(t *testing.T) {
// set to agent config dir
inv, _ := clitest.New(t,
"gitssh",
"--agent-url", client.URL.String(),
"--agent-url", client.SDK.URL.String(),
"--agent-token", token,
"--",
fmt.Sprintf("-p%d", addr.Port),
@@ -203,7 +205,7 @@ func TestGitSSH(t *testing.T) {
pty := ptytest.New(t)
cmdArgs := []string{
"gitssh",
"--agent-url", client.URL.String(),
"--agent-url", client.SDK.URL.String(),
"--agent-token", token,
"--",
"-F", config,
+211 -183
View File
@@ -2,23 +2,22 @@ package cli
import (
"bufio"
"bytes"
_ "embed"
"fmt"
"io"
"regexp"
"sort"
"strings"
"text/tabwriter"
"text/template"
"unicode"
"github.com/mitchellh/go-wordwrap"
"golang.org/x/crypto/ssh/terminal"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/pretty"
)
//go:embed help.tpl
@@ -44,203 +43,222 @@ func wrapTTY(s string) string {
return wordwrap.WrapString(s, uint(ttyWidth()))
}
var usageTemplate = template.Must(
template.New("usage").Funcs(
template.FuncMap{
"wrapTTY": func(s string) string {
return wrapTTY(s)
},
"trimNewline": func(s string) string {
return strings.TrimSuffix(s, "\n")
},
"typeHelper": func(opt *clibase.Option) string {
switch v := opt.Value.(type) {
case *clibase.Enum:
return strings.Join(v.Choices, "|")
default:
return v.Type()
}
},
"joinStrings": func(s []string) string {
return strings.Join(s, ", ")
},
"indent": func(body string, spaces int) string {
twidth := ttyWidth()
var usageTemplate = func() *template.Template {
var (
optionFg = pretty.FgColor(
cliui.Color("#04A777"),
)
headerFg = pretty.FgColor(
cliui.Color("#337CA0"),
)
)
return template.Must(
template.New("usage").Funcs(
template.FuncMap{
"version": func() string {
return buildinfo.Version()
},
"wrapTTY": func(s string) string {
return wrapTTY(s)
},
"trimNewline": func(s string) string {
return strings.TrimSuffix(s, "\n")
},
"keyword": func(s string) string {
txt := pretty.String(s)
optionFg.Format(txt)
return txt.String()
},
"prettyHeader": func(s string) string {
s = strings.ToUpper(s)
txt := pretty.String(s, ":")
headerFg.Format(txt)
return txt.String()
},
"typeHelper": func(opt *clibase.Option) string {
switch v := opt.Value.(type) {
case *clibase.Enum:
return strings.Join(v.Choices, "|")
default:
return v.Type()
}
},
"joinStrings": func(s []string) string {
return strings.Join(s, ", ")
},
"indent": func(body string, spaces int) string {
twidth := ttyWidth()
spacing := strings.Repeat(" ", spaces)
spacing := strings.Repeat(" ", spaces)
body = wordwrap.WrapString(body, uint(twidth-len(spacing)))
wrapLim := twidth - len(spacing)
body = wordwrap.WrapString(body, uint(wrapLim))
var sb strings.Builder
for _, line := range strings.Split(body, "\n") {
// Remove existing indent, if any.
line = strings.TrimSpace(line)
// Use spaces so we can easily calculate wrapping.
_, _ = sb.WriteString(spacing)
_, _ = sb.WriteString(line)
_, _ = sb.WriteString("\n")
}
return sb.String()
},
"formatSubcommand": func(cmd *clibase.Cmd) string {
// Minimize padding by finding the longest neighboring name.
maxNameLength := len(cmd.Name())
if parent := cmd.Parent; parent != nil {
for _, c := range parent.Children {
if len(c.Name()) > maxNameLength {
maxNameLength = len(c.Name())
sc := bufio.NewScanner(strings.NewReader(body))
var sb strings.Builder
for sc.Scan() {
// Remove existing indent, if any.
// line = strings.TrimSpace(line)
// Use spaces so we can easily calculate wrapping.
_, _ = sb.WriteString(spacing)
_, _ = sb.Write(sc.Bytes())
_, _ = sb.WriteString("\n")
}
return sb.String()
},
"formatSubcommand": func(cmd *clibase.Cmd) string {
// Minimize padding by finding the longest neighboring name.
maxNameLength := len(cmd.Name())
if parent := cmd.Parent; parent != nil {
for _, c := range parent.Children {
if len(c.Name()) > maxNameLength {
maxNameLength = len(c.Name())
}
}
}
}
var sb strings.Builder
_, _ = fmt.Fprintf(
&sb, "%s%s%s",
strings.Repeat(" ", 4), cmd.Name(), strings.Repeat(" ", maxNameLength-len(cmd.Name())+4),
)
var sb strings.Builder
_, _ = fmt.Fprintf(
&sb, "%s%s%s",
strings.Repeat(" ", 4), cmd.Name(), strings.Repeat(" ", maxNameLength-len(cmd.Name())+4),
)
// This is the point at which indentation begins if there's a
// next line.
descStart := sb.Len()
// This is the point at which indentation begins if there's a
// next line.
descStart := sb.Len()
twidth := ttyWidth()
twidth := ttyWidth()
for i, line := range strings.Split(
wordwrap.WrapString(cmd.Short, uint(twidth-descStart)), "\n",
) {
if i > 0 {
_, _ = sb.WriteString(strings.Repeat(" ", descStart))
for i, line := range strings.Split(
wordwrap.WrapString(cmd.Short, uint(twidth-descStart)), "\n",
) {
if i > 0 {
_, _ = sb.WriteString(strings.Repeat(" ", descStart))
}
_, _ = sb.WriteString(line)
_, _ = sb.WriteString("\n")
}
_, _ = sb.WriteString(line)
_, _ = sb.WriteString("\n")
}
return sb.String()
},
"envName": func(opt clibase.Option) string {
if opt.Env == "" {
return ""
}
return opt.Env
},
"flagName": func(opt clibase.Option) string {
return opt.Flag
},
"prettyHeader": func(s string) string {
return cliui.DefaultStyles.Bold.Render(s)
},
"isEnterprise": func(opt clibase.Option) bool {
return opt.Annotations.IsSet("enterprise")
},
"isDeprecated": func(opt clibase.Option) bool {
return len(opt.UseInstead) > 0
},
"useInstead": func(opt clibase.Option) string {
var sb strings.Builder
for i, s := range opt.UseInstead {
if i > 0 {
if i == len(opt.UseInstead)-1 {
_, _ = sb.WriteString(" and ")
return sb.String()
},
"envName": func(opt clibase.Option) string {
if opt.Env == "" {
return ""
}
return opt.Env
},
"flagName": func(opt clibase.Option) string {
return opt.Flag
},
"isEnterprise": func(opt clibase.Option) bool {
return opt.Annotations.IsSet("enterprise")
},
"isDeprecated": func(opt clibase.Option) bool {
return len(opt.UseInstead) > 0
},
"useInstead": func(opt clibase.Option) string {
var sb strings.Builder
for i, s := range opt.UseInstead {
if i > 0 {
if i == len(opt.UseInstead)-1 {
_, _ = sb.WriteString(" and ")
} else {
_, _ = sb.WriteString(", ")
}
}
if s.Flag != "" {
_, _ = sb.WriteString("--")
_, _ = sb.WriteString(s.Flag)
} else if s.FlagShorthand != "" {
_, _ = sb.WriteString("-")
_, _ = sb.WriteString(s.FlagShorthand)
} else if s.Env != "" {
_, _ = sb.WriteString("$")
_, _ = sb.WriteString(s.Env)
} else {
_, _ = sb.WriteString(", ")
_, _ = sb.WriteString(s.Name)
}
}
if s.Flag != "" {
_, _ = sb.WriteString("--")
_, _ = sb.WriteString(s.Flag)
} else if s.FlagShorthand != "" {
_, _ = sb.WriteString("-")
_, _ = sb.WriteString(s.FlagShorthand)
} else if s.Env != "" {
_, _ = sb.WriteString("$")
_, _ = sb.WriteString(s.Env)
} else {
_, _ = sb.WriteString(s.Name)
}
}
return sb.String()
},
"formatLong": func(long string) string {
// We intentionally don't wrap here because it would misformat
// examples, where the new line would start without the prior
// line's indentation.
return strings.TrimSpace(long)
},
"formatGroupDescription": func(s string) string {
s = strings.ReplaceAll(s, "\n", "")
s = s + "\n"
s = wrapTTY(s)
return s
},
"visibleChildren": func(cmd *clibase.Cmd) []*clibase.Cmd {
return filterSlice(cmd.Children, func(c *clibase.Cmd) bool {
return !c.Hidden
})
},
"optionGroups": func(cmd *clibase.Cmd) []optionGroup {
groups := []optionGroup{{
// Default group.
Name: "",
Description: "",
}}
return sb.String()
},
"formatGroupDescription": func(s string) string {
s = strings.ReplaceAll(s, "\n", "")
s = s + "\n"
s = wrapTTY(s)
return s
},
"visibleChildren": func(cmd *clibase.Cmd) []*clibase.Cmd {
return filterSlice(cmd.Children, func(c *clibase.Cmd) bool {
return !c.Hidden
})
},
"optionGroups": func(cmd *clibase.Cmd) []optionGroup {
groups := []optionGroup{{
// Default group.
Name: "",
Description: "",
}}
enterpriseGroup := optionGroup{
Name: "Enterprise",
Description: `These options are only available in the Enterprise Edition.`,
}
// Sort options lexicographically.
sort.Slice(cmd.Options, func(i, j int) bool {
return cmd.Options[i].Name < cmd.Options[j].Name
})
optionLoop:
for _, opt := range cmd.Options {
if opt.Hidden {
continue
}
// Enterprise options are always grouped separately.
if opt.Annotations.IsSet("enterprise") {
enterpriseGroup.Options = append(enterpriseGroup.Options, opt)
continue
}
if len(opt.Group.Ancestry()) == 0 {
// Just add option to default group.
groups[0].Options = append(groups[0].Options, opt)
continue
enterpriseGroup := optionGroup{
Name: "Enterprise",
Description: `These options are only available in the Enterprise Edition.`,
}
groupName := opt.Group.FullName()
// Sort options lexicographically.
sort.Slice(cmd.Options, func(i, j int) bool {
return cmd.Options[i].Name < cmd.Options[j].Name
})
for i, foundGroup := range groups {
if foundGroup.Name != groupName {
optionLoop:
for _, opt := range cmd.Options {
if opt.Hidden {
continue
}
groups[i].Options = append(groups[i].Options, opt)
continue optionLoop
// Enterprise options are always grouped separately.
if opt.Annotations.IsSet("enterprise") {
enterpriseGroup.Options = append(enterpriseGroup.Options, opt)
continue
}
if len(opt.Group.Ancestry()) == 0 {
// Just add option to default group.
groups[0].Options = append(groups[0].Options, opt)
continue
}
groupName := opt.Group.FullName()
for i, foundGroup := range groups {
if foundGroup.Name != groupName {
continue
}
groups[i].Options = append(groups[i].Options, opt)
continue optionLoop
}
groups = append(groups, optionGroup{
Name: groupName,
Description: opt.Group.Description,
Options: clibase.OptionSet{opt},
})
}
groups = append(groups, optionGroup{
Name: groupName,
Description: opt.Group.Description,
Options: clibase.OptionSet{opt},
sort.Slice(groups, func(i, j int) bool {
// Sort groups lexicographically.
return groups[i].Name < groups[j].Name
})
}
sort.Slice(groups, func(i, j int) bool {
// Sort groups lexicographically.
return groups[i].Name < groups[j].Name
})
// Always show enterprise group last.
groups = append(groups, enterpriseGroup)
// Always show enterprise group last.
groups = append(groups, enterpriseGroup)
return filterSlice(groups, func(g optionGroup) bool {
return len(g.Options) > 0
})
return filterSlice(groups, func(g optionGroup) bool {
return len(g.Options) > 0
})
},
},
},
).Parse(helpTemplateRaw),
)
).Parse(helpTemplateRaw),
)
}()
func filterSlice[T any](s []T, f func(T) bool) []T {
var r []T
@@ -254,31 +272,41 @@ func filterSlice[T any](s []T, f func(T) bool) []T {
// newLineLimiter makes working with Go templates more bearable. Without this,
// modifying the template is a slow toil of counting newlines and constantly
// checking that a change to one command's help doesn't clobber break another.
// checking that a change to one command's help doesn't break another.
type newlineLimiter struct {
w io.Writer
// w is not an interface since we call WriteRune byte-wise,
// and the devirtualization overhead is significant.
w *bufio.Writer
limit int
newLineCounter int
}
// isSpace is a based on unicode.IsSpace, but only checks ASCII characters.
func isSpace(b byte) bool {
switch b {
case '\t', '\n', '\v', '\f', '\r', ' ', 0x85, 0xA0:
return true
}
return false
}
func (lm *newlineLimiter) Write(p []byte) (int, error) {
rd := bytes.NewReader(p)
for r, n, _ := rd.ReadRune(); n > 0; r, n, _ = rd.ReadRune() {
for _, b := range p {
switch {
case r == '\r':
case b == '\r':
// Carriage returns can sneak into `help.tpl` when `git clone`
// is configured to automatically convert line endings.
continue
case r == '\n':
case b == '\n':
lm.newLineCounter++
if lm.newLineCounter > lm.limit {
continue
}
case !unicode.IsSpace(r):
case !isSpace(b):
lm.newLineCounter = 0
}
_, err := lm.w.Write([]byte(string(r)))
err := lm.w.WriteByte(b)
if err != nil {
return 0, err
}
+13 -11
View File
@@ -1,20 +1,22 @@
{{- /* Heavily inspired by the Go toolchain formatting. */ -}}
Usage: {{.FullUsage}}
{{- /* Heavily inspired by the Go toolchain and fd */ -}}
coder {{version}}
{{prettyHeader "Usage"}}
{{indent .FullUsage 2}}
{{ with .Short }}
{{- wrapTTY . }}
{{- indent . 2 | wrapTTY }}
{{"\n"}}
{{- end}}
{{ with .Aliases }}
{{ "\n" }}
{{ "Aliases:"}} {{ joinStrings .}}
{{ "\n" }}
{{" Aliases: "}} {{- joinStrings .}}
{{- end }}
{{- with .Long}}
{{- formatLong . }}
{{"\n"}}
{{- indent . 2}}
{{ "\n" }}
{{- end }}
{{ with visibleChildren . }}
@@ -34,11 +36,11 @@ Usage: {{.FullUsage}}
{{- else }}
{{- end }}
{{- range $index, $option := $group.Options }}
{{- if not (eq $option.FlagShorthand "") }}{{- print "\n -" $option.FlagShorthand ", " -}}
{{- if not (eq $option.FlagShorthand "") }}{{- print "\n "}} {{ keyword "-"}}{{keyword $option.FlagShorthand }}{{", "}}
{{- else }}{{- print "\n " -}}
{{- end }}
{{- with flagName $option }}--{{ . }}{{ end }} {{- with typeHelper $option }} {{ . }}{{ end }}
{{- with envName $option }}, ${{ . }}{{ end }}
{{- with flagName $option }}{{keyword "--"}}{{ keyword . }}{{ end }} {{- with typeHelper $option }} {{ . }}{{ end }}
{{- with envName $option }}, {{ print "$" . | keyword }}{{ end }}
{{- with $option.Default }} (default: {{ . }}){{ end }}
{{- with $option.Description }}
{{- $desc := $option.Description }}
@@ -47,7 +49,7 @@ Usage: {{.FullUsage}}
{{- end -}}
{{- end }}
{{- end }}
---
———
{{- if .Parent }}
Run `coder --help` for a list of global options.
{{- else }}
+4 -2
View File
@@ -7,6 +7,8 @@ import (
"github.com/google/uuid"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/schedule/cron"
@@ -119,9 +121,9 @@ func (r *RootCmd) list() *clibase.Cmd {
return err
}
if len(res.Workspaces) == 0 {
_, _ = fmt.Fprintln(inv.Stderr, cliui.DefaultStyles.Prompt.String()+"No workspaces found! Create one:")
pretty.Fprintf(inv.Stderr, cliui.DefaultStyles.Prompt, "No workspaces found! Create one:\n")
_, _ = fmt.Fprintln(inv.Stderr)
_, _ = fmt.Fprintln(inv.Stderr, " "+cliui.DefaultStyles.Code.Render("coder create <name>"))
_, _ = fmt.Fprintln(inv.Stderr, " "+pretty.Sprint(cliui.DefaultStyles.Code, "coder create <name>"))
_, _ = fmt.Fprintln(inv.Stderr)
return nil
}
+16 -14
View File
@@ -21,14 +21,15 @@ func TestList(t *testing.T) {
t.Run("Single", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, member, owner.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
inv, root := clitest.New(t, "ls")
clitest.SetupConfig(t, client, root)
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t).Attach(inv)
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitLong)
@@ -48,15 +49,16 @@ func TestList(t *testing.T) {
t.Run("JSON", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, member, owner.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
inv, root := clitest.New(t, "list", "--output=json")
clitest.SetupConfig(t, client, root)
clitest.SetupConfig(t, member, root)
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancelFunc()
+15 -16
View File
@@ -16,6 +16,8 @@ import (
"github.com/pkg/browser"
"golang.org/x/xerrors"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/userpassword"
@@ -43,7 +45,7 @@ func promptFirstUsername(inv *clibase.Invocation) (string, error) {
return "", xerrors.Errorf("get current user: %w", err)
}
username, err := cliui.Prompt(inv, cliui.PromptOptions{
Text: "What " + cliui.DefaultStyles.Field.Render("username") + " would you like?",
Text: "What " + pretty.Sprint(cliui.DefaultStyles.Field, "username") + " would you like?",
Default: currentUser.Username,
})
if errors.Is(err, cliui.Canceled) {
@@ -59,7 +61,7 @@ func promptFirstUsername(inv *clibase.Invocation) (string, error) {
func promptFirstPassword(inv *clibase.Invocation) (string, error) {
retry:
password, err := cliui.Prompt(inv, cliui.PromptOptions{
Text: "Enter a " + cliui.DefaultStyles.Field.Render("password") + ":",
Text: "Enter a " + pretty.Sprint(cliui.DefaultStyles.Field, "password") + ":",
Secret: true,
Validate: func(s string) error {
return userpassword.Validate(s)
@@ -69,7 +71,7 @@ retry:
return "", xerrors.Errorf("specify password prompt: %w", err)
}
confirm, err := cliui.Prompt(inv, cliui.PromptOptions{
Text: "Confirm " + cliui.DefaultStyles.Field.Render("password") + ":",
Text: "Confirm " + pretty.Sprint(cliui.DefaultStyles.Field, "password") + ":",
Secret: true,
Validate: cliui.ValidateNotEmpty,
})
@@ -78,7 +80,7 @@ retry:
}
if confirm != password {
_, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Error.Render("Passwords do not match"))
_, _ = fmt.Fprintln(inv.Stdout, pretty.Sprint(cliui.DefaultStyles.Error, "Passwords do not match"))
goto retry
}
@@ -115,12 +117,8 @@ func (r *RootCmd) loginWithPassword(
_, _ = fmt.Fprintf(
inv.Stdout,
cliui.DefaultStyles.Paragraph.Render(
fmt.Sprintf(
"Welcome to Coder, %s! You're authenticated.",
cliui.DefaultStyles.Keyword.Render(u.Username),
),
)+"\n",
"Welcome to Coder, %s! You're authenticated.",
pretty.Sprint(cliui.DefaultStyles.Keyword, u.Username),
)
return nil
@@ -177,7 +175,7 @@ func (r *RootCmd) login() *clibase.Cmd {
if err != nil {
// Checking versions isn't a fatal error so we print a warning
// and proceed.
_, _ = fmt.Fprintln(inv.Stderr, cliui.DefaultStyles.Warn.Render(err.Error()))
_, _ = fmt.Fprintln(inv.Stderr, pretty.Sprint(cliui.DefaultStyles.Warn, err.Error()))
}
hasFirstUser, err := client.HasFirstUser(ctx)
@@ -209,7 +207,7 @@ func (r *RootCmd) login() *clibase.Cmd {
if email == "" {
email, err = cliui.Prompt(inv, cliui.PromptOptions{
Text: "What's your " + cliui.DefaultStyles.Field.Render("email") + "?",
Text: "What's your " + pretty.Sprint(cliui.DefaultStyles.Field, "email") + "?",
Validate: func(s string) error {
err := validator.New().Var(s, "email")
if err != nil {
@@ -261,7 +259,9 @@ func (r *RootCmd) login() *clibase.Cmd {
_, _ = fmt.Fprintf(
inv.Stdout,
cliui.DefaultStyles.Paragraph.Render("Get started by creating a template: "+cliui.DefaultStyles.Code.Render("coder templates init"))+"\n")
"Get started by creating a template: %s\n",
pretty.Sprint(cliui.DefaultStyles.Code, "coder templates init"),
)
return nil
}
@@ -278,8 +278,7 @@ func (r *RootCmd) login() *clibase.Cmd {
}
sessionToken, err = cliui.Prompt(inv, cliui.PromptOptions{
Text: "Paste your token here:",
Secret: true,
Text: "Paste your token here:",
Validate: func(token string) error {
client.SetSessionToken(token)
_, err := client.User(ctx, codersdk.Me)
@@ -327,7 +326,7 @@ func (r *RootCmd) login() *clibase.Cmd {
return xerrors.Errorf("write server url: %w", err)
}
_, _ = fmt.Fprintf(inv.Stdout, Caret+"Welcome to Coder, %s! You're authenticated.\n", cliui.DefaultStyles.Keyword.Render(resp.Username))
_, _ = fmt.Fprintf(inv.Stdout, Caret+"Welcome to Coder, %s! You're authenticated.\n", pretty.Sprint(cliui.DefaultStyles.Keyword, resp.Username))
return nil
},
}
+12 -1
View File
@@ -3,11 +3,14 @@ package cli_test
import (
"context"
"fmt"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/coderd/coderdtest"
@@ -141,7 +144,7 @@ func TestLogin(t *testing.T) {
// Validate that we reprompt for matching passwords.
pty.ExpectMatch("Passwords do not match")
pty.ExpectMatch("Enter a " + cliui.DefaultStyles.Field.Render("password"))
pty.ExpectMatch("Enter a " + pretty.Sprint(cliui.DefaultStyles.Field, "password"))
pty.WriteLine("SomeSecurePassword!")
pty.ExpectMatch("Confirm")
@@ -168,6 +171,10 @@ func TestLogin(t *testing.T) {
pty.ExpectMatch("Paste your token here:")
pty.WriteLine(client.SessionToken())
if runtime.GOOS != "windows" {
// For some reason, the match does not show up on Windows.
pty.ExpectMatch(client.SessionToken())
}
pty.ExpectMatch("Welcome to Coder")
<-doneChan
})
@@ -191,6 +198,10 @@ func TestLogin(t *testing.T) {
pty.ExpectMatch("Paste your token here:")
pty.WriteLine("an-invalid-token")
if runtime.GOOS != "windows" {
// For some reason, the match does not show up on Windows.
pty.ExpectMatch("an-invalid-token")
}
pty.ExpectMatch("That's not a valid token!")
cancelFunc()
<-doneChan
+3 -1
View File
@@ -5,6 +5,8 @@ import (
"golang.org/x/xerrors"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
@@ -203,7 +205,7 @@ func (pr *ParameterResolver) resolveWithInput(resolved []codersdk.WorkspaceBuild
Value: parameterValue,
})
} else if action == WorkspaceUpdate && !tvp.Mutable && !firstTimeUse {
_, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Warn.Render(fmt.Sprintf("Parameter %q is not mutable, and cannot be customized after workspace creation.", tvp.Name)))
_, _ = fmt.Fprintln(inv.Stdout, pretty.Sprint(cliui.DefaultStyles.Warn, fmt.Sprintf("Parameter %q is not mutable, and cannot be customized after workspace creation.", tvp.Name)))
}
}
return resolved, nil
+9 -7
View File
@@ -10,6 +10,8 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
"github.com/coder/pretty"
"github.com/coder/coder/v2/cli/clibase"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
@@ -104,14 +106,14 @@ func (r *RootCmd) ping() *clibase.Cmd {
if p2p {
if !didP2p {
_, _ = fmt.Fprintln(inv.Stdout, "p2p connection established in",
cliui.DefaultStyles.DateTimeStamp.Render(time.Since(start).Round(time.Millisecond).String()),
pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, time.Since(start).Round(time.Millisecond).String()),
)
}
didP2p = true
via = fmt.Sprintf("%s via %s",
cliui.DefaultStyles.Fuchsia.Render("p2p"),
cliui.DefaultStyles.Code.Render(pong.Endpoint),
pretty.Sprint(cliui.DefaultStyles.Fuchsia, "p2p"),
pretty.Sprint(cliui.DefaultStyles.Code, pong.Endpoint),
)
} else {
derpName := "unknown"
@@ -120,15 +122,15 @@ func (r *RootCmd) ping() *clibase.Cmd {
derpName = derpRegion.RegionName
}
via = fmt.Sprintf("%s via %s",
cliui.DefaultStyles.Fuchsia.Render("proxied"),
cliui.DefaultStyles.Code.Render(fmt.Sprintf("DERP(%s)", derpName)),
pretty.Sprint(cliui.DefaultStyles.Fuchsia, "proxied"),
pretty.Sprint(cliui.DefaultStyles.Code, fmt.Sprintf("DERP(%s)", derpName)),
)
}
_, _ = fmt.Fprintf(inv.Stdout, "pong from %s %s in %s\n",
cliui.DefaultStyles.Keyword.Render(workspaceName),
pretty.Sprint(cliui.DefaultStyles.Keyword, workspaceName),
via,
cliui.DefaultStyles.DateTimeStamp.Render(dur.String()),
pretty.Sprint(cliui.DefaultStyles.DateTimeStamp, dur.String()),
)
if n == int(pingNum) {
+4 -13
View File
@@ -6,11 +6,9 @@ import (
"github.com/stretchr/testify/assert"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/agent"
"github.com/coder/coder/v2/agent/agenttest"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/pty/ptytest"
"github.com/coder/coder/v2/testutil"
)
@@ -29,15 +27,8 @@ func TestPing(t *testing.T) {
inv.Stderr = pty.Output()
inv.Stdout = pty.Output()
agentClient := agentsdk.New(client.URL)
agentClient.SetSessionToken(agentToken)
agentCloser := agent.New(agent.Options{
Client: agentClient,
Logger: slogtest.Make(t, nil).Named("agent"),
})
defer func() {
_ = agentCloser.Close()
}()
_ = agenttest.New(t, client.URL, agentToken)
_ = coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
+25 -33
View File
@@ -7,12 +7,15 @@ import (
"net"
"sync"
"testing"
"time"
"github.com/google/uuid"
"github.com/pion/udp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/agent"
"github.com/coder/coder/v2/agent/agenttest"
"github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/codersdk"
@@ -25,10 +28,11 @@ func TestPortForward_None(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
inv, root := clitest.New(t, "port-forward", "blah")
clitest.SetupConfig(t, client, root)
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t).Attach(inv)
inv.Stderr = pty.Output()
@@ -129,8 +133,9 @@ func TestPortForward(t *testing.T) {
// non-parallel setup).
var (
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user = coderdtest.CreateFirstUser(t, client)
workspace = runAgent(t, client, user.UserID)
admin = coderdtest.CreateFirstUser(t, client)
member, _ = coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
workspace = runAgent(t, client, member)
)
for _, c := range cases {
@@ -148,7 +153,7 @@ func TestPortForward(t *testing.T) {
// Launch port-forward in a goroutine so we can start dialing
// the "local" listener.
inv, root := clitest.New(t, "-v", "port-forward", workspace.Name, flag)
clitest.SetupConfig(t, client, root)
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t)
inv.Stdin = pty.Input()
inv.Stdout = pty.Output()
@@ -195,7 +200,7 @@ func TestPortForward(t *testing.T) {
// Launch port-forward in a goroutine so we can start dialing
// the "local" listeners.
inv, root := clitest.New(t, "-v", "port-forward", workspace.Name, flag1, flag2)
clitest.SetupConfig(t, client, root)
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t)
inv.Stdin = pty.Input()
inv.Stdout = pty.Output()
@@ -250,7 +255,7 @@ func TestPortForward(t *testing.T) {
// Launch port-forward in a goroutine so we can start dialing
// the "local" listeners.
inv, root := clitest.New(t, append([]string{"-v", "port-forward", workspace.Name}, flags...)...)
clitest.SetupConfig(t, client, root)
clitest.SetupConfig(t, member, root)
pty := ptytest.New(t).Attach(inv)
inv.Stderr = pty.Output()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
@@ -291,46 +296,33 @@ func TestPortForward(t *testing.T) {
// runAgent creates a fake workspace and starts an agent locally for that
// workspace. The agent will be cleaned up on test completion.
// nolint:unused
func runAgent(t *testing.T, client *codersdk.Client, userID uuid.UUID) codersdk.Workspace {
func runAgent(t *testing.T, adminClient, userClient *codersdk.Client) codersdk.Workspace {
ctx := context.Background()
user, err := client.User(ctx, userID.String())
user, err := userClient.User(ctx, codersdk.Me)
require.NoError(t, err, "specified user does not exist")
require.Greater(t, len(user.OrganizationIDs), 0, "user has no organizations")
orgID := user.OrganizationIDs[0]
// Setup template
agentToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{
version := coderdtest.CreateTemplateVersion(t, adminClient, orgID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: echo.PlanComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(agentToken),
})
// Create template and workspace
template := coderdtest.CreateTemplate(t, client, orgID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, orgID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
template := coderdtest.CreateTemplate(t, adminClient, orgID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, adminClient, version.ID)
workspace := coderdtest.CreateWorkspace(t, userClient, orgID, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, adminClient, workspace.LatestBuild.ID)
// Start workspace agent in a goroutine
inv, root := clitest.New(t, "agent", "--agent-token", agentToken, "--agent-url", client.URL.String())
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
inv.Stdin = pty.Input()
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()
errC := make(chan error)
agentCtx, agentCancel := context.WithCancel(ctx)
t.Cleanup(func() {
agentCancel()
err := <-errC
require.NoError(t, err)
})
go func() {
errC <- inv.WithContext(agentCtx).Run()
}()
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
_ = agenttest.New(t, adminClient.URL, agentToken,
func(o *agent.Options) {
o.SSHMaxTimeout = 60 * time.Second
},
)
coderdtest.AwaitWorkspaceAgents(t, adminClient, workspace.ID)
return workspace
}

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