Compare commits

...

185 Commits

Author SHA1 Message Date
Kyle Carberry 67952cf95e fix: move the web terminal out of the dashboard authentication layout (#5699)
Fixes #5698. This was a regression.
2023-01-12 21:44:29 +00:00
Mathias Fredriksson 269e0b3261 ci: Fix release tag push (#5696) 2023-01-12 22:18:10 +02:00
Bruno Quaresma 3861d1c555 refactor: Wrap forms into dashboard layout (#5697) 2023-01-12 17:08:31 -03:00
sharkymark bef6f67b70 docs: remove plans to license restrict oidc and git auth (#5672) 2023-01-12 19:21:56 +00:00
Bruno Quaresma e6072eff59 refactor: Wrap authenticated routes (#5695) 2023-01-12 15:52:16 -03:00
Bruno Quaresma f9f7283e16 refactor: Move deploy settings machine to the layout (#5693) 2023-01-12 17:02:11 +00:00
Bruno Quaresma cd1a2d2d5d refactor: Refactor site roles machine to be used in the page (#5692) 2023-01-12 13:53:46 -03:00
Bruno Quaresma f5a7538637 refactor: Make the navbar wider (#5689) 2023-01-12 16:34:56 +00:00
Mathias Fredriksson a5073a8770 ci: Fix release workflow input booleans, remove snapshot (#5688)
* s/github.event.inputs/inputs/g

* Add run name and prevent non-dry-run releases on non-main branches

* Add logrun to lib.sh
2023-01-12 15:50:58 +00:00
Kira Pilot 575bfabfcb fix: audit log workspace build URL should form with the correct workspace owner (#5674)
* removing workspaceOwner

* querying for workspace build
2023-01-12 09:51:30 -05:00
Kyle Carberry 41b58cd027 fix: open VS Code Remote in the same window to prevent flashing (#5684)
Fixes #5676.
2023-01-11 23:47:44 +00:00
Mathias Fredriksson c7e1ecfe36 ci: Fix release workflow inputs (#5681) 2023-01-11 23:32:25 +00:00
Presley Pizzo 1df72ee093 fix: handle NaN in build time estimate (#5679) 2023-01-11 15:56:21 -05:00
Mathias Fredriksson c0d9e32300 ci: Allow missing commit metadata to be ignored in releases (#5678) 2023-01-11 20:14:04 +00:00
Presley Pizzo 627fbe5874 fix: make build table show empty instead of loading when none are recent (#5666)
* Fix builds to show empty instead of loading

* Switch to backend fix

* Increase e2e test timeout

* Format
2023-01-11 12:18:06 -05:00
Bruno Quaresma a5d39adf3e refactor: Extract ssh logic from auth service (#5670)
* refactor: Extract ssh logic from auth service

* Update site/src/i18n/en/userSettingsPage.json

Co-authored-by: Kira Pilot <kira@coder.com>

Co-authored-by: Kira Pilot <kira@coder.com>
2023-01-11 17:04:42 +00:00
Mathias Fredriksson 8e4af79cb2 ci: Do release tagging in CI and add --draft support (#5652)
* ci: Do release tagging in CI and add --draft support

* Add -h, --help to release.sh

* Add -h, --help to increment_version_tag.sh

* Limit release concurrency

* Add automatic release watching

* ci: Add git config, tag as "GitHub Actions Bot"

Co-authored-by: Dean Sheather <dean@deansheather.com>
2023-01-11 18:38:01 +02:00
Dean Sheather e72a2ad907 feat: add SIGQUIT/SIGTRAP handler for the CLI (#5665) 2023-01-11 16:22:20 +00:00
Steve Miller 69241d06e7 docs: Update WebIDE Section Headers (#5669)
* Update header indents

* Bump To Rerun CI
2023-01-11 15:48:29 +00:00
Marcin Tojek d9436fab69 docs: API enterprise (#5625)
* docs: audit, deploymentconfig, files, parameters

* Swagger comments in workspacebuilds.go

* structs in workspacebuilds.go

* workspaceagents: instance identity

* workspaceagents.go in progress

* workspaceagents.go in progress

* Agents

* workspacebuilds.go

* /workspaces

* templates.go, templateversions.go

* templateversion.go in progress

* cancel

* templateversions

* wip

* Merge

* x-apidocgen

* NullTime hack not needed anymore

* Fix: x-apidocgen

* Members

* Fixes

* Fix

* WIP

* WIP

* Users

* Logout

* User profile

* Status suspend activate

* User roles

* User tokens

* Keys

* SSH key

* All

* Typo

* Fix

* Entitlements

* Groups

* SCIM

* Fix

* Fix

* Clean templates

* Sort API pages

* Fix: HashedSecret

* General is first
2023-01-11 16:05:42 +01:00
Marcin Tojek 8e9cbdd71b docs: API users (#5620)
* docs: audit, deploymentconfig, files, parameters

* Swagger comments in workspacebuilds.go

* structs in workspacebuilds.go

* workspaceagents: instance identity

* workspaceagents.go in progress

* workspaceagents.go in progress

* Agents

* workspacebuilds.go

* /workspaces

* templates.go, templateversions.go

* templateversion.go in progress

* cancel

* templateversions

* wip

* Merge

* x-apidocgen

* NullTime hack not needed anymore

* Fix: x-apidocgen

* Members

* Fixes

* Fix

* WIP

* WIP

* Users

* Logout

* User profile

* Status suspend activate

* User roles

* User tokens

* Keys

* SSH key

* All

* Typo

* Fix

* Fix

* Fix: LoginWithPasswordRequest
2023-01-11 14:08:04 +01:00
Marcin Tojek 84120767a7 docs: API templateversions, templates, members, organizations (#5546)
* docs: audit, deploymentconfig, files, parameters

* Swagger comments in workspacebuilds.go

* structs in workspacebuilds.go

* workspaceagents: instance identity

* workspaceagents.go in progress

* workspaceagents.go in progress

* Agents

* workspacebuilds.go

* /workspaces

* templates.go, templateversions.go

* templateversion.go in progress

* cancel

* templateversions

* wip

* Merge

* x-apidocgen

* NullTime hack not needed anymore

* Fix: x-apidocgen

* Members

* Fixes

* Fix
2023-01-11 12:16:09 +01:00
Mathias Fredriksson 5a3985e6be test: Use global swagger handler to avoid data race in tests (#5668) 2023-01-11 12:42:49 +02:00
Ammar Bandukwala 41cefef95a docs: fix minor mistake about resource persistence 2023-01-11 02:12:51 +00:00
Muhammad Atif Ali 370934afdf ci: allow writing security events for CodeQL (#5514) 2023-01-10 19:40:32 -06:00
Joe Previte 2296432e8b docs: update space on prem link (#5628) 2023-01-10 19:37:35 -06:00
Kyle Carberry 01652e8afb fix: disable pointer events on app icons (#5664)
Ben accidentally clicked to open this in a new tab
which seemed kinda janky UX-wise on our part.
2023-01-10 21:42:33 +00:00
Bruno Quaresma f5d623ff3f refactor: User settings page (#5661) 2023-01-10 17:57:08 -03:00
Ammar Bandukwala d5ab06ed68 feat: improve copy in new template wizard (#5659) 2023-01-10 18:46:08 +00:00
Kira Pilot 0171ccbf62 chore: forbid direct react import (#5658) 2023-01-10 13:30:48 -05:00
Michel Racic efee03fdec fix(site): changing password no longer silently trims space chars in a password (#5640) 2023-01-10 11:34:58 -06:00
Presley Pizzo 56a69b7eea chore: add e2e tests for basic template and workspace flow (#5637)
* Fix type error in first user setup

* Save auth state

* Add template creation - wip

Remove saved auth state because it wasn't working

* Try adding the rest of the tests

Can't see if they work yet, waiting on a release

* Update playwright

* Update gitignore

* Write tests

* Format

* Update ignores

* Check that start worked

Co-authored-by: Ben Potter <ben@coder.com>

Co-authored-by: Ben Potter <ben@coder.com>
2023-01-10 12:30:44 -05:00
Cian Johnston 19ae42af53 chore: update lima example to use --with-terraform arg (#5655)
#5586 added the capability for install.sh to download and install Terraform automatically.
Using this now in the example Lima specification.
Also no longer hard-coding the instance name in favour of {{.Instance.Name}} in the output
that gets emitted upon successful instance provisioning.
2023-01-10 17:25:46 +00:00
Kira Pilot f96365a181 chore: remove redundant icon stories (#5656) 2023-01-10 11:53:51 -05:00
Muhammad Atif Ali dda8170427 fix(ci): fixed $vesrion being empty in packages.yaml (#5650) 2023-01-10 10:40:06 -06:00
Colin Adler 4f3ac95a39 fix(helm): use correct antiaffinity label (#5649) 2023-01-10 10:18:58 -06:00
Colin Adler 2effea5806 fix(helm): use correct prometheus port (#5644) 2023-01-10 10:16:56 -06:00
Colin Adler d34540ca30 fix: ignore EINVAL when fsyncing /dev/stdout (#5648) 2023-01-10 10:15:53 -06:00
Kyle Carberry d2ef727064 feat: add experimental button to open vscode locally (#5654)
* feat: add experimental button to open vscode locally

This uses the new Coder extension to open up any workspace
with a single click.

* Update site/src/components/VSCodeDesktopButton/VSCodeDesktopButton.stories.tsx

Co-authored-by: Kira Pilot <kira@coder.com>

Co-authored-by: Kira Pilot <kira@coder.com>
2023-01-10 16:07:40 +00:00
Marcin Tojek a23a471034 docs: update swaggo/swag v1.8.9 (#5590)
* docs: update swaggo/swag v1.8.9

* Fix: format

* swaggo: time.Duration

* swaggo: provisionertype

* Fix: AuthorizationObject

* Fix: enums

* Fix: netip.Addr

* Fix: clickable response properties
2023-01-10 15:47:08 +01:00
Mathias Fredriksson bbe33fef41 chore: Revert title case in release notes (#5653) 2023-01-10 16:04:33 +02:00
Kyle Carberry 52d7dfa253 docs: remove unfinished sentence in templates.md (#5647)
Fixes #5643.
2023-01-09 22:24:09 -06:00
Kyle Carberry 9f6edab53b feat: replace vscodeipc with vscodessh (#5645)
The VS Code extension has been refactored to use VS Code
Remote SSH instead of using the private API.

This changes the structure to continue using SSH, but
output network information periodically to a file.
2023-01-10 04:23:17 +00:00
Joe Previte fa7deaaa5c feat: add storybook for /deployment/security (#5610)
* refactor: move securitysettings to dir

* refactor: split page view SecuritySettingsPage

* feat: add storybook for security page

* fixup
2023-01-09 20:44:43 +00:00
Bruno Quaresma f70726b43c refactor: Extract security logic from auth service (#5635) 2023-01-09 13:18:32 -07:00
Bruno Quaresma fe16b2a06d refactor: Add spacing in the bottom of a page (#5633) 2023-01-09 13:17:48 -07:00
Colin Adler 7bcbf197c1 fix: print correct listen adress in coder server (#5634) 2023-01-09 13:59:23 -06:00
Mathias Fredriksson 68324c7263 chore: Add security section to release notes (#5636) 2023-01-09 19:57:42 +00:00
Muhammad Atif Ali eb8d5b4408 fix(ci): fix winget package submission (#5630)
* fix(ci): fix winget package submission

I removed the step to calculate the version, as somehow the $version was not populated with the version.
Also, GitHub actions suggest removing `:set-output:` as it is deprecated. 

This commit should probably fix the winget package submission using `wingetcreate` cli.

* fixed a typo
2023-01-09 19:46:10 +00:00
Mathias Fredriksson aec15905b5 chore: Add more categories and titles for release notes (#5632)
Co-authored-by: Dean Sheather <dean@deansheather.com>
2023-01-09 21:19:07 +02:00
Bruno Quaresma 70d71bc7bc refactor: Do not display port forward button if it is disabled (#5604) 2023-01-09 13:38:31 -03:00
dependabot[bot] 34225b0380 chore: bump luxon from 3.1.1 to 3.2.1 in /site (#5624)
Bumps [luxon](https://github.com/moment/luxon) from 3.1.1 to 3.2.1.
- [Release notes](https://github.com/moment/luxon/releases)
- [Changelog](https://github.com/moment/luxon/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moment/luxon/compare/3.1.1...3.2.1)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-09 13:16:42 -03:00
Joe Previte 6807ad0d1b feat: add storybook for /deployment/userauth (#5609)
* refactor: move deploysettingspage to dir

* refactor: split page/view UserAuthSettings

* feat: add storybook for user auth

* Update site/src/components/DeploySettingsLayout/OptionsTable.tsx
2023-01-09 16:01:22 +00:00
Colin Adler a4ca8ffa65 fix: don't hang forever getting pg version (#5614) 2023-01-06 21:07:22 -06:00
Colin Adler 888766c10d fix: respect global --url flag in coder login (#5613) 2023-01-06 20:57:25 -06:00
Nathanial Spearing 9b602f55e0 feat: Added --with-terraform argument to install coder and terraform together (#5586)
* - Added a `--install-terraform` argument
- Added a unzip command check to the standalone function
	- Cleaner error and help redirect the user to a solution
- Added help info for `--install-terraform` argument
- Fixed standalone install typo (ard64 -> arm64)

* - Corrected formatting errors, and renamed functions

* - Fixed typos
- Added recommend changes for consistency

* Removed unzip check in standalone function

* Fixed styling

* Moved the TERRAFORM_VERSION Var up
2023-01-06 11:54:06 -06:00
Joe Previte 763147e5f2 feat: add storybook for /deployment/network (#5603)
* refactor: move NetworkSettingsPage to dir

* refactor: split page/view NetworkSettings

* feat: add storybook for NetworkSettingsPage
2023-01-06 17:14:01 +00:00
Joe Previte 242676bac3 feat: add storybook for /deployment/gitauth (#5596)
* refactor: move GitAuthSettingsPage to dir

* refactor: split page and view GitAuthSettingsPage

* fixup!: formatting

* refactor: narrow props in git auth view

* feat: add storybook for GitAuthSettingsPageView

* fixup: formatting
2023-01-06 16:58:20 +00:00
Bruno Quaresma aa68e0f8c9 fix: Too many requests during watching template version (#5602) 2023-01-06 13:31:49 -03:00
Dean Sheather f1fe2b5c06 feat: add GPG forwarding to coder ssh (#5482) 2023-01-06 07:52:19 +00:00
Joe Previte 59e919ab4a feat: add storybook for /deployments/general (#5595)
* refactor: split GeneralSettings page <> View

* feat: add story for generalsettingspageview

* Update site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx

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

Co-authored-by: Asher <ash@coder.com>
2023-01-05 23:06:16 +00:00
dependabot[bot] 421e529763 chore: bump json5 from 1.0.1 to 1.0.2 in /site (#5553)
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-05 15:55:56 -07:00
Joe Previte bb03df8148 feat: add storybook for /deployment/appearance page (#5582)
* wip

* wip: move appearancesettingspage

* refactor: separate page and view ApperanceSettings

* refactor: create storybook from AppearanceSettingsView

* fixup: formatting and types
2023-01-05 16:16:54 -05:00
Bruno Quaresma 0d30a1eb72 fix: Display service banner after login (#5594) 2023-01-05 21:10:15 +00:00
dependabot[bot] 8ee3e2c541 chore: bump chartjs-adapter-date-fns from 2.0.0 to 3.0.0 in /site (#5528)
* chore: bump chartjs-adapter-date-fns from 2.0.0 to 3.0.0 in /site

Bumps [chartjs-adapter-date-fns](https://github.com/chartjs/chartjs-adapter-date-fns) from 2.0.0 to 3.0.0.
- [Release notes](https://github.com/chartjs/chartjs-adapter-date-fns/releases)
- [Commits](https://github.com/chartjs/chartjs-adapter-date-fns/compare/v2.0.0...v3.0.0)

---
updated-dependencies:
- dependency-name: chartjs-adapter-date-fns
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

* added transformer for esm

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kira Pilot <kira.pilot23@gmail.com>
2023-01-05 15:58:04 -05:00
Dean Sheather 5a968e2f93 feat: add flag to disaable all rate limits (#5570) 2023-01-05 18:05:20 +00:00
Bruno Quaresma ab7e676b54 refactor: Refactor user menu (#5591) 2023-01-05 14:06:58 -03:00
Niklas Rosenstein dcf6c20132 feat: add coder.volumes parameter to Helm chart (#5551)
Co-authored-by: Dean Sheather <dean@deansheather.com>
2023-01-06 01:53:29 +10:00
Marcin Tojek 66fa2a1a8c docs: API workspace agents and builds (#5538) 2023-01-05 15:27:10 +01:00
Cian Johnston e6b17b6ea7 chore: update Lima example (#5588)
* chore: lima: update ubuntu image version

* fix: lima: make docker socket usable by Lima user without sudo

* fix: lima: set access URL to host.lima.internal

* apply suggestion from PR
2023-01-05 11:47:33 +00:00
Muhammad Atif Ali 0124289f1a fix(ci): fix winget installer workflow (#5569)
* fix(ci): add GH_TOKEN env

* chore: fix windows installer build filename

Co-authored-by: Dean Sheather <dean@deansheather.com>
2023-01-05 04:56:00 +00:00
Ben Potter 04d45f3c1c fix!: remove AUTO_IMPORT_TEMPLATE for Kubernetes installs (#5401)
* fix!: remove AUTO_IMPORT_TEMPLATE

* chore: remove template auto importing

Co-authored-by: Dean Sheather <dean@deansheather.com>
2023-01-05 04:04:32 +00:00
dependabot[bot] 24592332e2 chore: bump github.com/gohugoio/hugo from 0.107.0 to 0.109.0 (#5541)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-05 10:34:53 +10:30
dependabot[bot] 2db9df4491 chore: bump github.com/prometheus/common from 0.37.0 to 0.39.0 (#5544)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-04 23:42:13 +00:00
dependabot[bot] c0dfbdf143 chore: bump emoji-mart from 5.3.3 to 5.4.0 in /site (#5527)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-05 10:00:56 +10:30
Ammar Bandukwala 0b63825a07 site: fix copy in users roles view (#5583) 2023-01-04 17:13:04 -06:00
Joe Previte a231c1a384 fix: styles for <AlertBanner /> (#5579)
* feat: add new story for LoginPageView

* fix: update <AlertBanner /> styles

- align text to the left
- add padding to the top of span inside

* fixup: formatting
2023-01-04 14:46:41 -07:00
Kyle Carberry c51b5a05db fix: add case for non-entitled logo url (#5580)
This was missing from my prior contribution, which
would lead any user to believe they could customize the logo.
2023-01-04 21:39:54 +00:00
Kyle Carberry 0dba2defd1 feat: enable enterprise users to specify a custom logo (#5566)
* feat: enable enterprise users to specify a custom logo

This adds a field in deployment settings that allows users to specify
the URL to a custom logo that will display in the dashboard.

This also groups service banner into a new appearance settings page.
It adds a Fieldset component to allow for modular fields moving forward.

* Fix tests
2023-01-04 15:31:45 -06:00
Bruno Quaresma 175be621cf refactor: Improve roles UI (#5576) 2023-01-04 18:30:35 -03:00
Jan Losinski de0601d611 feat: allow configurable username claim field in OIDC (#5507)
Co-authored-by: Colin Adler <colin1adler@gmail.com>
2023-01-04 15:16:31 -06:00
Kyle Carberry 8968a00035 fix: add spacing between the copyright and login box (#5578)
I forgot to commit this before merging my prior PR!
2023-01-04 16:00:25 -03:00
Mathias Fredriksson ebe1b56c08 chore: Switch from npm to yarn in scripts/apidocgen (#5575) 2023-01-04 19:38:48 +01:00
Kyle Carberry a36cd0bd7b refactor: move footer items into the user dropdown (#5562)
* refactor: move footer items into the user dropdown

The items at the bottom looked unprofessional. Users don't
always need to be prompted to join our Discord or see the
active version of Coder.

This moves the items in the user dropdown which looks better.

* Update site/src/components/UserDropdownContent/UserDropdownContent.tsx

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

* Fix import order

Co-authored-by: Asher <ash@coder.com>
2023-01-04 12:36:25 -06:00
Marcin Tojek 925b29836c docs: improve authentication page (#5567)
* docs: improve authentication page

* Update docs/admin/automation.md

Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>

* Update docs/admin/automation.md

Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>

* Fix

* Fix

Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2023-01-04 10:14:47 -08:00
Dean Sheather 91a4c2dce1 chore: remove address from deployment page (#5565) 2023-01-04 23:50:55 +10:00
Steven Masley 5e540e3439 chore: Log out the failed audit log on failures (#5561) 2023-01-03 17:22:57 -06:00
Bruno Quaresma 4e14cc5207 refactor: Remove template UI from experimental (#5555) 2023-01-03 19:29:38 +00:00
Steven Masley c5128db484 docs: Add auditor role to roles table (#5557)
* docs: Add auditor role to roles table
* make fmt
2023-01-03 12:55:26 -06:00
dependabot[bot] 3e2477f255 chore: bump actions/stale from 6.0.0 to 7.0.0 (#5515)
Bumps [actions/stale](https://github.com/actions/stale) from 6.0.0 to 7.0.0.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/v6.0.0...v7.0.0)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-03 12:51:26 -06:00
dependabot[bot] ed114ec341 chore: bump golang.org/x/tools from 0.3.0 to 0.4.0 (#5542)
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.3.0 to 0.4.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.3.0...v0.4.0)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-03 12:44:37 -06:00
dependabot[bot] f1419bbc49 chore: bump golang.org/x/term from 0.2.0 to 0.3.0 (#5543)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.2.0 to 0.3.0.
- [Release notes](https://github.com/golang/term/releases)
- [Commits](https://github.com/golang/term/compare/v0.2.0...v0.3.0)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-03 12:44:31 -06:00
Marcin Tojek e67d131514 docs: audit, deploymentconfig, files, parameters (#5506)
* docs: audit, deploymentconfig, files, parameters

* Fix: mark as binary

* Fix: show format in docs

* Fix: use .swaggo

* Fix: swagger notice

* Swagger notice
2023-01-03 19:21:10 +01:00
Bruno Quaresma 829cfee29d refactor: Improve users table view for non admins (#5547) 2023-01-03 13:21:58 -03:00
dependabot[bot] 5e36fd522c chore: bump cronstrue from 2.14.0 to 2.21.0 in /site (#5545)
Bumps [cronstrue](https://github.com/bradymholt/cronstrue) from 2.14.0 to 2.21.0.
- [Release notes](https://github.com/bradymholt/cronstrue/releases)
- [Changelog](https://github.com/bradymholt/cRonstrue/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bradymholt/cronstrue/compare/v2.14.0...v2.21.0)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-03 13:21:42 -03:00
dependabot[bot] 3969a8b58b chore: bump prettier from 2.7.1 to 2.8.1 in /site (#5526)
Bumps [prettier](https://github.com/prettier/prettier) from 2.7.1 to 2.8.1.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.7.1...2.8.1)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-03 10:00:14 -05:00
Mathias Fredriksson 856f0ab6f5 chore: Improve project-wide prettier formatting and ignored files (#5505)
* chore: Improve project-wide prettier formatting and ignored files

* chore: `Run make fmt/prettier`

* Fix gitignore for `.vscode` folder so that ! works

* Add comment in `.prettierrc.yaml` to explain `.editorconfig`

* Remove scripts/apidocgen/markdown-template/README.md

* Use `yq` for processing prettierrc, update lib.sh dependency check

* Add `yq` to Dockerfile and Nix
2023-01-03 15:11:13 +02:00
dependabot[bot] 5435bceaf0 chore: bump tj-actions/branch-names from 6.3 to 6.4 (#5518)
Bumps [tj-actions/branch-names](https://github.com/tj-actions/branch-names) from 6.3 to 6.4.
- [Release notes](https://github.com/tj-actions/branch-names/releases)
- [Changelog](https://github.com/tj-actions/branch-names/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/branch-names/compare/v6.3...v6.4)

---
updated-dependencies:
- dependency-name: tj-actions/branch-names
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-02 14:42:58 -06:00
Marcin Tojek 8bb7e17bf1 chore!: remove GET workspaceagents/me/report-stats (#5530)
* chore!: remove GET workspaceagents/me/report-stats

* Fix: tests
2023-01-02 21:38:51 +01:00
dependabot[bot] d124fab642 chore: bump jaxxstorm/action-install-gh-release from 1.7.1 to 1.9.0 (#5516)
Bumps [jaxxstorm/action-install-gh-release](https://github.com/jaxxstorm/action-install-gh-release) from 1.7.1 to 1.9.0.
- [Release notes](https://github.com/jaxxstorm/action-install-gh-release/releases)
- [Commits](https://github.com/jaxxstorm/action-install-gh-release/compare/v1.7.1...v1.9.0)

---
updated-dependencies:
- dependency-name: jaxxstorm/action-install-gh-release
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-02 14:37:17 -06:00
dependabot[bot] 4b093115e2 chore: bump google-github-actions/setup-gcloud from 0 to 1 (#5517)
Bumps [google-github-actions/setup-gcloud](https://github.com/google-github-actions/setup-gcloud) from 0 to 1.
- [Release notes](https://github.com/google-github-actions/setup-gcloud/releases)
- [Changelog](https://github.com/google-github-actions/setup-gcloud/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google-github-actions/setup-gcloud/compare/v0...v1)

---
updated-dependencies:
- dependency-name: google-github-actions/setup-gcloud
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-02 14:37:06 -06:00
Ammar Bandukwala 05dc83e522 docs: add hero image to About (#5539)
A reddit comment recently linked to this page, so we want to
make it convert better.
2023-01-02 14:36:36 -06:00
dependabot[bot] b6dab5fbf7 chore: bump golang.org/x/text from 0.4.0 to 0.5.0 (#5521)
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.4.0 to 0.5.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.4.0...v0.5.0)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-02 14:34:25 -06:00
dependabot[bot] 54eb6a5b42 chore: bump github.com/valyala/fasthttp from 1.41.0 to 1.43.0 (#5522)
Bumps [github.com/valyala/fasthttp](https://github.com/valyala/fasthttp) from 1.41.0 to 1.43.0.
- [Release notes](https://github.com/valyala/fasthttp/releases)
- [Commits](https://github.com/valyala/fasthttp/compare/v1.41.0...v1.43.0)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-02 14:34:02 -06:00
dependabot[bot] 8d254bd94e chore: bump github.com/prometheus/client_model from 0.2.0 to 0.3.0 (#5523)
Bumps [github.com/prometheus/client_model](https://github.com/prometheus/client_model) from 0.2.0 to 0.3.0.
- [Release notes](https://github.com/prometheus/client_model/releases)
- [Commits](https://github.com/prometheus/client_model/compare/v0.2.0...v0.3.0)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-02 14:33:51 -06:00
dependabot[bot] f711abb236 chore: bump github.com/elastic/go-sysinfo from 1.8.1 to 1.9.0 (#5524)
Bumps [github.com/elastic/go-sysinfo](https://github.com/elastic/go-sysinfo) from 1.8.1 to 1.9.0.
- [Release notes](https://github.com/elastic/go-sysinfo/releases)
- [Changelog](https://github.com/elastic/go-sysinfo/blob/main/CHANGELOG.md)
- [Commits](https://github.com/elastic/go-sysinfo/compare/v1.8.1...v1.9.0)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-02 14:33:41 -06:00
dependabot[bot] 86c1753e2b chore: bump react-i18next from 12.0.0 to 12.1.1 in /site (#5525)
Bumps [react-i18next](https://github.com/i18next/react-i18next) from 12.0.0 to 12.1.1.
- [Release notes](https://github.com/i18next/react-i18next/releases)
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v12.0.0...v12.1.1)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-02 10:35:39 -03:00
Geoffrey Huntley 26b54cd144 chore(autofix): upgrade-examples-terraform-provider-coder (#5498)
Automatically generated via https://github.com/coder/autofix
2022-12-23 12:25:56 +10:30
Dean Sheather 3e2e2ac49e fix: enforce unique agent names per workspace (#5497) 2022-12-22 15:20:35 -08:00
sharkymark 461c0d0d39 docs: v1 docs redirect (#5509)
* docs: v1 docs redirect

* fix link

Co-authored-by: Ben <me@bpmct.net>
2022-12-22 15:14:36 -08:00
Muhammad Atif Ali 341c4329f4 ci: enable CodeQL code scanning (#5279)
Co-authored-by: Dean Sheather <dean@deansheather.com>
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
Co-authored-by: Geoffrey Huntley <ghuntley@ghuntley.com>
2022-12-22 22:12:55 +02:00
Presley Pizzo c8f34bbad7 Test enabling deadline change buttons (#5508) 2022-12-22 13:47:07 -05:00
Presley Pizzo 418022943a fix: use template default ttl when enabling auto-stop (#5494)
* Fetch default ttl - wip

* Convert ms to hours

* Format

* Fix story

* Add test
2022-12-22 12:52:27 -05:00
Marcin Tojek cfd02d959c docs: api root, buildinfo, csp (#5493)
* docs: Applications

* WIP

* WIP

* WIP

* Fix: consume

* Fix: @Description

* Fix

* docs: apiroot, buildinfo, csp

* Fix: buildinfo

* docs: updatecheck

* docs: apiroot

* Fix: s/none//g

* Fix: godoc nice

* Fix: description

* Fix: It

* Fix: code sample trim empty line

* More fixes

* Fix: br

* Merge

* Fix: no-security on updatecheck

* Fix: code tags

* Fix: enumerated values in code tags

* Rephrased

* Address PR comments

* Fix: URL, id

* Fix: array items

* Fix: any property

* Fix: array item singular
2022-12-22 15:53:14 +01:00
Bruno Quaresma c505e8b207 feat: Add create template from the UI (#5427) 2022-12-21 18:07:00 -03:00
Dean Sheather 43b61ce33c chore: support underscores in agent bin filenames (#5496) 2022-12-21 21:06:38 +00:00
Mathias Fredriksson bae69df8f9 build: Fix site/bin tar/zstd build step in rare error cases (#5495) 2022-12-21 22:59:49 +02:00
Colin Adler ac27cf8c07 fix: properly apply metadata when multiple resources share the same id (#5443) 2022-12-21 13:48:49 -05:00
whitney-coder 308a0602b6 Update high-availability.md (#5473)
Capitalization corrections
2022-12-21 09:47:32 -08:00
Presley Pizzo 0eb25306ad fix: stop time incrementer on workspace page (#5406)
* Check template default ttl while setting max

* Lint

* Remove template default from max ttl consideration

* Finish removing template

* Fix disabling buttons

* Simplify, wip

* Handle NaN

* Format

* Add aria labels

* Explain NaN handling

* Use more realistic storybook args
2022-12-21 10:44:18 -05:00
Presley Pizzo 8d9528545a feat: offer to restart workspace when ttl is changed (#5391)
* Update xstate machine

* Fix autoStopChanged

* Add dialog

* Restart workspace

* Clearing location doesn't work and doesn't seem necessary

* Fix test

* Fix second test

* Format

* Lint

* Use i18n

* Switch to fire and forget restart

* Improve error handling

* Format

* Format

* Update site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx

Co-authored-by: Kira Pilot <kira@coder.com>

* Fix name of guard

* Make done state final

* Format

Co-authored-by: Kira Pilot <kira@coder.com>
2022-12-21 10:11:54 -05:00
Marcin Tojek 2bbeff53f9 docs: applications and authorization (#5477)
* docs: Applications

* WIP

* WIP

* WIP

* Fix: consume

* Fix: @Description

* Fix

* Fix: s/none//g

* Fix: godoc nice

* Fix: description

* Fix: It

* Fix: code sample trim empty line

* More fixes

* Fix: br
2022-12-21 15:37:30 +01:00
Mathias Fredriksson 935bb99bed test: Merge env maps to simplify (#5481) 2022-12-20 20:40:41 +00:00
Dean Sheather 2ac31684f4 fix: use UIDs in Dockerfile (#5480) 2022-12-20 12:22:27 -08:00
Mathias Fredriksson c5cfefe3b2 test: Generate golden files for all (visible) CLI commands (#5479) 2022-12-20 22:17:51 +02:00
Mathias Fredriksson c7ce3e70da feat: Add --raw-url to coder server postgres-builtin-* commands (#5478) 2022-12-20 18:51:17 +00:00
Dean Sheather 50dfc2082b feat: endpoint to logout app subdomain URLs (#5428)
Co-authored-by: Bruno Quaresma <bruno@coder.com>
2022-12-20 18:45:13 +00:00
sharkymark 86257ce7fc docs: add contact us form for sales; improve enterprise page (#5459)
Co-authored-by: Geoffrey Huntley <ghuntley@ghuntley.com>
2022-12-20 13:01:28 +00:00
Mathias Fredriksson ca31f1b782 test: Update go-scp to fix data race (#5469) 2022-12-20 09:33:11 +00:00
Mathias Fredriksson a7e8f98e33 feat: Unhide workspace rename command (#5464) 2022-12-19 22:11:10 +02:00
Steven Masley e3cf759968 test: Unit tests creating fake audit logs require create permission (#5455) 2022-12-19 14:02:52 -06:00
Dean Sheather 1bc4eb5329 fix: fix security vulnerabilities reported by CodeQL (#5467) 2022-12-19 19:25:59 +00:00
Dean Sheather e359f3cd23 fix: change TLS client auth default to "none" (#5468) 2022-12-19 19:14:37 +00:00
Marcin Tojek dc6d271293 feat: Build framework for generating API docs (#5383)
* WIP

* Gen

* WIP

* chi swagger

* WIP

* WIP

* WIP

* GetWorkspaces

* GetWorkspaces

* Markdown

* Use widdershins

* WIP

* WIP

* WIP

* Markdown template

* Fix: makefile

* fmt

* Fix: comment

* Enable swagger conditionally

* fix: site

* Default false

* Flag tests

* fix

* fix

* template fixes

* Fix

* Fix

* Fix

* WIP

* Formatted

* Cleanup

* Templates

* BEGIN END SECTION

* subshell exit code

* Fix

* Fix merge

* WIP

* Fix

* Fix fmt

* Fix

* Generic api.md page

* Fix merge

* Link pages

* Fix

* Fix

* Fix: links

* Add icon

* Write manifest file

* Fix fmt

* Fix: enterprise

* Fix: Swagger.Enable

* Fix: rename apidocs to apidoc

* Fix: find -not -prune

* Fix: json not available

* Fix: rename Coderd API to Coder API

* Fix: npm exec

* Fix: api dir

* Fix: by ID

* Fix: string uuid

* Fix: include deleted

* Fix: indirect go.mod

* Fix: source lib.sh

* Fix: shellcheck

* Fix: pushd popd

* Fix: fmt

* Fix: improve workspaces

* Fix: swagger-enable

* Fix

* Fix: mention only HTTP 200

* Fix: IDs

* Fix: https

* Fix: icon

* More APis

* Fix: format swagger.json

* Fix: SwaggerEndpoint

* Fix: SCRIPT_DIR

* Fix: PROJECT_ROOT

* Fix: use code tags in schemas.md

* Fix: examples

* Fix: examples

* Fix: improve format

* Fix: date-time,enums

* Fix: include_deleted

* Fix: array of

* Fix: parameter, response

* Fix: string time or null

* Workspaces: more docs

* Workspaces: more docs

* Fix: renderDisplayName

* Fix: ActiveUserCount

* Fix

* Fix: typo

* Templates: docs

* Notice: incomplete
2022-12-19 18:43:46 +01:00
Kyle Carberry f239ca7ee3 fix: add the "workflow" scope for managing GitHub Actions with gitauth (#5461)
Seen in Discord: https://discord.com/channels/747933592273027093/1054155742871031858/1054155742871031858
2022-12-19 15:17:17 +02:00
Mathias Fredriksson 9983c07e13 build: Improve speed of find commands in Makefile (#5463) 2022-12-19 14:41:36 +02:00
Mathias Fredriksson 5a786edc3d test: Fix new name too long for cli/rename (#5462) 2022-12-19 11:58:22 +00:00
Kyle Carberry e61234f260 feat: Add vscodeipc subcommand for VS Code Extension (#5326)
* Add extio

* feat: Add `vscodeipc` subcommand for VS Code Extension

This enables the VS Code extension to communicate with a Coder client.
The extension will download the slim binary from `/bin/*` for the
respective client architecture and OS, then execute `coder vscodeipc`
for the connecting workspace.

* Add authentication header, improve comments, and add tests for the CLI

* Update cli/vscodeipc_test.go

Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>

* Update cli/vscodeipc_test.go

Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>

* Update cli/vscodeipc/vscodeipc_test.go

Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>

* Fix requested changes

* Fix IPC tests

* Fix shell execution

* Fix nix flake

* Silence usage

Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2022-12-18 17:50:06 -06:00
Kyle Carberry d1f8fec1d3 fix: use static base date for timeline tests (#5460)
It was returning "yesterday" since today is Sunday ;p
2022-12-18 15:27:09 -06:00
Dean Sheather 88d3496a99 fix: fix helm prometheus block causing failures (#5458) 2022-12-19 04:48:08 +10:00
Joe Previte a19c6fc988 fix: update coder dotfiles in dogfood (#5451)
I wasn't calling the environment variable I set.
2022-12-16 12:08:13 -07:00
ElliotG e76f947da2 Added sessionAffinity to values.yaml (#5448) 2022-12-16 09:58:43 -07:00
Dean Sheather 0c0e3f0e4d fix: fix nested dirs in example tars (#5447) 2022-12-17 02:19:19 +10:00
Kyle Carberry fcd5511403 fix: add provisioner tags to template push (#5446)
This was previously only on template create!
2022-12-16 15:13:03 +00:00
Mathias Fredriksson ffb8df9655 test: Disable error on agent log in scaletest/reconnectingpty (#5445)
They way the reconnectingpty tests behave inherently will cause the
agent to occasionally log an error (e.g. due to test disconnecting at a
certain time), allowing these error logs to fail the test will cause
these tests to be flakey.

It's best for these tests to only rely on the observed behavior.
2022-12-16 16:13:31 +02:00
Mathias Fredriksson e2aec2709b test: Fix scaletest/reconnectingpty commands for use in powershell (#5439) 2022-12-16 12:18:14 +02:00
Steven Masley 79c71d2d2c chore: Upgrade to sqlc version 2 yaml configuration (#5442)
* chore: Upgrade to sqlc version 2 yaml configuration
2022-12-15 20:40:11 +00:00
Joe Previte fceac39143 refactor: pin code-server to 4.8.3 (#5440)
* chore(templates): pin code-server to 4.8.3

* docs: use code-server 4.8.3 in install snippets
2022-12-15 13:14:49 -07:00
Dean Sheather 31d38d4246 feat: allow http and https listening simultaneously (#5365) 2022-12-15 20:09:19 +00:00
Dean Sheather 787b8b2a51 fix: fix app hostname returning port number (#5441) 2022-12-16 04:43:00 +10:00
Mathias Fredriksson 44c10bbe3c build: Fix parallelism of make -j build (#5438) 2022-12-15 18:36:37 +02:00
Dean Sheather 6b6eac2518 feat: remove loadtest cmd, add new scaletest cmd (#5310) 2022-12-15 15:04:24 +00:00
Mathias Fredriksson 306fe4a91b ci: Fix release publish script (#5436) 2022-12-15 14:31:57 +00:00
Mathias Fredriksson e96fdbed26 feat: Add release.sh script and detect breaking changes (#5366)
This commit introduces three new scripts:

- `release.sh` To be run by a user on their local machine to preview and
  create a new release (tag + push)
- `check_commit_metadata.sh` For e.g. detecting breaking changes
- `genereate_release_notes.sh` To display the generated release notes,
  used for previews and in `publish_release.sh`

The `release.sh` script can be run without arguments, and it will
automatically determine if we're to do a patch or minor release. A minor
release can be forced via `--minor` flag.

Breaking changes can be annotated either via commit/merge title prefix
(`feat!:`, `feat(api)!:`), or by adding the `release/breaking` label to
the PR that was merged (on GitHub).

Related #5233
2022-12-15 15:41:30 +02:00
Mathias Fredriksson 4bc420dc48 test: Fix data race in loadtest/reconnectingpty (#5431) 2022-12-15 15:06:58 +02:00
Mathias Fredriksson 25ebebac5f ci: Improve gotestsum failure detection, prevent early exit (#5420) 2022-12-15 12:47:42 +02:00
Kyle Carberry d170d27e80 feat: add external property to coder_app (#5425)
* Add schema

* feat: add `external` property to `coder_app`

This allows exposing applications that open an external URL.
2022-12-14 15:54:18 -06:00
Kyle Carberry 8bc247d0c9 fix: use proper validate url for gitauth (#5426)
This was preventing custom validation URLs from being
used to verify git tokens.
2022-12-14 21:02:35 +00:00
Kyle Carberry 84995b7320 fix: preserve workspace resource metadata order (#5421)
Fixes #4511.
2022-12-14 19:08:22 +00:00
Kyle Carberry c0b251ac52 fix: improve error messages when the agent token is invalid (#5423)
I'm not sure why this issue is common, but it seems to be
based on: https://github.com/coder/coder/issues/4551.

This improves the error messages to be unique,
and also fixes a small edge-case bug a user ran into.
2022-12-14 12:24:22 -06:00
Mathias Fredriksson b39ba02bf0 ci: Increase Go mock db test timeout to 5m (#5413)
Our Windows test-runner often takes close to 3m to complete the test,
this was producing a few false failures due to us adding tests over time
and test times increasing.
2022-12-14 19:37:01 +02:00
Steven Masley 27386d49d0 fix: No org admins until organizations are in the UI (#5414)
* fix: No org admins until organizations are in the UI

Until organizations have management UI, we should not set any org
admins. This goes around the site wide perms transparently and
is confusing to users.

Default user is no longer an org admin, so the demotion test makes
no sense
2022-12-14 11:05:42 -06:00
Mathias Fredriksson 012a9e759e fix: Avoid deadlock in AgentReportStats Close during agent Close (#5415)
Since AgentReportStats takes a stats function which was doing mutex
locking on agent shutdown, it was possible for there to be a deadlock
depending on how the AgentReportsStats Close function is implemented.

This mostly seems to happen on Windows test runners as it's pretty hard
to hit this edge case. The bug currently only exists in the test
implementation of AgentReportStats, however, this was refactored to be
more robust in case of future changes.
2022-12-14 18:45:46 +02:00
Kyle Carberry 8e702d89bb fix: improve the warning mismatch to display the release assets on windows (#5418)
* fix: improve the warning mismatch to display the release assets on windows

Fixes #4226.

* Update cli/root.go

Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>

* Update cli/root.go

Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>

Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2022-12-14 16:36:28 +00:00
Marcin Tojek b103685170 chore: Collect gotests.xml files (#5398)
* chore: Use datadog-ci to collect gotests.xml

* WIP

* Fix: github ref

* fix

* just store gotests.xml
2022-12-14 16:23:31 +01:00
Arthur Normand ad0dd1be5d fix: Add client certs to OAuth HTTPClient context (#5126) 2022-12-14 16:44:29 +02:00
Mathias Fredriksson 663f7a3f12 ci: Output tail of gotestsum.json if test timed out (#5411)
This is to eternalize the log in case "re-run failed" is used, which
erases artifacts from previous run.
2022-12-14 15:04:39 +02:00
Mathias Fredriksson 2a4ef38a4f fix(site): Use correct UUID for web terminal when first opened (#5404) 2022-12-14 15:03:47 +02:00
Geoffrey Huntley 90b0adabc1 docs(readme): uppercase H in Self-Hosted (#5412) 2022-12-14 13:01:51 +00:00
Geoffrey Huntley ec2293a4e4 docs(readme): update comparison table (#5405) 2022-12-14 10:35:20 +01:00
Mathias Fredriksson 1a018c571b chore: Add PR Lint workflow (#5387)
Fixes #5381
2022-12-14 16:34:08 +10:00
Ricky Grassmuck f7baf45ae3 feat: support partial parameter files (#5392)
Fixes https://github.com/coder/coder/issues/5390
2022-12-13 19:58:57 -06:00
Joe Previte 5a568d8a9b refactor: conditionally use dotfiles in dogfood template (#5332)
* feat: add dotfiles_uri var to dogfood template

* refactor: use dotfiles if dotfiles var exists

This ensures the `coder dotfiles` command only runs if the dotfiles var
in the template is not empty.

* Update dogfood/main.tf

* refactor: assign variable to shell variable

* Update dogfood/main.tf

* fixup!: add default value
2022-12-13 21:15:25 +00:00
sharkymark 8df02f42c0 docs: make it clear the CLI must be downloaded to use templates (#5373) 2022-12-13 19:31:09 +00:00
Mathias Fredriksson 4fc4c01cea fix: Enable reconnectingpty loadtest and fix/improve logging (#5403)
* fix: Enable reconnectingpty loadtest and fix/improve logging

This commit re-enabled reconnectingpty loadtests after a logging
refactor of `(*agent).handleReconnectingPTY`. The reasons the tests were
flaking was that `logger.Error` was being called and `slogtest` failing
the test.

We could have set the option for `slogtest` to disable failing, but that
could hide real issues. The current approach improves reconnectingpty
logging overall and provides more insight into what's happening. It's
expected that reconnectingpty sessions fail after the agent is closed,
so calling `logger.Error` at that point is not wanted.

Ref: #5322
2022-12-13 21:28:07 +02:00
Garrett Delfosse 560c8ce0f6 fix: allow example files to be reused and not error (#5402) 2022-12-13 16:27:37 +00:00
Marcin Tojek 50d1c7191a fix: double quote in fake_cancel.sh (#5399) 2022-12-13 11:03:34 +01:00
Colin Adler 1c42a20865 chore: add debugging to agent stats report (#5395) 2022-12-13 01:03:03 -06:00
Geoffrey Huntley d72d312e1f docs(readme): update comparison table (#5394) 2022-12-13 00:55:23 +00:00
Kira Pilot a071bfa8aa fix: Store dismissedBanner key in localStorage (#5388)
* fix: Store dismissedBanner key in localStorage

* cleanup

* removed comment

* spelling

* fixed eslint

* wote test
2022-12-12 16:17:29 -05:00
Garrett Delfosse 40a5c0476f feat: add flag for token lifetime (#5385) 2022-12-12 15:39:31 -05:00
Mathias Fredriksson 760419a965 chore: Refactor agent tests to avoid t.Run when not needed (#5376)
It turns out that writing tests that contain subtests should probably be
limited to table-based tests and tests that share a common setup shared
between tests.

Writing tests with a subtest like this:

```
func TestSomething(t *testing.T) {
	t.Run("Subtest", func(t *testing.t) {})
}
```

Has the following disadvantages:

- It can lead to multiple tests failing with `(unknown)` status when
  only one of the subtests hang (never exit)
- In Go 1.20rc1, using `t.Setenv` is no longer allowed if the parent
  test is parallel
2022-12-12 22:20:46 +02:00
Geoffrey Huntley 08a6a18226 docs(comparisons): update comparison table (#5371) 2022-12-12 19:14:15 +00:00
Bruno Quaresma e7fc21e285 chore: Add react-syntax-highlight back (#5369) 2022-12-12 15:46:33 -03:00
Kyle Carberry 2b864cee9e fix: Remove @main tag from pkg.go.dev in docs links (#5384)
This seems to have broken, but removing the `main` tag makes
it resolve to the latest version.

See: https://github.com/coder/coder/actions/runs/3675316304/jobs/6215503383
2022-12-12 18:06:58 +00:00
597 changed files with 53557 additions and 7530 deletions
+33 -33
View File
@@ -6,27 +6,27 @@ ENV EDITOR=vim
RUN apt-get update && apt-get upgrade --yes
RUN apt-get install --yes \
ca-certificates \
bash-completion \
build-essential \
curl \
cmake \
direnv \
emacs-nox \
gnupg \
htop \
jq \
less \
lsb-release \
lsof \
man-db \
nano \
neovim \
ssl-cert \
sudo \
unzip \
xz-utils \
zip
ca-certificates \
bash-completion \
build-essential \
curl \
cmake \
direnv \
emacs-nox \
gnupg \
htop \
jq \
less \
lsb-release \
lsof \
man-db \
nano \
neovim \
ssl-cert \
sudo \
unzip \
xz-utils \
zip
# configure locales to UTF8
RUN apt-get install locales && locale-gen en_US.UTF-8
@@ -39,22 +39,22 @@ RUN direnv hook bash >> $HOME/.bashrc
RUN sh <(curl -L https://nixos.org/nix/install) --daemon
RUN mkdir -p $HOME/.config/nix $HOME/.config/nixpkgs \
&& echo 'sandbox = false' >> $HOME/.config/nix/nix.conf \
&& echo '{ allowUnfree = true; }' >> $HOME/.config/nixpkgs/config.nix \
&& echo '. $HOME/.nix-profile/etc/profile.d/nix.sh' >> $HOME/.bashrc
&& echo 'sandbox = false' >> $HOME/.config/nix/nix.conf \
&& echo '{ allowUnfree = true; }' >> $HOME/.config/nixpkgs/config.nix \
&& echo '. $HOME/.nix-profile/etc/profile.d/nix.sh' >> $HOME/.bashrc
# install docker and configure daemon to use vfs as GitHub codespaces requires vfs
# https://github.com/moby/moby/issues/13742#issuecomment-725197223
RUN mkdir -p /etc/apt/keyrings \
&& curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg \
&& echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null \
&& apt-get update \
&& apt-get install --yes docker-ce docker-ce-cli containerd.io docker-compose-plugin \
&& mkdir -p /etc/docker \
&& echo '{"cgroup-parent":"/actions_job","storage-driver":"vfs"}' >> /etc/docker/daemon.json
&& curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg \
&& echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null \
&& apt-get update \
&& apt-get install --yes docker-ce docker-ce-cli containerd.io docker-compose-plugin \
&& mkdir -p /etc/docker \
&& echo '{"cgroup-parent":"/actions_job","storage-driver":"vfs"}' >> /etc/docker/daemon.json
# install golang and language tooling
ENV GO_VERSION=1.19
@@ -67,6 +67,7 @@ RUN echo 'export PATH=$GOPATH/bin:$PATH' >> $HOME/.bashrc
RUN bash -c ". $HOME/.bashrc \
go install -v golang.org/x/tools/gopls@latest \
&& go install -v mvdan.cc/sh/v3/cmd/shfmt@latest \
&& go install -v github.com/mikefarah/yq/v4@v4.30.6 \
"
# install nodejs
@@ -80,4 +81,3 @@ RUN bash -c "$(curl -fsSL https://raw.githubusercontent.com/horta/zstd.install/m
RUN echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | sudo tee /etc/apt/sources.list.d/goreleaser.list \
&& apt update \
&& apt install nfpm
+17 -11
View File
@@ -1,18 +1,24 @@
// For format details, see https://aka.ms/devcontainer.json
{
"name": "Development environments on your infrastructure",
"name": "Development environments on your infrastructure",
// Sets the run context to one level up instead of the .devcontainer folder.
"context": ".",
// Sets the run context to one level up instead of the .devcontainer folder.
"context": ".",
// Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename.
"dockerFile": "Dockerfile",
// Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename.
"dockerFile": "Dockerfile",
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
"postStartCommand": "dockerd",
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// privileged is required by GitHub codespaces - https://github.com/microsoft/vscode-dev-containers/issues/727
"runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined", "--privileged", "--init" ]
"postStartCommand": "dockerd",
// privileged is required by GitHub codespaces - https://github.com/microsoft/vscode-dev-containers/issues/727
"runArgs": [
"--cap-add=SYS_PTRACE",
"--security-opt",
"seccomp=unconfined",
"--privileged",
"--init"
]
}
+1 -1
View File
@@ -7,7 +7,7 @@ trim_trailing_whitespace = true
insert_final_newline = true
indent_style = tab
[*.{md,json,yaml,yml,tf,tfvars}]
[*.{md,json,yaml,yml,tf,tfvars,nix}]
indent_style = space
indent_size = 2
+2 -2
View File
@@ -38,7 +38,7 @@ updates:
# Ignore patch updates for all dependencies
- dependency-name: "*"
update-types:
- version-update:semver-patch
- version-update:semver-patch
- package-ecosystem: "npm"
directory: "/site/"
@@ -53,7 +53,7 @@ updates:
# Ignore patch updates for all dependencies
- dependency-name: "*"
update-types:
- version-update:semver-patch
- version-update:semver-patch
# Ignore major updates to Node.js types, because they need to
# correspond to the Node.js engine version
- dependency-name: "@types/node"
-56
View File
@@ -1,56 +0,0 @@
###############################################################################
# This file configures "Semantic Pull Requests", which is documented here:
# https://github.com/zeke/semantic-pull-requests
#
# This action/spec implements the "Conventional Commits" RFC which is
# available here:
# https://www.notion.so/coderhq/Conventional-commits-1d51287f58b64026bb29393f277734ed
###############################################################################
# We have no valid scopes right now.
# A scope should be added when commits aren't aligning with associated change anymore.
scopes:
# We only check that the PR title is semantic. The PR title is automatically
# applied to the "Squash & Merge" flow as the suggested commit message, so this
# should suffice unless someone drastically alters the message in that flow.
titleOnly: true
# Types are the 'tag' types in a commit or PR title. For example, in
#
# chore: fix thing
#
# 'chore' is the type.
types:
# A build of any kind.
- build
# Any code task that operates outside of CI, docs, or the product. Examples
# include configurations, linters etc.
- chore
# Any work performed on CI.
- ci
- example
# Work that directly implements or supports the implementation of a feature.
- feat
# A fix for either a released or unrelesed bug.
- fix
# A fix for a released bug (regression fix) that is intended for patch-release
# purposes.
- hotfix
# A refactor changes code structure without any behavioral change.
- refactor
# A git revert for any style of commit.
- revert
# Adding tests of any kind. Should be separate from feature or fix
# implementations. For example, if a commit adds a fix + test, it's a fix
# commit. If a commit is simply bumping coverage, it's a test commit.
- test
+7 -7
View File
@@ -3,7 +3,7 @@ on:
issue_comment:
types: [created]
pull_request_target:
types: [opened,closed,synchronize]
types: [opened, closed, synchronize]
jobs:
CLAssistant:
@@ -15,12 +15,12 @@ jobs:
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
PERSONAL_ACCESS_TOKEN : ${{ secrets.CDRCOMMUNITY_GITHUB_TOKEN }}
PERSONAL_ACCESS_TOKEN: ${{ secrets.CDRCOMMUNITY_GITHUB_TOKEN }}
with:
remote-organization-name: 'coder'
remote-repository-name: 'cla'
path-to-signatures: 'v2022-09-04/signatures.json'
path-to-document: 'https://github.com/coder/cla/blob/main/README.md'
remote-organization-name: "coder"
remote-repository-name: "cla"
path-to-signatures: "v2022-09-04/signatures.json"
path-to-document: "https://github.com/coder/cla/blob/main/README.md"
# branch should not be protected
branch: 'main'
branch: "main"
allowlist: dependabot*
+67
View File
@@ -0,0 +1,67 @@
name: "CodeQL"
permissions:
security-events: write
on:
push:
branches: ["main"]
pull_request:
# The branches below must be a subset of the branches above
branches: ["main"]
schedule:
# run every week at 10:24 on Thursday
- cron: "24 10 * * 4"
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ["go", "javascript"]
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
- name: Setup Go
if: matrix.language == 'go'
uses: actions/setup-go@v3
with:
go-version: "~1.19"
- name: Go Cache Paths
if: matrix.language == 'go'
id: go-cache-paths
run: |
echo "::set-output name=go-mod::$(go env GOMODCACHE)"
- name: Go Mod Cache
if: matrix.language == 'go'
uses: actions/cache@v3
with:
path: ${{ steps.go-cache-paths.outputs.go-mod }}
key: ${{ runner.os }}-release-go-mod-${{ hashFiles('**/go.sum') }}
- name: Remove Makefile # workaround to prevent CodeQL from building site
if: matrix.language == 'go'
run: |
# Disable Analysis step from trying to build the project.
rm Makefile
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"
+63 -15
View File
@@ -222,6 +222,8 @@ jobs:
run: go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.26
- name: Install goimports
run: go install golang.org/x/tools/cmd/goimports@latest
- name: Install yq
run: go run github.com/mikefarah/yq/v4@v4.30.6
- name: Install Protoc
run: |
@@ -313,7 +315,7 @@ jobs:
key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}
- name: Install gotestsum
uses: jaxxstorm/action-install-gh-release@v1.7.1
uses: jaxxstorm/action-install-gh-release@v1.9.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -338,8 +340,23 @@ jobs:
else
echo ::set-output name=cover::false
fi
set -x
gotestsum --junitfile="gotests.xml" --jsonfile="gotestsum.json" --packages="./..." --debug -- -parallel=8 -timeout=3m -short -failfast $COVERAGE_FLAGS
set +e
gotestsum --junitfile="gotests.xml" --jsonfile="gotestsum.json" --packages="./..." --debug -- -parallel=8 -timeout=5m -short -failfast $COVERAGE_FLAGS
ret=$?
if ((ret)); then
# Eternalize test timeout logs because "re-run failed" erases
# artifacts and gotestsum doesn't always capture it:
# https://github.com/gotestyourself/gotestsum/issues/292
# Multiple test packages could've failed, each one may or may
# not run into the edge case. PS. Don't summon ShellCheck here.
for testWithStack in $(grep 'panic: test timed out' gotestsum.json | grep -E -o '("Test":[^,}]*)'); do
if [ -n "$testWithStack" ] && grep -q "${testWithStack}.*PASS" gotestsum.json; then
echo "Conditions met for gotestsum stack trace missing bug, outputting panic trace:"
grep -A 999999 "${testWithStack}.*panic: test timed out" gotestsum.json
fi
done
fi
exit $ret
- uses: actions/upload-artifact@v3
if: success() || failure()
@@ -348,6 +365,13 @@ jobs:
path: ./gotestsum.json
retention-days: 7
- uses: actions/upload-artifact@v3
if: success() || failure()
with:
name: gotests-${{ matrix.os }}.xml
path: ./gotests.xml
retention-days: 30
- uses: codecov/codecov-action@v3
# This action has a tendency to error out unexpectedly, it has
# the `fail_ci_if_error` option that defaults to `false`, but
@@ -394,7 +418,7 @@ jobs:
key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}
- name: Install gotestsum
uses: jaxxstorm/action-install-gh-release@v1.7.1
uses: jaxxstorm/action-install-gh-release@v1.9.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -407,7 +431,24 @@ jobs:
terraform_wrapper: false
- name: Test with PostgreSQL Database
run: make test-postgres
run: |
set +e
make test-postgres
ret=$?
if ((ret)); then
# Eternalize test timeout logs because "re-run failed" erases
# artifacts and gotestsum doesn't always capture it:
# https://github.com/gotestyourself/gotestsum/issues/292
# Multiple test packages could've failed, each one may or may
# not run into the edge case. PS. Don't summon ShellCheck here.
for testWithStack in $(grep 'panic: test timed out' gotestsum.json | grep -E -o '("Test":[^,}]*)'); do
if [ -n "$testWithStack" ] && grep -q "${testWithStack}.*PASS" gotestsum.json; then
echo "Conditions met for gotestsum stack trace missing bug, outputting panic trace:"
grep -A 999999 "${testWithStack}.*panic: test timed out" gotestsum.json
fi
done
fi
exit $ret
- uses: actions/upload-artifact@v3
if: success() || failure()
@@ -416,6 +457,13 @@ jobs:
path: ./gotestsum.json
retention-days: 7
- uses: actions/upload-artifact@v3
if: success() || failure()
with:
name: gotests-postgres.xml
path: ./gotests.xml
retention-days: 30
- uses: codecov/codecov-action@v3
# This action has a tendency to error out unexpectedly, it has
# the `fail_ci_if_error` option that defaults to `false`, but
@@ -451,7 +499,7 @@ jobs:
service_account: coder-ci@coder-dogfood.iam.gserviceaccount.com
- name: Set up Google Cloud SDK
uses: google-github-actions/setup-gcloud@v0
uses: google-github-actions/setup-gcloud@v1
- uses: actions/setup-go@v3
with:
@@ -690,18 +738,18 @@ jobs:
markdown-link-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
# For the main branch:
- if: github.ref == 'refs/heads/main' && !github.event.pull_request.head.repo.fork
uses: gaurav-nelson/github-action-markdown-link-check@v1
with:
- uses: actions/checkout@master
# For the main branch:
- if: github.ref == 'refs/heads/main' && !github.event.pull_request.head.repo.fork
uses: gaurav-nelson/github-action-markdown-link-check@v1
with:
use-quiet-mode: yes
use-verbose-mode: yes
config-file: .github/workflows/mlc_config.json
# For pull requests:
- if: github.ref != 'refs/heads/main' || github.event.pull_request.head.repo.fork
uses: gaurav-nelson/github-action-markdown-link-check@v1
with:
# For pull requests:
- if: github.ref != 'refs/heads/main' || github.event.pull_request.head.repo.fork
uses: gaurav-nelson/github-action-markdown-link-check@v1
with:
use-quiet-mode: yes
use-verbose-mode: yes
check-modified-files-only: yes
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
steps:
- name: Get branch name
id: branch-name
uses: tj-actions/branch-names@v6.3
uses: tj-actions/branch-names@v6.4
- name: "Branch name to Docker tag name"
id: docker-tag-name
+20 -20
View File
@@ -1,22 +1,22 @@
{
"ignorePatterns": [
{
"pattern": ":\/\/localhost"
},
{
"pattern": ":\/\/.*.?example\\.com"
},
{
"pattern": "developer.github.com"
},
{
"pattern": "docs.github.com"
},
{
"pattern": "support.google.com"
},
{
"pattern": "tailscale.com"
}
]
"ignorePatterns": [
{
"pattern": "://localhost"
},
{
"pattern": "://.*.?example\\.com"
},
{
"pattern": "developer.github.com"
},
{
"pattern": "docs.github.com"
},
{
"pattern": "support.google.com"
},
{
"pattern": "tailscale.com"
}
]
}
+22 -13
View File
@@ -15,32 +15,41 @@ jobs:
run: |
Invoke-WebRequest https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe
# the package version is the same as the release tag without the leading
# "v", and with a trailing ".0" (e.g. "v1.2.3" -> "1.2.3.0")
- name: Calculate package version
id: version
run: |
$version = $env:CODER_VERSION -replace "^v", ""
$version += ".0"
echo "::set-output name=version::$version"
- name: Submit updated manifest to winget-pkgs
run: |
$release_assets = gh release view --repo coder/coder "$env:CODER_VERSION" --json assets | `
ConvertFrom-Json
# Get the installer URL from the release assets.
$installer_url = $release_assets.assets | `
Where-Object name -Match ".*_windows_amd64_installer.exe$" | `
Select -ExpandProperty url
echo "Installer URL: $installer_url"
# The package version is the same as the tag minus the leading "v".
$version = $env:CODER_VERSION.Trim('v')
echo "Package version: $version"
# The URL "|X64" suffix forces the architecture as it cannot be
# sniffed properly from the URL. wingetcreate checks both the URL and
# binary magic bytes for the architecture and they need to both match,
# but they only check for `x64`, `win64` and `_64` in the URL. Our URL
# contains `amd64` which doesn't match sadly.
#
# wingetcreate will still do the binary magic bytes check, so if we
# accidentally change the architecture of the installer, it will fail
# submission.
.\wingetcreate.exe update Coder.Coder `
--submit `
--version "${{ steps.version.outputs.version }}" `
--urls "$installer_url" `
--version "${version}" `
--urls "${installer_url}|X64" `
--token "${{ secrets.CDRCI_GITHUB_TOKEN }}"
env:
# For gh CLI:
GH_TOKEN: ${{ github.token }}
- name: Comment on PR
run: |
# find the PR that wingetcreate just made
@@ -48,4 +57,4 @@ jobs:
ConvertFrom-Json`
$pr_number = $pr_list[0].number
gh pr comment --repo microsoft/winget-pkgs "$pr_number" --body "🤖 cc: @deansheather"
gh pr comment --repo microsoft/winget-pkgs "$pr_number" --body "🤖 cc: @deansheather @matifali"
+20
View File
@@ -0,0 +1,20 @@
name: Lint PR
on:
pull_request_target:
types:
- opened
- reopened
- edited
- synchronize
jobs:
main:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
requireScope: false
+109 -12
View File
@@ -1,19 +1,32 @@
# GitHub release workflow.
name: release
name: Release
run-name: Release ${{ github.ref_name }}${{ inputs.dry_run && ' (DRYRUN)' || '' }}
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
snapshot:
description: Force a dev version to be generated, implies dry_run.
increment:
description: Preferred version increment (release script may promote e.g. patch to minor depending on changes).
type: choice
required: true
default: patch
options:
- patch
- minor
- major
draft:
description: Create a draft release (for manually editing release notes before publishing).
type: boolean
required: true
default: false
dry_run:
description: Perform a dry-run release.
type: boolean
required: true
default: false
ignore_missing_commit_metadata:
description: WARNING! This option disables the requirement that all commits have a PR. Not needed for dry_run.
type: boolean
default: false
permissions:
# Required to publish a release
@@ -23,19 +36,36 @@ permissions:
# Necessary for GCP authentication (https://github.com/google-github-actions/setup-gcloud#usage)
id-token: write
concurrency: ${{ github.workflow }}-${{ github.ref }}
env:
CODER_RELEASE: ${{ github.event.inputs.snapshot && 'false' || 'true' }}
# Use `inputs` (vs `github.event.inputs`) to ensure that booleans are actual
# booleans, not strings.
# https://github.blog/changelog/2022-06-10-github-actions-inputs-unified-across-manual-and-reusable-workflows/
CODER_RELEASE: ${{ !inputs.dry_run }}
CODER_RELEASE_INCREMENT: ${{ inputs.increment }}
CODER_RELEASE_DRAFT: ${{ inputs.draft }}
CODER_DRY_RUN: ${{ inputs.dry_run }}
jobs:
release:
name: Create and publish
runs-on: ${{ github.repository_owner == 'coder' && 'ubuntu-latest-16-cores' || 'ubuntu-latest' }}
env:
# Necessary for Docker manifest
DOCKER_CLI_EXPERIMENTAL: "enabled"
steps:
- name: Check release on main (or dry-run)
if: ${{ github.ref_name != 'main' && !inputs.dry_run }}
run: |
echo "Release not allowed on ${{ github.ref_name }}, use dry-run."
exit 1
- uses: actions/checkout@v3
with:
fetch-depth: 0
# Set token for pushing protected tag (vX.X.X).
token: ${{ secrets.RELEASE_GITHUB_PAT }}
# If the event that triggered the build was an annotated tag (which our
# tags are supposed to be), actions/checkout has a bug where the tag in
@@ -45,6 +75,59 @@ jobs:
- name: Fetch git tags
run: git fetch --tags --force
# Configure git user name/email for creating annotated version tag.
- name: Setup git config
run: |
git config user.name "Coder CI"
git config user.email "dean+cdrci@coder.com"
- name: Create release tag and release notes
run: |
set -euo pipefail
ref=HEAD
old_version="$(git describe --abbrev=0 "$ref^1")"
if [[ "${{ inputs.ignore_missing_commit_metadata }}" == *t* ]]; then
export CODER_IGNORE_MISSING_COMMIT_METADATA=1
fi
# Warn if CODER_IGNORE_MISSING_COMMIT_METADATA is set any other way
# than via dry-run.
if [[ ${CODER_IGNORE_MISSING_COMMIT_METADATA:-0} != 0 ]]; then
echo "WARNING: CODER_IGNORE_MISSING_COMMIT_METADATA is enabled and we will ignore missing commit metadata." 1>&2
fi
version_args=()
if [[ $CODER_DRY_RUN == *t* ]]; then
# Allow dry-run of branches to pass.
export CODER_IGNORE_MISSING_COMMIT_METADATA=1
version_args+=(--dry-run)
fi
# Cache commit metadata.
. ./scripts/release/check_commit_metadata.sh "$old_version" "$ref"
declare -p version_args
# Create new release tag (note that this tag is not pushed before
# release.sh is run).
version="$(
./scripts/release/tag_version.sh \
"${version_args[@]}" \
--ref "$ref" \
--"$CODER_RELEASE_INCREMENT"
)"
# Generate notes.
release_notes_file="$(mktemp -t release_notes.XXXXXX)"
./scripts/release/generate_release_notes.sh --old-version "$old_version" --new-version "$version" --ref "$ref" >> "$release_notes_file"
echo CODER_RELEASE_NOTES_FILE="$release_notes_file" >> $GITHUB_ENV
- name: Echo release notes
run: |
set -euo pipefail
cat "$CODER_RELEASE_NOTES_FILE"
- name: Docker Login
uses: docker/login-action@v2
with:
@@ -157,8 +240,20 @@ jobs:
- name: Publish release
run: |
./scripts/publish_release.sh \
${{ (github.event.inputs.dry_run || github.event.inputs.snapshot) && '--dry-run' }} \
set -euo pipefail
publish_args=()
if [[ $CODER_RELEASE_DRAFT == *t* ]]; then
publish_args+=(--draft)
fi
if [[ $CODER_DRY_RUN == *t* ]]; then
publish_args+=(--dry-run)
fi
declare -p publish_args
./scripts/release/publish.sh \
"${publish_args[@]}" \
--release-notes-file "$CODER_RELEASE_NOTES_FILE" \
./build/*_installer.exe \
./build/*.zip \
./build/*.tar.gz \
@@ -176,9 +271,10 @@ jobs:
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
- name: Setup GCloud SDK
uses: 'google-github-actions/setup-gcloud@v0'
uses: "google-github-actions/setup-gcloud@v1"
- name: Publish Helm Chart
if: ${{ !inputs.dry_run }}
run: |
set -euo pipefail
version="$(./scripts/version.sh)"
@@ -189,12 +285,13 @@ jobs:
gsutil -h "Cache-Control:no-cache,max-age=0" cp build/helm/coder_helm_${version}.tgz gs://helm.coder.com/v2
gsutil -h "Cache-Control:no-cache,max-age=0" cp build/helm/index.yaml gs://helm.coder.com/v2
- name: Upload artifacts to actions (if dry-run or snapshot)
if: ${{ github.event.inputs.dry_run || github.event.inputs.snapshot }}
- name: Upload artifacts to actions (if dry-run)
if: ${{ inputs.dry_run }}
uses: actions/upload-artifact@v2
with:
name: release-artifacts
path: |
./build/*_installer.exe
./build/*.zip
./build/*.tar.gz
./build/*.tgz
+3 -3
View File
@@ -13,10 +13,10 @@ jobs:
steps:
# v5.1.0 has a weird bug that makes stalebot add then remove its own label
# https://github.com/actions/stale/pull/775
- uses: actions/stale@v6.0.0
- uses: actions/stale@v7.0.0
with:
stale-issue-label: 'stale'
stale-pr-label: 'stale'
stale-issue-label: "stale"
stale-pr-label: "stale"
# Pull Requests become stale more quickly due to merge conflicts.
# Also, we promote minimizing WIP.
days-before-pr-stale: 7
+2 -2
View File
@@ -11,8 +11,8 @@ jobs:
- uses: wow-actions/welcome@v1
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FIRST_PR_REACTIONS: '+1, hooray, rocket, heart'
FIRST_PR_REACTIONS: "+1, hooray, rocket, heart"
FIRST_PR_COMMENT: |
👋 Welcome @{{ author }} to Coder! Yo @coder/docs this is @{{ author }}'s first pull-request here!
👋 Welcome @{{ author }} to Coder! Yo @coder/docs this is @{{ author }}'s first pull-request here!
FIRST_PR_MERGED: |
🎉 Thanks for the contribution @{{ author }}! Yo @coder/docs @{{ author }}'s first contribution has been merged! 👀👀👀
+26 -33
View File
@@ -1,40 +1,36 @@
###############################################################################
# NOTICE #
# If you change this file, kindly copy-pasta your change into .prettierignore #
# and .eslintignore as well. See the following discussions to understand why #
# we have to resort to this duplication (at least for now): #
# #
# https://github.com/prettier/prettier/issues/8048 #
# https://github.com/prettier/prettier/issues/8506 #
# https://github.com/prettier/prettier/issues/8679 #
###############################################################################
node_modules
vendor
# Common ignore patterns, these rules applies in both root and subdirectories.
.DS_Store
.eslintcache
yarn-error.log
.gitpod.yml
.idea
**/*.swp
gotests.coverage
gotests.xml
gotestsum.json
.idea
.gitpod.yml
.DS_Store
node_modules/
vendor/
yarn-error.log
# VSCode settings.
**/.vscode/*
# Allow VSCode recommendations and default settings in project root.
!/.vscode/extensions.json
!/.vscode/settings.json
# Front-end ignore patterns.
.next/
site/**/*.typegen.ts
site/build-storybook.log
site/coverage/
site/storybook-static/
site/test-results/*
site/e2e/test-results/*
site/e2e/storageState.json
site/playwright-report/*
# Make target for updating golden files.
cli/testdata/.gen-golden
# Front-end ignore
.next/
site/.eslintcache
site/.next/
site/node_modules/
site/storybook-static/
site/test-results/
site/yarn-error.log
coverage/
site/**/*.typegen.ts
site/build-storybook.log
# Build
/build/
/dist/
@@ -46,10 +42,7 @@ site/out/
*.lock.hcl
.terraform/
.vscode/*.log
.vscode/launch.json
**/*.swp
.coderv2/*
/.coderv2/*
**/__debug_bin
# direnv
+1 -1
View File
@@ -103,7 +103,7 @@ linters-settings:
settings:
ruleguard:
failOn: all
rules: '${configDir}/scripts/rules.go'
rules: "${configDir}/scripts/rules.go"
staticcheck:
# https://staticcheck.io/docs/options#checks
+63
View File
@@ -0,0 +1,63 @@
# Code generated by Makefile (.gitignore .prettierignore.include). DO NOT EDIT.
# .gitignore:
# Common ignore patterns, these rules applies in both root and subdirectories.
.DS_Store
.eslintcache
.gitpod.yml
.idea
**/*.swp
gotests.coverage
gotests.xml
gotestsum.json
node_modules/
vendor/
yarn-error.log
# VSCode settings.
**/.vscode/*
# Allow VSCode recommendations and default settings in project root.
!/.vscode/extensions.json
!/.vscode/settings.json
# Front-end ignore patterns.
.next/
site/**/*.typegen.ts
site/build-storybook.log
site/coverage/
site/storybook-static/
site/test-results/*
site/e2e/test-results/*
site/e2e/storageState.json
site/playwright-report/*
# Make target for updating golden files.
cli/testdata/.gen-golden
# Build
/build/
/dist/
site/out/
*.tfstate
*.tfstate.backup
*.tfplan
*.lock.hcl
.terraform/
/.coderv2/*
**/__debug_bin
# direnv
.envrc
# .prettierignore.include:
# Helm templates contain variables that are invalid YAML and can't be formatted
# by Prettier.
helm/templates/*.yaml
# Terraform state files used in tests, these are automatically generated.
# Example: provisioner/terraform/testdata/instance-id/instance-id.tfstate.json
**/testdata/**/*.tf*.json
# Testdata shouldn't be formatted.
scripts/apitypings/testdata/**/*.ts
+10
View File
@@ -0,0 +1,10 @@
# Helm templates contain variables that are invalid YAML and can't be formatted
# by Prettier.
helm/templates/*.yaml
# Terraform state files used in tests, these are automatically generated.
# Example: provisioner/terraform/testdata/instance-id/instance-id.tfstate.json
**/testdata/**/*.tf*.json
# Testdata shouldn't be formatted.
scripts/apitypings/testdata/**/*.ts
+16
View File
@@ -0,0 +1,16 @@
# 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.
printWidth: 80
semi: false
trailingComma: all
overrides:
- files:
- README.md
options:
proseWrap: preserve
- files:
- "site/**/*.yaml"
- "site/**/*.yml"
options:
proseWrap: always
+8
View File
@@ -0,0 +1,8 @@
// Replace all NullTime with string
replace github.com/coder/coder/codersdk.NullTime string
// Prevent swaggo from rendering enums for time.Duration
replace time.Duration int64
// Do not expose "echo" provider
replace github.com/coder/coder/codersdk.ProvisionerType string
// Do not render netip.Addr
replace netip.Addr string
+1
View File
@@ -1,5 +1,6 @@
{
"recommendations": [
"github.vscode-codeql",
"golang.go",
"hashicorp.terraform",
"esbenp.prettier-vscode",
+6 -6
View File
@@ -14,15 +14,15 @@ LABEL \
org.opencontainers.image.source="https://github.com/coder/coder" \
org.opencontainers.image.version="$CODER_VERSION"
# The coder binary is injected by scripts/build_docker.sh.
COPY --chown=coder:coder --chmod=755 coder /opt/coder
# Create coder group and user. We cannot use `addgroup` and `adduser` because
# they won't work if we're building the image for a different architecture.
COPY --chown=root:root --chmod=644 group passwd /etc/
COPY --chown=coder:coder --chmod=700 empty-dir /home/coder
COPY --chown=0:0 --chmod=644 group passwd /etc/
COPY --chown=1000:1000 --chmod=700 empty-dir /home/coder
USER coder:coder
# The coder binary is injected by scripts/build_docker.sh.
COPY --chown=1000:1000 --chmod=755 coder /opt/coder
USER 1000:1000
ENV HOME=/home/coder
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt
WORKDIR /home/coder
+111 -18
View File
@@ -44,10 +44,17 @@ else
ZSTDFLAGS := -6
endif
# Common paths to exclude from find commands, this rule is written so
# that it can be it can be used in a chain of AND statements (meaning
# you can simply write `find . $(FIND_EXCLUSIONS) -name thing-i-want`).
# Note, all find statements should be written with `.` or `./path` as
# the search path so that these exclusions match.
FIND_EXCLUSIONS= \
-not \( \( -path '*/.git/*' -o -path './build/*' -o -path './vendor/*' -o -path './.coderv2/*' -o -path '*/node_modules/*' -o -path './site/out/*' \) -prune \)
# Source files used for make targets, evaluated on use.
GO_SRC_FILES = $(shell find . -not \( -path './.git/*' -o -path './build/*' -o -path './vendor/*' -o -path './.coderv2/*' -o -path './site/node_modules/*' -o -path './site/out/*' \) -type f -name '*.go')
GO_SRC_FILES = $(shell find . $(FIND_EXCLUSIONS) -type f -name '*.go')
# All the shell files in the repo, excluding ignored files.
SHELL_SRC_FILES = $(shell find . -not \( -path './.git/*' -o -path './build/*' -o -path './vendor/*' -o -path './.coderv2/*' -o -path './site/node_modules/*' -o -path './site/out/*' \) -type f -name '*.sh')
SHELL_SRC_FILES = $(shell find . $(FIND_EXCLUSIONS) -type f -name '*.sh')
# All ${OS}_${ARCH} combos we build for. Windows binaries have the .exe suffix.
OS_ARCHES := \
@@ -101,19 +108,26 @@ build-fat build-full build: $(CODER_FAT_BINARIES)
release: $(CODER_FAT_BINARIES) $(CODER_ALL_ARCHIVES) $(CODER_ALL_PACKAGES) $(CODER_ARCH_IMAGES) build/coder_helm_$(VERSION).tgz
.PHONY: release
build/coder-slim_$(VERSION)_checksums.sha1 site/out/bin/coder.sha1: $(CODER_SLIM_BINARIES)
build/coder-slim_$(VERSION)_checksums.sha1: site/out/bin/coder.sha1
cp "$<" "$@"
site/out/bin/coder.sha1: $(CODER_SLIM_BINARIES)
pushd ./site/out/bin
openssl dgst -r -sha1 coder-* | tee coder.sha1
popd
cp "site/out/bin/coder.sha1" "build/coder-slim_$(VERSION)_checksums.sha1"
build/coder-slim_$(VERSION).tar: build/coder-slim_$(VERSION)_checksums.sha1 $(CODER_SLIM_BINARIES)
pushd ./site/out/bin
tar cf "../../../build/$(@F)" coder-*
popd
build/coder-slim_$(VERSION).tar.zst site/out/bin/coder.tar.zst: build/coder-slim_$(VERSION).tar
# delete the uncompressed binaries from the embedded dir
rm -f site/out/bin/coder-*
site/out/bin/coder.tar.zst: build/coder-slim_$(VERSION).tar.zst
cp "$<" "$@"
build/coder-slim_$(VERSION).tar.zst: build/coder-slim_$(VERSION).tar
zstd $(ZSTDFLAGS) \
--force \
--long \
@@ -121,10 +135,6 @@ build/coder-slim_$(VERSION).tar.zst site/out/bin/coder.tar.zst: build/coder-slim
-o "build/coder-slim_$(VERSION).tar.zst" \
"build/coder-slim_$(VERSION).tar"
cp "build/coder-slim_$(VERSION).tar.zst" "site/out/bin/coder.tar.zst"
# delete the uncompressed binaries from the embedded dir
rm site/out/bin/coder-*
# Redirect from version-less targets to the versioned ones. There is a similar
# target for slim binaries below.
#
@@ -338,7 +348,7 @@ build/coder_helm_$(VERSION).tgz:
--version "$(VERSION)" \
--output "$@"
site/out/index.html: site/package.json $(shell find ./site -not -path './site/node_modules/*' -type f \( -name '*.ts' -o -name '*.tsx' \))
site/out/index.html: site/package.json $(shell find ./site $(FIND_EXCLUSIONS) -type f \( -name '*.ts' -o -name '*.tsx' \))
./scripts/yarn_install.sh
cd site
yarn build
@@ -359,9 +369,9 @@ fmt/prettier:
cd site
# Avoid writing files in CI to reduce file write activity
ifdef CI
yarn run format:check . ../*.md ../docs
yarn run format:check
else
yarn run format:write . ../*.md ../docs
yarn run format:write
endif
.PHONY: fmt/prettier
@@ -400,13 +410,32 @@ gen: \
provisionersdk/proto/provisioner.pb.go \
provisionerd/proto/provisionerd.pb.go \
site/src/api/typesGenerated.ts \
docs/admin/prometheus.md
docs/admin/prometheus.md \
coderd/apidoc/swagger.json \
.prettierignore.include \
.prettierignore \
site/.prettierrc.yaml \
site/.prettierignore \
site/.eslintignore
.PHONY: gen
# Mark all generated files as fresh so make thinks they're up-to-date. This is
# used during releases so we don't run generation scripts.
gen/mark-fresh:
files="coderd/database/dump.sql coderd/database/querier.go provisionersdk/proto/provisioner.pb.go provisionerd/proto/provisionerd.pb.go site/src/api/typesGenerated.ts docs/admin/prometheus.md"
files="\
coderd/database/dump.sql \
coderd/database/querier.go \
provisionersdk/proto/provisioner.pb.go \
provisionerd/proto/provisionerd.pb.go \
site/src/api/typesGenerated.ts \
docs/admin/prometheus.md \
coderd/apidoc/swagger.json \
.prettierignore.include \
.prettierignore \
site/.prettierrc.yaml \
site/.prettierignore \
site/.eslintignore \
"
for file in $$files; do
echo "$$file"
if [ ! -f "$$file" ]; then
@@ -444,7 +473,7 @@ 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 -type f -name '*.go')
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
yarn run format:types
@@ -452,16 +481,80 @@ site/src/api/typesGenerated.ts: scripts/apitypings/main.go $(shell find codersdk
docs/admin/prometheus.md: scripts/metricsdocgen/main.go scripts/metricsdocgen/metrics
go run scripts/metricsdocgen/main.go
cd site
yarn run format:write ../docs/admin/prometheus.md
yarn run format:write:only ../docs/admin/prometheus.md
coderd/apidoc/swagger.json: $(shell find ./scripts/apidocgen -not \( -path './scripts/apidocgen/node_modules' -prune \) -type f) $(wildcard coderd/*.go) $(wildcard enterprise/coderd/*.go) $(wildcard codersdk/*.go) .swaggo
./scripts/apidocgen/generate.sh
cd site
yarn run format:write:only ../docs/api ../docs/manifest.json ../coderd/apidoc/swagger.json
update-golden-files: cli/testdata/.gen-golden
.PHONY: update-golden-files
cli/testdata/.gen-golden: $(wildcard cli/testdata/*.golden) $(GO_SRC_FILES)
go test ./cli -run=TestCommandHelp -update
touch "$@"
# Generate a prettierrc for the site package that uses relative paths for
# overrides. This allows us to share the same prettier config between the
# site and the root of the repo.
site/.prettierrc.yaml: .prettierrc.yaml
. ./scripts/lib.sh
dependencies yq
echo "# Code generated by Makefile (../$<). DO NOT EDIT." > "$@"
echo "" >> "$@"
# Replace all listed override files with relative paths inside site/.
# - ./ -> ../
# - ./site -> ./
yq \
'.overrides[].files |= map(. | sub("^./"; "") | sub("^"; "../") | sub("../site/"; "./"))' \
"$<" >> "$@"
# Combine .gitignore with .prettierignore.include to generate .prettierignore.
.prettierignore: .gitignore .prettierignore.include
echo "# Code generated by Makefile ($^). DO NOT EDIT." > "$@"
echo "" >> "$@"
for f in $^; do
echo "# $${f}:" >> "$@"
cat "$$f" >> "$@"
done
# Generate ignore files based on gitignore into the site directory. We turn all
# rules into relative paths for the `site/` directory (where applicable),
# following the pattern format defined by git:
# https://git-scm.com/docs/gitignore#_pattern_format
#
# This is done for compatibility reasons, see:
# https://github.com/prettier/prettier/issues/8048
# https://github.com/prettier/prettier/issues/8506
# https://github.com/prettier/prettier/issues/8679
site/.eslintignore site/.prettierignore: .prettierignore Makefile
rm -f "$@"
touch "$@"
# Skip generated by header, inherit `.prettierignore` header as-is.
while read -r rule; do
# Remove leading ! if present to simplify rule, added back at the end.
tmp="$${rule#!}"
ignore="$${rule%"$$tmp"}"
rule="$$tmp"
case "$$rule" in
# Comments or empty lines (include).
\#*|'') ;;
# Generic rules (include).
\*\**) ;;
# Site prefixed rules (include).
site/*) rule="$${rule#site/}";;
./site/*) rule="$${rule#./site/}";;
# Rules that are non-generic and don't start with site (rewrite).
/*) rule=.."$$rule";;
*/?*) rule=../"$$rule";;
*) ;;
esac
echo "$${ignore}$${rule}" >> "$@"
done < "$<"
test: test-clean
gotestsum --debug -- -v -short ./...
.PHONY: test
+19 -10
View File
@@ -66,7 +66,7 @@ curl -L https://coder.com/install.sh | sh -s -- --help
Once installed, you can start a production deployment<sup>1</sup> with a single command:
```sh
```console
# Automatically sets up an external access URL on *.try.coder.app
coder server
@@ -88,18 +88,27 @@ Find our templates [here](./examples/templates).
## Comparison
Please file [an issue](https://github.com/coder/coder/issues/new) if any information is out of date. Also refer to: [What Coder is not](https://coder.com/docs/coder-oss/latest/index#what-coder-is-not).
Please file [an issue](https://github.com/coder/coder/issues/new) if any information is out of date. Also refer to:
| Tool | Type | Delivery Model | Cost | Environments |
| :---------------------------------------------------------- | :------- | :----------------- | :---------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Coder](https://github.com/coder/coder) | Platform | OSS + Self-Managed | Pay your cloud | All [Terraform](https://www.terraform.io/registry/providers) resources, all clouds, multi-architecture: Linux, Mac, Windows, containers, VMs, amd64, arm64 |
| [code-server](https://github.com/cdr/code-server) | Web IDE | OSS + Self-Managed | Pay your cloud | Linux, Mac, Windows, containers, VMs, amd64, arm64 |
| [Coder (Classic)](https://coder.com/docs) | Platform | Self-Managed | Pay your cloud + license fees | Kubernetes Linux Containers |
| [GitHub Codespaces](https://github.com/features/codespaces) | Platform | SaaS | 2x Azure Compute | Linux Virtual Machines |
- [What Coder is not](https://coder.com/docs/coder-oss/latest/index#what-coder-is-not?utm_source=github.com/coder/coder&utm_medium=github&utm_campaign=readme.md).
- [The Self-Hosting Paradox](https://coder.com/blog/the-self-hosting-paradox?utm_source=github.com/coder/coder&utm_medium=github&utm_campaign=readme.md).
- [GitHub Codespaces, Coder, and Enterprise Customers](https://coder.com/blog/github-codespaces-coder-and-enterprise-customers?utm_source=github.com/coder/coder&utm_medium=github&utm_campaign=readme.md)
- [How our development team shares one giant bare metal machine](https://coder.com/blog/how-our-development-team-shares-one-giant-bare-metal-machine?utm_source=github.com/coder/coder&utm_medium=github&utm_campaign=readme.md).
---
| Tool | Type | Delivery Model | Cost | Internet Access Required | Latency and Data Sovereignty | Security isolation model | Product quality | Service Availability | Environments | IDE |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Coder](https://coder.com/blog/how-our-development-team-shares-one-giant-bare-metal-machine?utm_source=github.com/coder/coder&utm_medium=github&utm_campaign=readme.md) | Platform | OSS + Self-Managed | Pay your cloud | No | Self-Hosted | Unopinionated (whatever/wherever you choose to deploy thus 100% configurable) | [Defect history](https://github.com/coder/coder/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Abug) | Self-Hosted | All [Terraform](https://www.terraform.io/registry/providers) resources, all clouds, multi-architecture: Linux, Mac, Windows, containers, VMs, amd64, arm64 | Anything (vim, emacs, theia, code-server, openvscode-server, entire jetbrains suite inc gateway remote development, visual studio code desktop, visual studio for mac, visual studio for windows) you choose to install and deploy |
| [code-server](https://coder.com/blog/code-server-multiple-users?utm_source=github.com/coder/coder&utm_medium=github&utm_campaign=readme.md) | Web IDE | OSS + Self-Managed | Pay your cloud | No | Self-Hosted | Self-Hosted docker container | [Defect history](https://github.com/coder/code-server/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Abug) | Self-hosted | Linux, Mac, Windows, containers, VMs, amd64, arm64 | [code-server](https://github.com/coder/code-server) (VSCode MIT) [with restrictions](https://ghuntley.com/fracture) |
| [openvscode-server](https://github.com/gitpod-io/openvscode-server) | Web IDE | OSS + Self-Managed | Pay your cloud | No | Self-Hosted | Self-Hosted docker container | [Defect history](https://github.com/gitpod-io/openvscode-server) | Self-hosted | Linux, Mac, Windows, containers, VMs, amd64 | [openvscode-server](https://github.com/gitpod-io/openvscode-server) (VSCode MIT) [with restrictions](https://ghuntley.com/fracture) |
| [Amazon CodeCatalyst](https://coder.com/blog/the-self-hosting-paradox?utm_source=github.com/coder/coder?utm_source=github.com/coder/coder&utm_medium=github&utm_campaign=readme.md) | Platform | SaaS | Pay AWS | Yes | US West (Oregon) | ["all customer multi-tenancy isolation is done through virtual machines" for security reasons](https://devclass.com/2022/12/05/interview-why-aws-prefers-vms-for-code-isolation-and-tips-on-developing-for-lambda/) | N/A | [Service Health](https://health.aws.amazon.com/health/status) | Linux Virtual Machines | Cloud9, Visual Studio Code Desktop ([no restrictions](https://ghuntley.com/fracture)) and JetBrains Gateway |
| [CodeAnywhere](https://coder.com/blog/the-self-hosting-paradox?utm_source=github.com/coder/coder?utm_source=github.com/coder/coder&utm_medium=github&utm_campaign=readme.md) | Platform | SaaS | Per user | Yes | N/A | N/A | N/A | N/A | N/A | Theia |
| [GitHub Codespaces](https://coder.com/blog/github-codespaces-coder-and-enterprise-customers?utm_source=github.com/coder/coder&utm_medium=github&utm_campaign=readme.md) | Platform | SaaS | 2x Azure Compute | Yes | Four regions (US West, US East, Europe West, Southeast Asia) | ["two codespaces are never co-located on the same VM"](https://docs.github.com/en/codespaces/codespaces-reference/security-in-github-codespaces) | N/A | [Incident History](https://www.githubstatus.com/history) | Linux Virtual Machines, [GPUs supported](https://docs.github.com/en/codespaces/developing-in-codespaces/getting-started-with-github-codespaces-for-machine-learning) | Visual Studio Code ([no restrictions](https://ghuntley.com/fracture)) and JetBrains Gateway |
| [Gitpod](https://coder.com/blog/the-self-hosting-paradox?utm_source=github.com/coder/coder?utm_source=github.com/coder/coder&utm_medium=github&utm_campaign=readme.md) | Platform | [SaaS](https://news.ycombinator.com/item?id=33907897) | [Credits](https://www.gitpod.io/pricing) | Yes | Two regions (Europe, US) | [All customers intermixed on the same machine isolated via runc](https://kinvolk.io/blog/2020/12/improving-kubernetes-and-container-security-with-user-namespaces/) | [Defect history](https://github.com/gitpod-io/gitpod/issues?q=is%3Aissue+label%3A%22type%3A+bug%22+sort%3Aupdated-desc+) | [Incident history](https://www.gitpodstatus.com/history) | Basic Linux containers, [GPUs](https://github.com/gitpod-io/gitpod/issues/10650) and [kubernetes/k3s](https://github.com/gitpod-io/gitpod/issues/4889) is not yet possible | [openvscode-server](https://github.com/gitpod-io/openvscode-server) (VSCode MIT) [with restrictions](https://ghuntley.com/fracture) inhibiting functionality of [.NET](https://www.isdotnetopen.com), [Python](https://visualstudiomagazine.com/articles/2021/11/05/vscode-python-nov21.aspx), [C](https://marketplace.visualstudio.com/items/ms-vscode.cpptools/license), [C++](https://marketplace.visualstudio.com/items/ms-vscode.cpptools/license), [Jupyter](https://visualstudiomagazine.com/articles/2021/11/05/vscode-python-nov21.aspx) and usage of [GitHub Co-pilot](https://github.com/gitpod-io/gitpod/issues/10032). Visual Studio Code Desktop ([no restrictions](https://ghuntley.com/fracture)) and JetBrains Gateway supported |
| [Google Cloud Workstations](https://coder.com/blog/the-self-hosting-paradox?utm_source=github.com/coder/coder?utm_source=github.com/coder/coder&utm_medium=github&utm_campaign=readme.md) | Platform | SaaS (Preview, not GA) | Pay Google | Yes | southamerica-west1, us-east1, us-central1, us-west1, asia-east1, asia-southeast1, europe-north1, europe-southwest1, europe-west1, europe-west2, europe-west3, europe-west4 | N/A | N/A | Not generally available, offered in preview mode. | Linux | code-oss ([with restrictions](https://ghuntley.com/fracture)), Visual Studio Code Desktop ([no restrictions](https://ghuntley.com/fracture)) and JetBrains Gateway |
| [JetBrains Space](https://coder.com/blog/the-self-hosting-paradox?utm_source=github.com/coder/coder?utm_source=github.com/coder/coder&utm_medium=github&utm_campaign=readme.md) | Platform | SaaS + On-Prem ([Dev environments are not supported](https://www.jetbrains.com/help/space-on-premises/space-on-premises-installation.html)) | Pay JetBrains | Yes | EU Ireland region (eu-west-1) | EC2 | N/A | [Service Health](https://status.jetbrains.space/) | Linux Virtual Machines | JetBrains Suite |
| [Microsoft DevBox](https://coder.com/blog/the-self-hosting-paradox?utm_source=github.com/coder/coder?utm_source=github.com/coder/coder&utm_medium=github&utm_campaign=readme.md) | Platform | SaaS (Preview, not GA) | Pay Microsoft | Yes | Australia East, Europe West, Japan East, Canada Central, UK South, US East, US East 2, US South Central, and US West 3 | Microsoft Azure Virtual Machine | N/A | Not generally available, offered in preview mode. | Windows Virtual Machine | Any application that runs on Windows via Microsoft Remote Desktop |
_Last updated: 5/27/22_
_Last updated: 14/12/2022_
## Community and Support
+95 -54
View File
@@ -30,6 +30,7 @@ import (
"github.com/spf13/afero"
"go.uber.org/atomic"
gossh "golang.org/x/crypto/ssh"
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
"tailscale.com/net/speedtest"
"tailscale.com/tailcfg"
@@ -90,7 +91,7 @@ func New(options Options) io.Closer {
}
}
ctx, cancelFunc := context.WithCancel(context.Background())
server := &agent{
a := &agent{
reconnectingPTYTimeout: options.ReconnectingPTYTimeout,
logger: options.Logger,
closeCancel: cancelFunc,
@@ -101,8 +102,8 @@ func New(options Options) io.Closer {
filesystem: options.Filesystem,
tempDir: options.TempDir,
}
server.init(ctx)
return server
a.init(ctx)
return a
}
type agent struct {
@@ -225,6 +226,25 @@ func (a *agent) run(ctx context.Context) error {
_ = network.Close()
return xerrors.New("agent is closed")
}
// Report statistics from the created network.
cl, err := a.client.AgentReportStats(ctx, a.logger, func() *codersdk.AgentStats {
stats := network.ExtractTrafficStats()
return convertAgentStats(stats)
})
if err != nil {
a.logger.Error(ctx, "report stats", slog.Error(err))
} else {
if err = a.trackConnGoroutine(func() {
// This is OK because the agent never re-creates the tailnet
// and the only shutdown indicator is agent.Close().
<-a.closed
_ = cl.Close()
}); err != nil {
a.logger.Debug(ctx, "report stats goroutine", slog.Error(err))
_ = cl.Close()
}
}
} else {
// Update the DERP map!
network.SetDERPMap(metadata.DERPMap)
@@ -284,7 +304,18 @@ func (a *agent) createTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) (_
if err != nil {
return
}
go a.sshServer.HandleConn(conn)
closed := make(chan struct{})
_ = a.trackConnGoroutine(func() {
select {
case <-network.Closed():
case <-closed:
}
_ = conn.Close()
})
_ = a.trackConnGoroutine(func() {
defer close(closed)
a.sshServer.HandleConn(conn)
})
}
}); err != nil {
return nil, err
@@ -300,10 +331,12 @@ func (a *agent) createTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) (_
}
}()
if err = a.trackConnGoroutine(func() {
logger := a.logger.Named("reconnecting-pty")
for {
conn, err := reconnectingPTYListener.Accept()
if err != nil {
a.logger.Debug(ctx, "accept pty failed", slog.Error(err))
logger.Debug(ctx, "accept pty failed", slog.Error(err))
return
}
// This cannot use a JSON decoder, since that can
@@ -324,7 +357,9 @@ func (a *agent) createTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) (_
if err != nil {
continue
}
go a.handleReconnectingPTY(ctx, msg, conn)
go func() {
_ = a.handleReconnectingPTY(ctx, logger, msg, conn)
}()
}
}); err != nil {
return nil, err
@@ -456,12 +491,16 @@ func (a *agent) init(ctx context.Context) {
if err != nil {
panic(err)
}
sshLogger := a.logger.Named("ssh-server")
forwardHandler := &ssh.ForwardedTCPHandler{}
unixForwardHandler := &forwardedUnixHandler{log: a.logger}
a.sshServer = &ssh.Server{
ChannelHandlers: map[string]ssh.ChannelHandler{
"direct-tcpip": ssh.DirectTCPIPHandler,
"session": ssh.DefaultSessionHandler,
"direct-tcpip": ssh.DirectTCPIPHandler,
"direct-streamlocal@openssh.com": directStreamLocalHandler,
"session": ssh.DefaultSessionHandler,
},
ConnectionFailedCallback: func(conn net.Conn, err error) {
sshLogger.Info(ctx, "ssh connection ended", slog.Error(err))
@@ -501,8 +540,10 @@ func (a *agent) init(ctx context.Context) {
return true
},
RequestHandlers: map[string]ssh.RequestHandler{
"tcpip-forward": forwardHandler.HandleSSHRequest,
"cancel-tcpip-forward": forwardHandler.HandleSSHRequest,
"tcpip-forward": forwardHandler.HandleSSHRequest,
"cancel-tcpip-forward": forwardHandler.HandleSSHRequest,
"streamlocal-forward@openssh.com": unixForwardHandler.HandleSSHRequest,
"cancel-streamlocal-forward@openssh.com": unixForwardHandler.HandleSSHRequest,
},
ServerConfigCallback: func(ctx ssh.Context) *gossh.ServerConfig {
return &gossh.ServerConfig{
@@ -556,28 +597,6 @@ func (a *agent) init(ctx context.Context) {
}
go a.runLoop(ctx)
cl, err := a.client.AgentReportStats(ctx, a.logger, func() *codersdk.AgentStats {
stats := map[netlogtype.Connection]netlogtype.Counts{}
a.closeMutex.Lock()
if a.network != nil {
stats = a.network.ExtractTrafficStats()
}
a.closeMutex.Unlock()
return convertAgentStats(stats)
})
if err != nil {
a.logger.Error(ctx, "report stats", slog.Error(err))
return
}
if err = a.trackConnGoroutine(func() {
<-a.closed
_ = cl.Close()
}); err != nil {
a.logger.Error(ctx, "report stats goroutine", slog.Error(err))
_ = cl.Close()
return
}
}
func convertAgentStats(counts map[netlogtype.Connection]netlogtype.Counts) *codersdk.AgentStats {
@@ -798,38 +817,56 @@ func (a *agent) handleSSHSession(session ssh.Session) (retErr error) {
return cmd.Wait()
}
func (a *agent) handleReconnectingPTY(ctx context.Context, msg codersdk.ReconnectingPTYInit, conn net.Conn) {
func (a *agent) handleReconnectingPTY(ctx context.Context, logger slog.Logger, msg codersdk.ReconnectingPTYInit, conn net.Conn) (retErr error) {
defer conn.Close()
connectionID := uuid.NewString()
logger = logger.With(slog.F("id", msg.ID), slog.F("connection_id", connectionID))
defer func() {
if err := retErr; err != nil {
a.closeMutex.Lock()
closed := a.isClosed()
a.closeMutex.Unlock()
// If the agent is closed, we don't want to
// log this as an error since it's expected.
if closed {
logger.Debug(ctx, "session error after agent close", slog.Error(err))
} else {
logger.Error(ctx, "session error", slog.Error(err))
}
}
logger.Debug(ctx, "session closed")
}()
var rpty *reconnectingPTY
rawRPTY, ok := a.reconnectingPTYs.Load(msg.ID)
if ok {
logger.Debug(ctx, "connecting to existing session")
rpty, ok = rawRPTY.(*reconnectingPTY)
if !ok {
a.logger.Error(ctx, "found invalid type in reconnecting pty map", slog.F("id", msg.ID))
return
return xerrors.Errorf("found invalid type in reconnecting pty map: %T", rawRPTY)
}
} else {
logger.Debug(ctx, "creating new session")
// Empty command will default to the users shell!
cmd, err := a.createCommand(ctx, msg.Command, nil)
if err != nil {
a.logger.Error(ctx, "create reconnecting pty command", slog.Error(err))
return
return xerrors.Errorf("create command: %w", err)
}
cmd.Env = append(cmd.Env, "TERM=xterm-256color")
// Default to buffer 64KiB.
circularBuffer, err := circbuf.NewBuffer(64 << 10)
if err != nil {
a.logger.Error(ctx, "create circular buffer", slog.Error(err))
return
return xerrors.Errorf("create circular buffer: %w", err)
}
ptty, process, err := pty.Start(cmd)
if err != nil {
a.logger.Error(ctx, "start reconnecting pty command", slog.F("id", msg.ID), slog.Error(err))
return
return xerrors.Errorf("start command: %w", err)
}
ctx, cancelFunc := context.WithCancel(ctx)
@@ -873,7 +910,7 @@ func (a *agent) handleReconnectingPTY(ctx context.Context, msg codersdk.Reconnec
_, err = rpty.circularBuffer.Write(part)
rpty.circularBufferMutex.Unlock()
if err != nil {
a.logger.Error(ctx, "reconnecting pty write buffer", slog.Error(err), slog.F("id", msg.ID))
logger.Error(ctx, "write to circular buffer", slog.Error(err))
break
}
rpty.activeConnsMutex.Lock()
@@ -889,23 +926,27 @@ func (a *agent) handleReconnectingPTY(ctx context.Context, msg codersdk.Reconnec
rpty.Close()
a.reconnectingPTYs.Delete(msg.ID)
}); err != nil {
a.logger.Error(ctx, "start reconnecting pty routine", slog.F("id", msg.ID), slog.Error(err))
return
return xerrors.Errorf("start routine: %w", err)
}
}
// Resize the PTY to initial height + width.
err := rpty.ptty.Resize(msg.Height, msg.Width)
if err != nil {
// We can continue after this, it's not fatal!
a.logger.Error(ctx, "resize reconnecting pty", slog.F("id", msg.ID), slog.Error(err))
logger.Error(ctx, "resize", slog.Error(err))
}
// Write any previously stored data for the TTY.
rpty.circularBufferMutex.RLock()
_, err = conn.Write(rpty.circularBuffer.Bytes())
prevBuf := slices.Clone(rpty.circularBuffer.Bytes())
rpty.circularBufferMutex.RUnlock()
// Note that there is a small race here between writing buffered
// data and storing conn in activeConns. This is likely a very minor
// edge case, but we should look into ways to avoid it. Holding
// activeConnsMutex would be one option, but holding this mutex
// while also holding circularBufferMutex seems dangerous.
_, err = conn.Write(prevBuf)
if err != nil {
a.logger.Warn(ctx, "write reconnecting pty buffer", slog.F("id", msg.ID), slog.Error(err))
return
return xerrors.Errorf("write buffer to conn: %w", err)
}
// Multiple connections to the same TTY are permitted.
// This could easily be used for terminal sharing, but
@@ -946,16 +987,16 @@ func (a *agent) handleReconnectingPTY(ctx context.Context, msg codersdk.Reconnec
for {
err = decoder.Decode(&req)
if xerrors.Is(err, io.EOF) {
return
return nil
}
if err != nil {
a.logger.Warn(ctx, "reconnecting pty buffer read error", slog.F("id", msg.ID), slog.Error(err))
return
logger.Warn(ctx, "read conn", slog.Error(err))
return nil
}
_, err = rpty.ptty.Input().Write([]byte(req.Data))
if err != nil {
a.logger.Warn(ctx, "write to reconnecting pty", slog.F("id", msg.ID), slog.Error(err))
return
logger.Warn(ctx, "write to pty", slog.Error(err))
return nil
}
// Check if a resize needs to happen!
if req.Height == 0 || req.Width == 0 {
@@ -964,7 +1005,7 @@ func (a *agent) handleReconnectingPTY(ctx context.Context, msg codersdk.Reconnec
err = rpty.ptty.Resize(req.Height, req.Width)
if err != nil {
// We can continue after this, it's not fatal!
a.logger.Error(ctx, "resize reconnecting pty", slog.F("id", msg.ID), slog.Error(err))
logger.Error(ctx, "resize", slog.Error(err))
}
}
}
+863 -629
View File
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -34,10 +34,11 @@ func NewWorkspaceAppHealthReporter(logger slog.Logger, apps []codersdk.Workspace
hasHealthchecksEnabled := false
health := make(map[uuid.UUID]codersdk.WorkspaceAppHealth, 0)
for _, app := range apps {
health[app.ID] = app.Health
if !hasHealthchecksEnabled && app.Health != codersdk.WorkspaceAppHealthDisabled {
hasHealthchecksEnabled = true
if app.Health == codersdk.WorkspaceAppHealthDisabled {
continue
}
health[app.ID] = app.Health
hasHealthchecksEnabled = true
}
// no need to run this loop if no health checks are configured.
@@ -77,7 +78,7 @@ func NewWorkspaceAppHealthReporter(logger slog.Logger, apps []codersdk.Workspace
return err
}
// successful healthcheck is a non-5XX status code
res.Body.Close()
_ = res.Body.Close()
if res.StatusCode >= http.StatusInternalServerError {
return xerrors.Errorf("error status code: %d", res.StatusCode)
}
+124 -127
View File
@@ -19,148 +19,145 @@ import (
"github.com/coder/coder/testutil"
)
func TestAppHealth(t *testing.T) {
func TestAppHealth_Healthy(t *testing.T) {
t.Parallel()
t.Run("Healthy", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
apps := []codersdk.WorkspaceApp{
{
Slug: "app1",
Healthcheck: codersdk.Healthcheck{},
Health: codersdk.WorkspaceAppHealthDisabled,
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
apps := []codersdk.WorkspaceApp{
{
Slug: "app1",
Healthcheck: codersdk.Healthcheck{},
Health: codersdk.WorkspaceAppHealthDisabled,
},
{
Slug: "app2",
Healthcheck: codersdk.Healthcheck{
// URL: We don't set the URL for this test because the setup will
// create a httptest server for us and set it for us.
Interval: 1,
Threshold: 1,
},
{
Slug: "app2",
Healthcheck: codersdk.Healthcheck{
// URL: We don't set the URL for this test because the setup will
// create a httptest server for us and set it for us.
Interval: 1,
Threshold: 1,
},
Health: codersdk.WorkspaceAppHealthInitializing,
},
}
handlers := []http.Handler{
nil,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
httpapi.Write(r.Context(), w, http.StatusOK, nil)
}),
}
getApps, closeFn := setupAppReporter(ctx, t, apps, handlers)
defer closeFn()
Health: codersdk.WorkspaceAppHealthInitializing,
},
}
handlers := []http.Handler{
nil,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
httpapi.Write(r.Context(), w, http.StatusOK, nil)
}),
}
getApps, closeFn := setupAppReporter(ctx, t, apps, handlers)
defer closeFn()
apps, err := getApps(ctx)
require.NoError(t, err)
require.EqualValues(t, codersdk.WorkspaceAppHealthDisabled, apps[0].Health)
require.Eventually(t, func() bool {
apps, err := getApps(ctx)
require.NoError(t, err)
require.EqualValues(t, codersdk.WorkspaceAppHealthDisabled, apps[0].Health)
require.Eventually(t, func() bool {
apps, err := getApps(ctx)
if err != nil {
return false
}
if err != nil {
return false
}
return apps[1].Health == codersdk.WorkspaceAppHealthHealthy
}, testutil.WaitLong, testutil.IntervalSlow)
})
return apps[1].Health == codersdk.WorkspaceAppHealthHealthy
}, testutil.WaitLong, testutil.IntervalSlow)
}
t.Run("500", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
apps := []codersdk.WorkspaceApp{
{
Slug: "app2",
Healthcheck: codersdk.Healthcheck{
// URL: We don't set the URL for this test because the setup will
// create a httptest server for us and set it for us.
Interval: 1,
Threshold: 1,
},
Health: codersdk.WorkspaceAppHealthInitializing,
func TestAppHealth_500(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
apps := []codersdk.WorkspaceApp{
{
Slug: "app2",
Healthcheck: codersdk.Healthcheck{
// URL: We don't set the URL for this test because the setup will
// create a httptest server for us and set it for us.
Interval: 1,
Threshold: 1,
},
Health: codersdk.WorkspaceAppHealthInitializing,
},
}
handlers := []http.Handler{
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
httpapi.Write(r.Context(), w, http.StatusInternalServerError, nil)
}),
}
getApps, closeFn := setupAppReporter(ctx, t, apps, handlers)
defer closeFn()
require.Eventually(t, func() bool {
apps, err := getApps(ctx)
if err != nil {
return false
}
handlers := []http.Handler{
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
httpapi.Write(r.Context(), w, http.StatusInternalServerError, nil)
}),
}
getApps, closeFn := setupAppReporter(ctx, t, apps, handlers)
defer closeFn()
require.Eventually(t, func() bool {
apps, err := getApps(ctx)
if err != nil {
return false
}
return apps[0].Health == codersdk.WorkspaceAppHealthUnhealthy
}, testutil.WaitLong, testutil.IntervalSlow)
})
return apps[0].Health == codersdk.WorkspaceAppHealthUnhealthy
}, testutil.WaitLong, testutil.IntervalSlow)
}
t.Run("Timeout", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
apps := []codersdk.WorkspaceApp{
{
Slug: "app2",
Healthcheck: codersdk.Healthcheck{
// URL: We don't set the URL for this test because the setup will
// create a httptest server for us and set it for us.
Interval: 1,
Threshold: 1,
},
Health: codersdk.WorkspaceAppHealthInitializing,
func TestAppHealth_Timeout(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
apps := []codersdk.WorkspaceApp{
{
Slug: "app2",
Healthcheck: codersdk.Healthcheck{
// URL: We don't set the URL for this test because the setup will
// create a httptest server for us and set it for us.
Interval: 1,
Threshold: 1,
},
Health: codersdk.WorkspaceAppHealthInitializing,
},
}
handlers := []http.Handler{
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// sleep longer than the interval to cause the health check to time out
time.Sleep(2 * time.Second)
httpapi.Write(r.Context(), w, http.StatusOK, nil)
}),
}
getApps, closeFn := setupAppReporter(ctx, t, apps, handlers)
defer closeFn()
require.Eventually(t, func() bool {
apps, err := getApps(ctx)
if err != nil {
return false
}
handlers := []http.Handler{
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// sleep longer than the interval to cause the health check to time out
time.Sleep(2 * time.Second)
httpapi.Write(r.Context(), w, http.StatusOK, nil)
}),
}
getApps, closeFn := setupAppReporter(ctx, t, apps, handlers)
defer closeFn()
require.Eventually(t, func() bool {
apps, err := getApps(ctx)
if err != nil {
return false
}
return apps[0].Health == codersdk.WorkspaceAppHealthUnhealthy
}, testutil.WaitLong, testutil.IntervalSlow)
})
return apps[0].Health == codersdk.WorkspaceAppHealthUnhealthy
}, testutil.WaitLong, testutil.IntervalSlow)
}
t.Run("NotSpamming", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
apps := []codersdk.WorkspaceApp{
{
Slug: "app2",
Healthcheck: codersdk.Healthcheck{
// URL: We don't set the URL for this test because the setup will
// create a httptest server for us and set it for us.
Interval: 1,
Threshold: 1,
},
Health: codersdk.WorkspaceAppHealthInitializing,
func TestAppHealth_NotSpamming(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
apps := []codersdk.WorkspaceApp{
{
Slug: "app2",
Healthcheck: codersdk.Healthcheck{
// URL: We don't set the URL for this test because the setup will
// create a httptest server for us and set it for us.
Interval: 1,
Threshold: 1,
},
}
Health: codersdk.WorkspaceAppHealthInitializing,
},
}
var counter = new(int32)
handlers := []http.Handler{
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt32(counter, 1)
}),
}
_, closeFn := setupAppReporter(ctx, t, apps, handlers)
defer closeFn()
// Ensure we haven't made more than 2 (expected 1 + 1 for buffer) requests in the last second.
// if there is a bug where we are spamming the healthcheck route this will catch it.
time.Sleep(time.Second)
require.LessOrEqual(t, *counter, int32(2))
})
counter := new(int32)
handlers := []http.Handler{
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt32(counter, 1)
}),
}
_, closeFn := setupAppReporter(ctx, t, apps, handlers)
defer closeFn()
// Ensure we haven't made more than 2 (expected 1 + 1 for buffer) requests in the last second.
// if there is a bug where we are spamming the healthcheck route this will catch it.
time.Sleep(time.Second)
require.LessOrEqual(t, *counter, int32(2))
}
func setupAppReporter(ctx context.Context, t *testing.T, apps []codersdk.WorkspaceApp, handlers []http.Handler) (agent.WorkspaceAgentApps, func()) {
+1 -1
View File
@@ -32,7 +32,7 @@ func (lp *listeningPortsHandler) getListeningPorts() ([]codersdk.ListeningPort,
seen := make(map[uint16]struct{}, len(tabs))
ports := []codersdk.ListeningPort{}
for _, tab := range tabs {
if tab.LocalAddr == nil || tab.LocalAddr.Port < uint16(codersdk.MinimumListeningPort) {
if tab.LocalAddr == nil || tab.LocalAddr.Port < codersdk.MinimumListeningPort {
continue
}
+203
View File
@@ -0,0 +1,203 @@
package agent
import (
"context"
"fmt"
"net"
"os"
"path/filepath"
"sync"
"github.com/gliderlabs/ssh"
gossh "golang.org/x/crypto/ssh"
"golang.org/x/xerrors"
"cdr.dev/slog"
)
// streamLocalForwardPayload describes the extra data sent in a
// streamlocal-forward@openssh.com containing the socket path to bind to.
type streamLocalForwardPayload struct {
SocketPath string
}
// forwardedStreamLocalPayload describes the data sent as the payload in the new
// channel request when a Unix connection is accepted by the listener.
type forwardedStreamLocalPayload struct {
SocketPath string
Reserved uint32
}
// forwardedUnixHandler is a clone of ssh.ForwardedTCPHandler that does
// streamlocal forwarding (aka. unix forwarding) instead of TCP forwarding.
type forwardedUnixHandler struct {
sync.Mutex
log slog.Logger
forwards map[string]net.Listener
}
func (h *forwardedUnixHandler) HandleSSHRequest(ctx ssh.Context, _ *ssh.Server, req *gossh.Request) (bool, []byte) {
h.Lock()
if h.forwards == nil {
h.forwards = make(map[string]net.Listener)
}
h.Unlock()
conn, ok := ctx.Value(ssh.ContextKeyConn).(*gossh.ServerConn)
if !ok {
h.log.Warn(ctx, "SSH unix forward request from client with no gossh connection")
return false, nil
}
switch req.Type {
case "streamlocal-forward@openssh.com":
var reqPayload streamLocalForwardPayload
err := gossh.Unmarshal(req.Payload, &reqPayload)
if err != nil {
h.log.Warn(ctx, "parse streamlocal-forward@openssh.com request payload from client", slog.Error(err))
return false, nil
}
addr := reqPayload.SocketPath
h.Lock()
_, ok := h.forwards[addr]
h.Unlock()
if ok {
h.log.Warn(ctx, "SSH unix forward request for socket path that is already being forwarded (maybe to another client?)",
slog.F("socket_path", addr),
)
return false, nil
}
// Create socket parent dir if not exists.
parentDir := filepath.Dir(addr)
err = os.MkdirAll(parentDir, 0700)
if err != nil {
h.log.Warn(ctx, "create parent dir for SSH unix forward request",
slog.F("parent_dir", parentDir),
slog.F("socket_path", addr),
slog.Error(err),
)
return false, nil
}
ln, err := net.Listen("unix", addr)
if err != nil {
h.log.Warn(ctx, "listen on Unix socket for SSH unix forward request",
slog.F("socket_path", addr),
slog.Error(err),
)
return false, nil
}
// The listener needs to successfully start before it can be added to
// the map, so we don't have to worry about checking for an existing
// listener.
//
// This is also what the upstream TCP version of this code does.
h.Lock()
h.forwards[addr] = ln
h.Unlock()
ctx, cancel := context.WithCancel(ctx)
go func() {
<-ctx.Done()
_ = ln.Close()
}()
go func() {
defer cancel()
for {
c, err := ln.Accept()
if err != nil {
if !xerrors.Is(err, net.ErrClosed) {
h.log.Warn(ctx, "accept on local Unix socket for SSH unix forward request",
slog.F("socket_path", addr),
slog.Error(err),
)
}
// closed below
break
}
payload := gossh.Marshal(&forwardedStreamLocalPayload{
SocketPath: addr,
})
go func() {
ch, reqs, err := conn.OpenChannel("forwarded-streamlocal@openssh.com", payload)
if err != nil {
h.log.Warn(ctx, "open SSH channel to forward Unix connection to client",
slog.F("socket_path", addr),
slog.Error(err),
)
_ = c.Close()
return
}
go gossh.DiscardRequests(reqs)
Bicopy(ctx, ch, c)
}()
}
h.Lock()
ln2, ok := h.forwards[addr]
if ok && ln2 == ln {
delete(h.forwards, addr)
}
h.Unlock()
_ = ln.Close()
}()
return true, nil
case "cancel-streamlocal-forward@openssh.com":
var reqPayload streamLocalForwardPayload
err := gossh.Unmarshal(req.Payload, &reqPayload)
if err != nil {
h.log.Warn(ctx, "parse cancel-streamlocal-forward@openssh.com request payload from client", slog.Error(err))
return false, nil
}
h.Lock()
ln, ok := h.forwards[reqPayload.SocketPath]
h.Unlock()
if ok {
_ = ln.Close()
}
return true, nil
default:
return false, nil
}
}
// directStreamLocalPayload describes the extra data sent in a
// direct-streamlocal@openssh.com channel request containing the socket path.
type directStreamLocalPayload struct {
SocketPath string
Reserved1 string
Reserved2 uint32
}
func directStreamLocalHandler(_ *ssh.Server, _ *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {
var reqPayload directStreamLocalPayload
err := gossh.Unmarshal(newChan.ExtraData(), &reqPayload)
if err != nil {
_ = newChan.Reject(gossh.ConnectionFailed, "could not parse direct-streamlocal@openssh.com channel payload")
return
}
var dialer net.Dialer
dconn, err := dialer.DialContext(ctx, "unix", reqPayload.SocketPath)
if err != nil {
_ = newChan.Reject(gossh.ConnectionFailed, fmt.Sprintf("dial unix socket %q: %+v", reqPayload.SocketPath, err.Error()))
return
}
ch, reqs, err := newChan.Accept()
if err != nil {
_ = dconn.Close()
return
}
go gossh.DiscardRequests(reqs)
Bicopy(ctx, ch, dconn)
}
+2
View File
@@ -39,6 +39,8 @@ func workspaceAgent() *cobra.Command {
ctx, cancel := context.WithCancel(cmd.Context())
defer cancel()
go dumpHandler(ctx)
rawURL, err := cmd.Flags().GetString(varAgentURL)
if err != nil {
return xerrors.Errorf("CODER_AGENT_URL must be set: %w", err)
+5 -1
View File
@@ -8,6 +8,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"github.com/spf13/cobra"
@@ -55,7 +56,7 @@ func CreateTemplateVersionSource(t *testing.T, responses *echo.Responses) string
directory := t.TempDir()
f, err := ioutil.TempFile(directory, "*.tf")
require.NoError(t, err)
f.Close()
_ = f.Close()
data, err := echo.Tar(responses)
require.NoError(t, err)
extractTar(t, data, directory)
@@ -70,6 +71,9 @@ func extractTar(t *testing.T, data []byte, directory string) {
break
}
require.NoError(t, err)
if header.Name == "." || strings.Contains(header.Name, "..") {
continue
}
// #nosec
path := filepath.Join(directory, header.Name)
mode := header.FileInfo().Mode()
+1 -1
View File
@@ -9,7 +9,7 @@ gitauth:
# Multiple providers are an Enterprise feature.
# Contact sales@coder.com for a license.
#
#
# If multiple providers are used, a unique "id"
# must be provided for each one.
# - id: example
+29 -5
View File
@@ -232,13 +232,16 @@ func TestCreate(t *testing.T) {
ProvisionApply: echo.ProvisionComplete,
ProvisionPlan: echo.ProvisionComplete,
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
_ = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
tempDir := t.TempDir()
removeTmpDirUntilSuccessAfterTest(t, tempDir)
parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml")
_, _ = parameterFile.WriteString("zone: \"bananas\"")
cmd, root := clitest.New(t, "create", "my-workspace", "--template", template.Name, "--parameter-file", parameterFile.Name())
_, _ = parameterFile.WriteString("username: \"boingo\"")
cmd, root := clitest.New(t, "create", "", "--parameter-file", parameterFile.Name())
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t)
@@ -247,11 +250,32 @@ func TestCreate(t *testing.T) {
go func() {
defer close(doneChan)
err := cmd.Execute()
assert.EqualError(t, err, "Parameter value absent in parameter file for \"region\"!")
assert.NoError(t, err)
}()
matches := []struct {
match string
write string
}{
{
match: "Specify a name",
write: "my-workspace",
},
{
match: fmt.Sprintf("Enter a value (default: %q):", defaultValue),
write: "bingo",
},
{
match: "Confirm create?",
write: "yes",
},
}
for _, m := range matches {
pty.ExpectMatch(m.match)
pty.WriteLine(m.write)
}
<-doneChan
})
t.Run("FailedDryRun", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
+90 -22
View File
@@ -32,12 +32,22 @@ func newConfig() *codersdk.DeploymentConfig {
Usage: "Specifies the wildcard hostname to use for workspace applications in the form \"*.example.com\".",
Flag: "wildcard-access-url",
},
// DEPRECATED: Use HTTPAddress or TLS.Address instead.
Address: &codersdk.DeploymentConfigField[string]{
Name: "Address",
Usage: "Bind address of the server.",
Flag: "address",
Shorthand: "a",
Default: "127.0.0.1:3000",
// Deprecated, so we don't have a default. If set, it will overwrite
// HTTPAddress and TLS.Address and print a warning.
Hidden: true,
Default: "",
},
HTTPAddress: &codersdk.DeploymentConfigField[string]{
Name: "Address",
Usage: "HTTP bind address of the server. Unset to disable the HTTP endpoint.",
Flag: "http-address",
Default: "127.0.0.1:3000",
},
AutobuildPollInterval: &codersdk.DeploymentConfigField[time.Duration]{
Name: "Autobuild Poll Interval",
@@ -238,6 +248,12 @@ func newConfig() *codersdk.DeploymentConfig {
Flag: "oidc-ignore-email-verified",
Default: false,
},
UsernameField: &codersdk.DeploymentConfigField[string]{
Name: "OIDC Username Field",
Usage: "OIDC claim field to use as the username.",
Flag: "oidc-username-field",
Default: "preferred_username",
},
},
Telemetry: &codersdk.TelemetryConfig{
@@ -267,6 +283,18 @@ func newConfig() *codersdk.DeploymentConfig {
Usage: "Whether TLS will be enabled.",
Flag: "tls-enable",
},
Address: &codersdk.DeploymentConfigField[string]{
Name: "TLS Address",
Usage: "HTTPS bind address of the server.",
Flag: "tls-address",
Default: "127.0.0.1:3443",
},
RedirectHTTP: &codersdk.DeploymentConfigField[bool]{
Name: "Redirect HTTP to HTTPS",
Usage: "Whether HTTP requests will be redirected to the access URL (if it's a https URL and TLS is enabled). Requests to local IP addresses are never redirected regardless of this setting.",
Flag: "tls-redirect-http-to-https",
Default: true,
},
CertFiles: &codersdk.DeploymentConfigField[[]string]{
Name: "TLS Certificate Files",
Usage: "Path to each certificate for TLS. It requires a PEM-encoded file. To configure the listener to use a CA certificate, concatenate the primary certificate and the CA certificate together. The primary certificate should appear first in the combined file.",
@@ -281,7 +309,7 @@ func newConfig() *codersdk.DeploymentConfig {
Name: "TLS Client Auth",
Usage: "Policy the server will follow for TLS Client Authentication. Accepted values are \"none\", \"request\", \"require-any\", \"verify-if-given\", or \"require-and-verify\".",
Flag: "tls-client-auth",
Default: "request",
Default: "none",
},
KeyFiles: &codersdk.DeploymentConfigField[[]string]{
Name: "TLS Key Files",
@@ -334,12 +362,6 @@ func newConfig() *codersdk.DeploymentConfig {
Flag: "ssh-keygen-algorithm",
Default: "ed25519",
},
AutoImportTemplates: &codersdk.DeploymentConfigField[[]string]{
Name: "Auto Import Templates",
Usage: "Templates to auto-import. Available auto-importable templates are: kubernetes",
Flag: "auto-import-template",
Hidden: true,
},
MetricsCacheRefreshInterval: &codersdk.DeploymentConfigField[time.Duration]{
Name: "Metrics Cache Refresh Interval",
Usage: "How frequently metrics are refreshed",
@@ -407,11 +429,22 @@ func newConfig() *codersdk.DeploymentConfig {
Default: 10 * time.Minute,
},
},
APIRateLimit: &codersdk.DeploymentConfigField[int]{
Name: "API Rate Limit",
Usage: "Maximum number of requests per minute allowed to the API per user, or per IP address for unauthenticated users. Negative values mean no rate limit. Some API endpoints are always rate limited regardless of this value to prevent denial-of-service attacks.",
Flag: "api-rate-limit",
Default: 512,
RateLimit: &codersdk.RateLimitConfig{
DisableAll: &codersdk.DeploymentConfigField[bool]{
Name: "Disable All Rate Limits",
Usage: "Disables all rate limits. This is not recommended in production.",
Flag: "dangerous-disable-rate-limits",
Default: false,
},
API: &codersdk.DeploymentConfigField[int]{
Name: "API Rate Limit",
Usage: "Maximum number of requests per minute allowed to the API per user, or per IP address for unauthenticated users. Negative values mean no rate limit. Some API endpoints have separate strict rate limits regardless of this value to prevent denial-of-service or brute force attacks.",
// Change the env from the auto-generated CODER_RATE_LIMIT_API to the
// old value to avoid breaking existing deployments.
EnvOverride: "CODER_API_RATE_LIMIT",
Flag: "api-rate-limit",
Default: 512,
},
},
Experimental: &codersdk.DeploymentConfigField[bool]{
Name: "Experimental",
@@ -424,6 +457,20 @@ func newConfig() *codersdk.DeploymentConfig {
Flag: "update-check",
Default: flag.Lookup("test.v") == nil && !buildinfo.IsDev(),
},
MaxTokenLifetime: &codersdk.DeploymentConfigField[time.Duration]{
Name: "Max Token Lifetime",
Usage: "The maximum lifetime duration for any user creating a token.",
Flag: "max-token-lifetime",
Default: 24 * 30 * time.Hour,
},
Swagger: &codersdk.SwaggerConfig{
Enable: &codersdk.DeploymentConfigField[bool]{
Name: "Enable swagger endpoint",
Usage: "Expose the swagger endpoint via /swagger.",
Flag: "swagger-enable",
Default: false,
},
},
}
}
@@ -462,21 +509,30 @@ func setConfig(prefix string, vip *viper.Viper, target interface{}) {
// assigned a value.
if strings.HasPrefix(typ.Name(), "DeploymentConfigField[") {
value := val.FieldByName("Value").Interface()
env, ok := val.FieldByName("EnvOverride").Interface().(string)
if !ok {
panic("DeploymentConfigField[].EnvOverride must be a string")
}
if env == "" {
env = formatEnv(prefix)
}
switch value.(type) {
case string:
vip.MustBindEnv(prefix, formatEnv(prefix))
vip.MustBindEnv(prefix, env)
val.FieldByName("Value").SetString(vip.GetString(prefix))
case bool:
vip.MustBindEnv(prefix, formatEnv(prefix))
vip.MustBindEnv(prefix, env)
val.FieldByName("Value").SetBool(vip.GetBool(prefix))
case int:
vip.MustBindEnv(prefix, formatEnv(prefix))
vip.MustBindEnv(prefix, env)
val.FieldByName("Value").SetInt(int64(vip.GetInt(prefix)))
case time.Duration:
vip.MustBindEnv(prefix, formatEnv(prefix))
vip.MustBindEnv(prefix, env)
val.FieldByName("Value").SetInt(int64(vip.GetDuration(prefix)))
case []string:
vip.MustBindEnv(prefix, formatEnv(prefix))
vip.MustBindEnv(prefix, env)
// As of October 21st, 2022 we supported delimiting a string
// with a comma, but Viper only supports with a space. This
// is a small hack around it!
@@ -544,6 +600,9 @@ func readSliceFromViper[T any](vip *viper.Viper, key string, value any) []T {
// Ensure the env entry for this key is registered
// before checking value.
//
// We don't support DeploymentConfigField[].EnvOverride for array flags so
// this is fine to just use `formatEnv` here.
vip.MustBindEnv(configKey, formatEnv(configKey))
value := vip.Get(configKey)
@@ -590,7 +649,7 @@ func setViperDefaults(prefix string, vip *viper.Viper, target interface{}) {
val := reflect.ValueOf(target).Elem()
val = reflect.Indirect(val)
typ := val.Type()
if strings.HasPrefix(typ.Name(), "DeploymentConfigField") {
if strings.HasPrefix(typ.Name(), "DeploymentConfigField[") {
value := val.FieldByName("Default").Interface()
vip.SetDefault(prefix, value)
return
@@ -627,7 +686,7 @@ func AttachFlags(flagset *pflag.FlagSet, vip *viper.Viper, enterprise bool) {
func setFlags(prefix string, flagset *pflag.FlagSet, vip *viper.Viper, target interface{}, enterprise bool) {
val := reflect.Indirect(reflect.ValueOf(target))
typ := val.Type()
if strings.HasPrefix(typ.Name(), "DeploymentConfigField") {
if strings.HasPrefix(typ.Name(), "DeploymentConfigField[") {
isEnt := val.FieldByName("Enterprise").Bool()
if enterprise != isEnt {
return
@@ -636,15 +695,24 @@ func setFlags(prefix string, flagset *pflag.FlagSet, vip *viper.Viper, target in
if flg == "" {
return
}
env, ok := val.FieldByName("EnvOverride").Interface().(string)
if !ok {
panic("DeploymentConfigField[].EnvOverride must be a string")
}
if env == "" {
env = formatEnv(prefix)
}
usage := val.FieldByName("Usage").String()
usage = fmt.Sprintf("%s\n%s", usage, cliui.Styles.Placeholder.Render("Consumes $"+formatEnv(prefix)))
usage = fmt.Sprintf("%s\n%s", usage, cliui.Styles.Placeholder.Render("Consumes $"+env))
shorthand := val.FieldByName("Shorthand").String()
hidden := val.FieldByName("Hidden").Bool()
value := val.FieldByName("Default").Interface()
// Allow currently set environment variables
// to override default values in help output.
vip.MustBindEnv(prefix, formatEnv(prefix))
vip.MustBindEnv(prefix, env)
switch value.(type) {
case string:
-391
View File
@@ -1,391 +0,0 @@
package cli
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/spf13/cobra"
"go.opentelemetry.io/otel/trace"
"golang.org/x/xerrors"
"github.com/coder/coder/cli/cliflag"
"github.com/coder/coder/coderd/tracing"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/loadtest/harness"
)
const loadtestTracerName = "coder_loadtest"
func loadtest() *cobra.Command {
var (
configPath string
outputSpecs []string
traceEnable bool
traceCoder bool
traceHoneycombAPIKey string
tracePropagate bool
)
cmd := &cobra.Command{
Use: "loadtest --config <path> [--output json[:path]] [--output text[:path]]]",
Short: "Load test the Coder API",
// TODO: documentation and a JSON schema file
Long: "Perform load tests against the Coder server. The load tests are configurable via a JSON file.",
Example: formatExamples(
example{
Description: "Run a loadtest with the given configuration file",
Command: "coder loadtest --config path/to/config.json",
},
example{
Description: "Run a loadtest, reading the configuration from stdin",
Command: "cat path/to/config.json | coder loadtest --config -",
},
example{
Description: "Run a loadtest outputting JSON results instead",
Command: "coder loadtest --config path/to/config.json --output json",
},
example{
Description: "Run a loadtest outputting JSON results to a file",
Command: "coder loadtest --config path/to/config.json --output json:path/to/results.json",
},
example{
Description: "Run a loadtest outputting text results to stdout and JSON results to a file",
Command: "coder loadtest --config path/to/config.json --output text --output json:path/to/results.json",
},
),
Hidden: true,
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := tracing.SetTracerName(cmd.Context(), loadtestTracerName)
config, err := loadLoadTestConfigFile(configPath, cmd.InOrStdin())
if err != nil {
return err
}
outputs, err := parseLoadTestOutputs(outputSpecs)
if err != nil {
return err
}
client, err := CreateClient(cmd)
if err != nil {
return err
}
me, err := client.User(ctx, codersdk.Me)
if err != nil {
return xerrors.Errorf("fetch current user: %w", err)
}
// Only owners can do loadtests. This isn't a very strong check but
// there's not much else we can do. Ratelimits are enforced for
// non-owners so hopefully that limits the damage if someone
// disables this check and runs it against a non-owner account.
ok := false
for _, role := range me.Roles {
if role.Name == "owner" {
ok = true
break
}
}
if !ok {
return xerrors.Errorf("Not logged in as a site owner. Load testing is only available to site owners.")
}
// Setup tracing and start a span.
var (
shouldTrace = traceEnable || traceCoder || traceHoneycombAPIKey != ""
tracerProvider trace.TracerProvider = trace.NewNoopTracerProvider()
closeTracingOnce sync.Once
closeTracing = func(_ context.Context) error {
return nil
}
)
if shouldTrace {
tracerProvider, closeTracing, err = tracing.TracerProvider(ctx, loadtestTracerName, tracing.TracerOpts{
Default: traceEnable,
Coder: traceCoder,
Honeycomb: traceHoneycombAPIKey,
})
if err != nil {
return xerrors.Errorf("initialize tracing: %w", err)
}
defer func() {
closeTracingOnce.Do(func() {
// Allow time for traces to flush even if command
// context is canceled.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = closeTracing(ctx)
})
}()
}
tracer := tracerProvider.Tracer(loadtestTracerName)
// Disable ratelimits and propagate tracing spans for future
// requests. Individual tests will setup their own loggers.
client.BypassRatelimits = true
client.PropagateTracing = tracePropagate
// Prepare the test.
runStrategy := config.Strategy.ExecutionStrategy()
cleanupStrategy := config.CleanupStrategy.ExecutionStrategy()
th := harness.NewTestHarness(runStrategy, cleanupStrategy)
for i, t := range config.Tests {
name := fmt.Sprintf("%s-%d", t.Type, i)
for j := 0; j < t.Count; j++ {
id := strconv.Itoa(j)
runner, err := t.NewRunner(client.Clone())
if err != nil {
return xerrors.Errorf("create %q runner for %s/%s: %w", t.Type, name, id, err)
}
th.AddRun(name, id, &runnableTraceWrapper{
tracer: tracer,
spanName: fmt.Sprintf("%s/%s", name, id),
runner: runner,
})
}
}
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Running load test...")
testCtx := ctx
if config.Timeout > 0 {
var cancel func()
testCtx, cancel = context.WithTimeout(testCtx, time.Duration(config.Timeout))
defer cancel()
}
// TODO: live progress output
err = th.Run(testCtx)
if err != nil {
return xerrors.Errorf("run test harness (harness failure, not a test failure): %w", err)
}
// Print the results.
res := th.Results()
for _, output := range outputs {
var (
w = cmd.OutOrStdout()
c io.Closer
)
if output.path != "-" {
f, err := os.Create(output.path)
if err != nil {
return xerrors.Errorf("create output file: %w", err)
}
w, c = f, f
}
switch output.format {
case loadTestOutputFormatText:
res.PrintText(w)
case loadTestOutputFormatJSON:
err = json.NewEncoder(w).Encode(res)
if err != nil {
return xerrors.Errorf("encode JSON: %w", err)
}
}
if c != nil {
err = c.Close()
if err != nil {
return xerrors.Errorf("close output file: %w", err)
}
}
}
// Cleanup.
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "\nCleaning up...")
err = th.Cleanup(ctx)
if err != nil {
return xerrors.Errorf("cleanup tests: %w", err)
}
// Upload traces.
if shouldTrace {
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "\nUploading traces...")
closeTracingOnce.Do(func() {
ctx, cancel := context.WithTimeout(ctx, 1*time.Minute)
defer cancel()
err := closeTracing(ctx)
if err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "\nError uploading traces: %+v\n", err)
}
})
}
if res.TotalFail > 0 {
return xerrors.New("load test failed, see above for more details")
}
return nil
},
}
cliflag.StringVarP(cmd.Flags(), &configPath, "config", "", "CODER_LOADTEST_CONFIG_PATH", "", "Path to the load test configuration file, or - to read from stdin.")
cliflag.StringArrayVarP(cmd.Flags(), &outputSpecs, "output", "", "CODER_LOADTEST_OUTPUTS", []string{"text"}, "Output formats, see usage for more information.")
cliflag.BoolVarP(cmd.Flags(), &traceEnable, "trace", "", "CODER_LOADTEST_TRACE", false, "Whether application tracing data is collected. It exports to a backend configured by environment variables. See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md")
cliflag.BoolVarP(cmd.Flags(), &traceCoder, "trace-coder", "", "CODER_LOADTEST_TRACE_CODER", false, "Whether opentelemetry traces are sent to Coder. We recommend keeping this disabled unless we advise you to enable it.")
cliflag.StringVarP(cmd.Flags(), &traceHoneycombAPIKey, "trace-honeycomb-api-key", "", "CODER_LOADTEST_TRACE_HONEYCOMB_API_KEY", "", "Enables trace exporting to Honeycomb.io using the provided API key.")
cliflag.BoolVarP(cmd.Flags(), &tracePropagate, "trace-propagate", "", "CODER_LOADTEST_TRACE_PROPAGATE", false, "Enables trace propagation to the Coder backend, which will be used to correlate server-side spans with client-side spans. Only enable this if the server is configured with the exact same tracing configuration as the client.")
return cmd
}
func loadLoadTestConfigFile(configPath string, stdin io.Reader) (LoadTestConfig, error) {
if configPath == "" {
return LoadTestConfig{}, xerrors.New("config is required")
}
var (
configReader io.ReadCloser
)
if configPath == "-" {
configReader = io.NopCloser(stdin)
} else {
f, err := os.Open(configPath)
if err != nil {
return LoadTestConfig{}, xerrors.Errorf("open config file %q: %w", configPath, err)
}
configReader = f
}
var config LoadTestConfig
err := json.NewDecoder(configReader).Decode(&config)
_ = configReader.Close()
if err != nil {
return LoadTestConfig{}, xerrors.Errorf("read config file %q: %w", configPath, err)
}
err = config.Validate()
if err != nil {
return LoadTestConfig{}, xerrors.Errorf("validate config: %w", err)
}
return config, nil
}
type loadTestOutputFormat string
const (
loadTestOutputFormatText loadTestOutputFormat = "text"
loadTestOutputFormatJSON loadTestOutputFormat = "json"
// TODO: html format
)
type loadTestOutput struct {
format loadTestOutputFormat
// Up to one path (the first path) will have the value "-" which signifies
// stdout.
path string
}
func parseLoadTestOutputs(outputs []string) ([]loadTestOutput, error) {
var stdoutFormat loadTestOutputFormat
validFormats := map[loadTestOutputFormat]struct{}{
loadTestOutputFormatText: {},
loadTestOutputFormatJSON: {},
}
var out []loadTestOutput
for i, o := range outputs {
parts := strings.SplitN(o, ":", 2)
format := loadTestOutputFormat(parts[0])
if _, ok := validFormats[format]; !ok {
return nil, xerrors.Errorf("invalid output format %q in output flag %d", parts[0], i)
}
if len(parts) == 1 {
if stdoutFormat != "" {
return nil, xerrors.Errorf("multiple output flags specified for stdout")
}
stdoutFormat = format
continue
}
if len(parts) != 2 {
return nil, xerrors.Errorf("invalid output flag %d: %q", i, o)
}
out = append(out, loadTestOutput{
format: format,
path: parts[1],
})
}
// Default to --output text
if stdoutFormat == "" && len(out) == 0 {
stdoutFormat = loadTestOutputFormatText
}
if stdoutFormat != "" {
out = append([]loadTestOutput{{
format: stdoutFormat,
path: "-",
}}, out...)
}
return out, nil
}
type runnableTraceWrapper struct {
tracer trace.Tracer
spanName string
runner harness.Runnable
span trace.Span
}
var _ harness.Runnable = &runnableTraceWrapper{}
var _ harness.Cleanable = &runnableTraceWrapper{}
func (r *runnableTraceWrapper) Run(ctx context.Context, id string, logs io.Writer) error {
ctx, span := r.tracer.Start(ctx, r.spanName, trace.WithNewRoot())
defer span.End()
r.span = span
traceID := "unknown trace ID"
spanID := "unknown span ID"
if span.SpanContext().HasTraceID() {
traceID = span.SpanContext().TraceID().String()
}
if span.SpanContext().HasSpanID() {
spanID = span.SpanContext().SpanID().String()
}
_, _ = fmt.Fprintf(logs, "Trace ID: %s\n", traceID)
_, _ = fmt.Fprintf(logs, "Span ID: %s\n\n", spanID)
// Make a separate span for the run itself so the sub-spans are grouped
// neatly. The cleanup span is also a child of the above span so this is
// important for readability.
ctx2, span2 := r.tracer.Start(ctx, r.spanName+" run")
defer span2.End()
return r.runner.Run(ctx2, id, logs)
}
func (r *runnableTraceWrapper) Cleanup(ctx context.Context, id string) error {
c, ok := r.runner.(harness.Cleanable)
if !ok {
return nil
}
if r.span != nil {
ctx = trace.ContextWithSpanContext(ctx, r.span.SpanContext())
}
ctx, span := r.tracer.Start(ctx, r.spanName+" cleanup")
defer span.End()
return c.Cleanup(ctx, id)
}
-309
View File
@@ -1,309 +0,0 @@
package cli_test
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli"
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/loadtest/harness"
"github.com/coder/coder/loadtest/placebo"
"github.com/coder/coder/loadtest/workspacebuild"
"github.com/coder/coder/pty/ptytest"
"github.com/coder/coder/testutil"
)
func TestLoadTest(t *testing.T) {
t.Skipf("This test is flakey. See https://github.com/coder/coder/issues/4942")
t.Parallel()
t.Run("PlaceboFromStdin", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
config := cli.LoadTestConfig{
Strategy: cli.LoadTestStrategy{
Type: cli.LoadTestStrategyTypeLinear,
},
CleanupStrategy: cli.LoadTestStrategy{
Type: cli.LoadTestStrategyTypeLinear,
},
Tests: []cli.LoadTest{
{
Type: cli.LoadTestTypePlacebo,
Count: 10,
Placebo: &placebo.Config{
Sleep: httpapi.Duration(10 * time.Millisecond),
},
},
},
Timeout: httpapi.Duration(testutil.WaitShort),
}
configBytes, err := json.Marshal(config)
require.NoError(t, err)
cmd, root := clitest.New(t, "loadtest", "--config", "-")
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
cmd.SetIn(bytes.NewReader(configBytes))
cmd.SetOut(pty.Output())
cmd.SetErr(pty.Output())
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancelFunc()
done := make(chan any)
go func() {
errC := cmd.ExecuteContext(ctx)
assert.NoError(t, errC)
close(done)
}()
pty.ExpectMatch("Test results:")
pty.ExpectMatch("Pass: 10")
cancelFunc()
<-done
})
t.Run("WorkspaceBuildFromFile", 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)
config := cli.LoadTestConfig{
Strategy: cli.LoadTestStrategy{
Type: cli.LoadTestStrategyTypeConcurrent,
ConcurrencyLimit: 2,
},
CleanupStrategy: cli.LoadTestStrategy{
Type: cli.LoadTestStrategyTypeConcurrent,
ConcurrencyLimit: 2,
},
Tests: []cli.LoadTest{
{
Type: cli.LoadTestTypeWorkspaceBuild,
Count: 2,
WorkspaceBuild: &workspacebuild.Config{
OrganizationID: user.OrganizationID,
UserID: user.UserID.String(),
Request: codersdk.CreateWorkspaceRequest{
TemplateID: template.ID,
},
},
},
},
Timeout: httpapi.Duration(testutil.WaitLong),
}
d := t.TempDir()
configPath := filepath.Join(d, "/config.loadtest.json")
f, err := os.Create(configPath)
require.NoError(t, err)
defer f.Close()
err = json.NewEncoder(f).Encode(config)
require.NoError(t, err)
_ = f.Close()
cmd, root := clitest.New(t, "loadtest", "--config", configPath)
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
cmd.SetErr(pty.Output())
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancelFunc()
done := make(chan any)
go func() {
errC := cmd.ExecuteContext(ctx)
assert.NoError(t, errC)
close(done)
}()
pty.ExpectMatch("Test results:")
pty.ExpectMatch("Pass: 2")
<-done
cancelFunc()
})
t.Run("OutputFormats", func(t *testing.T) {
t.Parallel()
t.Skip("This test is flakey. See: https://github.com/coder/coder/actions/runs/3415360091/jobs/5684401383")
type outputFlag struct {
format string
path string
}
dir := t.TempDir()
cases := []struct {
name string
outputs []outputFlag
errContains string
}{
{
name: "Default",
outputs: []outputFlag{},
},
{
name: "ExplicitText",
outputs: []outputFlag{{format: "text"}},
},
{
name: "JSON",
outputs: []outputFlag{
{
format: "json",
path: filepath.Join(dir, "results.json"),
},
},
},
{
name: "TextAndJSON",
outputs: []outputFlag{
{
format: "text",
},
{
format: "json",
path: filepath.Join(dir, "results.json"),
},
},
},
{
name: "TextAndJSON2",
outputs: []outputFlag{
{
format: "text",
},
{
format: "text",
path: filepath.Join(dir, "results.txt"),
},
{
format: "json",
path: filepath.Join(dir, "results.json"),
},
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
config := cli.LoadTestConfig{
Strategy: cli.LoadTestStrategy{
Type: cli.LoadTestStrategyTypeLinear,
},
CleanupStrategy: cli.LoadTestStrategy{
Type: cli.LoadTestStrategyTypeLinear,
},
Tests: []cli.LoadTest{
{
Type: cli.LoadTestTypePlacebo,
Count: 10,
Placebo: &placebo.Config{
Sleep: httpapi.Duration(10 * time.Millisecond),
},
},
},
Timeout: httpapi.Duration(testutil.WaitShort),
}
configBytes, err := json.Marshal(config)
require.NoError(t, err)
args := []string{"loadtest", "--config", "-"}
for _, output := range c.outputs {
flag := output.format
if output.path != "" {
flag += ":" + output.path
}
args = append(args, "--output", flag)
}
cmd, root := clitest.New(t, args...)
clitest.SetupConfig(t, client, root)
cmd.SetIn(bytes.NewReader(configBytes))
out := bytes.NewBuffer(nil)
cmd.SetOut(out)
pty := ptytest.New(t)
cmd.SetErr(pty.Output())
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancelFunc()
done := make(chan any)
go func() {
errC := cmd.ExecuteContext(ctx)
if c.errContains != "" {
assert.Error(t, errC)
assert.Contains(t, errC.Error(), c.errContains)
} else {
assert.NoError(t, errC)
}
close(done)
}()
<-done
if c.errContains != "" {
return
}
if len(c.outputs) == 0 {
// This is the default output format when no flags are
// specified.
c.outputs = []outputFlag{{format: "text"}}
}
for i, output := range c.outputs {
msg := fmt.Sprintf("flag %d", i)
var b []byte
if output.path == "" {
b = out.Bytes()
} else {
b, err = os.ReadFile(output.path)
require.NoError(t, err, msg)
}
t.Logf("output %d:\n\n%s", i, string(b))
switch output.format {
case "text":
require.Contains(t, string(b), "Test results:", msg)
require.Contains(t, string(b), "Pass: 10", msg)
case "json":
var res harness.Results
err = json.Unmarshal(b, &res)
require.NoError(t, err, msg)
require.Equal(t, 10, res.TotalRuns, msg)
require.Equal(t, 10, res.TotalPass, msg)
require.Len(t, res.Runs, 10, msg)
}
}
})
}
})
}
-220
View File
@@ -1,220 +0,0 @@
package cli
import (
"time"
"golang.org/x/xerrors"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/loadtest/agentconn"
"github.com/coder/coder/loadtest/harness"
"github.com/coder/coder/loadtest/placebo"
"github.com/coder/coder/loadtest/reconnectingpty"
"github.com/coder/coder/loadtest/workspacebuild"
)
// LoadTestConfig is the overall configuration for a call to `coder loadtest`.
type LoadTestConfig struct {
Strategy LoadTestStrategy `json:"strategy"`
CleanupStrategy LoadTestStrategy `json:"cleanup_strategy"`
Tests []LoadTest `json:"tests"`
// Timeout sets a timeout for the entire test run, to control the timeout
// for each individual run use strategy.timeout.
Timeout httpapi.Duration `json:"timeout"`
}
type LoadTestStrategyType string
const (
LoadTestStrategyTypeLinear LoadTestStrategyType = "linear"
LoadTestStrategyTypeConcurrent LoadTestStrategyType = "concurrent"
)
type LoadTestStrategy struct {
// Type is the type of load test strategy to use. Strategies determine how
// to run tests concurrently.
Type LoadTestStrategyType `json:"type"`
// ConcurrencyLimit is the maximum number of concurrent runs. This only
// applies if type == "concurrent". Negative values disable the concurrency
// limit and attempts to perform all runs concurrently. The default value is
// 100.
ConcurrencyLimit int `json:"concurrency_limit"`
// Shuffle determines whether or not to shuffle the test runs before
// executing them.
Shuffle bool `json:"shuffle"`
// Timeout is the maximum amount of time to run each test for. This is
// independent of the timeout specified in the test run. A timeout of 0
// disables the timeout.
Timeout httpapi.Duration `json:"timeout"`
}
func (s LoadTestStrategy) ExecutionStrategy() harness.ExecutionStrategy {
var strategy harness.ExecutionStrategy
switch s.Type {
case LoadTestStrategyTypeLinear:
strategy = harness.LinearExecutionStrategy{}
case LoadTestStrategyTypeConcurrent:
limit := s.ConcurrencyLimit
if limit < 0 {
return harness.ConcurrentExecutionStrategy{}
}
if limit == 0 {
limit = 100
}
strategy = harness.ParallelExecutionStrategy{
Limit: limit,
}
default:
panic("unreachable, unknown strategy type " + s.Type)
}
if s.Timeout > 0 {
strategy = harness.TimeoutExecutionStrategyWrapper{
Timeout: time.Duration(s.Timeout),
Inner: strategy,
}
}
if s.Shuffle {
strategy = harness.ShuffleExecutionStrategyWrapper{
Inner: strategy,
}
}
return strategy
}
type LoadTestType string
const (
LoadTestTypeAgentConn LoadTestType = "agentconn"
LoadTestTypePlacebo LoadTestType = "placebo"
LoadTestTypeReconnectingPTY LoadTestType = "reconnectingpty"
LoadTestTypeWorkspaceBuild LoadTestType = "workspacebuild"
)
type LoadTest struct {
// Type is the type of load test to run.
Type LoadTestType `json:"type"`
// Count is the number of test runs to execute with this configuration. If
// the count is 0 or negative, defaults to 1.
Count int `json:"count"`
// AgentConn must be set if type == "agentconn".
AgentConn *agentconn.Config `json:"agentconn,omitempty"`
// Placebo must be set if type == "placebo".
Placebo *placebo.Config `json:"placebo,omitempty"`
// ReconnectingPTY must be set if type == "reconnectingpty".
ReconnectingPTY *reconnectingpty.Config `json:"reconnectingpty,omitempty"`
// WorkspaceBuild must be set if type == "workspacebuild".
WorkspaceBuild *workspacebuild.Config `json:"workspacebuild,omitempty"`
}
func (t LoadTest) NewRunner(client *codersdk.Client) (harness.Runnable, error) {
switch t.Type {
case LoadTestTypeAgentConn:
if t.AgentConn == nil {
return nil, xerrors.New("agentconn config must be set")
}
return agentconn.NewRunner(client, *t.AgentConn), nil
case LoadTestTypePlacebo:
if t.Placebo == nil {
return nil, xerrors.New("placebo config must be set")
}
return placebo.NewRunner(*t.Placebo), nil
case LoadTestTypeReconnectingPTY:
if t.ReconnectingPTY == nil {
return nil, xerrors.New("reconnectingpty config must be set")
}
return reconnectingpty.NewRunner(client, *t.ReconnectingPTY), nil
case LoadTestTypeWorkspaceBuild:
if t.WorkspaceBuild == nil {
return nil, xerrors.Errorf("workspacebuild config must be set")
}
return workspacebuild.NewRunner(client, *t.WorkspaceBuild), nil
default:
return nil, xerrors.Errorf("unknown test type %q", t.Type)
}
}
func (c *LoadTestConfig) Validate() error {
err := c.Strategy.Validate()
if err != nil {
return xerrors.Errorf("validate strategy: %w", err)
}
err = c.CleanupStrategy.Validate()
if err != nil {
return xerrors.Errorf("validate cleanup_strategy: %w", err)
}
for i, test := range c.Tests {
err := test.Validate()
if err != nil {
return xerrors.Errorf("validate test %d: %w", i, err)
}
}
return nil
}
func (s *LoadTestStrategy) Validate() error {
switch s.Type {
case LoadTestStrategyTypeLinear:
case LoadTestStrategyTypeConcurrent:
default:
return xerrors.Errorf("invalid load test strategy type: %q", s.Type)
}
if s.Timeout < 0 {
return xerrors.Errorf("invalid load test strategy timeout: %q", s.Timeout)
}
return nil
}
func (t *LoadTest) Validate() error {
switch t.Type {
case LoadTestTypeAgentConn:
if t.AgentConn == nil {
return xerrors.Errorf("agentconn test type must specify agentconn")
}
err := t.AgentConn.Validate()
if err != nil {
return xerrors.Errorf("validate agentconn: %w", err)
}
case LoadTestTypePlacebo:
if t.Placebo == nil {
return xerrors.Errorf("placebo test type must specify placebo")
}
err := t.Placebo.Validate()
if err != nil {
return xerrors.Errorf("validate placebo: %w", err)
}
case LoadTestTypeReconnectingPTY:
if t.ReconnectingPTY == nil {
return xerrors.Errorf("reconnectingpty test type must specify reconnectingpty")
}
err := t.ReconnectingPTY.Validate()
if err != nil {
return xerrors.Errorf("validate reconnectingpty: %w", err)
}
case LoadTestTypeWorkspaceBuild:
if t.WorkspaceBuild == nil {
return xerrors.New("workspacebuild test type must specify workspacebuild")
}
err := t.WorkspaceBuild.Validate()
if err != nil {
return xerrors.Errorf("validate workspacebuild: %w", err)
}
default:
return xerrors.Errorf("invalid load test type: %q", t.Type)
}
return nil
}
+11 -2
View File
@@ -49,9 +49,18 @@ func login() *cobra.Command {
cmd := &cobra.Command{
Use: "login <url>",
Short: "Authenticate with Coder deployment",
Args: cobra.ExactArgs(1),
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
rawURL := args[0]
rawURL := ""
if len(args) == 0 {
var err error
rawURL, err = cmd.Flags().GetString(varURL)
if err != nil {
return xerrors.Errorf("get global url flag")
}
} else {
rawURL = args[0]
}
if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") {
scheme := "https"
+33 -1
View File
@@ -68,13 +68,45 @@ func TestLogin(t *testing.T) {
<-doneChan
})
t.Run("InitialUserFlags", func(t *testing.T) {
t.Run("InitialUserTTYFlag", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
// The --force-tty flag is required on Windows, because the `isatty` library does not
// accurately detect Windows ptys when they are not attached to a process:
// https://github.com/mattn/go-isatty/issues/59
doneChan := make(chan struct{})
root, _ := clitest.New(t, "--url", client.URL.String(), "login", "--force-tty")
pty := ptytest.New(t)
root.SetIn(pty.Input())
root.SetOut(pty.Output())
go func() {
defer close(doneChan)
err := root.Execute()
assert.NoError(t, err)
}()
matches := []string{
"first user?", "yes",
"username", "testuser",
"email", "user@coder.com",
"password", "password",
"password", "password", // Confirm.
"trial", "yes",
}
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
pty.WriteLine(value)
}
pty.ExpectMatch("Welcome to Coder")
<-doneChan
})
t.Run("InitialUserFlags", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
doneChan := make(chan struct{})
root, _ := clitest.New(t, "login", client.URL.String(), "--first-user-username", "testuser", "--first-user-email", "user@coder.com", "--first-user-password", "password", "--first-user-trial")
pty := ptytest.New(t)
root.SetIn(pty.Input())
+7 -4
View File
@@ -36,18 +36,21 @@ func createParameterMapFromFile(parameterFile string) (map[string]string, error)
return nil, xerrors.Errorf("Parameter file name is not specified")
}
// Returns a parameter value from a given map, if the map exists, else takes input from the user.
// Throws an error if the map exists but does not include a value for the parameter.
// Returns a parameter value from a given map, if the map does not exist or does not contain the item, it takes input from the user.
// Throws an error if there are any errors with the users input.
func getParameterValueFromMapOrInput(cmd *cobra.Command, parameterMap map[string]string, parameterSchema codersdk.ParameterSchema) (string, error) {
var parameterValue string
var err error
if parameterMap != nil {
var ok bool
parameterValue, ok = parameterMap[parameterSchema.Name]
if !ok {
return "", xerrors.Errorf("Parameter value absent in parameter file for %q!", parameterSchema.Name)
parameterValue, err = cliui.ParameterSchema(cmd, parameterSchema)
if err != nil {
return "", err
}
}
} else {
var err error
parameterValue, err = cliui.ParameterSchema(cmd, parameterSchema)
if err != nil {
return "", err
+2 -5
View File
@@ -16,10 +16,6 @@ func rename() *cobra.Command {
Use: "rename <workspace> <new name>",
Short: "Rename a workspace",
Args: cobra.ExactArgs(2),
// Keep hidden until renaming is safe, see:
// * https://github.com/coder/coder/issues/3000
// * https://github.com/coder/coder/issues/3386
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
client, err := CreateClient(cmd)
if err != nil {
@@ -31,8 +27,9 @@ func rename() *cobra.Command {
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s\n\n",
cliui.Styles.Wrap.Render("WARNING: A rename can result in data loss if a resource references the workspace name in the template (e.g volumes)."),
cliui.Styles.Wrap.Render("WARNING: A rename can result in data loss if a resource references the workspace name in the template (e.g volumes). Please backup any data before proceeding."),
)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "See: %s\n\n", "https://coder.com/docs/coder-oss/latest/templates/resource-persistence#%EF%B8%8F-persistence-pitfalls")
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: fmt.Sprintf("Type %q to confirm rename:", workspace.Name),
Validate: func(s string) error {
+3 -1
View File
@@ -27,7 +27,9 @@ func TestRename(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
want := workspace.Name + "-test"
// Only append one letter because it's easy to exceed maximum length:
// E.g. "compassionate-chandrasekhar82" + "t".
want := workspace.Name + "t"
cmd, root := clitest.New(t, "rename", workspace.Name, want, "--yes")
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
+1 -1
View File
@@ -40,7 +40,7 @@ func TestResetPassword(t *testing.T) {
serverDone := make(chan struct{})
serverCmd, cfg := clitest.New(t,
"server",
"--address", ":0",
"--http-address", ":0",
"--access-url", "http://example.com",
"--postgres-url", connectionURL,
"--cache-dir", t.TempDir(),
+103 -3
View File
@@ -8,7 +8,11 @@ import (
"net/http"
"net/url"
"os"
"os/signal"
"path/filepath"
"runtime"
"strings"
"syscall"
"text/template"
"time"
@@ -76,7 +80,6 @@ func Core() []*cobra.Command {
dotfiles(),
gitssh(),
list(),
loadtest(),
login(),
logout(),
parameters(),
@@ -84,6 +87,7 @@ func Core() []*cobra.Command {
publickey(),
rename(),
resetPassword(),
scaletest(),
schedules(),
show(),
speedtest(),
@@ -96,6 +100,7 @@ func Core() []*cobra.Command {
update(),
users(),
versionCmd(),
vscodeSSH(),
workspaceAgent(),
}
}
@@ -583,12 +588,17 @@ func checkVersions(cmd *cobra.Command, client *codersdk.Client) error {
}
fmtWarningText := `version mismatch: client %s, server %s
download the server version with: 'curl -L https://coder.com/install.sh | sh -s -- --version %s'
`
// Our installation script doesn't work on Windows, so instead we direct the user
// to the GitHub release page to download the latest installer.
if runtime.GOOS == "windows" {
fmtWarningText += `download the server version from: https://github.com/coder/coder/releases/v%s`
} else {
fmtWarningText += `download the server version with: 'curl -L https://coder.com/install.sh | sh -s -- --version %s'`
}
if !buildinfo.VersionsMatch(clientVersion, info.Version) {
warn := cliui.Styles.Warn.Copy().Align(lipgloss.Left)
// Trim the leading 'v', our install.sh script does not handle this case well.
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), warn.Render(fmtWarningText), clientVersion, info.Version, strings.TrimPrefix(info.CanonicalVersion(), "v"))
_, _ = fmt.Fprintln(cmd.ErrOrStderr())
}
@@ -624,3 +634,93 @@ func (h *headerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
}
return h.transport.RoundTrip(req)
}
// dumpHandler provides a custom SIGQUIT and SIGTRAP handler that dumps the
// stacktrace of all goroutines to stderr and a well-known file in the home
// directory. This is useful for debugging deadlock issues that may occur in
// production in workspaces, since the default Go runtime will only dump to
// stderr (which is often difficult/impossible to read in a workspace).
//
// SIGQUITs will still cause the program to exit (similarly to the default Go
// runtime behavior).
//
// A SIGQUIT handler will not be registered if GOTRACEBACK=crash.
//
// On Windows this immediately returns.
func dumpHandler(ctx context.Context) {
if runtime.GOOS == "windows" {
// free up the goroutine since it'll be permanently blocked anyways
return
}
listenSignals := []os.Signal{syscall.SIGTRAP}
if os.Getenv("GOTRACEBACK") != "crash" {
listenSignals = append(listenSignals, syscall.SIGQUIT)
}
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, listenSignals...)
defer signal.Stop(sigs)
for {
sigStr := ""
select {
case <-ctx.Done():
return
case sig := <-sigs:
switch sig {
case syscall.SIGQUIT:
sigStr = "SIGQUIT"
case syscall.SIGTRAP:
sigStr = "SIGTRAP"
}
}
// Start with a 1MB buffer and keep doubling it until we can fit the
// entire stacktrace, stopping early once we reach 64MB.
buf := make([]byte, 1_000_000)
stacklen := 0
for {
stacklen = runtime.Stack(buf, true)
if stacklen < len(buf) {
break
}
if 2*len(buf) > 64_000_000 {
// Write a message to the end of the buffer saying that it was
// truncated.
const truncatedMsg = "\n\n\nstack trace truncated due to size\n"
copy(buf[len(buf)-len(truncatedMsg):], truncatedMsg)
break
}
buf = make([]byte, 2*len(buf))
}
_, _ = fmt.Fprintf(os.Stderr, "%s:\n%s\n", sigStr, buf[:stacklen])
// Write to a well-known file.
dir, err := os.UserHomeDir()
if err != nil {
dir = os.TempDir()
}
fpath := filepath.Join(dir, fmt.Sprintf("coder-agent-%s.dump", time.Now().Format("2006-01-02T15:04:05.000Z")))
_, _ = fmt.Fprintf(os.Stderr, "writing dump to %q\n", fpath)
f, err := os.Create(fpath)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "failed to open dump file: %v\n", err.Error())
goto done
}
_, err = f.Write(buf[:stacklen])
_ = f.Close()
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "failed to write dump file: %v\n", err.Error())
goto done
}
done:
if sigStr == "SIGQUIT" {
//nolint:revive
os.Exit(1)
}
}
}
+55 -8
View File
@@ -3,10 +3,12 @@ package cli_test
import (
"bytes"
"flag"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
@@ -30,40 +32,68 @@ var updateGoldenFiles = flag.Bool("update", false, "update .golden files")
func TestCommandHelp(t *testing.T) {
t.Parallel()
tests := []struct {
commonEnv := map[string]string{
"CODER_CONFIG_DIR": "/tmp/coder-cli-test-config",
}
type testCase struct {
name string
cmd []string
env map[string]string
}{
}
tests := []testCase{
{
name: "coder --help",
cmd: []string{"--help"},
env: map[string]string{
"CODER_CONFIG_DIR": "/tmp/coder-cli-test-config",
},
},
{
name: "coder server --help",
cmd: []string{"server", "--help"},
env: map[string]string{
"CODER_CONFIG_DIR": "/tmp/coder-cli-test-config",
"CODER_CACHE_DIRECTORY": "/tmp/coder-cli-test-cache",
},
},
}
root := cli.Root(cli.AGPL())
ExtractCommandPathsLoop:
for _, cp := range extractVisibleCommandPaths(nil, root.Commands()) {
name := fmt.Sprintf("coder %s --help", strings.Join(cp, " "))
cmd := append(cp, "--help")
for _, tt := range tests {
if tt.name == name {
continue ExtractCommandPathsLoop
}
}
tests = append(tests, testCase{name: name, cmd: cmd})
}
wd, err := os.Getwd()
require.NoError(t, err)
if runtime.GOOS == "windows" {
wd = strings.ReplaceAll(wd, "\\", "\\\\")
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
env := make(map[string]string)
for k, v := range commonEnv {
env[k] = v
}
for k, v := range tt.env {
env[k] = v
}
// Unset all CODER_ environment variables for a clean slate.
for _, kv := range os.Environ() {
name := strings.Split(kv, "=")[0]
if _, ok := tt.env[name]; !ok && strings.HasPrefix(name, "CODER_") {
if _, ok := env[name]; !ok && strings.HasPrefix(name, "CODER_") {
t.Setenv(name, "")
}
}
// Override environment variables for a reproducible test.
for k, v := range tt.env {
for k, v := range env {
t.Setenv(k, v)
}
@@ -79,6 +109,10 @@ func TestCommandHelp(t *testing.T) {
// Remove CRLF newlines (Windows).
got = bytes.ReplaceAll(got, []byte{'\r', '\n'}, []byte{'\n'})
// The `coder templates create --help` command prints the path
// to the working directory (--directory flag default value).
got = bytes.ReplaceAll(got, []byte(wd), []byte("/tmp/coder-cli-test-workdir"))
gf := filepath.Join("testdata", strings.Replace(tt.name, " ", "_", -1)+".golden")
if *updateGoldenFiles {
t.Logf("update golden file for: %q: %s", tt.name, gf)
@@ -95,6 +129,19 @@ func TestCommandHelp(t *testing.T) {
}
}
func extractVisibleCommandPaths(cmdPath []string, cmds []*cobra.Command) [][]string {
var cmdPaths [][]string
for _, c := range cmds {
if c.Hidden {
continue
}
cmdPath := append(cmdPath, c.Name())
cmdPaths = append(cmdPaths, cmdPath)
cmdPaths = append(cmdPaths, extractVisibleCommandPaths(cmdPath, c.Commands())...)
}
return cmdPaths
}
func TestRoot(t *testing.T) {
t.Parallel()
t.Run("FormatCobraError", func(t *testing.T) {
+866
View File
@@ -0,0 +1,866 @@
package cli
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/google/uuid"
"github.com/spf13/cobra"
"go.opentelemetry.io/otel/trace"
"golang.org/x/xerrors"
"github.com/coder/coder/cli/cliflag"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/tracing"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/cryptorand"
"github.com/coder/coder/scaletest/agentconn"
"github.com/coder/coder/scaletest/createworkspaces"
"github.com/coder/coder/scaletest/harness"
"github.com/coder/coder/scaletest/reconnectingpty"
"github.com/coder/coder/scaletest/workspacebuild"
)
const scaletestTracerName = "coder_scaletest"
func scaletest() *cobra.Command {
cmd := &cobra.Command{
Use: "scaletest",
Short: "Run a scale test against the Coder API",
Long: "Perform scale tests against the Coder server.",
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
cmd.AddCommand(
scaletestCleanup(),
scaletestCreateWorkspaces(),
)
return cmd
}
type scaletestTracingFlags struct {
traceEnable bool
traceCoder bool
traceHoneycombAPIKey string
tracePropagate bool
}
func (s *scaletestTracingFlags) attach(cmd *cobra.Command) {
cliflag.BoolVarP(cmd.Flags(), &s.traceEnable, "trace", "", "CODER_LOADTEST_TRACE", false, "Whether application tracing data is collected. It exports to a backend configured by environment variables. See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md")
cliflag.BoolVarP(cmd.Flags(), &s.traceCoder, "trace-coder", "", "CODER_LOADTEST_TRACE_CODER", false, "Whether opentelemetry traces are sent to Coder. We recommend keeping this disabled unless we advise you to enable it.")
cliflag.StringVarP(cmd.Flags(), &s.traceHoneycombAPIKey, "trace-honeycomb-api-key", "", "CODER_LOADTEST_TRACE_HONEYCOMB_API_KEY", "", "Enables trace exporting to Honeycomb.io using the provided API key.")
cliflag.BoolVarP(cmd.Flags(), &s.tracePropagate, "trace-propagate", "", "CODER_LOADTEST_TRACE_PROPAGATE", false, "Enables trace propagation to the Coder backend, which will be used to correlate server-side spans with client-side spans. Only enable this if the server is configured with the exact same tracing configuration as the client.")
}
// provider returns a trace.TracerProvider, a close function and a bool showing
// whether tracing is enabled or not.
func (s *scaletestTracingFlags) provider(ctx context.Context) (trace.TracerProvider, func(context.Context) error, bool, error) {
shouldTrace := s.traceEnable || s.traceCoder || s.traceHoneycombAPIKey != ""
if !shouldTrace {
tracerProvider := trace.NewNoopTracerProvider()
return tracerProvider, func(_ context.Context) error { return nil }, false, nil
}
tracerProvider, closeTracing, err := tracing.TracerProvider(ctx, scaletestTracerName, tracing.TracerOpts{
Default: s.traceEnable,
Coder: s.traceCoder,
Honeycomb: s.traceHoneycombAPIKey,
})
if err != nil {
return nil, nil, false, xerrors.Errorf("initialize tracing: %w", err)
}
var closeTracingOnce sync.Once
return tracerProvider, func(ctx context.Context) error {
var err error
closeTracingOnce.Do(func() {
err = closeTracing(ctx)
})
return err
}, true, nil
}
type scaletestStrategyFlags struct {
cleanup bool
concurrency int
timeout time.Duration
timeoutPerJob time.Duration
}
func (s *scaletestStrategyFlags) attach(cmd *cobra.Command) {
concurrencyLong, concurrencyEnv, concurrencyDescription := "concurrency", "CODER_LOADTEST_CONCURRENCY", "Number of concurrent jobs to run. 0 means unlimited."
timeoutLong, timeoutEnv, timeoutDescription := "timeout", "CODER_LOADTEST_TIMEOUT", "Timeout for the entire test run. 0 means unlimited."
jobTimeoutLong, jobTimeoutEnv, jobTimeoutDescription := "job-timeout", "CODER_LOADTEST_JOB_TIMEOUT", "Timeout per job. Jobs may take longer to complete under higher concurrency limits."
if s.cleanup {
concurrencyLong, concurrencyEnv, concurrencyDescription = "cleanup-"+concurrencyLong, "CODER_LOADTEST_CLEANUP_CONCURRENCY", strings.ReplaceAll(concurrencyDescription, "jobs", "cleanup jobs")
timeoutLong, timeoutEnv, timeoutDescription = "cleanup-"+timeoutLong, "CODER_LOADTEST_CLEANUP_TIMEOUT", strings.ReplaceAll(timeoutDescription, "test", "cleanup")
jobTimeoutLong, jobTimeoutEnv, jobTimeoutDescription = "cleanup-"+jobTimeoutLong, "CODER_LOADTEST_CLEANUP_JOB_TIMEOUT", strings.ReplaceAll(jobTimeoutDescription, "jobs", "cleanup jobs")
}
cliflag.IntVarP(cmd.Flags(), &s.concurrency, concurrencyLong, "", concurrencyEnv, 1, concurrencyDescription)
cliflag.DurationVarP(cmd.Flags(), &s.timeout, timeoutLong, "", timeoutEnv, 30*time.Minute, timeoutDescription)
cliflag.DurationVarP(cmd.Flags(), &s.timeoutPerJob, jobTimeoutLong, "", jobTimeoutEnv, 5*time.Minute, jobTimeoutDescription)
}
func (s *scaletestStrategyFlags) toStrategy() harness.ExecutionStrategy {
var strategy harness.ExecutionStrategy
if s.concurrency == 1 {
strategy = harness.LinearExecutionStrategy{}
} else if s.concurrency == 0 {
strategy = harness.ConcurrentExecutionStrategy{}
} else {
strategy = harness.ParallelExecutionStrategy{
Limit: s.concurrency,
}
}
if s.timeoutPerJob > 0 {
strategy = harness.TimeoutExecutionStrategyWrapper{
Timeout: s.timeoutPerJob,
Inner: strategy,
}
}
return strategy
}
func (s *scaletestStrategyFlags) toContext(ctx context.Context) (context.Context, context.CancelFunc) {
if s.timeout > 0 {
return context.WithTimeout(ctx, s.timeout)
}
return context.WithCancel(ctx)
}
type scaleTestOutputFormat string
const (
scaleTestOutputFormatText scaleTestOutputFormat = "text"
scaleTestOutputFormatJSON scaleTestOutputFormat = "json"
// TODO: html format
)
type scaleTestOutput struct {
format scaleTestOutputFormat
// Zero or one (the first) path will have the path set to "-" to indicate
// stdout.
path string
}
func (o *scaleTestOutput) write(res harness.Results, stdout io.Writer) error {
var (
w = stdout
c io.Closer
)
if o.path != "-" {
f, err := os.Create(o.path)
if err != nil {
return xerrors.Errorf("create output file: %w", err)
}
w, c = f, f
}
switch o.format {
case scaleTestOutputFormatText:
res.PrintText(w)
case scaleTestOutputFormatJSON:
err := json.NewEncoder(w).Encode(res)
if err != nil {
return xerrors.Errorf("encode JSON: %w", err)
}
}
// Sync the file to disk if it's a file.
if s, ok := w.(interface{ Sync() error }); ok {
err := s.Sync()
// On Linux, EINVAL is returned when calling fsync on /dev/stdout. We
// can safely ignore this error.
if err != nil && !xerrors.Is(err, syscall.EINVAL) {
return xerrors.Errorf("flush output file: %w", err)
}
}
if c != nil {
err := c.Close()
if err != nil {
return xerrors.Errorf("close output file: %w", err)
}
}
return nil
}
type scaletestOutputFlags struct {
outputSpecs []string
}
func (s *scaletestOutputFlags) attach(cmd *cobra.Command) {
cliflag.StringArrayVarP(cmd.Flags(), &s.outputSpecs, "output", "", "CODER_SCALETEST_OUTPUTS", []string{"text"}, `Output format specs in the format "<format>[:<path>]". Not specifying a path will default to stdout. Available formats: text, json.`)
}
func (s *scaletestOutputFlags) parse() ([]scaleTestOutput, error) {
var stdoutFormat scaleTestOutputFormat
validFormats := map[scaleTestOutputFormat]struct{}{
scaleTestOutputFormatText: {},
scaleTestOutputFormatJSON: {},
}
var out []scaleTestOutput
for i, o := range s.outputSpecs {
parts := strings.SplitN(o, ":", 2)
format := scaleTestOutputFormat(parts[0])
if _, ok := validFormats[format]; !ok {
return nil, xerrors.Errorf("invalid output format %q in output flag %d", parts[0], i)
}
if len(parts) == 1 {
if stdoutFormat != "" {
return nil, xerrors.Errorf("multiple output flags specified for stdout")
}
stdoutFormat = format
continue
}
if len(parts) != 2 {
return nil, xerrors.Errorf("invalid output flag %d: %q", i, o)
}
out = append(out, scaleTestOutput{
format: format,
path: parts[1],
})
}
// Default to --output text
if stdoutFormat == "" && len(out) == 0 {
stdoutFormat = scaleTestOutputFormatText
}
if stdoutFormat != "" {
out = append([]scaleTestOutput{{
format: stdoutFormat,
path: "-",
}}, out...)
}
return out, nil
}
func requireAdmin(ctx context.Context, client *codersdk.Client) (codersdk.User, error) {
me, err := client.User(ctx, codersdk.Me)
if err != nil {
return codersdk.User{}, xerrors.Errorf("fetch current user: %w", err)
}
// Only owners can do scaletests. This isn't a very strong check but there's
// not much else we can do. Ratelimits are enforced for non-owners so
// hopefully that limits the damage if someone disables this check and runs
// it against a non-owner account on a production deployment.
ok := false
for _, role := range me.Roles {
if role.Name == "owner" {
ok = true
break
}
}
if !ok {
return me, xerrors.Errorf("Not logged in as a site owner. Scale testing is only available to site owners.")
}
return me, nil
}
// userCleanupRunner is a runner that deletes a user in the Run phase.
type userCleanupRunner struct {
client *codersdk.Client
userID uuid.UUID
}
var _ harness.Runnable = &userCleanupRunner{}
// Run implements Runnable.
func (r *userCleanupRunner) Run(ctx context.Context, _ string, _ io.Writer) error {
if r.userID == uuid.Nil {
return nil
}
ctx, span := tracing.StartSpan(ctx)
defer span.End()
err := r.client.DeleteUser(ctx, r.userID)
if err != nil {
return xerrors.Errorf("delete user %q: %w", r.userID, err)
}
return nil
}
func scaletestCleanup() *cobra.Command {
var (
cleanupStrategy = &scaletestStrategyFlags{cleanup: true}
)
cmd := &cobra.Command{
Use: "cleanup",
Short: "Cleanup any orphaned scaletest resources",
Long: "Cleanup scaletest workspaces, then cleanup scaletest users. The strategy flags will apply to each stage of the cleanup process.",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
client, err := CreateClient(cmd)
if err != nil {
return err
}
_, err = requireAdmin(ctx, client)
if err != nil {
return err
}
client.BypassRatelimits = true
cmd.PrintErrln("Fetching scaletest workspaces...")
var (
pageNumber = 0
limit = 100
workspaces []codersdk.Workspace
)
for {
page, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
Name: "scaletest-",
Offset: pageNumber * limit,
Limit: limit,
})
if err != nil {
return xerrors.Errorf("fetch scaletest workspaces page %d: %w", pageNumber, err)
}
pageNumber++
if len(page.Workspaces) == 0 {
break
}
pageWorkspaces := make([]codersdk.Workspace, 0, len(page.Workspaces))
for _, w := range page.Workspaces {
if isScaleTestWorkspace(w) {
pageWorkspaces = append(pageWorkspaces, w)
}
}
workspaces = append(workspaces, pageWorkspaces...)
}
cmd.PrintErrf("Found %d scaletest workspaces\n", len(workspaces))
if len(workspaces) != 0 {
cmd.Println("Deleting scaletest workspaces...")
harness := harness.NewTestHarness(cleanupStrategy.toStrategy(), harness.ConcurrentExecutionStrategy{})
for i, w := range workspaces {
const testName = "cleanup-workspace"
r := workspacebuild.NewCleanupRunner(client, w.ID)
harness.AddRun(testName, strconv.Itoa(i), r)
}
ctx, cancel := cleanupStrategy.toContext(ctx)
defer cancel()
err := harness.Run(ctx)
if err != nil {
return xerrors.Errorf("run test harness to delete workspaces (harness failure, not a test failure): %w", err)
}
cmd.Println("Done deleting scaletest workspaces:")
res := harness.Results()
res.PrintText(cmd.ErrOrStderr())
if res.TotalFail > 0 {
return xerrors.Errorf("failed to delete scaletest workspaces")
}
}
cmd.PrintErrln("Fetching scaletest users...")
pageNumber = 0
limit = 100
var users []codersdk.User
for {
page, err := client.Users(ctx, codersdk.UsersRequest{
Search: "scaletest-",
Pagination: codersdk.Pagination{
Offset: pageNumber * limit,
Limit: limit,
},
})
if err != nil {
return xerrors.Errorf("fetch scaletest users page %d: %w", pageNumber, err)
}
pageNumber++
if len(page.Users) == 0 {
break
}
pageUsers := make([]codersdk.User, 0, len(page.Users))
for _, u := range page.Users {
if isScaleTestUser(u) {
pageUsers = append(pageUsers, u)
}
}
users = append(users, pageUsers...)
}
cmd.PrintErrf("Found %d scaletest users\n", len(users))
if len(workspaces) != 0 {
cmd.Println("Deleting scaletest users...")
harness := harness.NewTestHarness(cleanupStrategy.toStrategy(), harness.ConcurrentExecutionStrategy{})
for i, u := range users {
const testName = "cleanup-users"
r := &userCleanupRunner{
client: client,
userID: u.ID,
}
harness.AddRun(testName, strconv.Itoa(i), r)
}
ctx, cancel := cleanupStrategy.toContext(ctx)
defer cancel()
err := harness.Run(ctx)
if err != nil {
return xerrors.Errorf("run test harness to delete users (harness failure, not a test failure): %w", err)
}
cmd.Println("Done deleting scaletest users:")
res := harness.Results()
res.PrintText(cmd.ErrOrStderr())
if res.TotalFail > 0 {
return xerrors.Errorf("failed to delete scaletest users")
}
}
return nil
},
}
cleanupStrategy.attach(cmd)
return cmd
}
func scaletestCreateWorkspaces() *cobra.Command {
var (
count int
template string
parametersFile string
parameters []string // key=value
noPlan bool
noCleanup bool
// TODO: implement this flag
// noCleanupFailures bool
noWaitForAgents bool
runCommand string
runTimeout time.Duration
runExpectTimeout bool
runExpectOutput string
runLogOutput bool
// TODO: customizable agent, currently defaults to the first agent found
// if there are multiple
connectURL string // http://localhost:4/
connectMode string // derp or direct
connectHold time.Duration
connectInterval time.Duration
connectTimeout time.Duration
tracingFlags = &scaletestTracingFlags{}
strategy = &scaletestStrategyFlags{}
cleanupStrategy = &scaletestStrategyFlags{cleanup: true}
output = &scaletestOutputFlags{}
)
cmd := &cobra.Command{
Use: "create-workspaces",
Short: "Creates many workspaces and waits for them to be ready",
Long: `Creates many users, then creates a workspace for each user and waits for them finish building and fully come online. Optionally runs a command inside each workspace, and connects to the workspace over WireGuard.
It is recommended that all rate limits are disabled on the server before running this scaletest. This test generates many login events which will be rate limited against the (most likely single) IP.`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
client, err := CreateClient(cmd)
if err != nil {
return err
}
me, err := requireAdmin(ctx, client)
if err != nil {
return err
}
client.BypassRatelimits = true
if count <= 0 {
return xerrors.Errorf("--count is required and must be greater than 0")
}
outputs, err := output.parse()
if err != nil {
return xerrors.Errorf("could not parse --output flags")
}
var tpl codersdk.Template
if template == "" {
return xerrors.Errorf("--template is required")
}
if id, err := uuid.Parse(template); err == nil && id != uuid.Nil {
tpl, err = client.Template(ctx, id)
if err != nil {
return xerrors.Errorf("get template by ID %q: %w", template, err)
}
} else {
// List templates in all orgs until we find a match.
orgLoop:
for _, orgID := range me.OrganizationIDs {
tpls, err := client.TemplatesByOrganization(ctx, orgID)
if err != nil {
return xerrors.Errorf("list templates in org %q: %w", orgID, err)
}
for _, t := range tpls {
if t.Name == template {
tpl = t
break orgLoop
}
}
}
}
if tpl.ID == uuid.Nil {
return xerrors.Errorf("could not find template %q in any organization", template)
}
templateVersion, err := client.TemplateVersion(ctx, tpl.ActiveVersionID)
if err != nil {
return xerrors.Errorf("get template version %q: %w", tpl.ActiveVersionID, err)
}
parameterSchemas, err := client.TemplateVersionSchema(ctx, templateVersion.ID)
if err != nil {
return xerrors.Errorf("get template version schema %q: %w", templateVersion.ID, err)
}
paramsMap := map[string]string{}
if parametersFile != "" {
fileMap, err := createParameterMapFromFile(parametersFile)
if err != nil {
return xerrors.Errorf("read parameters file %q: %w", parametersFile, err)
}
paramsMap = fileMap
}
for _, p := range parameters {
parts := strings.SplitN(p, "=", 2)
if len(parts) != 2 {
return xerrors.Errorf("invalid parameter %q", p)
}
paramsMap[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
}
params := []codersdk.CreateParameterRequest{}
for _, p := range parameterSchemas {
value, ok := paramsMap[p.Name]
if !ok {
value = ""
}
params = append(params, codersdk.CreateParameterRequest{
Name: p.Name,
SourceValue: value,
SourceScheme: codersdk.ParameterSourceSchemeData,
DestinationScheme: p.DefaultDestinationScheme,
})
}
// 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",
ParameterValues: params,
})
if err != nil {
return xerrors.Errorf("start dry run workspace creation: %w", err)
}
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "Planning workspace...")
err = cliui.ProvisionerJob(cmd.Context(), cmd.OutOrStdout(), cliui.ProvisionerJobOptions{
Fetch: func() (codersdk.ProvisionerJob, error) {
return client.TemplateVersionDryRun(cmd.Context(), templateVersion.ID, dryRun.ID)
},
Cancel: func() error {
return client.CancelTemplateVersionDryRun(cmd.Context(), templateVersion.ID, dryRun.ID)
},
Logs: func() (<-chan codersdk.ProvisionerJobLog, io.Closer, error) {
return client.TemplateVersionDryRunLogsAfter(cmd.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)
}
}
tracerProvider, closeTracing, tracingEnabled, err := tracingFlags.provider(ctx)
if err != nil {
return xerrors.Errorf("create tracer provider: %w", err)
}
defer func() {
// Allow time for traces to flush even if command context is
// canceled.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = closeTracing(ctx)
}()
tracer := tracerProvider.Tracer(scaletestTracerName)
th := harness.NewTestHarness(strategy.toStrategy(), cleanupStrategy.toStrategy())
for i := 0; i < count; i++ {
const name = "workspacebuild"
id := strconv.Itoa(i)
username, email, err := newScaleTestUser(id)
if err != nil {
return xerrors.Errorf("create scaletest username and email: %w", err)
}
workspaceName, err := newScaleTestWorkspace(id)
if err != nil {
return xerrors.Errorf("create scaletest workspace name: %w", err)
}
config := createworkspaces.Config{
User: createworkspaces.UserConfig{
// TODO: configurable org
OrganizationID: me.OrganizationIDs[0],
Username: username,
Email: email,
},
Workspace: workspacebuild.Config{
OrganizationID: me.OrganizationIDs[0],
// UserID is set by the test automatically.
Request: codersdk.CreateWorkspaceRequest{
TemplateID: tpl.ID,
Name: workspaceName,
ParameterValues: params,
},
NoWaitForAgents: noWaitForAgents,
},
NoCleanup: noCleanup,
}
if runCommand != "" {
config.ReconnectingPTY = &reconnectingpty.Config{
// AgentID is set by the test automatically.
Init: codersdk.ReconnectingPTYInit{
ID: uuid.Nil,
Height: 24,
Width: 80,
Command: runCommand,
},
Timeout: httpapi.Duration(runTimeout),
ExpectTimeout: runExpectTimeout,
ExpectOutput: runExpectOutput,
LogOutput: runLogOutput,
}
}
if connectURL != "" {
config.AgentConn = &agentconn.Config{
// AgentID is set by the test automatically.
// The ConnectionMode gets validated by the Validate()
// call below.
ConnectionMode: agentconn.ConnectionMode(connectMode),
HoldDuration: httpapi.Duration(connectHold),
Connections: []agentconn.Connection{
{
URL: connectURL,
Interval: httpapi.Duration(connectInterval),
Timeout: httpapi.Duration(connectTimeout),
},
},
}
}
err = config.Validate()
if err != nil {
return xerrors.Errorf("validate config: %w", err)
}
var runner harness.Runnable = createworkspaces.NewRunner(client, config)
if tracingEnabled {
runner = &runnableTraceWrapper{
tracer: tracer,
spanName: fmt.Sprintf("%s/%s", name, id),
runner: runner,
}
}
th.AddRun(name, id, runner)
}
// TODO: live progress output
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Running load test...")
testCtx, testCancel := strategy.toContext(ctx)
defer testCancel()
err = th.Run(testCtx)
if err != nil {
return xerrors.Errorf("run test harness (harness failure, not a test failure): %w", err)
}
res := th.Results()
for _, o := range outputs {
err = o.write(res, cmd.OutOrStdout())
if err != nil {
return xerrors.Errorf("write output %q to %q: %w", o.format, o.path, err)
}
}
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "\nCleaning up...")
cleanupCtx, cleanupCancel := cleanupStrategy.toContext(ctx)
defer cleanupCancel()
err = th.Cleanup(cleanupCtx)
if err != nil {
return xerrors.Errorf("cleanup tests: %w", err)
}
// Upload traces.
if tracingEnabled {
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "\nUploading traces...")
ctx, cancel := context.WithTimeout(ctx, 1*time.Minute)
defer cancel()
err := closeTracing(ctx)
if err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "\nError uploading traces: %+v\n", err)
}
}
if res.TotalFail > 0 {
return xerrors.New("load test failed, see above for more details")
}
return nil
},
}
cliflag.IntVarP(cmd.Flags(), &count, "count", "c", "CODER_LOADTEST_COUNT", 1, "Required: Number of workspaces to create.")
cliflag.StringVarP(cmd.Flags(), &template, "template", "t", "CODER_LOADTEST_TEMPLATE", "", "Required: Name or ID of the template to use for workspaces.")
cliflag.StringVarP(cmd.Flags(), &parametersFile, "parameters-file", "", "CODER_LOADTEST_PARAMETERS_FILE", "", "Path to a YAML file containing the parameters to use for each workspace.")
cliflag.StringArrayVarP(cmd.Flags(), &parameters, "parameter", "", "CODER_LOADTEST_PARAMETERS", []string{}, "Parameters to use for each workspace. Can be specified multiple times. Overrides any existing parameters with the same name from --parameters-file. Format: key=value")
cliflag.BoolVarP(cmd.Flags(), &noPlan, "no-plan", "", "CODER_LOADTEST_NO_PLAN", false, "Skip the dry-run step to plan the workspace creation. This step ensures that the given parameters are valid for the given template.")
cliflag.BoolVarP(cmd.Flags(), &noCleanup, "no-cleanup", "", "CODER_LOADTEST_NO_CLEANUP", false, "Do not clean up resources after the test completes. You can cleanup manually using `coder scaletest cleanup`.")
// cliflag.BoolVarP(cmd.Flags(), &noCleanupFailures, "no-cleanup-failures", "", "CODER_LOADTEST_NO_CLEANUP_FAILURES", false, "Do not clean up resources from failed jobs to aid in debugging failures. You can cleanup manually using `coder scaletest cleanup`.")
cliflag.BoolVarP(cmd.Flags(), &noWaitForAgents, "no-wait-for-agents", "", "CODER_LOADTEST_NO_WAIT_FOR_AGENTS", false, "Do not wait for agents to start before marking the test as succeeded. This can be useful if you are running the test against a template that does not start the agent quickly.")
cliflag.StringVarP(cmd.Flags(), &runCommand, "run-command", "", "CODER_LOADTEST_RUN_COMMAND", "", "Command to run inside each workspace using reconnecting-pty (i.e. web terminal protocol). If not specified, no command will be run.")
cliflag.DurationVarP(cmd.Flags(), &runTimeout, "run-timeout", "", "CODER_LOADTEST_RUN_TIMEOUT", 5*time.Second, "Timeout for the command to complete.")
cliflag.BoolVarP(cmd.Flags(), &runExpectTimeout, "run-expect-timeout", "", "CODER_LOADTEST_RUN_EXPECT_TIMEOUT", false, "Expect the command to timeout. If the command does not finish within the given --run-timeout, it will be marked as succeeded. If the command finishes before the timeout, it will be marked as failed.")
cliflag.StringVarP(cmd.Flags(), &runExpectOutput, "run-expect-output", "", "CODER_LOADTEST_RUN_EXPECT_OUTPUT", "", "Expect the command to output the given string (on a single line). If the command does not output the given string, it will be marked as failed.")
cliflag.BoolVarP(cmd.Flags(), &runLogOutput, "run-log-output", "", "CODER_LOADTEST_RUN_LOG_OUTPUT", false, "Log the output of the command to the test logs. This should be left off unless you expect small amounts of output. Large amounts of output will cause high memory usage.")
cliflag.StringVarP(cmd.Flags(), &connectURL, "connect-url", "", "CODER_LOADTEST_CONNECT_URL", "", "URL to connect to inside the the workspace over WireGuard. If not specified, no connections will be made over WireGuard.")
cliflag.StringVarP(cmd.Flags(), &connectMode, "connect-mode", "", "CODER_LOADTEST_CONNECT_MODE", "derp", "Mode to use for connecting to the workspace. Can be 'derp' or 'direct'.")
cliflag.DurationVarP(cmd.Flags(), &connectHold, "connect-hold", "", "CODER_LOADTEST_CONNECT_HOLD", 30*time.Second, "How long to hold the WireGuard connection open for.")
cliflag.DurationVarP(cmd.Flags(), &connectInterval, "connect-interval", "", "CODER_LOADTEST_CONNECT_INTERVAL", time.Second, "How long to wait between making requests to the --connect-url once the connection is established.")
cliflag.DurationVarP(cmd.Flags(), &connectTimeout, "connect-timeout", "", "CODER_LOADTEST_CONNECT_TIMEOUT", 5*time.Second, "Timeout for each request to the --connect-url.")
tracingFlags.attach(cmd)
strategy.attach(cmd)
cleanupStrategy.attach(cmd)
output.attach(cmd)
return cmd
}
type runnableTraceWrapper struct {
tracer trace.Tracer
spanName string
runner harness.Runnable
span trace.Span
}
var _ harness.Runnable = &runnableTraceWrapper{}
var _ harness.Cleanable = &runnableTraceWrapper{}
func (r *runnableTraceWrapper) Run(ctx context.Context, id string, logs io.Writer) error {
ctx, span := r.tracer.Start(ctx, r.spanName, trace.WithNewRoot())
defer span.End()
r.span = span
traceID := "unknown trace ID"
spanID := "unknown span ID"
if span.SpanContext().HasTraceID() {
traceID = span.SpanContext().TraceID().String()
}
if span.SpanContext().HasSpanID() {
spanID = span.SpanContext().SpanID().String()
}
_, _ = fmt.Fprintf(logs, "Trace ID: %s\n", traceID)
_, _ = fmt.Fprintf(logs, "Span ID: %s\n\n", spanID)
// Make a separate span for the run itself so the sub-spans are grouped
// neatly. The cleanup span is also a child of the above span so this is
// important for readability.
ctx2, span2 := r.tracer.Start(ctx, r.spanName+" run")
defer span2.End()
return r.runner.Run(ctx2, id, logs)
}
func (r *runnableTraceWrapper) Cleanup(ctx context.Context, id string) error {
c, ok := r.runner.(harness.Cleanable)
if !ok {
return nil
}
if r.span != nil {
ctx = trace.ContextWithSpanContext(ctx, r.span.SpanContext())
}
ctx, span := r.tracer.Start(ctx, r.spanName+" cleanup")
defer span.End()
return c.Cleanup(ctx, id)
}
// newScaleTestUser returns a random username and email address that can be used
// for scale testing. The returned username is prefixed with "scaletest-" and
// the returned email address is suffixed with "@scaletest.local".
func newScaleTestUser(id string) (username string, email string, err error) {
randStr, err := cryptorand.String(8)
return fmt.Sprintf("scaletest-%s-%s", randStr, id), fmt.Sprintf("%s-%s@scaletest.local", randStr, id), err
}
// newScaleTestWorkspace returns a random workspace name that can be used for
// scale testing. The returned workspace name is prefixed with "scaletest-" and
// suffixed with the given id.
func newScaleTestWorkspace(id string) (name string, err error) {
randStr, err := cryptorand.String(8)
return fmt.Sprintf("scaletest-%s-%s", randStr, id), err
}
func isScaleTestUser(user codersdk.User) bool {
return strings.HasSuffix(user.Email, "@scaletest.local")
}
func isScaleTestWorkspace(workspace codersdk.Workspace) bool {
if !strings.HasPrefix(workspace.OwnerName, "scaletest-") {
return false
}
return strings.HasPrefix(workspace.Name, "scaletest-")
}
+200
View File
@@ -0,0 +1,200 @@
package cli_test
import (
"context"
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/pty/ptytest"
"github.com/coder/coder/scaletest/harness"
"github.com/coder/coder/testutil"
)
func TestScaleTest(t *testing.T) {
t.Skipf("This test is flakey. See https://github.com/coder/coder/issues/4942")
t.Parallel()
// This test does a create-workspaces scale test with --no-cleanup, checks
// that the created resources are OK, and then runs a cleanup.
t.Run("WorkspaceBuildNoCleanup", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancelFunc()
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)
// Write a parameters file.
tDir := t.TempDir()
paramsFile := filepath.Join(tDir, "params.yaml")
outputFile := filepath.Join(tDir, "output.json")
f, err := os.Create(paramsFile)
require.NoError(t, err)
defer f.Close()
_, err = f.WriteString(`---
param1: foo
param2: true
param3: 1
`)
require.NoError(t, err)
err = f.Close()
require.NoError(t, err)
cmd, root := clitest.New(t, "scaletest", "create-workspaces",
"--count", "2",
"--template", template.Name,
"--parameters-file", paramsFile,
"--parameter", "param1=bar",
"--parameter", "param4=baz",
"--no-cleanup",
// This flag is important for tests because agents will never be
// started.
"--no-wait-for-agents",
// Run and connect flags cannot be tested because they require an
// agent.
"--concurrency", "2",
"--timeout", "30s",
"--job-timeout", "15s",
"--cleanup-concurrency", "1",
"--cleanup-timeout", "30s",
"--cleanup-job-timeout", "15s",
"--output", "text",
"--output", "json:"+outputFile,
)
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
cmd.SetOut(pty.Output())
cmd.SetErr(pty.Output())
done := make(chan any)
go func() {
err := cmd.ExecuteContext(ctx)
assert.NoError(t, err)
close(done)
}()
pty.ExpectMatch("Test results:")
pty.ExpectMatch("Pass: 2")
select {
case <-done:
case <-ctx.Done():
}
cancelFunc()
<-done
// Recreate the context.
ctx, cancelFunc = context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancelFunc()
// Verify the output file.
f, err = os.Open(outputFile)
require.NoError(t, err)
defer f.Close()
var res harness.Results
err = json.NewDecoder(f).Decode(&res)
require.NoError(t, err)
require.EqualValues(t, 2, res.TotalRuns)
require.EqualValues(t, 2, res.TotalPass)
// Find the workspaces and users and check that they are what we expect.
workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
Offset: 0,
Limit: 100,
})
require.NoError(t, err)
require.Len(t, workspaces.Workspaces, 2)
seenUsers := map[string]struct{}{}
for _, w := range workspaces.Workspaces {
// Sadly we can't verify params as the API doesn't seem to return
// them.
// Verify that the user is a unique scaletest user.
u, err := client.User(ctx, w.OwnerID.String())
require.NoError(t, err)
_, ok := seenUsers[u.ID.String()]
require.False(t, ok, "user has more than one workspace")
seenUsers[u.ID.String()] = struct{}{}
require.Contains(t, u.Username, "scaletest-")
require.Contains(t, u.Email, "scaletest")
}
require.Len(t, seenUsers, len(workspaces.Workspaces))
// Check that there are exactly 3 users.
users, err := client.Users(ctx, codersdk.UsersRequest{
Pagination: codersdk.Pagination{
Offset: 0,
Limit: 100,
},
})
require.NoError(t, err)
require.Len(t, users.Users, len(seenUsers)+1)
// Cleanup.
cmd, root = clitest.New(t, "scaletest", "cleanup",
"--cleanup-concurrency", "1",
"--cleanup-timeout", "30s",
"--cleanup-job-timeout", "15s",
)
clitest.SetupConfig(t, client, root)
pty = ptytest.New(t)
cmd.SetOut(pty.Output())
cmd.SetErr(pty.Output())
done = make(chan any)
go func() {
err := cmd.ExecuteContext(ctx)
assert.NoError(t, err)
close(done)
}()
pty.ExpectMatch("Test results:")
pty.ExpectMatch("Pass: 2")
pty.ExpectMatch("Test results:")
pty.ExpectMatch("Pass: 2")
select {
case <-done:
case <-ctx.Done():
}
cancelFunc()
<-done
// Recreate the context (again).
ctx, cancelFunc = context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancelFunc()
// Verify that the workspaces are gone.
workspaces, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{
Offset: 0,
Limit: 100,
})
require.NoError(t, err)
require.Len(t, workspaces.Workspaces, 0)
// Verify that the users are gone.
users, err = client.Users(ctx, codersdk.UsersRequest{
Pagination: codersdk.Pagination{
Offset: 0,
Limit: 100,
},
})
require.NoError(t, err)
require.Len(t, users.Users, 1)
})
}
+294 -118
View File
@@ -81,10 +81,46 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
Use: "server",
Short: "Start a Coder server",
RunE: func(cmd *cobra.Command, args []string) error {
// Main command context for managing cancellation of running
// services.
ctx, cancel := context.WithCancel(cmd.Context())
defer cancel()
go dumpHandler(ctx)
cfg, err := deployment.Config(cmd.Flags(), vip)
if err != nil {
return xerrors.Errorf("getting deployment config: %w", err)
}
// Validate bind addresses.
if cfg.Address.Value != "" {
cmd.PrintErr(cliui.Styles.Warn.Render("WARN:") + " --address and -a are deprecated, please use --http-address and --tls-address instead")
if cfg.TLS.Enable.Value {
cfg.HTTPAddress.Value = ""
cfg.TLS.Address.Value = cfg.Address.Value
} else {
cfg.HTTPAddress.Value = cfg.Address.Value
cfg.TLS.Address.Value = ""
}
}
if cfg.TLS.Enable.Value && cfg.TLS.Address.Value == "" {
return xerrors.Errorf("TLS address must be set if TLS is enabled")
}
if !cfg.TLS.Enable.Value && cfg.HTTPAddress.Value == "" {
return xerrors.Errorf("either HTTP or TLS must be enabled")
}
// Disable rate limits if the `--dangerous-disable-rate-limits` flag
// was specified.
loginRateLimit := 60
filesRateLimit := 12
if cfg.RateLimit.DisableAll.Value {
cfg.RateLimit.API.Value = -1
loginRateLimit = -1
filesRateLimit = -1
}
printLogo(cmd)
logger := slog.Make(sloghuman.Sink(cmd.ErrOrStderr()))
if ok, _ := cmd.Flags().GetBool(varVerbose); ok {
@@ -94,11 +130,6 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
logger = logger.AppendSinks(tracing.SlogSink{})
}
// Main command context for managing cancellation
// of running services.
ctx, cancel := context.WithCancel(cmd.Context())
defer cancel()
// Register signals early on so that graceful shutdown can't
// be interrupted by additional signals. Note that we avoid
// shadowing cancel() (from above) here because notifyStop()
@@ -186,14 +217,54 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
}()
}
listener, err := net.Listen("tcp", cfg.Address.Value)
if err != nil {
return xerrors.Errorf("listen %q: %w", cfg.Address.Value, err)
}
defer listener.Close()
var (
httpListener net.Listener
httpURL *url.URL
)
if cfg.HTTPAddress.Value != "" {
httpListener, err = net.Listen("tcp", cfg.HTTPAddress.Value)
if err != nil {
return xerrors.Errorf("listen %q: %w", cfg.HTTPAddress.Value, err)
}
defer httpListener.Close()
var tlsConfig *tls.Config
listenAddrStr := httpListener.Addr().String()
// For some reason if 0.0.0.0:x is provided as the http address,
// httpListener.Addr().String() likes to return it as an ipv6
// address (i.e. [::]:x). If the input ip is 0.0.0.0, try to
// coerce the output back to ipv4 to make it less confusing.
if strings.Contains(cfg.HTTPAddress.Value, "0.0.0.0") {
listenAddrStr = strings.ReplaceAll(listenAddrStr, "[::]", "0.0.0.0")
}
// We want to print out the address the user supplied, not the
// loopback device.
cmd.Println("Started HTTP listener at", (&url.URL{Scheme: "http", Host: listenAddrStr}).String())
// Set the http URL we want to use when connecting to ourselves.
tcpAddr, tcpAddrValid := httpListener.Addr().(*net.TCPAddr)
if !tcpAddrValid {
return xerrors.Errorf("invalid TCP address type %T", httpListener.Addr())
}
if tcpAddr.IP.IsUnspecified() {
tcpAddr.IP = net.IPv4(127, 0, 0, 1)
}
httpURL = &url.URL{
Scheme: "http",
Host: tcpAddr.String(),
}
}
var (
tlsConfig *tls.Config
httpsListener net.Listener
httpsURL *url.URL
)
if cfg.TLS.Enable.Value {
if cfg.TLS.Address.Value == "" {
return xerrors.New("tls address must be set if tls is enabled")
}
tlsConfig, err = configureTLS(
cfg.TLS.MinVersion.Value,
cfg.TLS.ClientAuth.Value,
@@ -204,25 +275,63 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
if err != nil {
return xerrors.Errorf("configure tls: %w", err)
}
listener = tls.NewListener(listener, tlsConfig)
httpsListenerInner, err := net.Listen("tcp", cfg.TLS.Address.Value)
if err != nil {
return xerrors.Errorf("listen %q: %w", cfg.TLS.Address.Value, err)
}
defer httpsListenerInner.Close()
httpsListener = tls.NewListener(httpsListenerInner, tlsConfig)
defer httpsListener.Close()
listenAddrStr := httpsListener.Addr().String()
// For some reason if 0.0.0.0:x is provided as the https
// address, httpsListener.Addr().String() likes to return it as
// an ipv6 address (i.e. [::]:x). If the input ip is 0.0.0.0,
// try to coerce the output back to ipv4 to make it less
// confusing.
if strings.Contains(cfg.HTTPAddress.Value, "0.0.0.0") {
listenAddrStr = strings.ReplaceAll(listenAddrStr, "[::]", "0.0.0.0")
}
// We want to print out the address the user supplied, not the
// loopback device.
cmd.Println("Started TLS/HTTPS listener at", (&url.URL{Scheme: "https", Host: listenAddrStr}).String())
// Set the https URL we want to use when connecting to
// ourselves.
tcpAddr, tcpAddrValid := httpsListener.Addr().(*net.TCPAddr)
if !tcpAddrValid {
return xerrors.Errorf("invalid TCP address type %T", httpsListener.Addr())
}
if tcpAddr.IP.IsUnspecified() {
tcpAddr.IP = net.IPv4(127, 0, 0, 1)
}
httpsURL = &url.URL{
Scheme: "https",
Host: tcpAddr.String(),
}
}
tcpAddr, valid := listener.Addr().(*net.TCPAddr)
if !valid {
return xerrors.New("must be listening on tcp")
// Sanity check that at least one listener was started.
if httpListener == nil && httpsListener == nil {
return xerrors.New("must listen on at least one address")
}
// If just a port is specified, assume localhost.
if tcpAddr.IP.IsUnspecified() {
tcpAddr.IP = net.IPv4(127, 0, 0, 1)
// Prefer HTTP because it's less prone to TLS errors over localhost.
localURL := httpsURL
if httpURL != nil {
localURL = httpURL
}
// If no access URL is specified, fallback to the
// bounds URL.
localURL := &url.URL{
Scheme: "http",
Host: tcpAddr.String(),
}
if cfg.TLS.Enable.Value {
localURL.Scheme = "https"
ctx, httpClient, err := configureHTTPClient(
ctx,
cfg.TLS.ClientCertFile.Value,
cfg.TLS.ClientKeyFile.Value,
cfg.TLS.ClientCAFile.Value,
)
if err != nil {
return xerrors.Errorf("configure http client: %w", err)
}
var (
@@ -279,6 +388,15 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
cmd.Printf("%s The access URL %s %s, this may cause unexpected problems when creating workspaces. Generate a unique *.try.coder.app URL by not specifying an access URL.\n", cliui.Styles.Warn.Render("Warning:"), cliui.Styles.Field.Render(accessURLParsed.String()), reason)
}
// Redirect from the HTTP listener to the access URL if:
// 1. The redirect flag is enabled.
// 2. HTTP listening is enabled (obviously).
// 3. TLS is enabled (otherwise they're likely using a reverse proxy
// which can do this instead).
// 4. The access URL has been set manually (not a tunnel).
// 5. The access URL is HTTPS.
shouldRedirectHTTPToAccessURL := cfg.TLS.RedirectHTTP.Value && cfg.HTTPAddress.Value != "" && cfg.TLS.Enable.Value && tunnel == nil && accessURLParsed.Scheme == "https"
// A newline is added before for visibility in terminal output.
cmd.Printf("\nView the Web UI: %s\n", accessURLParsed.String())
@@ -293,27 +411,6 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
return xerrors.Errorf("parse ssh keygen algorithm %s: %w", cfg.SSHKeygenAlgorithm.Value, err)
}
// Validate provided auto-import templates.
var (
validatedAutoImportTemplates = make([]coderd.AutoImportTemplate, len(cfg.AutoImportTemplates.Value))
seenValidatedAutoImportTemplates = make(map[coderd.AutoImportTemplate]struct{}, len(cfg.AutoImportTemplates.Value))
)
for i, autoImportTemplate := range cfg.AutoImportTemplates.Value {
var v coderd.AutoImportTemplate
switch autoImportTemplate {
case "kubernetes":
v = coderd.AutoImportTemplateKubernetes
default:
return xerrors.Errorf("auto import template %q is not supported", autoImportTemplate)
}
if _, ok := seenValidatedAutoImportTemplates[v]; ok {
return xerrors.Errorf("auto import template %q is specified more than once", v)
}
seenValidatedAutoImportTemplates[v] = struct{}{}
validatedAutoImportTemplates[i] = v
}
defaultRegion := &tailcfg.DERPRegion{
EmbeddedRelay: true,
RegionID: cfg.DERP.Server.RegionID.Value,
@@ -371,12 +468,14 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
SSHKeygenAlgorithm: sshKeygenAlgorithm,
TracerProvider: tracerProvider,
Telemetry: telemetry.NewNoop(),
AutoImportTemplates: validatedAutoImportTemplates,
MetricsCacheRefreshInterval: cfg.MetricsCacheRefreshInterval.Value,
AgentStatsRefreshInterval: cfg.AgentStatRefreshInterval.Value,
DeploymentConfig: cfg,
PrometheusRegistry: prometheus.NewRegistry(),
APIRateLimit: cfg.APIRateLimit.Value,
APIRateLimit: cfg.RateLimit.API.Value,
LoginRateLimit: loginRateLimit,
FilesRateLimit: filesRateLimit,
HTTPClient: httpClient,
}
if tlsConfig != nil {
options.TLSCertificates = tlsConfig.Certificates
@@ -424,11 +523,6 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
return xerrors.Errorf("OIDC issuer URL must be set!")
}
ctx, err := handleOauth2ClientCertificates(ctx, cfg)
if err != nil {
return xerrors.Errorf("configure oidc client certificates: %w", err)
}
if cfg.OIDC.IgnoreEmailVerified.Value {
logger.Warn(ctx, "coder will not check email_verified for OIDC logins")
}
@@ -452,8 +546,9 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
Verifier: oidcProvider.Verifier(&oidc.Config{
ClientID: cfg.OIDC.ClientID.Value,
}),
EmailDomain: cfg.OIDC.EmailDomain.Value,
AllowSignups: cfg.OIDC.AllowSignups.Value,
EmailDomain: cfg.OIDC.EmailDomain.Value,
AllowSignups: cfg.OIDC.AllowSignups.Value,
UsernameField: cfg.OIDC.UsernameField.Value,
}
}
@@ -467,6 +562,15 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
return xerrors.Errorf("dial postgres: %w", err)
}
defer sqlDB.Close()
pingCtx, pingCancel := context.WithTimeout(ctx, 15*time.Second)
defer pingCancel()
err = sqlDB.PingContext(pingCtx)
if err != nil {
return xerrors.Errorf("ping postgres: %w", err)
}
// Ensure the PostgreSQL version is >=13.0.0!
version, err := sqlDB.QueryContext(ctx, "SHOW server_version;")
if err != nil {
@@ -487,10 +591,6 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
}
logger.Debug(ctx, "connected to postgresql", slog.F("version", versionStr))
err = sqlDB.Ping()
if err != nil {
return xerrors.Errorf("ping postgres: %w", err)
}
err = migrations.Up(sqlDB)
if err != nil {
return xerrors.Errorf("migrate up: %w", err)
@@ -604,6 +704,10 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
), cfg.Prometheus.Address.Value, "prometheus")()
}
if cfg.Swagger.Enable.Value {
options.SwaggerEndpoint = cfg.Swagger.Enable.Value
}
// We use a separate coderAPICloser so the Enterprise API
// can have it's own close functions. This is cleaner
// than abstracting the Coder API itself.
@@ -613,16 +717,22 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
}
client := codersdk.New(localURL)
if cfg.TLS.Enable.Value {
// Secure transport isn't needed for locally communicating!
if localURL.Scheme == "https" && isLocalhost(localURL.Hostname()) {
// The certificate will likely be self-signed or for a different
// hostname, so we need to skip verification.
client.HTTPClient.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
//nolint:gosec
InsecureSkipVerify: true,
},
}
defer client.HTTPClient.CloseIdleConnections()
}
defer client.HTTPClient.CloseIdleConnections()
// This is helpful for tests, but can be silently ignored.
// Coder may be ran as users that don't have permission to write in the homedir,
// such as via the systemd service.
_ = config.URL().Write(client.URL.String())
// Since errCh only has one buffered slot, all routines
// sending on it must be wrapped in a select/default to
@@ -651,40 +761,65 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
shutdownConnsCtx, shutdownConns := context.WithCancel(ctx)
defer shutdownConns()
// ReadHeaderTimeout is purposefully not enabled. It caused some issues with
// websockets over the dev tunnel.
// Wrap the server in middleware that redirects to the access URL if
// the request is not to a local IP.
var handler http.Handler = coderAPI.RootHandler
if shouldRedirectHTTPToAccessURL {
handler = redirectHTTPToAccessURL(handler, accessURLParsed)
}
// ReadHeaderTimeout is purposefully not enabled. It caused some
// issues with websockets over the dev tunnel.
// See: https://github.com/coder/coder/pull/3730
//nolint:gosec
server := &http.Server{
// These errors are typically noise like "TLS: EOF". Vault does similar:
httpServer := &http.Server{
// These errors are typically noise like "TLS: EOF". Vault does
// similar:
// https://github.com/hashicorp/vault/blob/e2490059d0711635e529a4efcbaa1b26998d6e1c/command/server.go#L2714
ErrorLog: log.New(io.Discard, "", 0),
Handler: coderAPI.RootHandler,
Handler: handler,
BaseContext: func(_ net.Listener) context.Context {
return shutdownConnsCtx
},
}
defer func() {
_ = shutdownWithTimeout(server.Shutdown, 5*time.Second)
_ = shutdownWithTimeout(httpServer.Shutdown, 5*time.Second)
}()
eg := errgroup.Group{}
eg.Go(func() error {
// Make sure to close the tunnel listener if we exit so the
// errgroup doesn't wait forever!
if tunnel != nil {
defer tunnel.Listener.Close()
// We call this in the routine so we can kill the other listeners if
// one of them fails.
closeListenersNow := func() {
if httpListener != nil {
_ = httpListener.Close()
}
if httpsListener != nil {
_ = httpsListener.Close()
}
if tunnel != nil {
_ = tunnel.Listener.Close()
}
}
return server.Serve(listener)
})
if tunnel != nil {
eg := errgroup.Group{}
if httpListener != nil {
eg.Go(func() error {
defer listener.Close()
return server.Serve(tunnel.Listener)
defer closeListenersNow()
return httpServer.Serve(httpListener)
})
}
if httpsListener != nil {
eg.Go(func() error {
defer closeListenersNow()
return httpServer.Serve(httpsListener)
})
}
if tunnel != nil {
eg.Go(func() error {
defer closeListenersNow()
return httpServer.Serve(tunnel.Listener)
})
}
go func() {
select {
case errCh <- eg.Wait():
@@ -693,9 +828,10 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
}()
hasFirstUser, err := client.HasFirstUser(ctx)
if !hasFirstUser && err == nil {
cmd.Println()
cmd.Println("Get started by creating the first user (in a new terminal):")
if err != nil {
cmd.Println("\nFailed to check for the first user: " + err.Error())
} else if !hasFirstUser {
cmd.Println("\nGet started by creating the first user (in a new terminal):")
cmd.Println(cliui.Styles.Code.Render("coder login " + accessURLParsed.String()))
}
@@ -712,11 +848,6 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
autobuildExecutor := executor.New(ctx, options.Database, logger, autobuildPoller.C)
autobuildExecutor.Run()
// This is helpful for tests, but can be silently ignored.
// Coder may be ran as users that don't have permission to write in the homedir,
// such as via the systemd service.
_ = config.URL().Write(client.URL.String())
// Currently there is no way to ask the server to shut
// itself down, so any exit signal will result in a non-zero
// exit of the server.
@@ -753,7 +884,7 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
// in-flight requests, give in-flight requests 5 seconds to
// complete.
cmd.Println("Shutting down API server...")
err = shutdownWithTimeout(server.Shutdown, 3*time.Second)
err = shutdownWithTimeout(httpServer.Shutdown, 3*time.Second)
if err != nil {
cmd.Printf("API server shutdown took longer than 3s: %s\n", err)
} else {
@@ -817,7 +948,8 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
},
}
root.AddCommand(&cobra.Command{
var pgRawURL bool
postgresBuiltinURLCmd := &cobra.Command{
Use: "postgres-builtin-url",
Short: "Output the connection URL for the built-in PostgreSQL deployment.",
RunE: func(cmd *cobra.Command, _ []string) error {
@@ -826,37 +958,49 @@ func Server(vip *viper.Viper, newAPI func(context.Context, *coderd.Options) (*co
if err != nil {
return err
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "psql %q\n", url)
if pgRawURL {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s\n", url)
} else {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s\n", cliui.Styles.Code.Render(fmt.Sprintf("psql %q", url)))
}
return nil
},
})
root.AddCommand(&cobra.Command{
}
postgresBuiltinServeCmd := &cobra.Command{
Use: "postgres-builtin-serve",
Short: "Run the built-in PostgreSQL deployment.",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
cfg := createConfig(cmd)
logger := slog.Make(sloghuman.Sink(cmd.ErrOrStderr()))
if ok, _ := cmd.Flags().GetBool(varVerbose); ok {
logger = logger.Leveled(slog.LevelDebug)
}
url, closePg, err := startBuiltinPostgres(cmd.Context(), cfg, logger)
ctx, cancel := signal.NotifyContext(ctx, InterruptSignals...)
defer cancel()
url, closePg, err := startBuiltinPostgres(ctx, cfg, logger)
if err != nil {
return err
}
defer func() { _ = closePg() }()
cmd.Println(cliui.Styles.Code.Render("psql \"" + url + "\""))
if pgRawURL {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s\n", url)
} else {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s\n", cliui.Styles.Code.Render(fmt.Sprintf("psql %q", url)))
}
stopChan := make(chan os.Signal, 1)
defer signal.Stop(stopChan)
signal.Notify(stopChan, os.Interrupt)
<-stopChan
<-ctx.Done()
return nil
},
})
}
postgresBuiltinURLCmd.Flags().BoolVar(&pgRawURL, "raw-url", false, "Output the raw connection URL instead of a psql command.")
postgresBuiltinServeCmd.Flags().BoolVar(&pgRawURL, "raw-url", false, "Output the raw connection URL instead of a psql command.")
root.AddCommand(postgresBuiltinURLCmd, postgresBuiltinServeCmd)
deployment.AttachFlags(root.Flags(), vip, false)
@@ -1088,19 +1232,27 @@ func configureTLS(tlsMinVersion, tlsClientAuth string, tlsCertFiles, tlsKeyFiles
return nil, nil //nolint:nilnil
}
err = configureCAPool(tlsClientCAFile, tlsConfig)
if err != nil {
return nil, err
}
return tlsConfig, nil
}
func configureCAPool(tlsClientCAFile string, tlsConfig *tls.Config) error {
if tlsClientCAFile != "" {
caPool := x509.NewCertPool()
data, err := os.ReadFile(tlsClientCAFile)
if err != nil {
return nil, xerrors.Errorf("read %q: %w", tlsClientCAFile, err)
return xerrors.Errorf("read %q: %w", tlsClientCAFile, err)
}
if !caPool.AppendCertsFromPEM(data) {
return nil, xerrors.Errorf("failed to parse CA certificate in tls-client-ca-file")
return xerrors.Errorf("failed to parse CA certificate in tls-client-ca-file")
}
tlsConfig.ClientCAs = caPool
}
return tlsConfig, nil
return nil
}
//nolint:revive // Ignore flag-parameter: parameter 'allowEveryone' seems to be a control flag, avoid control coupling (revive)
@@ -1293,7 +1445,7 @@ func startBuiltinPostgres(ctx context.Context, cfg config.Root, logger slog.Logg
if err != nil {
return "", nil, xerrors.Errorf("read postgres port: %w", err)
}
pgPort, err := strconv.Atoi(pgPortRaw)
pgPort, err := strconv.ParseUint(pgPortRaw, 10, 16)
if err != nil {
return "", nil, xerrors.Errorf("parse postgres port: %w", err)
}
@@ -1319,20 +1471,44 @@ func startBuiltinPostgres(ctx context.Context, cfg config.Root, logger slog.Logg
return connectionURL, ep.Stop, nil
}
func handleOauth2ClientCertificates(ctx context.Context, cfg *codersdk.DeploymentConfig) (context.Context, error) {
if cfg.TLS.ClientCertFile.Value != "" && cfg.TLS.ClientKeyFile.Value != "" {
certificates, err := loadCertificates([]string{cfg.TLS.ClientCertFile.Value}, []string{cfg.TLS.ClientKeyFile.Value})
func configureHTTPClient(ctx context.Context, clientCertFile, clientKeyFile string, tlsClientCAFile string) (context.Context, *http.Client, error) {
if clientCertFile != "" && clientKeyFile != "" {
certificates, err := loadCertificates([]string{clientCertFile}, []string{clientKeyFile})
if err != nil {
return nil, err
return ctx, nil, err
}
return context.WithValue(ctx, oauth2.HTTPClient, &http.Client{
tlsClientConfig := &tls.Config{ //nolint:gosec
Certificates: certificates,
}
err = configureCAPool(tlsClientCAFile, tlsClientConfig)
if err != nil {
return nil, nil, err
}
httpClient := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{ //nolint:gosec
Certificates: certificates,
},
TLSClientConfig: tlsClientConfig,
},
}), nil
}
return context.WithValue(ctx, oauth2.HTTPClient, httpClient), httpClient, nil
}
return ctx, nil
return ctx, &http.Client{}, nil
}
func redirectHTTPToAccessURL(handler http.Handler, accessURL *url.URL) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.TLS == nil {
http.Redirect(w, r, accessURL.String(), http.StatusTemporaryRedirect)
return
}
handler.ServeHTTP(w, r)
})
}
// isLocalhost returns true if the host points to the local machine. Intended to
// be called with `u.Hostname()`.
func isLocalhost(host string) bool {
return host == "localhost" || host == "127.0.0.1" || host == "::1"
}
+419 -19
View File
@@ -55,7 +55,7 @@ func TestServer(t *testing.T) {
root, cfg := clitest.New(t,
"server",
"--address", ":0",
"--http-address", ":0",
"--access-url", "http://example.com",
"--postgres-url", connectionURL,
"--cache-dir", t.TempDir(),
@@ -89,7 +89,7 @@ func TestServer(t *testing.T) {
root, cfg := clitest.New(t,
"server",
"--address", ":0",
"--http-address", ":0",
"--access-url", "http://example.com",
"--cache-dir", t.TempDir(),
)
@@ -118,6 +118,19 @@ func TestServer(t *testing.T) {
pty.ExpectMatch("psql")
})
t.Run("BuiltinPostgresURLRaw", func(t *testing.T) {
t.Parallel()
root, _ := clitest.New(t, "server", "postgres-builtin-url", "--raw-url")
pty := ptytest.New(t)
root.SetOutput(pty.Output())
err := root.Execute()
require.NoError(t, err)
got := pty.ReadLine()
if !strings.HasPrefix(got, "postgres://") {
t.Fatalf("expected postgres URL to start with \"postgres://\", got %q", got)
}
})
// Validate that a warning is printed that it may not be externally
// reachable.
@@ -129,7 +142,7 @@ func TestServer(t *testing.T) {
root, cfg := clitest.New(t,
"server",
"--in-memory",
"--address", ":0",
"--http-address", ":0",
"--access-url", "http://localhost:3000/",
"--cache-dir", t.TempDir(),
)
@@ -161,7 +174,7 @@ func TestServer(t *testing.T) {
root, cfg := clitest.New(t,
"server",
"--in-memory",
"--address", ":0",
"--http-address", ":0",
"--access-url", "https://foobarbaz.mydomain",
"--cache-dir", t.TempDir(),
)
@@ -191,7 +204,7 @@ func TestServer(t *testing.T) {
root, cfg := clitest.New(t,
"server",
"--in-memory",
"--address", ":0",
"--http-address", ":0",
"--access-url", "https://google.com",
"--cache-dir", t.TempDir(),
)
@@ -220,7 +233,7 @@ func TestServer(t *testing.T) {
root, _ := clitest.New(t,
"server",
"--in-memory",
"--address", ":0",
"--http-address", ":0",
"--access-url", "google.com",
"--cache-dir", t.TempDir(),
)
@@ -236,9 +249,10 @@ func TestServer(t *testing.T) {
root, _ := clitest.New(t,
"server",
"--in-memory",
"--address", ":0",
"--http-address", "",
"--access-url", "http://example.com",
"--tls-enable",
"--tls-address", ":0",
"--tls-min-version", "tls9",
"--cache-dir", t.TempDir(),
)
@@ -253,9 +267,10 @@ func TestServer(t *testing.T) {
root, _ := clitest.New(t,
"server",
"--in-memory",
"--address", ":0",
"--http-address", "",
"--access-url", "http://example.com",
"--tls-enable",
"--tls-address", ":0",
"--tls-client-auth", "something",
"--cache-dir", t.TempDir(),
)
@@ -310,7 +325,7 @@ func TestServer(t *testing.T) {
args := []string{
"server",
"--in-memory",
"--address", ":0",
"--http-address", ":0",
"--access-url", "http://example.com",
"--cache-dir", t.TempDir(),
}
@@ -331,9 +346,10 @@ func TestServer(t *testing.T) {
root, cfg := clitest.New(t,
"server",
"--in-memory",
"--address", ":0",
"--http-address", "",
"--access-url", "http://example.com",
"--tls-enable",
"--tls-address", ":0",
"--tls-cert-file", certPath,
"--tls-key-file", keyPath,
"--cache-dir", t.TempDir(),
@@ -371,9 +387,10 @@ func TestServer(t *testing.T) {
root, cfg := clitest.New(t,
"server",
"--in-memory",
"--address", ":0",
"--http-address", "",
"--access-url", "http://example.com",
"--tls-enable",
"--tls-address", ":0",
"--tls-cert-file", cert1Path,
"--tls-key-file", key1Path,
"--tls-cert-file", cert2Path,
@@ -443,6 +460,389 @@ func TestServer(t *testing.T) {
cancelFunc()
require.NoError(t, <-errC)
})
t.Run("TLSAndHTTP", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
certPath, keyPath := generateTLSCertificate(t)
root, _ := clitest.New(t,
"server",
"--in-memory",
"--http-address", ":0",
"--access-url", "https://example.com",
"--tls-enable",
"--tls-redirect-http-to-https=false",
"--tls-address", ":0",
"--tls-cert-file", certPath,
"--tls-key-file", keyPath,
"--cache-dir", t.TempDir(),
)
pty := ptytest.New(t)
root.SetOutput(pty.Output())
root.SetErr(pty.Output())
errC := make(chan error, 1)
go func() {
errC <- root.ExecuteContext(ctx)
}()
// We can't use waitAccessURL as it will only return the HTTP URL.
const httpLinePrefix = "Started HTTP listener at "
pty.ExpectMatch(httpLinePrefix)
httpLine := pty.ReadLine()
httpAddr := strings.TrimSpace(strings.TrimPrefix(httpLine, httpLinePrefix))
require.NotEmpty(t, httpAddr)
const tlsLinePrefix = "Started TLS/HTTPS listener at "
pty.ExpectMatch(tlsLinePrefix)
tlsLine := pty.ReadLine()
tlsAddr := strings.TrimSpace(strings.TrimPrefix(tlsLine, tlsLinePrefix))
require.NotEmpty(t, tlsAddr)
// Verify HTTP
httpURL, err := url.Parse(httpAddr)
require.NoError(t, err)
client := codersdk.New(httpURL)
client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
_, err = client.HasFirstUser(ctx)
require.NoError(t, err)
// Verify TLS
tlsURL, err := url.Parse(tlsAddr)
require.NoError(t, err)
client = codersdk.New(tlsURL)
client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
client.HTTPClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
//nolint:gosec
InsecureSkipVerify: true,
},
},
}
_, err = client.HasFirstUser(ctx)
require.NoError(t, err)
cancelFunc()
require.NoError(t, <-errC)
})
t.Run("TLSRedirect", func(t *testing.T) {
t.Parallel()
cases := []struct {
name string
httpListener bool
tlsListener bool
accessURL string
// Empty string means no redirect.
expectRedirect string
}{
{
name: "OK",
httpListener: true,
tlsListener: true,
accessURL: "https://example.com",
expectRedirect: "https://example.com",
},
{
name: "NoTLSListener",
httpListener: true,
tlsListener: false,
accessURL: "https://example.com",
expectRedirect: "",
},
{
name: "NoHTTPListener",
httpListener: false,
tlsListener: true,
accessURL: "https://example.com",
expectRedirect: "",
},
}
for _, c := range cases {
c := c
t.Run(c.name, func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
httpListenAddr := ""
if c.httpListener {
httpListenAddr = ":0"
}
certPath, keyPath := generateTLSCertificate(t)
flags := []string{
"server",
"--in-memory",
"--cache-dir", t.TempDir(),
"--http-address", httpListenAddr,
}
if c.tlsListener {
flags = append(flags,
"--tls-enable",
"--tls-address", ":0",
"--tls-cert-file", certPath,
"--tls-key-file", keyPath,
)
}
if c.accessURL != "" {
flags = append(flags, "--access-url", c.accessURL)
}
root, _ := clitest.New(t, flags...)
pty := ptytest.New(t)
root.SetOutput(pty.Output())
root.SetErr(pty.Output())
errC := make(chan error, 1)
go func() {
errC <- root.ExecuteContext(ctx)
}()
var (
httpAddr string
tlsAddr string
)
// We can't use waitAccessURL as it will only return the HTTP URL.
if c.httpListener {
const httpLinePrefix = "Started HTTP listener at "
pty.ExpectMatch(httpLinePrefix)
httpLine := pty.ReadLine()
httpAddr = strings.TrimSpace(strings.TrimPrefix(httpLine, httpLinePrefix))
require.NotEmpty(t, httpAddr)
}
if c.tlsListener {
const tlsLinePrefix = "Started TLS/HTTPS listener at "
pty.ExpectMatch(tlsLinePrefix)
tlsLine := pty.ReadLine()
tlsAddr = strings.TrimSpace(strings.TrimPrefix(tlsLine, tlsLinePrefix))
require.NotEmpty(t, tlsAddr)
}
// Verify HTTP redirects (or not)
if c.httpListener {
httpURL, err := url.Parse(httpAddr)
require.NoError(t, err)
client := codersdk.New(httpURL)
client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
resp, err := client.Request(ctx, http.MethodGet, "/api/v2/buildinfo", nil)
require.NoError(t, err)
defer resp.Body.Close()
if c.expectRedirect == "" {
require.Equal(t, http.StatusOK, resp.StatusCode)
} else {
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
require.Equal(t, c.expectRedirect, resp.Header.Get("Location"))
}
}
// Verify TLS
if c.tlsListener {
tlsURL, err := url.Parse(tlsAddr)
require.NoError(t, err)
client := codersdk.New(tlsURL)
client.HTTPClient = &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
//nolint:gosec
InsecureSkipVerify: true,
},
},
}
_, err = client.HasFirstUser(ctx)
require.NoError(t, err)
cancelFunc()
require.NoError(t, <-errC)
}
})
}
})
t.Run("CanListenUnspecifiedv4", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
root, _ := clitest.New(t,
"server",
"--in-memory",
"--http-address", "0.0.0.0:0",
"--access-url", "http://example.com",
)
pty := ptytest.New(t)
root.SetOutput(pty.Output())
root.SetErr(pty.Output())
errC := make(chan error, 1)
go func() {
errC <- root.ExecuteContext(ctx)
}()
pty.ExpectMatch("Started HTTP listener at http://0.0.0.0:")
cancelFunc()
require.NoError(t, <-errC)
})
t.Run("CanListenUnspecifiedv6", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
root, _ := clitest.New(t,
"server",
"--in-memory",
"--http-address", "[::]:0",
"--access-url", "http://example.com",
)
pty := ptytest.New(t)
root.SetOutput(pty.Output())
root.SetErr(pty.Output())
errC := make(chan error, 1)
go func() {
errC <- root.ExecuteContext(ctx)
}()
pty.ExpectMatch("Started HTTP listener at http://[::]:")
cancelFunc()
require.NoError(t, <-errC)
})
t.Run("NoAddress", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
root, _ := clitest.New(t,
"server",
"--in-memory",
"--http-address", "",
"--tls-enable=false",
"--tls-address", "",
)
err := root.ExecuteContext(ctx)
require.Error(t, err)
require.ErrorContains(t, err, "either HTTP or TLS must be enabled")
})
t.Run("NoTLSAddress", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
root, _ := clitest.New(t,
"server",
"--in-memory",
"--tls-enable=true",
"--tls-address", "",
)
err := root.ExecuteContext(ctx)
require.Error(t, err)
require.ErrorContains(t, err, "TLS address must be set if TLS is enabled")
})
// DeprecatedAddress is a test for the deprecated --address flag. If
// specified, --http-address and --tls-address are both ignored, a warning
// is printed, and the server will either be HTTP-only or TLS-only depending
// on if --tls-enable is set.
t.Run("DeprecatedAddress", func(t *testing.T) {
t.Parallel()
t.Run("HTTP", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
root, cfg := clitest.New(t,
"server",
"--in-memory",
"--address", ":0",
"--access-url", "http://example.com",
"--cache-dir", t.TempDir(),
)
pty := ptytest.New(t)
root.SetOutput(pty.Output())
root.SetErr(pty.Output())
errC := make(chan error, 1)
go func() {
errC <- root.ExecuteContext(ctx)
}()
pty.ExpectMatch("--address and -a are deprecated")
accessURL := waitAccessURL(t, cfg)
require.Equal(t, "http", accessURL.Scheme)
client := codersdk.New(accessURL)
_, err := client.HasFirstUser(ctx)
require.NoError(t, err)
cancelFunc()
require.NoError(t, <-errC)
})
t.Run("TLS", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
certPath, keyPath := generateTLSCertificate(t)
root, cfg := clitest.New(t,
"server",
"--in-memory",
"--address", ":0",
"--access-url", "http://example.com",
"--tls-enable",
"--tls-cert-file", certPath,
"--tls-key-file", keyPath,
"--cache-dir", t.TempDir(),
)
pty := ptytest.New(t)
root.SetOutput(pty.Output())
root.SetErr(pty.Output())
errC := make(chan error, 1)
go func() {
errC <- root.ExecuteContext(ctx)
}()
pty.ExpectMatch("--address and -a are deprecated")
accessURL := waitAccessURL(t, cfg)
require.Equal(t, "https", accessURL.Scheme)
client := codersdk.New(accessURL)
client.HTTPClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
//nolint:gosec
InsecureSkipVerify: true,
},
},
}
_, err := client.HasFirstUser(ctx)
require.NoError(t, err)
cancelFunc()
require.NoError(t, <-errC)
})
})
// This cannot be ran in parallel because it uses a signal.
//nolint:paralleltest
t.Run("Shutdown", func(t *testing.T) {
@@ -456,7 +856,7 @@ func TestServer(t *testing.T) {
root, cfg := clitest.New(t,
"server",
"--in-memory",
"--address", ":0",
"--http-address", ":0",
"--access-url", "http://example.com",
"--provisioner-daemons", "1",
"--cache-dir", t.TempDir(),
@@ -483,7 +883,7 @@ func TestServer(t *testing.T) {
root, _ := clitest.New(t,
"server",
"--in-memory",
"--address", ":0",
"--http-address", ":0",
"--access-url", "http://example.com",
"--trace=true",
"--cache-dir", t.TempDir(),
@@ -521,7 +921,7 @@ func TestServer(t *testing.T) {
root, _ := clitest.New(t,
"server",
"--in-memory",
"--address", ":0",
"--http-address", ":0",
"--access-url", "http://example.com",
"--telemetry",
"--telemetry-url", server.URL,
@@ -552,7 +952,7 @@ func TestServer(t *testing.T) {
root, cfg := clitest.New(t,
"server",
"--in-memory",
"--address", ":0",
"--http-address", ":0",
"--access-url", "http://example.com",
"--provisioner-daemons", "1",
"--prometheus-enable",
@@ -605,7 +1005,7 @@ func TestServer(t *testing.T) {
root, cfg := clitest.New(t,
"server",
"--in-memory",
"--address", ":0",
"--http-address", ":0",
"--access-url", "http://example.com",
"--oauth2-github-allow-everyone",
"--oauth2-github-client-id", "fake",
@@ -646,7 +1046,7 @@ func TestServer(t *testing.T) {
root, cfg := clitest.New(t,
"server",
"--in-memory",
"--address", ":0",
"--http-address", ":0",
"--access-url", "http://example.com",
)
serverErr := make(chan error, 1)
@@ -674,7 +1074,7 @@ func TestServer(t *testing.T) {
root, cfg := clitest.New(t,
"server",
"--in-memory",
"--address", ":0",
"--http-address", ":0",
"--access-url", "http://example.com",
"--api-rate-limit", val,
)
@@ -702,7 +1102,7 @@ func TestServer(t *testing.T) {
root, cfg := clitest.New(t,
"server",
"--in-memory",
"--address", ":0",
"--http-address", ":0",
"--access-url", "http://example.com",
"--api-rate-limit", "-1",
)
+2 -2
View File
@@ -71,7 +71,7 @@ func speedtest() *cobra.Command {
return ctx.Err()
case <-ticker.C:
}
dur, err := conn.Ping(ctx)
dur, p2p, err := conn.Ping(ctx)
if err != nil {
continue
}
@@ -80,7 +80,7 @@ func speedtest() *cobra.Command {
continue
}
peer := status.Peer[status.Peers()[0]]
if peer.CurAddr == "" && direct {
if !p2p && direct {
cmd.Printf("Waiting for a direct connection... (%dms via %s)\n", dur.Milliseconds(), peer.Relay)
continue
}
+209 -4
View File
@@ -1,12 +1,15 @@
package cli
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
@@ -21,6 +24,7 @@ import (
"golang.org/x/term"
"golang.org/x/xerrors"
"github.com/coder/coder/agent"
"github.com/coder/coder/cli/cliflag"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/coderd/autobuild/notify"
@@ -39,6 +43,7 @@ func ssh() *cobra.Command {
stdio bool
shuffle bool
forwardAgent bool
forwardGPG bool
identityAgent string
wsPollInterval time.Duration
)
@@ -138,7 +143,7 @@ func ssh() *cobra.Command {
if forwardAgent && identityAgent != "" {
err = gosshagent.ForwardToRemote(sshClient, identityAgent)
if err != nil {
return xerrors.Errorf("forward agent failed: %w", err)
return xerrors.Errorf("forward agent: %w", err)
}
err = gosshagent.RequestAgentForwarding(sshSession)
if err != nil {
@@ -146,6 +151,22 @@ func ssh() *cobra.Command {
}
}
if forwardGPG {
if workspaceAgent.OperatingSystem == "windows" {
return xerrors.New("GPG forwarding is not supported for Windows workspaces")
}
err = uploadGPGKeys(ctx, sshClient)
if err != nil {
return xerrors.Errorf("upload GPG public keys and ownertrust to workspace: %w", err)
}
closer, err := forwardGPGAgent(ctx, cmd.ErrOrStderr(), sshClient)
if err != nil {
return xerrors.Errorf("forward GPG socket: %w", err)
}
defer closer.Close()
}
stdoutFile, validOut := cmd.OutOrStdout().(*os.File)
stdinFile, validIn := cmd.InOrStdin().(*os.File)
if validOut && validIn && isatty.IsTerminal(stdoutFile.Fd()) {
@@ -199,10 +220,12 @@ func ssh() *cobra.Command {
_ = sshSession.WindowChange(height, width)
}
}
err = sshSession.Wait()
if err != nil {
// If the connection drops unexpectedly, we get an ExitMissingError but no other
// error details, so try to at least give the user a better message
// If the connection drops unexpectedly, we get an
// ExitMissingError but no other error details, so try to at
// least give the user a better message
if errors.Is(err, &gossh.ExitMissingError{}) {
return xerrors.New("SSH connection ended unexpectedly")
}
@@ -216,6 +239,7 @@ func ssh() *cobra.Command {
cliflag.BoolVarP(cmd.Flags(), &shuffle, "shuffle", "", "CODER_SSH_SHUFFLE", false, "Specifies whether to choose a random workspace")
_ = cmd.Flags().MarkHidden("shuffle")
cliflag.BoolVarP(cmd.Flags(), &forwardAgent, "forward-agent", "A", "CODER_SSH_FORWARD_AGENT", false, "Specifies whether to forward the SSH agent specified in $SSH_AUTH_SOCK")
cliflag.BoolVarP(cmd.Flags(), &forwardGPG, "forward-gpg", "G", "CODER_SSH_FORWARD_GPG", false, "Specifies whether to forward the GPG agent. Unsupported on Windows workspaces, but supports all clients. Requires gnupg (gpg, gpgconf) on both the client and workspace. The GPG agent must already be running locally and will not be started for you. If a GPG agent is already running in the workspace, it will be attempted to be killed.")
cliflag.StringVarP(cmd.Flags(), &identityAgent, "identity-agent", "", "CODER_SSH_IDENTITY_AGENT", "", "Specifies which identity agent to use (overrides $SSH_AUTH_SOCK), forward agent must also be enabled")
cliflag.DurationVarP(cmd.Flags(), &wsPollInterval, "workspace-poll-interval", "", "CODER_WORKSPACE_POLL_INTERVAL", workspacePollInterval, "Specifies how often to poll for workspace automated shutdown.")
return cmd
@@ -232,7 +256,7 @@ func getWorkspaceAndAgent(ctx context.Context, cmd *cobra.Command, client *coder
)
if shuffle {
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
Owner: codersdk.Me,
Owner: userID,
})
if err != nil {
return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, err
@@ -364,3 +388,184 @@ func verifyWorkspaceOutdated(client *codersdk.Client, workspace codersdk.Workspa
func buildWorkspaceLink(serverURL *url.URL, workspace codersdk.Workspace) *url.URL {
return serverURL.ResolveReference(&url.URL{Path: fmt.Sprintf("@%s/%s", workspace.OwnerName, workspace.Name)})
}
// runLocal runs a command on the local machine.
func runLocal(ctx context.Context, stdin io.Reader, name string, args ...string) ([]byte, error) {
cmd := exec.CommandContext(ctx, name, args...)
cmd.Stdin = stdin
out, err := cmd.Output()
if err != nil {
var stderr []byte
if exitErr := new(exec.ExitError); errors.As(err, &exitErr) {
stderr = exitErr.Stderr
}
return out, xerrors.Errorf(
"`%s %s` failed: stderr: %s\n\nstdout: %s\n\n%w",
name,
strings.Join(args, " "),
bytes.TrimSpace(stderr),
bytes.TrimSpace(out),
err,
)
}
return out, nil
}
// runRemoteSSH runs a command on a remote machine/workspace via SSH.
func runRemoteSSH(sshClient *gossh.Client, stdin io.Reader, cmd string) ([]byte, error) {
sess, err := sshClient.NewSession()
if err != nil {
return nil, xerrors.Errorf("create SSH session")
}
defer sess.Close()
stderr := bytes.NewBuffer(nil)
sess.Stdin = stdin
sess.Stderr = stderr
out, err := sess.Output(cmd)
if err != nil {
return out, xerrors.Errorf(
"`%s` failed: stderr: %s\n\nstdout: %s:\n\n%w",
cmd,
bytes.TrimSpace(stderr.Bytes()),
bytes.TrimSpace(out),
err,
)
}
return out, nil
}
func uploadGPGKeys(ctx context.Context, sshClient *gossh.Client) error {
// Check if the agent is running in the workspace already.
//
// Note: we don't support windows in the workspace for GPG forwarding so
// using shell commands is fine.
//
// Note: we sleep after killing the agent because it doesn't always die
// immediately.
agentSocketBytes, err := runRemoteSSH(sshClient, nil, `
set -eux
agent_socket=$(gpgconf --list-dir agent-socket)
echo "$agent_socket"
if [ -S "$agent_socket" ]; then
echo "agent socket exists, attempting to kill it" >&2
gpgconf --kill gpg-agent
rm -f "$agent_socket"
sleep 1
fi
test ! -S "$agent_socket"
`)
agentSocket := strings.TrimSpace(string(agentSocketBytes))
if err != nil {
return xerrors.Errorf("check if agent socket is running (check if %q exists): %w", agentSocket, err)
}
if agentSocket == "" {
return xerrors.Errorf("agent socket path is empty, check the output of `gpgconf --list-dir agent-socket`")
}
// Read the user's public keys and ownertrust from GPG.
pubKeyExport, err := runLocal(ctx, nil, "gpg", "--armor", "--export")
if err != nil {
return xerrors.Errorf("export local public keys from GPG: %w", err)
}
ownerTrustExport, err := runLocal(ctx, nil, "gpg", "--export-ownertrust")
if err != nil {
return xerrors.Errorf("export local ownertrust from GPG: %w", err)
}
// Import the public keys and ownertrust into the workspace.
_, err = runRemoteSSH(sshClient, bytes.NewReader(pubKeyExport), "gpg --import")
if err != nil {
return xerrors.Errorf("import public keys into workspace: %w", err)
}
_, err = runRemoteSSH(sshClient, bytes.NewReader(ownerTrustExport), "gpg --import-ownertrust")
if err != nil {
return xerrors.Errorf("import ownertrust into workspace: %w", err)
}
// Kill the agent in the workspace if it was started by one of the above
// commands.
_, err = runRemoteSSH(sshClient, nil, fmt.Sprintf("gpgconf --kill gpg-agent && rm -f %q", agentSocket))
if err != nil {
return xerrors.Errorf("kill existing agent in workspace: %w", err)
}
return nil
}
func localGPGExtraSocket(ctx context.Context) (string, error) {
localSocket, err := runLocal(ctx, nil, "gpgconf", "--list-dir", "agent-extra-socket")
if err != nil {
return "", xerrors.Errorf("get local GPG agent socket: %w", err)
}
return string(bytes.TrimSpace(localSocket)), nil
}
func remoteGPGAgentSocket(sshClient *gossh.Client) (string, error) {
remoteSocket, err := runRemoteSSH(sshClient, nil, "gpgconf --list-dir agent-socket")
if err != nil {
return "", xerrors.Errorf("get remote GPG agent socket: %w", err)
}
return string(bytes.TrimSpace(remoteSocket)), nil
}
// cookieAddr is a special net.Addr accepted by sshForward() which includes a
// cookie which is written to the connection before forwarding.
type cookieAddr struct {
net.Addr
cookie []byte
}
// sshForwardRemote starts forwarding connections from a remote listener to a
// local address via SSH in a goroutine.
//
// Accepts a `cookieAddr` as the local address.
func sshForwardRemote(ctx context.Context, stderr io.Writer, sshClient *gossh.Client, localAddr, remoteAddr net.Addr) (io.Closer, error) {
listener, err := sshClient.Listen(remoteAddr.Network(), remoteAddr.String())
if err != nil {
return nil, xerrors.Errorf("listen on remote SSH address %s: %w", remoteAddr.String(), err)
}
go func() {
for {
remoteConn, err := listener.Accept()
if err != nil {
if ctx.Err() == nil {
_, _ = fmt.Fprintf(stderr, "Accept SSH listener connection: %+v\n", err)
}
return
}
go func() {
defer remoteConn.Close()
localConn, err := net.Dial(localAddr.Network(), localAddr.String())
if err != nil {
_, _ = fmt.Fprintf(stderr, "Dial local address %s: %+v\n", localAddr.String(), err)
return
}
defer localConn.Close()
if c, ok := localAddr.(cookieAddr); ok {
_, err = localConn.Write(c.cookie)
if err != nil {
_, _ = fmt.Fprintf(stderr, "Write cookie to local connection: %+v\n", err)
return
}
}
agent.Bicopy(ctx, localConn, remoteConn)
}()
}
}()
return listener, nil
}
+26
View File
@@ -5,9 +5,12 @@ package cli
import (
"context"
"io"
"net"
"os"
"os/signal"
gossh "golang.org/x/crypto/ssh"
"golang.org/x/sys/unix"
)
@@ -20,3 +23,26 @@ func listenWindowSize(ctx context.Context) <-chan os.Signal {
}()
return windowSize
}
func forwardGPGAgent(ctx context.Context, stderr io.Writer, sshClient *gossh.Client) (io.Closer, error) {
localSocket, err := localGPGExtraSocket(ctx)
if err != nil {
return nil, err
}
remoteSocket, err := remoteGPGAgentSocket(sshClient)
if err != nil {
return nil, err
}
localAddr := &net.UnixAddr{
Name: localSocket,
Net: "unix",
}
remoteAddr := &net.UnixAddr{
Name: remoteSocket,
Net: "unix",
}
return sshForwardRemote(ctx, stderr, sshClient, localAddr, remoteAddr)
}
+250 -1
View File
@@ -1,15 +1,20 @@
package cli_test
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"errors"
"fmt"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
@@ -27,6 +32,7 @@ import (
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
"github.com/coder/coder/pty"
"github.com/coder/coder/pty/ptytest"
"github.com/coder/coder/testutil"
)
@@ -65,6 +71,8 @@ func setupWorkspaceForAgent(t *testing.T, mutate func([]*proto.Agent) []*proto.A
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)
workspace, err := client.Workspace(context.Background(), workspace.ID)
require.NoError(t, err)
return client, workspace, agentToken
}
@@ -224,7 +232,7 @@ func TestSSH(t *testing.T) {
})
// Start up ssh agent listening on unix socket.
tmpdir := t.TempDir()
tmpdir := tempDirUnixSocket(t)
agentSock := filepath.Join(tmpdir, "agent.sock")
l, err := net.Listen("unix", agentSock)
require.NoError(t, err)
@@ -281,6 +289,224 @@ func TestSSH(t *testing.T) {
pty.WriteLine("exit")
<-cmdDone
})
//nolint:paralleltest // This test uses t.Setenv.
t.Run("ForwardGPG", func(t *testing.T) {
if runtime.GOOS == "windows" {
// While GPG forwarding from a Windows client works, we currently do
// not support forwarding to a Windows workspace. Our tests use the
// same platform for the "client" and "workspace" as they run in the
// same process.
t.Skip("Test not supported on windows")
}
// This key is for dean@coder.com.
const randPublicKeyFingerprint = "7BDFBA0CC7F5A96537C806C427BC6335EB5117F1"
const randPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBF6SWkEBEADB8sAhBaT36VQ6HEhAmtKexLldu1HUdXNw16rdF+1wiBzSFfJN
aPeX4Y9iFIZgC2wU0wOjJ04BpioyOLtJngbThI5WpeoQ/1yQZOpnDaCMPPLp+uJ+
Gy4tMZYWQq21PukrFm3XDRGKjVN58QN6uCPb1S/YzteP8Epmq590GYIYLiAHnMt6
5iyxIFhXj/fq5Fddp2+efI7QWvNl2wTNnCaTziOSKYcbNmQpn9gy0WvKktWYtB8E
JJtWES0DzgCnDpm/hYx79Wkb+F7qY54y2uauDx+z97QXrON47lsIyGm8/T59ZfSd
/yrBqDLHYrHlt9RkFpAnBzO402y2eHsKTB6/EAHv9H2apxahyJlcxGbE5QE+fOJk
LdPlako0cSljz0g9Icesr2nZL0MhWwLnwk7DHkg/PUUijkbuR/TD9dti2/yOTFrf
Y7DdZpoZ0ZkcGu9lMh2vOTWc96RNCyIZfE5WNDKKo+u5Txzndsc/qIgKohwDSxTC
3hAulG5Wt05UeyHBEAAvGV2szG88VsGwd1juqXAbEzk+kLQzNyoQX188/4V4X+MV
pY9Wz7JudmQpB/3+YTcA/ziK/+wu3c2wNlr7gMZYMOwDWTLfW64nux7zHWDytrP0
HfgJIgqP7F7SnChpTFdb1hr1WDox99ZG+/eDkwxnuXYWm9xx5/crqQ0POQARAQAB
tClEZWFuIFNoZWF0aGVyICh3b3JrIGtleSkgPGRlYW5AY29kZXIuY29tPokCVAQT
AQgAPhYhBHvfugzH9allN8gGxCe8YzXrURfxBQJeklpBAhsDBQkJZgGABQsJCAcC
BhUKCQgLAgQWAgMBAh4BAheAAAoJECe8YzXrURfxIVkP/3UJMzvIjTNF63WiK4xk
TXlBbPKodnzUmAJ+8DVXmJMJpNsSI2czw6eFUXMcrT3JMlviOXhRWMLHr2FsQhyS
AJOQo0x9z7nntPIkvj96ihCdgRn7VN1WzaMwOOesGPr57StWLE84bg9/R0aSsxtX
LgfBCyNkv6FFlruhnw8+JdZJEjvIXQ9swvwD6L68ZLWIWcdnj/CjQmnmgFA+O4UO
SFXMUjklbrq8mJ0sAPUUATJK0SOTyqkZPkhqjlTZa8p0XoJF25trhwLhzDi4GPR6
SK/9SkqB/go9ZwkNZOjs2tP7eMExy4zQ21MFH09JMKQB7H5CG8GwdMwz4+VKc9aP
y9Ncova/p7Y8kJ7oQPWhACJT1jMP6620oC2N/7wwS0Vtc6E9LoPrfXC2TtvOA9qx
aOf6riWSjo8BEcXDuMtlW4g6IQFNd0+wcgcKrAd+vPLZnG4rtYL0Etdd1ymBT4pi
5E5uT8oUT9rLHX+2tD/E8SE5PzsaKEOJKzcOB8ESb3YBGic7+VvX/AuJuSFsuWnZ
FqAUENqfdz6+0dEJe1pfWyje+Q+o7B7u+ffMT4dOQOC8NfHFnz1kU+DA3VDE6xsu
3YN1L8KlYON92s9VWDA8VuvmU2d9pq5ysUeg133ftDSwj3X+5GYcBv4VFcSRCBW5
w0hDpMDun1t8xcXdo1LQ4R4NuQINBF6SWkEBEADF4Nrhlqc5M3Sz9sNHDJZR68zb
4CjkoOpYwsKj/ZCukzRCGKpT5Agn0zOycUjbAyCZVjREeIRRURyAhfpOmZY5yF6b
PD93+04OzWk1AaDRmMfvi1Crn/WUEVHIbDaisxDzNuAJgLrt93I/lOz06GczhCb6
sPBeKuaXCLl/5LSwTahGWsweeSCmfyrYsOc11T+SjdyWXWXEpzFNNIhvqiEoJCw3
IcdktTBJYuHsN4jh5kVemi/ttqRN3z7rBMKR1sPG3ux1MfCfSTSCeZLTN9eVvqm9
ne8brk8ZC6sdwlZ9IofPbmSaAh+F5Kfcnd3KjmyQ63t+8plpJ2YH3Fx6IwTwVEQ8
Ii3WQInTpBSPqf0EwnzRBvhYeKusRpcmX3JSmosLbd5uhvJdgotzuwZYzgay/6DL
OlwElZ//ecXNhU8iYmx1BwNuquvGcGVpkP5eaaT6O9qDznB7TT0xztfAK0LaAuRJ
HOFCc8iiHtQ4o0OkRhg/0KkUGBU5Iw5SIDimkgwJMtD3ZiYOqLaXS6kmmVw2u6YD
LB8rTpegz/tcX+4uyfnIZ28JCOYFTeaDT4FixFW2hrfo/VJzMI5IIv9XAAmtAiEU
f+CY2BT6kg9NkQuke0p4/W8yTaScapYZa5I2bzFpJJyzh1TKE6x3qcbBs9vVX+6E
vK4FflNwu9WSWojO2wARAQABiQI8BBgBCAAmFiEEe9+6DMf1qWU3yAbEJ7xjNetR
F/EFAl6SWkECGwwFCQlmAYAACgkQJ7xjNetRF/FpnQ//SIYePQzhvWj9drnT2krG
dUGSxCN0pA2UQZNkreAaKmyxn2/6xEdxYSz0iUEk+I0HKay+NLCxJ5PDoDBypFtM
f0yOnbWRObhim8HmED4JRw678G4hRU7KEN0L/9SUYlsBNbgr1xYM/CUX/Ih9NT+P
eApxs2VgjKii6m81nfBCFpWSxAs+TOnbshp8dlDZk9kxjFH9+h1ffgZjntqeyiWe
F1UE1Wh32MbJdtc2Y3mrA6i+7+3OXmqMHoiG1obhISgdpaCJ/ub3ywnAmeXSiAKE
IuS6CriR71Wqv8LMQ8kPM8On9Q26d1dsKKBnlFop9oexxf1AFsbbf9gkcgb+uNno
1Qr/R6l2H1TcV1gmiyQLzVnkgLRORosLvSlFrisrsLv9uTYYgcGvwKiU/o3PTdQg
fv0D7LB+a3C9KsCBFjihW3bTOcHKX2sAWEQXZMtKGf5aNTBmWQ+eKWUGpudXIvLE
od5lgfk9p8T1R50KDieG/+2X95zxFSYBoPRAfp7JNT7h+TZ55qUmQXZGI1VqhWiq
b6y/yqfI17JCm4oWpXYbgeruLuye2c/ptDc3S3d26hbWYiWKVT4bLtUGR0wuE6lS
DK0u4LK+mnrYfIvRDYJGx18/nbLpR+ivWLIssJT2Jyyj8w9+hk10XkODySNjHCxj
p7KeSZdlk47pMBGOfnvEmoQ=
=OxHv
-----END PGP PUBLIC KEY BLOCK-----`
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
gpgPath, err := exec.LookPath("gpg")
if err != nil {
t.Skip("gpg not found")
}
gpgConfPath, err := exec.LookPath("gpgconf")
if err != nil {
t.Skip("gpgconf not found")
}
gpgAgentPath, err := exec.LookPath("gpg-agent")
if err != nil {
t.Skip("gpg-agent not found")
}
// Setup GPG home directory on the "client".
gnupgHomeClient := tempDirUnixSocket(t)
t.Setenv("GNUPGHOME", gnupgHomeClient)
// Get the agent extra socket path.
var (
stdout = bytes.NewBuffer(nil)
stderr = bytes.NewBuffer(nil)
)
c := exec.CommandContext(ctx, gpgConfPath, "--list-dir", "agent-extra-socket")
c.Stdout = stdout
c.Stderr = stderr
err = c.Run()
require.NoError(t, err, "get extra socket path failed: %s", stderr.String())
extraSocketPath := strings.TrimSpace(stdout.String())
// Generate private key non-interactively.
genKeyScript := `
Key-Type: 1
Key-Length: 2048
Subkey-Type: 1
Subkey-Length: 2048
Name-Real: Coder Test
Name-Email: test@coder.com
Expire-Date: 0
%no-protection
`
c = exec.CommandContext(ctx, gpgPath, "--batch", "--gen-key")
c.Stdin = strings.NewReader(genKeyScript)
out, err := c.CombinedOutput()
require.NoError(t, err, "generate key failed: %s", out)
// Import a random public key.
stdin := strings.NewReader(randPublicKey + "\n")
c = exec.CommandContext(ctx, gpgPath, "--import", "-")
c.Stdin = stdin
out, err = c.CombinedOutput()
require.NoError(t, err, "import key failed: %s", out)
// Set ultimate trust on imported key.
stdin = strings.NewReader(randPublicKeyFingerprint + ":6:\n")
c = exec.CommandContext(ctx, gpgPath, "--import-ownertrust")
c.Stdin = stdin
out, err = c.CombinedOutput()
require.NoError(t, err, "import ownertrust failed: %s", out)
// Start the GPG agent.
agentCmd := exec.CommandContext(ctx, gpgAgentPath, "--no-detach", "--extra-socket", extraSocketPath)
agentCmd.Env = append(agentCmd.Env, "GNUPGHOME="+gnupgHomeClient)
agentPTY, agentProc, err := pty.Start(agentCmd, pty.WithPTYOption(pty.WithGPGTTY()))
require.NoError(t, err, "launch agent failed")
defer func() {
_ = agentProc.Kill()
_ = agentPTY.Close()
}()
// Get the agent socket path in the "workspace".
gnupgHomeWorkspace := tempDirUnixSocket(t)
stdout = bytes.NewBuffer(nil)
stderr = bytes.NewBuffer(nil)
c = exec.CommandContext(ctx, gpgConfPath, "--list-dir", "agent-socket")
c.Env = append(c.Env, "GNUPGHOME="+gnupgHomeWorkspace)
c.Stdout = stdout
c.Stderr = stderr
err = c.Run()
require.NoError(t, err, "get agent socket path in workspace failed: %s", stderr.String())
workspaceAgentSocketPath := strings.TrimSpace(stdout.String())
require.NotEqual(t, extraSocketPath, workspaceAgentSocketPath, "socket path should be different")
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
agentClient := codersdk.New(client.URL)
agentClient.SetSessionToken(agentToken)
agentCloser := agent.New(agent.Options{
Client: agentClient,
EnvironmentVariables: map[string]string{
"GNUPGHOME": gnupgHomeWorkspace,
},
Logger: slogtest.Make(t, nil).Named("agent"),
})
defer agentCloser.Close()
cmd, root := clitest.New(t,
"ssh",
workspace.Name,
"--forward-gpg",
)
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
cmd.SetErr(pty.Output())
cmdDone := tGo(t, func() {
err := cmd.ExecuteContext(ctx)
assert.NoError(t, err, "ssh command failed")
})
// Prevent the test from hanging if the asserts below kill the test
// early. This will cause the command to exit with an error, which will
// let the t.Cleanup'd `<-done` inside of `tGo` exit and not hang.
// Without this, the test will hang forever on failure, preventing the
// real error from being printed.
t.Cleanup(cancel)
pty.WriteLine("echo hello 'world'")
pty.ExpectMatch("hello world")
// Check the GNUPGHOME was correctly inherited via shell.
pty.WriteLine("env && echo env-''-command-done")
match := pty.ExpectMatch("env--command-done")
require.Contains(t, match, "GNUPGHOME="+gnupgHomeWorkspace, match)
// Get the agent extra socket path in the "workspace" via shell.
pty.WriteLine("gpgconf --list-dir agent-socket && echo gpgconf-''-agentsocket-command-done")
pty.ExpectMatch(workspaceAgentSocketPath)
pty.ExpectMatch("gpgconf--agentsocket-command-done")
// List the keys in the "workspace".
pty.WriteLine("gpg --list-keys && echo gpg-''-listkeys-command-done")
listKeysOutput := pty.ExpectMatch("gpg--listkeys-command-done")
require.Contains(t, listKeysOutput, "[ultimate] Coder Test <test@coder.com>")
require.Contains(t, listKeysOutput, "[ultimate] Dean Sheather (work key) <dean@coder.com>")
// Try to sign something. This demonstrates that the forwarding is
// working as expected, since the workspace doesn't have access to the
// private key directly and must use the forwarded agent.
pty.WriteLine("echo 'hello world' | gpg --clearsign && echo gpg-''-sign-command-done")
pty.ExpectMatch("BEGIN PGP SIGNED MESSAGE")
pty.ExpectMatch("Hash:")
pty.ExpectMatch("hello world")
pty.ExpectMatch("gpg--sign-command-done")
// And we're done.
pty.WriteLine("exit")
<-cmdDone
})
}
// tGoContext runs fn in a goroutine passing a context that will be
@@ -354,3 +580,26 @@ func (*stdioConn) SetReadDeadline(_ time.Time) error {
func (*stdioConn) SetWriteDeadline(_ time.Time) error {
return nil
}
// tempDirUnixSocket returns a temporary directory that can safely hold unix
// sockets (probably).
//
// During tests on darwin we hit the max path length limit for unix sockets
// pretty easily in the default location, so this function uses /tmp instead to
// get shorter paths.
func tempDirUnixSocket(t *testing.T) string {
t.Helper()
if runtime.GOOS == "darwin" {
testName := strings.ReplaceAll(t.Name(), "/", "_")
dir, err := os.MkdirTemp("/tmp", fmt.Sprintf("coder-test-%s-", testName))
require.NoError(t, err, "create temp dir for gpg test")
t.Cleanup(func() {
err := os.RemoveAll(dir)
assert.NoError(t, err, "remove temp dir", dir)
})
return dir
}
return t.TempDir()
}
+78
View File
@@ -4,9 +4,16 @@
package cli
import (
"bufio"
"context"
"io"
"net"
"os"
"strconv"
"time"
gossh "golang.org/x/crypto/ssh"
"golang.org/x/xerrors"
)
func listenWindowSize(ctx context.Context) <-chan os.Signal {
@@ -25,3 +32,74 @@ func listenWindowSize(ctx context.Context) <-chan os.Signal {
}()
return windowSize
}
func forwardGPGAgent(ctx context.Context, stderr io.Writer, sshClient *gossh.Client) (io.Closer, error) {
// Read TCP port and cookie from extra socket file. A gpg-agent socket
// file looks like the following:
//
// 49955
// abcdefghijklmnop
//
// The first line is the TCP port that gpg-agent is listening on, and
// the second line is a 16 byte cookie that MUST be sent as the first
// bytes of any connection to this port (otherwise the connection is
// closed by gpg-agent).
localSocket, err := localGPGExtraSocket(ctx)
if err != nil {
return nil, err
}
f, err := os.Open(localSocket)
if err != nil {
return nil, xerrors.Errorf("open gpg-agent-extra socket file %q: %w", localSocket, err)
}
// Scan lines from file to get port and cookie.
var (
port uint16
cookie []byte
scanner = bufio.NewScanner(f)
)
for i := 0; scanner.Scan(); i++ {
switch i {
case 0:
port64, err := strconv.ParseUint(scanner.Text(), 10, 16)
if err != nil {
return nil, xerrors.Errorf("parse gpg-agent-extra socket file %q: line 1: convert string to integer: %w", localSocket, err)
}
port = uint16(port64)
case 1:
cookie = scanner.Bytes()
if len(cookie) != 16 {
return nil, xerrors.Errorf("parse gpg-agent-extra socket file %q: line 2: expected 16 bytes, got %v bytes", localSocket, len(cookie))
}
default:
return nil, xerrors.Errorf("parse gpg-agent-extra socket file %q: file contains more than 2 lines", localSocket)
}
}
err = scanner.Err()
if err != nil {
return nil, xerrors.Errorf("parse gpg-agent-extra socket file: %q: %w", localSocket, err)
}
remoteSocket, err := remoteGPGAgentSocket(sshClient)
if err != nil {
return nil, err
}
localAddr := cookieAddr{
Addr: &net.TCPAddr{
IP: net.IPv4(127, 0, 0, 1),
Port: int(port),
},
cookie: cookie,
}
remoteAddr := &net.UnixAddr{
Name: remoteSocket,
Net: "unix",
}
return sshForwardRemote(ctx, stderr, sshClient, localAddr, remoteAddr)
}
+13 -2
View File
@@ -187,14 +187,25 @@ func TestTemplateCreate(t *testing.T) {
match string
write string
}{
{match: "Create and upload", write: "yes"},
{
match: "Create and upload",
write: "yes",
},
{
match: "Enter a value:",
write: "bingo",
},
{
match: "Confirm create?",
write: "yes",
},
}
for _, m := range matches {
pty.ExpectMatch(m.match)
pty.WriteLine(m.write)
}
require.EqualError(t, <-execDone, "Parameter value absent in parameter file for \"region\"!")
require.NoError(t, <-execDone)
})
t.Run("Recreate template with same name (create, delete, create)", func(t *testing.T) {
+1
View File
@@ -117,6 +117,7 @@ func templatePush() *cobra.Command {
cmd.Flags().StringVarP(&provisioner, "test.provisioner", "", "terraform", "Customize the provisioner backend")
cmd.Flags().StringVarP(&parameterFile, "parameter-file", "", "", "Specify a file path with parameter values.")
cmd.Flags().StringVarP(&versionName, "name", "", "", "Specify a name for the new template version. It will be automatically generated if not provided.")
cmd.Flags().StringArrayVarP(&provisionerTags, "provisioner-tag", "", []string{}, "Specify a set of tags to target provisioner daemons.")
cmd.Flags().BoolVar(&alwaysPrompt, "always-prompt", false, "Always prompt all parameters. Does not pull parameter values from active template version")
cliui.AllowSkipPrompt(cmd)
// This is for testing!
+2
View File
@@ -23,6 +23,7 @@ Commands:
port-forward Forward ports from machine to a workspace
publickey Output your Coder public key used for Git operations
reset-password Directly connect to the database to reset a user's password
scaletest Run a scale test against the Coder API
server Start a Coder server
state Manually manage Terraform state to fix broken workspaces
templates Manage templates
@@ -35,6 +36,7 @@ Workspace Commands:
create Create a workspace
delete Delete a workspace
list List workspaces
rename Rename a workspace
schedule Schedule automated start and stop times for workspaces
show Display details of a workspace's resources and agents
speedtest Run upload and download tests from your machine to a workspace
+43
View File
@@ -0,0 +1,43 @@
Add an SSH Host entry for your workspaces "ssh coder.workspace"
Usage:
coder config-ssh [flags]
Get Started:
- You can use -o (or --ssh-option) so set SSH options to be used for all your
workspaces:
$ coder config-ssh -o ForwardAgent=yes
- You can use --dry-run (or -n) to see the changes that would be made:
$ coder config-ssh --dry-run
Flags:
-n, --dry-run Perform a trial run with no changes made, showing a diff at
the end.
-h, --help help for config-ssh
--ssh-config-file string Specifies the path to an SSH config.
Consumes $CODER_SSH_CONFIG_FILE (default "~/.ssh/config")
-o, --ssh-option stringArray Specifies additional SSH options to embed in each host stanza.
--use-previous-options Specifies whether or not to keep options from previous run of
config-ssh.
Consumes $CODER_SSH_USE_PREVIOUS_OPTIONS
-y, --yes Bypass prompts
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+36
View File
@@ -0,0 +1,36 @@
Create a workspace
Usage:
coder create [name] [flags]
Flags:
-h, --help help for create
--parameter-file string Specify a file path with parameter values.
Consumes $CODER_PARAMETER_FILE
--start-at coder schedule start --help Specify the workspace autostart schedule. Check
coder schedule start --help for the syntax.
Consumes $CODER_WORKSPACE_START_AT
--stop-after duration Specify a duration after which the workspace
should shut down (e.g. 8h).
Consumes $CODER_WORKSPACE_STOP_AFTER (default
8h0m0s)
-t, --template string Specify a template name.
Consumes $CODER_TEMPLATE_NAME
-y, --yes Bypass prompts
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+30
View File
@@ -0,0 +1,30 @@
Delete a workspace
Usage:
coder delete <workspace> [flags]
Aliases:
delete, rm
Flags:
-h, --help help for delete
--orphan Delete a workspace without deleting its resources. This can delete a
workspace in a broken state, but may also lead to unaccounted cloud resources.
-y, --yes Bypass prompts
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+33
View File
@@ -0,0 +1,33 @@
Checkout and install a dotfiles repository from a Git URL
Usage:
coder dotfiles [git_repo_url] [flags]
Get Started:
- Check out and install a dotfiles repository without prompts:
$ coder dotfiles --yes git@github.com:example/dotfiles.git
Flags:
-h, --help help for dotfiles
--symlink-dir string Specifies the directory for the dotfiles symlink destinations. If
empty will use $HOME.
Consumes $CODER_SYMLINK_DIR
-y, --yes Bypass prompts
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+32
View File
@@ -0,0 +1,32 @@
List workspaces
Usage:
coder list [flags]
Aliases:
list, ls
Flags:
-a, --all Specifies whether all workspaces will be listed or not.
-c, --column stringArray Specify a column to filter in the table. Available columns are:
workspace, template, status, last_built, outdated, starts_at,
stops_after
-h, --help help for list
--search string Search for a workspace with a query. (default "owner:me")
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+36
View File
@@ -0,0 +1,36 @@
Authenticate with Coder deployment
Usage:
coder login <url> [flags]
Flags:
--first-user-email string Specifies an email address to use if creating the first
user for the deployment.
Consumes $CODER_FIRST_USER_EMAIL
--first-user-password string Specifies a password to use if creating the first user
for the deployment.
Consumes $CODER_FIRST_USER_PASSWORD
--first-user-trial Specifies whether a trial license should be provisioned
for the Coder deployment or not.
Consumes $CODER_FIRST_USER_TRIAL
--first-user-username string Specifies a username to use if creating the first user
for the deployment.
Consumes $CODER_FIRST_USER_USERNAME
-h, --help help for login
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+25
View File
@@ -0,0 +1,25 @@
Unauthenticate your local session
Usage:
coder logout [flags]
Flags:
-h, --help help for logout
-y, --yes Bypass prompts
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+51
View File
@@ -0,0 +1,51 @@
Forward ports from machine to a workspace
Usage:
coder port-forward <workspace> [flags]
Aliases:
port-forward, tunnel
Get Started:
- Port forward a single TCP port from 1234 in the workspace to port 5678 on
your local machine:
$ coder port-forward <workspace> --tcp 5678:1234
- Port forward a single UDP port from port 9000 to port 9000 on your local
machine:
$ coder port-forward <workspace> --udp 9000
- Port forward multiple TCP ports and a UDP port:
$ coder port-forward <workspace> --tcp 8080:8080 --tcp 9000:3000 --udp 5353:53
- Port forward multiple ports (TCP or UDP) in condensed syntax:
$ coder port-forward <workspace> --tcp 8080,9000:3000,9090-9092,10000-10002:10010-10012
Flags:
-h, --help help for port-forward
-p, --tcp stringArray Forward TCP port(s) from the workspace to the local machine.
Consumes $CODER_PORT_FORWARD_TCP
--udp stringArray Forward UDP port(s) from the workspace to the local machine. The UDP
connection has TCP-like semantics to support stateful UDP protocols.
Consumes $CODER_PORT_FORWARD_UDP
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+30
View File
@@ -0,0 +1,30 @@
Output your Coder public key used for Git operations
Usage:
coder publickey [flags]
Aliases:
publickey, pubkey
Flags:
-h, --help help for publickey
--reset Regenerate your public key. This will require updating the key on any services
it's registered with.
-y, --yes Bypass prompts
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+25
View File
@@ -0,0 +1,25 @@
Rename a workspace
Usage:
coder rename <workspace> <new name> [flags]
Flags:
-h, --help help for rename
-y, --yes Bypass prompts
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+26
View File
@@ -0,0 +1,26 @@
Directly connect to the database to reset a user's password
Usage:
coder reset-password <username> [flags]
Flags:
-h, --help help for reset-password
--postgres-url string URL of a PostgreSQL database to connect to.
Consumes $CODER_PG_CONNECTION_URL
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+32
View File
@@ -0,0 +1,32 @@
Perform scale tests against the Coder server.
Usage:
coder scaletest [flags]
coder scaletest [command]
Commands:
cleanup Cleanup any orphaned scaletest resources
create-workspaces Creates many workspaces and waits for them to be ready
Flags:
-h, --help help for scaletest
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
Use "coder scaletest [command] --help" for more information about a command.
+32
View File
@@ -0,0 +1,32 @@
Cleanup scaletest workspaces, then cleanup scaletest users. The strategy flags will apply to each stage of the cleanup process.
Usage:
coder scaletest cleanup [flags]
Flags:
--cleanup-concurrency int Number of concurrent cleanup jobs to run. 0 means
unlimited.
Consumes $CODER_LOADTEST_CLEANUP_CONCURRENCY (default 1)
--cleanup-job-timeout duration Timeout per job. Jobs may take longer to complete under
higher concurrency limits.
Consumes $CODER_LOADTEST_CLEANUP_JOB_TIMEOUT (default 5m0s)
--cleanup-timeout duration Timeout for the entire cleanup run. 0 means unlimited.
Consumes $CODER_LOADTEST_CLEANUP_TIMEOUT (default 30m0s)
-h, --help help for cleanup
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
@@ -0,0 +1,131 @@
Creates many users, then creates a workspace for each user and waits for them finish building and fully come online. Optionally runs a command inside each workspace, and connects to the workspace over WireGuard.
It is recommended that all rate limits are disabled on the server before running this scaletest. This test generates many login events which will be rate limited against the (most likely single) IP.
Usage:
coder scaletest create-workspaces [flags]
Flags:
--cleanup-concurrency int Number of concurrent cleanup jobs to run. 0 means
unlimited.
Consumes $CODER_LOADTEST_CLEANUP_CONCURRENCY
(default 1)
--cleanup-job-timeout duration Timeout per job. Jobs may take longer to complete
under higher concurrency limits.
Consumes $CODER_LOADTEST_CLEANUP_JOB_TIMEOUT
(default 5m0s)
--cleanup-timeout duration Timeout for the entire cleanup run. 0 means
unlimited.
Consumes $CODER_LOADTEST_CLEANUP_TIMEOUT (default
30m0s)
--concurrency int Number of concurrent jobs to run. 0 means
unlimited.
Consumes $CODER_LOADTEST_CONCURRENCY (default 1)
--connect-hold duration How long to hold the WireGuard connection open
for.
Consumes $CODER_LOADTEST_CONNECT_HOLD (default 30s)
--connect-interval duration How long to wait between making requests to the
--connect-url once the connection is established.
Consumes $CODER_LOADTEST_CONNECT_INTERVAL (default 1s)
--connect-mode string Mode to use for connecting to the workspace. Can
be 'derp' or 'direct'.
Consumes $CODER_LOADTEST_CONNECT_MODE (default "derp")
--connect-timeout duration Timeout for each request to the --connect-url.
Consumes $CODER_LOADTEST_CONNECT_TIMEOUT (default 5s)
--connect-url string URL to connect to inside the the workspace over
WireGuard. If not specified, no connections will
be made over WireGuard.
Consumes $CODER_LOADTEST_CONNECT_URL
-c, --count int Required: Number of workspaces to create.
Consumes $CODER_LOADTEST_COUNT (default 1)
-h, --help help for create-workspaces
--job-timeout duration Timeout per job. Jobs may take longer to complete
under higher concurrency limits.
Consumes $CODER_LOADTEST_JOB_TIMEOUT (default 5m0s)
--no-cleanup coder scaletest cleanup Do not clean up resources after the test
completes. You can cleanup manually using coder
scaletest cleanup.
Consumes $CODER_LOADTEST_NO_CLEANUP
--no-plan Skip the dry-run step to plan the workspace
creation. This step ensures that the given
parameters are valid for the given template.
Consumes $CODER_LOADTEST_NO_PLAN
--no-wait-for-agents Do not wait for agents to start before marking
the test as succeeded. This can be useful if you
are running the test against a template that does
not start the agent quickly.
Consumes $CODER_LOADTEST_NO_WAIT_FOR_AGENTS
--output stringArray Output format specs in the format
"<format>[:<path>]". Not specifying a path will
default to stdout. Available formats: text, json.
Consumes $CODER_SCALETEST_OUTPUTS (default [text])
--parameter stringArray Parameters to use for each workspace. Can be
specified multiple times. Overrides any existing
parameters with the same name from
--parameters-file. Format: key=value.
Consumes $CODER_LOADTEST_PARAMETERS
--parameters-file string Path to a YAML file containing the parameters to
use for each workspace.
Consumes $CODER_LOADTEST_PARAMETERS_FILE
--run-command string Command to run inside each workspace using
reconnecting-pty (i.e. web terminal protocol). If
not specified, no command will be run.
Consumes $CODER_LOADTEST_RUN_COMMAND
--run-expect-output string Expect the command to output the given string (on
a single line). If the command does not output
the given string, it will be marked as failed.
Consumes $CODER_LOADTEST_RUN_EXPECT_OUTPUT
--run-expect-timeout Expect the command to timeout. If the command
does not finish within the given --run-timeout,
it will be marked as succeeded. If the command
finishes before the timeout, it will be marked as
failed.
Consumes $CODER_LOADTEST_RUN_EXPECT_TIMEOUT
--run-log-output Log the output of the command to the test logs.
This should be left off unless you expect small
amounts of output. Large amounts of output will
cause high memory usage.
Consumes $CODER_LOADTEST_RUN_LOG_OUTPUT
--run-timeout duration Timeout for the command to complete.
Consumes $CODER_LOADTEST_RUN_TIMEOUT (default 5s)
-t, --template string Required: Name or ID of the template to use for
workspaces.
Consumes $CODER_LOADTEST_TEMPLATE
--timeout duration Timeout for the entire test run. 0 means
unlimited.
Consumes $CODER_LOADTEST_TIMEOUT (default 30m0s)
--trace Whether application tracing data is collected. It
exports to a backend configured by environment
variables. See:
https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md.
Consumes $CODER_LOADTEST_TRACE
--trace-coder Whether opentelemetry traces are sent to Coder.
We recommend keeping this disabled unless we
advise you to enable it.
Consumes $CODER_LOADTEST_TRACE_CODER
--trace-honeycomb-api-key string Enables trace exporting to Honeycomb.io using the
provided API key.
Consumes $CODER_LOADTEST_TRACE_HONEYCOMB_API_KEY
--trace-propagate Enables trace propagation to the Coder backend,
which will be used to correlate server-side spans
with client-side spans. Only enable this if the
server is configured with the exact same tracing
configuration as the client.
Consumes $CODER_LOADTEST_TRACE_PROPAGATE
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+34
View File
@@ -0,0 +1,34 @@
Schedule automated start and stop times for workspaces
Usage:
coder schedule { show | start | stop | override } <workspace> [flags]
coder schedule [command]
Commands:
override-stop Edit stop time of active workspace
show Show workspace schedule
start Edit workspace start schedule
stop Edit workspace stop schedule
Flags:
-h, --help help for schedule
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
Use "coder schedule [command] --help" for more information about a command.
+30
View File
@@ -0,0 +1,30 @@
Override the stop time of a currently running workspace instance.
* The new stop time is calculated from *now*.
* The new stop time must be at least 30 minutes in the future.
* The workspace template may restrict the maximum workspace runtime.
Usage:
coder schedule override-stop <workspace-name> <duration from now> [flags]
Get Started:
$ coder schedule override-stop my-workspace 90m
Flags:
-h, --help help for override-stop
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+28
View File
@@ -0,0 +1,28 @@
Shows the following information for the given workspace:
* The automatic start schedule
* The next scheduled start time
* The duration after which it will stop
* The next scheduled stop time
Usage:
coder schedule show <workspace-name> [flags]
Flags:
-h, --help help for show
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+37
View File
@@ -0,0 +1,37 @@
Schedules a workspace to regularly start at a specific time.
Schedule format: <start-time> [day-of-week] [location].
* Start-time (required) is accepted either in 12-hour (hh:mm{am|pm}) format, or 24-hour format hh:mm.
* Day-of-week (optional) allows specifying in the cron format, e.g. 1,3,5 or Mon-Fri.
Aliases such as @daily are not supported.
Default: * (every day)
* Location (optional) must be a valid location in the IANA timezone database.
If omitted, we will fall back to either the TZ environment variable or /etc/localtime.
You can check your corresponding location by visiting https://ipinfo.io - it shows in the demo widget on the right.
Usage:
coder schedule start <workspace-name> { <start-time> [day-of-week] [location] | manual } [flags]
Get Started:
- Set the workspace to start at 9:30am (in Dublin) from Monday to Friday:
$ coder schedule start my-workspace 9:30AM Mon-Fri Europe/Dublin
Flags:
-h, --help help for start
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+38
View File
@@ -0,0 +1,38 @@
Schedules a workspace to stop after a given duration has elapsed.
* Workspace runtime is measured from the time that the workspace build completed.
* The minimum scheduled stop time is 1 minute.
* The workspace template may place restrictions on the maximum shutdown time.
* Changes to workspace schedules only take effect upon the next build of the workspace,
and do not affect a running instance of a workspace.
When enabling scheduled stop, enter a duration in one of the following formats:
* 3h2m (3 hours and two minutes)
* 3h (3 hours)
* 2m (2 minutes)
* 2 (2 minutes)
Usage:
coder schedule stop <workspace-name> { <duration> | manual } [flags]
Get Started:
$ coder schedule stop my-workspace 2h30m
Flags:
-h, --help help for stop
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+31 -7
View File
@@ -14,16 +14,14 @@ Flags:
This must be accessible by all
provisioned workspaces.
Consumes $CODER_ACCESS_URL
-a, --address string Bind address of the server.
Consumes $CODER_ADDRESS (default
"127.0.0.1:3000")
--api-rate-limit int Maximum number of requests per minute
allowed to the API per user, or per IP
address for unauthenticated users.
Negative values mean no rate limit. Some
API endpoints are always rate limited
regardless of this value to prevent
denial-of-service attacks.
API endpoints have separate strict rate
limits regardless of this value to
prevent denial-of-service or brute force
attacks.
Consumes $CODER_API_RATE_LIMIT (default 512)
--cache-dir string The directory to cache temporary files.
If unspecified and $CACHE_DIRECTORY is
@@ -31,6 +29,9 @@ Flags:
with systemd.
Consumes $CODER_CACHE_DIRECTORY (default
"/tmp/coder-cli-test-cache")
--dangerous-disable-rate-limits Disables all rate limits. This is not
recommended in production.
Consumes $CODER_RATE_LIMIT_DISABLE_ALL
--derp-config-path string Path to read a DERP mapping from. See:
https://tailscale.com/kb/1118/custom-derp-servers/
Consumes $CODER_DERP_CONFIG_PATH
@@ -65,6 +66,14 @@ Flags:
production.
Consumes $CODER_EXPERIMENTAL
-h, --help help for server
--http-address string HTTP bind address of the server. Unset to
disable the HTTP endpoint.
Consumes $CODER_HTTP_ADDRESS (default
"127.0.0.1:3000")
--max-token-lifetime duration The maximum lifetime duration for any
user creating a token.
Consumes $CODER_MAX_TOKEN_LIFETIME
(default 720h0m0s)
--oauth2-github-allow-everyone Allow all logins, setting this option
means allowed orgs and teams must be
empty.
@@ -107,6 +116,9 @@ Flags:
OIDC.
Consumes $CODER_OIDC_SCOPES (default
[openid,profile,email])
--oidc-username-field string OIDC claim field to use as the username.
Consumes $CODER_OIDC_USERNAME_FIELD
(default "preferred_username")
--postgres-url string URL of a PostgreSQL database. If empty,
PostgreSQL binaries will be downloaded
from Maven
@@ -164,6 +176,8 @@ Flags:
"ecdsa", or "rsa4096".
Consumes $CODER_SSH_KEYGEN_ALGORITHM
(default "ed25519")
--swagger-enable Expose the swagger endpoint via /swagger.
Consumes $CODER_SWAGGER_ENABLE
--telemetry Whether telemetry is enabled or not.
Coder collects anonymized usage data to
help improve our product.
@@ -174,6 +188,9 @@ Flags:
product. Disabling telemetry also
disables this option.
Consumes $CODER_TELEMETRY_TRACE
--tls-address string HTTPS bind address of the server.
Consumes $CODER_TLS_ADDRESS (default
"127.0.0.1:3443")
--tls-cert-file strings Path to each certificate for TLS. It
requires a PEM-encoded file. To configure
the listener to use a CA certificate,
@@ -188,7 +205,7 @@ Flags:
"verify-if-given", or
"require-and-verify".
Consumes $CODER_TLS_CLIENT_AUTH (default
"request")
"none")
--tls-client-ca-file string PEM-encoded Certificate Authority file
used for checking the authenticity of
client
@@ -212,6 +229,13 @@ Flags:
"tls12" or "tls13"
Consumes $CODER_TLS_MIN_VERSION (default
"tls12")
--tls-redirect-http-to-https Whether HTTP requests will be redirected
to the access URL (if it's a https URL
and TLS is enabled). Requests to local IP
addresses are never redirected regardless
of this setting.
Consumes $CODER_TLS_REDIRECT_HTTP
(default true)
--trace Whether application tracing data is
collected. It exports to a backend
configured by environment variables. See:
@@ -0,0 +1,25 @@
Run the built-in PostgreSQL deployment.
Usage:
coder server postgres-builtin-serve [flags]
Flags:
-h, --help help for postgres-builtin-serve
--raw-url Output the raw connection URL instead of a psql command.
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
@@ -0,0 +1,25 @@
Output the connection URL for the built-in PostgreSQL deployment.
Usage:
coder server postgres-builtin-url [flags]
Flags:
-h, --help help for postgres-builtin-url
--raw-url Output the raw connection URL instead of a psql command.
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+24
View File
@@ -0,0 +1,24 @@
Display details of a workspace's resources and agents
Usage:
coder show <workspace> [flags]
Flags:
-h, --help help for show
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+28
View File
@@ -0,0 +1,28 @@
Run upload and download tests from your machine to a workspace
Usage:
coder speedtest <workspace> [flags]
Flags:
-d, --direct Specifies whether to wait for a direct connection before testing speed.
-h, --help help for speedtest
-r, --reverse Specifies whether to run in reverse mode where the client receives and
the server sends.
-t, --time duration Specifies the duration to monitor traffic. (default 5s)
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+45
View File
@@ -0,0 +1,45 @@
Start a shell into a workspace
Usage:
coder ssh <workspace> [flags]
Flags:
-A, --forward-agent Specifies whether to forward the SSH agent
specified in $SSH_AUTH_SOCK.
Consumes $CODER_SSH_FORWARD_AGENT
-G, --forward-gpg Specifies whether to forward the GPG agent.
Unsupported on Windows workspaces, but supports all
clients. Requires gnupg (gpg, gpgconf) on both the
client and workspace. The GPG agent must already be
running locally and will not be started for you. If
a GPG agent is already running in the workspace, it
will be attempted to be killed.
Consumes $CODER_SSH_FORWARD_GPG
-h, --help help for ssh
--identity-agent string Specifies which identity agent to use (overrides
$SSH_AUTH_SOCK), forward agent must also be
enabled.
Consumes $CODER_SSH_IDENTITY_AGENT
--stdio Specifies whether to emit SSH output over
stdin/stdout.
Consumes $CODER_SSH_STDIO
--workspace-poll-interval duration Specifies how often to poll for workspace automated
shutdown.
Consumes $CODER_WORKSPACE_POLL_INTERVAL (default 1m0s)
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+25
View File
@@ -0,0 +1,25 @@
Start a workspace
Usage:
coder start <workspace> [flags]
Flags:
-h, --help help for start
-y, --yes Bypass prompts
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+32
View File
@@ -0,0 +1,32 @@
Manually manage Terraform state to fix broken workspaces
Usage:
coder state [flags]
coder state [command]
Commands:
pull
push
Flags:
-h, --help help for state
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
Use "coder state [command] --help" for more information about a command.
+23
View File
@@ -0,0 +1,23 @@
Usage:
coder state pull <workspace> [file] [flags]
Flags:
-b, --build int Specify a workspace build to target by name.
-h, --help help for pull
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+23
View File
@@ -0,0 +1,23 @@
Usage:
coder state push <workspace> <file> [flags]
Flags:
-b, --build int Specify a workspace build to target by name.
-h, --help help for push
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+25
View File
@@ -0,0 +1,25 @@
Stop a workspace
Usage:
coder stop <workspace> [flags]
Flags:
-h, --help help for stop
-y, --yes Bypass prompts
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+55
View File
@@ -0,0 +1,55 @@
Templates are written in standard Terraform and describe the infrastructure for workspaces
Usage:
coder templates [flags]
coder templates [command]
Aliases:
templates, template
Get Started:
- Create a template for developers to create workspaces:
$ coder templates create
- Make changes to your template, and plan the changes:
$ coder templates plan my-template
- Push an update to the template. Your developers can update their workspaces:
$ coder templates push my-template
Commands:
create Create a template from the current directory or as specified by flag
delete Delete templates
edit Edit the metadata of a template by name.
init Get started with a templated template.
list List all the templates available for the organization
plan Plan a template push from the current directory
pull Download the latest version of a template to a path.
push Push a new template version from the current directory or as specified by flag
versions Manage different versions of the specified template
Flags:
-h, --help help for templates
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
Use "coder templates [command] --help" for more information about a command.
+31
View File
@@ -0,0 +1,31 @@
Create a template from the current directory or as specified by flag
Usage:
coder templates create [name] [flags]
Flags:
--default-ttl duration Specify a default TTL for workspaces created from this
template. (default 24h0m0s)
-d, --directory string Specify the directory to create from (default
"/tmp/coder-cli-test-workdir")
-h, --help help for create
--parameter-file string Specify a file path with parameter values.
--provisioner-tag stringArray Specify a set of tags to target provisioner daemons.
-y, --yes Bypass prompts
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+25
View File
@@ -0,0 +1,25 @@
Delete templates
Usage:
coder templates delete [name...] [flags]
Flags:
-h, --help help for delete
-y, --yes Bypass prompts
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+33
View File
@@ -0,0 +1,33 @@
Edit the metadata of a template by name.
Usage:
coder templates edit <template> [flags]
Flags:
--allow-user-cancel-workspace-jobs Allow users to cancel in-progress workspace jobs.
(default true)
--default-ttl duration Edit the template default time before shutdown -
workspaces created from this template to this value.
--description string Edit the template description
--display-name string Edit the template display name
-h, --help help for edit
--icon string Edit the template icon path
--name string Edit the template name
-y, --yes Bypass prompts
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+24
View File
@@ -0,0 +1,24 @@
Get started with a templated template.
Usage:
coder templates init [directory] [flags]
Flags:
-h, --help help for init
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+29
View File
@@ -0,0 +1,29 @@
List all the templates available for the organization
Usage:
coder templates list [flags]
Aliases:
list, ls
Flags:
-c, --column stringArray Specify a column to filter in the table. (default
[name,last_updated,used_by])
-h, --help help for list
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+24
View File
@@ -0,0 +1,24 @@
Plan a template push from the current directory
Usage:
coder templates plan <directory> [flags]
Flags:
-h, --help help for plan
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+25
View File
@@ -0,0 +1,25 @@
Download the latest version of a template to a path.
Usage:
coder templates pull <name> [destination] [flags]
Flags:
-h, --help help for pull
-y, --yes Bypass prompts
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+33
View File
@@ -0,0 +1,33 @@
Push a new template version from the current directory or as specified by flag
Usage:
coder templates push [template] [flags]
Flags:
--always-prompt Always prompt all parameters. Does not pull parameter
values from active template version
-d, --directory string Specify the directory to create from (default
"/tmp/coder-cli-test-workdir")
-h, --help help for push
--name string Specify a name for the new template version. It will be
automatically generated if not provided.
--parameter-file string Specify a file path with parameter values.
--provisioner-tag stringArray Specify a set of tags to target provisioner daemons.
-y, --yes Bypass prompts
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
+39
View File
@@ -0,0 +1,39 @@
Manage different versions of the specified template
Usage:
coder templates versions [flags]
coder templates versions [command]
Aliases:
versions, version
Get Started:
- List versions of a specific template:
$ coder templates versions list my-template
Commands:
list List all the versions of the specified template
Flags:
-h, --help help for versions
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE
Use "coder templates versions [command] --help" for more information about a command.
@@ -0,0 +1,24 @@
List all the versions of the specified template
Usage:
coder templates versions list <template> [flags]
Flags:
-h, --help help for list
Global Flags:
--global-config coder Path to the global coder config directory.
Consumes $CODER_CONFIG_DIR (default "/tmp/coder-cli-test-config")
--header stringArray HTTP headers added to all requests. Provide as "Key=Value".
Consumes $CODER_HEADER
--no-feature-warning Suppress warnings about unlicensed features.
Consumes $CODER_NO_FEATURE_WARNING
--no-version-warning Suppress warning when client and server versions do not match.
Consumes $CODER_NO_VERSION_WARNING
--token string Specify an authentication token. For security reasons setting
CODER_SESSION_TOKEN is preferred.
Consumes $CODER_SESSION_TOKEN
--url string URL to a deployment.
Consumes $CODER_URL
-v, --verbose Enable verbose output.
Consumes $CODER_VERBOSE

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