Compare commits

...

1179 Commits

Author SHA1 Message Date
Kyle Carberry 3ab1f6845c fix: add exp backoff to validate fresh git auth tokens (#8956)
A customer using GitHub in Australia reported that validating immediately
after refreshing the token would intermittently fail with a 401. Waiting
a few milliseconds with the exact same token on the exact same request
would resolve the issue. It seems likely that the write is not propagating
to the read replica in time.
2023-08-08 05:09:28 +00:00
Colin Adler ad513fa8b9 chore: fix release and security pipelines 2023-08-03 23:36:22 +00:00
Ben Potter 7ad944da5f chore: v2 changelog (#8890) 2023-08-03 21:45:32 +00:00
Colin Adler 8ee500c59d fix: generate typescript types for healthcheck pkg (#8846) 2023-08-03 16:20:46 -05:00
Cian Johnston 72780c8031 chore(docs): update scaling.md (#8883)
* chore(docs): update scaling docs

* fixup! chore(docs): update scaling docs

* Update docs/admin/scale.md

* update with compute resources provided to coder

* address PR comments
2023-08-03 21:59:07 +01:00
Ben Potter 1314cd8fcb chore: support manual changelog and version 2.0 for releases (#8880)
* chore(release): add manual changelog + 2.0 support

* add comment

* fix typo

* fmt
2023-08-03 12:41:54 -05:00
Muhammad Atif Ali 10da570916 fix: prevent PR deployment without manual trigger (#8882)
* fix: do not deploy without a manual trigger

This will fix automatic first deployment

* fix typo

* Update pr-deploy.yaml

* Update pr-deploy.yaml

* Update pr-deploy.yaml

* Update pr-deploy.yaml

* Update pr-deploy.yaml

* Update pr-deploy.yaml

* Update pr-deploy.yaml

* build at least once

* wait for certificate

* Update pr-deploy.yaml
2023-08-03 19:32:20 +03:00
Muhammad Atif Ali b47bd7ccb5 ci: implement automatic upgrade of PR deployment (#8876) 2023-08-03 18:11:07 +03:00
Mathias Fredriksson d3991fac26 feat(coderd): add parameter insights to template insights (#8656) 2023-08-03 14:43:23 +00:00
Bruno Quaresma 2ed453035e fix: fix develop script and pnpm command (#8878) 2023-08-03 14:15:45 +00:00
dependabot[bot] 7493b28155 chore: bump the storybook group in /site with 1 update (#8872)
Bumps the storybook group in /site with 1 update: [storybook-addon-react-router-v6](https://github.com/JesusTheHun/storybook-addon-react-router-v6).

- [Release notes](https://github.com/JesusTheHun/storybook-addon-react-router-v6/releases)
- [Changelog](https://github.com/JesusTheHun/storybook-addon-react-router-v6/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JesusTheHun/storybook-addon-react-router-v6/compare/v1.0.2...v2.0.0)

---
updated-dependencies:
- dependency-name: storybook-addon-react-router-v6
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: storybook
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-03 08:19:22 +03:00
Colin Adler e68d508469 chore: bump openssl in base image (#8811) 2023-08-02 16:16:34 -05:00
Bruno Quaresma b6018198b7 chore: remove insights from experimental (#8868) 2023-08-02 20:24:00 +00:00
Muhammad Atif Ali f7327dd2c6 fix: avoid using comment body directly (#8851) 2023-08-02 22:19:17 +03:00
Colin Adler 4b81cfaa0d chore: switch from yarn to pnpm (#8822) 2023-08-02 13:28:16 -05:00
Bruno Quaresma cf35c0dfc5 feat(site): add health warning and a health monitor page (#8844) 2023-08-02 14:49:24 -03:00
Bruno Quaresma 44f9b0228a fix(site): fix initial value for update parameters (#8863) 2023-08-02 14:37:02 -03:00
Bruno Quaresma 00cbb211b4 fix(site): show error when user exists (#8864) 2023-08-02 14:36:35 -03:00
Colin Adler eddd4f8888 chore: fix dupliate migration round 2 (#8867) 2023-08-02 16:28:30 +00:00
Steven Masley 4c1e63aae8 feat: add display_name field to groups (#8740)
* feat: add display_name field to groups

This is a non-unique human friendly group name for display
purposes. This means a display name can be used instead of
using an environment var to remap groups with OIDC names to
Coder names. Now groups can retain the OIDC name for mapping,
and use a display name for display purposes.
2023-08-02 10:53:06 -05:00
Dean Sheather 6ea32e4e80 fix: show current DERP name correctly in vscode (#8856) 2023-08-03 01:30:43 +10:00
Colin Adler aa7b267170 chore: add pnpm to dogfood image (#8865) 2023-08-02 10:26:47 -05:00
Colin Adler e482d39052 chore: fix duplicate migration (#8866) 2023-08-02 15:09:10 +00:00
Dean Sheather cd1e088f7c feat: add --derp-only flag to wsproxy (#8850) 2023-08-03 00:35:06 +10:00
Marcin Tojek d6e9870209 feat: add "dormant" user state (#8644) 2023-08-02 16:31:25 +02:00
Muhammad Atif Ali d2c7c8e1d8 fix: allow ci to publish a package (#8862) 2023-08-02 17:22:11 +03:00
Muhammad Atif Ali 57c7d7d4d2 ci: build and push ghcr.io/coder/coder-preview:main (#8832) 2023-08-02 15:45:14 +03:00
Dean Sheather 496ec6cfc5 fix: add read call to derp-map endpoint to avoid ws ping timeout (#8859) 2023-08-02 08:31:51 +00:00
Muhammad Atif Ali 75fcc24b60 fix: copy secret for the first deployment too (#8857) 2023-08-02 10:44:38 +03:00
Dean Sheather b955c5fefc fix: avoid agent runLoop exiting due to ws ping (#8852) 2023-08-02 07:25:07 +00:00
Muhammad Atif Ali f48e8dcf88 chore: update concurrency to use PR number (#8853) 2023-08-02 10:04:39 +03:00
Colin Adler 1472cce6c3 chore: increment GOCACHE key (#8849) 2023-08-02 03:33:52 +00:00
Dean Sheather 6b69970d7c fix: avoid infinite loop in agent derp-map (#8848) 2023-08-02 13:18:46 +10:00
Colin Adler 805eaca4b7 chore: use fetch-depth: 1 for most ci jobs (#8847) 2023-08-02 02:48:35 +00:00
dependabot[bot] 9b6750a4db chore: bump github.com/open-policy-agent/opa from 0.51.0 to 0.55.0 (#8808)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-01 21:48:02 -05:00
Ben Potter 4456d0bfea docs: sync roles (enterprise) (#8696)
* docs: role sync (enterprise)

* edits from code review

* fix typo
2023-08-01 13:47:55 -05:00
Eric Paulsen 924bccbfcf docs: rm microsoft vs code server docs (#8845) 2023-08-01 12:46:17 -05:00
Steven Masley 66649f97a8 chore: use system permission to prevent fetching all workspaces (#8843)
* chore: use system permission to prevent fetching all workspaces
2023-08-01 12:26:22 -05:00
Dean Sheather c575292ba6 fix: fix tailnet netcheck issues (#8802) 2023-08-02 01:50:43 +10:00
Steven Masley 929f2d585a fix: intiator user fields are included in the workspace build (#8836) 2023-08-01 10:35:43 -05:00
dependabot[bot] b7eafe1635 chore: bump github.com/gohugoio/hugo from 0.115.0 to 0.116.0 (#8807)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-01 10:27:23 -05:00
Colin Adler 437256d352 chore: upgrade slog (#8815) 2023-08-01 10:26:52 -05:00
Muhammad Atif Ali e23e5262b4 chore: wait 30 seconds before checking winget PR in release.yaml (#8835) 2023-08-01 14:57:25 +00:00
Muhammad Atif Ali b104bb7005 docs: update CONTRIBUTING.md with PR deployments instructions (#8831)
* docs: Update CONTRIBUTING.md with PR deployments instructions

* update
2023-08-01 16:48:32 +03:00
dependabot[bot] dcc63aec90 chore: bump the storybook group in /site with 6 updates (#8828)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-01 16:14:20 +03:00
dependabot[bot] 27c2fb8a56 chore: bump the eslint group in /site with 5 updates (#8829)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-01 15:19:07 +03:00
Muhammad Atif Ali 6a12912d6e ci: use a wildcard subdomain for PR deployments (#8801) 2023-08-01 12:02:42 +01:00
Marcin Tojek 336e663180 fix: parse insights start and end time (#8826) 2023-08-01 10:43:49 +02:00
dependabot[bot] a96c4a3f29 chore: bump framer-motion from 6.5.1 to 10.15.0 in /offlinedocs (#8817)
Bumps [framer-motion](https://github.com/framer/motion) from 6.5.1 to 10.15.0.
- [Changelog](https://github.com/framer/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/framer/motion/compare/v6.5.1...v10.15.0)

---
updated-dependencies:
- dependency-name: framer-motion
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-01 07:28:06 +03:00
dependabot[bot] 8d0e8f45e0 chore: bump eslint from 8.45.0 to 8.46.0 in /offlinedocs (#8816)
Bumps [eslint](https://github.com/eslint/eslint) from 8.45.0 to 8.46.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.45.0...v8.46.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-01 07:27:19 +03:00
dependabot[bot] 280fc9d5ef chore: bump typescript from 4.7.3 to 5.1.6 in /offlinedocs (#8819)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.7.3 to 5.1.6.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.7.3...v5.1.6)

---
updated-dependencies:
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-01 07:27:04 +03:00
Colin Adler b84bba8fd1 chore: increase fmt timeout (#8820) 2023-07-31 16:36:53 -05:00
Steven Masley cb36783e8c feat: convert to oidc out of experimental (#8742) 2023-07-31 10:30:34 -05:00
Colin Adler ed82b864d0 chore: reenable prisma image scan (#8810) 2023-07-31 10:12:24 -05:00
Muhammad Atif Ali af036b440b docs: allow multiple Coder deployments to use single GitHub OAuth app (#8786)
* docs: Add instructions for using the same GitHub OAuth app for multiple Coder deployments

* fmt

* typo

* Update auth.md
2023-07-31 10:00:48 -04:00
Marcin Tojek ddabe9cc7f feat: improve RBAC preconditions for Insights endpoint (#8794) 2023-07-31 13:44:32 +00:00
Colin Adler 4cc270b12b revert(enterprise): make pgcoord experimental again (#8797) 2023-07-28 18:38:32 -05:00
Colin Adler 98d9a65cf5 chore: mark site/src/api/typesGenerated.ts as generated (#8798) 2023-07-28 18:22:37 -05:00
Colin Adler 0b4f333a6f chore: add http debug support to pgcoord (#8795) 2023-07-28 17:59:31 -05:00
Bruno Quaresma a75346dd32 refactor(site): simplify workspaces page component structure (#8793) 2023-07-28 13:07:50 -03:00
Kyle Carberry bd944e0d21 chore: rename startup logs to agent logs (#8649)
* chore: rename startup logs to agent logs

This also adds a `source` property to every agent log. It
should allow us to group logs and display them nicer in
the UI as they stream in.

* Fix migration order

* Fix naming

* Rename the frontend

* Fix tests

* Fix down migration

* Match enums for workspace agent logs

* Fix inserting log source

* Fix migration order

* Fix logs tests

* Fix psql insert
2023-07-28 15:57:23 +00:00
Bruno Quaresma 78b06397a6 fix(site): fix metadata value changing width all the time (#8780) 2023-07-28 09:59:49 -03:00
Muhammad Atif Ali ea2ae10bc3 ci: update terraform to ~1.5 (#8766)
* ci: update terraform to 1.5.3

* Update action.yaml
2023-07-28 10:12:06 +00:00
Muhammad Atif Ali e029df61ff ci: update secret name in pr-cleanup.yaml (#8790) 2023-07-28 08:26:11 +00:00
Bruno Quaresma e8f82538e8 refactor(site): sort proxies in navbar by latency (#8781) 2023-07-27 21:50:11 -03:00
Kyle Carberry 91a0c7abe0 fix: improve the language of the template insights page (#8777) 2023-07-27 21:04:28 +00:00
Bruno Quaresma 471dc6126c chore: upgrade nodejs version to 18.x (#8768) 2023-07-27 17:54:38 -03:00
Bruno Quaresma 3114a60e5c ci: optimize chromatic (#8776)
* Update ci.yaml

Modified the Chromatic job configuration to reduce snapshot usage and only run Chromatic on necessary UI changes.

Signed-off-by: BrunoQuaresma <bruno_nonato_quaresma@hotmail.com>

* fix fmt

---------

Signed-off-by: BrunoQuaresma <bruno_nonato_quaresma@hotmail.com>
Co-authored-by: Reuben Ellis <ethriel3695@gmail.com>
2023-07-27 15:51:26 -03:00
dependabot[bot] c236a29307 chore: bump google.golang.org/grpc from 1.56.2 to 1.57.0 (#8757)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-27 12:49:13 -05:00
Bruno Quaresma 4d36b859d2 refactor(site): make few ui changes on template insights (#8774) 2023-07-27 14:37:04 -03:00
Cian Johnston 5c9167df36 chore(helm): update sample resource requests/limits (#8773)
The sample resource requests/limits are undersized.
2023-07-27 13:28:06 -04:00
dependabot[bot] ca710e52b6 chore: bump google.golang.org/api from 0.133.0 to 0.134.0 (#8756)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-27 16:50:18 +00:00
Yinon Avraham de60bff88e chore(docs): small fixes in JFrog integration docs (#8736) 2023-07-27 11:46:06 -05:00
Ammar Bandukwala edd9628aa6 fix(cli): correctly print deprecated warnings (#8771)
In the previous implementation, it was possible for default-set values
to trigger the deprecation warning.
2023-07-27 16:42:18 +00:00
Kyle Carberry ff794e4b23 chore: add e2e test for backwards ssh compatibility (#8761)
* chore: add e2e test for backwards ssh compatibility

* Use the SSH client directly

* fmt
2023-07-27 10:42:11 -05:00
Ben Potter 34dfbfa9d3 ci: add DB to PR deploys (#8770)
* ci: add DB to PR deploys

* add bitnami remo

* fix namespace

* change ingress host

* remove wildcard host
2023-07-27 14:50:53 +00:00
dependabot[bot] 975424b3a2 chore: bump the eslint group in /site with 5 updates (#8636)
* chore: bump the eslint group in /site with 5 updates

Bumps the eslint group in /site with 5 updates:

| Package | Update |
| --- | --- |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | 5.62.0 to 6.1.0 |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | 5.62.0 to 6.1.0 |
| [eslint](https://github.com/eslint/eslint) | 8.44.0 to 8.45.0 |
| [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) | 7.31.1 to 7.33.0 |
| [eslint-plugin-unicorn](https://github.com/sindresorhus/eslint-plugin-unicorn) | 47.0.0 to 48.0.0 |


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

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

Updates `eslint` from 8.44.0 to 8.45.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.44.0...v8.45.0)

Updates `eslint-plugin-react` from 7.31.1 to 7.33.0
- [Release notes](https://github.com/jsx-eslint/eslint-plugin-react/releases)
- [Changelog](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jsx-eslint/eslint-plugin-react/compare/v7.31.1...v7.33.0)

Updates `eslint-plugin-unicorn` from 47.0.0 to 48.0.0
- [Release notes](https://github.com/sindresorhus/eslint-plugin-unicorn/releases)
- [Commits](https://github.com/sindresorhus/eslint-plugin-unicorn/compare/v47.0.0...v48.0.0)

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

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

* ignore no-redundant-type-constituents

* Fix minor lint errors

* Discard changes to site/.eslintrc.yaml

* Fix redudant types

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Atif Ali <atif@coder.com>
Co-authored-by: BrunoQuaresma <bruno_nonato_quaresma@hotmail.com>
Co-authored-by: Muhammad Atif Ali <matifali@live.com>
2023-07-27 10:51:18 -03:00
Muhammad Atif Ali 92798e957f chore: bump typescript from 4.8.2 to 5.1.6 in /site (#8688) 2023-07-27 10:21:38 -03:00
Muhammad Atif Ali f6c36d903e ci: prevent running deploy on all comments (#8767) 2023-07-27 13:00:00 +00:00
Muhammad Atif Ali 3761205bae ci: further improve pr deployments (#8764) 2023-07-27 12:40:59 +00:00
Bruno Quaresma e85b88ca90 feat(site): add restart button when workspace is unhealthy (#8765) 2023-07-27 09:20:24 -03:00
Bruno Quaresma f54d385266 feat(site): add auto mode on create workspace form (#8651) 2023-07-27 08:47:58 -03:00
Muhammad Atif Ali bc55ffdf0d ci: improve deploy-pr workflow (#8735) 2023-07-27 13:07:53 +03:00
Cian Johnston 32829080ac feat(cli): add dashboard load test command (#8723) 2023-07-27 09:40:13 +01:00
Jon Ayers c3aface285 fix: fix coder stat mem (#8762)
- For cgroups v1 the wrong cgroup file was being read
  to determine max memory. This commit updates the file
  from '/sys/fs/cgroup/memory/memory.max_usage_in_bytes' to
  '/sys/fs/cgroup/memory/memory.limit_in_bytes'
2023-07-27 09:33:43 +01:00
Colin Adler 74c4553a3f fix(codersdk): always dial agents with WorkspaceAgentIP (#8760) 2023-07-27 03:44:44 +00:00
dependabot[bot] 7b08be8000 chore: bump google.golang.org/api from 0.132.0 to 0.133.0 (#8746)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-26 18:53:06 -05:00
Colin Adler 2811ab62d0 chore: fix workspace proxy flake (#8755) 2023-07-26 23:50:25 +00:00
Colin Adler 5cfa7082d1 chore: attempt to fix windows goleak failures (#8753) 2023-07-26 18:50:13 -05:00
Colin Adler 6af1f6a9d9 chore: fix Test_parseInsightsStartAndEndTime test (#8754) 2023-07-26 23:27:30 +00:00
Colin Adler dd2f79995b chore(tailnet): rewrite coordinator debug using html/template (#8752) 2023-07-26 22:54:21 +00:00
Ammar Bandukwala 02550a9062 fix(cli): apply log-filter to debug logs only (#8751) 2023-07-26 22:31:41 +00:00
Kyle Carberry 131babfb00 fix: request trial after password is validated (#8750) 2023-07-26 22:04:15 +00:00
Ammar Bandukwala 25e30c6f41 feat(cli): support fine-grained server log filtering (#8748) 2023-07-26 16:46:22 -05:00
Bruno Quaresma 4e9e480cc6 fix(site): fix latency values (#8749) 2023-07-26 17:14:45 -03:00
Ammar Bandukwala f28bac1b05 chore(scripts): use fake semver when using sapling (#8747)
The fact that the Sapling commit didn't conform to semver
broke the agent handshake with coderd.
2023-07-26 20:00:21 +00:00
Colin Adler 6b92abebb9 fix(tailnet): track agent names for http debug (#8744) 2023-07-26 18:44:10 +00:00
Bruno Quaresma b5dec61cd5 feat(site): show metadata stale data (#8745) 2023-07-26 15:41:07 -03:00
Dean Sheather 9ffbdc6696 hotfix: fix failed 32-bit builds (#8741) 2023-07-26 17:30:47 +00:00
Dean Sheather 2f0a9996e7 chore: add derpserver to wsproxy, add proxies to derpmap (#7311) 2023-07-27 02:21:04 +10:00
Marcin Tojek 70692c2e4e fix: unify parameter validation errors (#8738) 2023-07-26 17:44:50 +02:00
Steven Masley 2089006fbc feat!: drop reading other 'user' permission (#8650)
* feat: drop reading other 'user' permission

Members of the platform can no longer read or list other users.
Resources that have "created_by" or "initiated_by" still retain
user context, but only include username and avatar url.

Attempting to read a user found via those means will result in
a 404.

* Hide /users page for regular users
* make groups a privledged endpoint
* Permissions page for template perms
* Admin for a given template enables an endpoint for listing users/groups.
2023-07-26 10:33:48 -04:00
Bruno Quaresma 8649a10441 feat(site): add template insights page (#8722) 2023-07-26 13:13:56 +00:00
Muhammad Atif Ali da7e1eb75d ci: add a missing end quote (#8734) 2023-07-26 00:39:00 +00:00
Muhammad Atif Ali 0b5a01aa1b test: fix comments in deploy-pr.yaml (#8733) 2023-07-26 00:06:59 +00:00
Muhammad Atif Ali 26264bd3ef test: remove erroneous quotes from deploy-pr.yaml (#8732) 2023-07-25 23:16:57 +00:00
Muhammad Atif Ali 2c7d8ee06a fix: remove the usage of -u flag (#8731) 2023-07-25 22:54:42 +00:00
Colin Adler f07d2ff68d fix(enterprise): ensure creating a SCIM user is idempotent (#8730) 2023-07-25 17:49:52 -05:00
Muhammad Atif Ali 0feb855b8f fix: revert back to curl in deply-pr.yaml (#8729) 2023-07-25 22:40:26 +00:00
Muhammad Atif Ali 733ae5038a fix: a small issue with pr_deploy.yaml (#8728) 2023-07-25 22:15:44 +00:00
Muhammad Atif Ali 86d4c4ff0e test: improve pr deployments (#8720) 2023-07-26 00:56:38 +03:00
Muhammad Atif Ali 844ef76d39 fix: handle issue_comment event (#8726) 2023-07-25 19:52:44 +00:00
Muhammad Atif Ali 67f1123808 use gh cli (#8725) 2023-07-25 19:35:28 +00:00
Ammar Bandukwala ee652d525d docs: explain how to install the JFrog VS Code extension (#8724) 2023-07-25 18:06:30 +00:00
Muhammad Atif Ali 7447bfa320 fix: checkout correct branch for PR deployment (#8721) 2023-07-25 16:08:44 +00:00
Bruno Quaresma dfb1a1df3a feat(site): add terminal icon (#8719) 2023-07-25 11:41:29 -03:00
Ammar Bandukwala 2a01747804 feat(cli): add --var shorthand for --variable (#8710)
`--variable` is used frequently enough to deserve a shorthand. Unfortunately,
`-v` is taken by verbose, and `-V` is too easily confused with version or
verbose, so we're left with "--var".
2023-07-25 14:36:02 +00:00
Bruno Quaresma 30e16052d6 fix(coderd): update icon url to the right one (#8718) 2023-07-25 14:06:58 +00:00
Steven Masley de1a7a9210 chore: join user information to workspace_build and template_version (#8625)
* include minimial user on template version and build
* Add unit test to ensure join is superset
2023-07-25 09:14:38 -04:00
Bruno Quaresma fbb2a6a434 refactor(coderd): add avatar URL to the users' latency endpoint (#8701) 2023-07-25 12:15:46 +02:00
Marcin Tojek 7004013537 chore: update nix flake to include sqlc v1.19.1 (#8715) 2023-07-25 09:09:16 +00:00
Ammar Bandukwala f6f61dfa26 docs: fix duplicate sentence (#8712)
Resolves #8664
2023-07-25 02:22:41 +00:00
Ammar Bandukwala b8e792bf8a chore: make update-golden-files (#8711)
One of the recent helm changes snuck in CI breakage.
2023-07-25 01:18:53 +00:00
Ammar Bandukwala 5eb825155e docs: slightly improve wording in jfrog.md (#8709) 2023-07-24 20:05:54 -05:00
Ammar Bandukwala a0f478c02f docs: cleanup jfrog (#8708)
* Add a missing requirement
* Improve English
2023-07-24 19:29:30 -05:00
Ammar Bandukwala 8686b7a499 docs: explain JFrog integration 🐸 (#8682) 2023-07-24 23:16:09 +00:00
Eric Paulsen 29963433ee feat(helm)!: set enableDeployments default to true (#8704) 2023-07-24 17:13:28 -05:00
Eric Paulsen 353bfb618c fix: missing manifest for deployment logs doc (#8703) 2023-07-24 17:12:48 -05:00
Ben Potter adbabe4e09 fix: hide experiment CTA from OIDC copy (#8695) 2023-07-24 14:51:28 -05:00
Steven Masley ac559f101e fix: handle omitted role sync claim (#8697)
* fix: handle omitted role sync claim
2023-07-24 15:50:23 -04:00
Bruno Quaresma ac973a4b2c fix(site): fix error 'Reduce of empty array with no initial value' (#8700) 2023-07-24 14:17:27 -03:00
Ammar Bandukwala c32085ca10 chore(scripts): handle Sapling in version.sh (#8687) 2023-07-24 11:11:46 -05:00
Muhammad Atif Ali 6929792a58 docs: add coder-logstream-kube docs and update k8s example template (#8675)
* docs: add kubestream docs and update example template

* make fmt

* add to template's README

* add the seperate page

* make fmt

* make fmt

* add namespace to deployment resource

* fmt

* add screenshots

* link docs in template

* Add remaining examples

* enable deployments requirement

* Update deployment-logs.md

* rewording

* fix spelling

* how?

* cleanup

* Update docs/platforms/kubernetes/deployment-logs.md

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

* suggestions

* use online link

* refine

---------

Co-authored-by: Eric <ericpaulsen@coder.com>
Co-authored-by: Ben Potter <ben@coder.com>
2023-07-24 11:51:25 -04:00
Marcin Tojek b7ced94f0f fix: prevent repetition of template IDs in template_usage_by_day (#8693) 2023-07-24 15:07:01 +02:00
Steven Masley f827829afe feat: synchronize oidc user roles (#8595)
* feat: oidc user role sync
User roles come from oidc claims. Prevent manual user role changes
if set.
* allow mapping 1:many
2023-07-24 08:34:24 -04:00
dependabot[bot] 94541d201f chore: bump react-chartjs-2 in /site from 4.3.1 to 5.2.0 (#8632)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Muhammad Atif Ali <atif@coder.com>
2023-07-24 14:44:14 +03:00
dependabot[bot] 75265ecf68 chore: bump fs-extra from 10.1.0 to 11.1.1 in /offlinedocs (#8630)
Bumps [fs-extra](https://github.com/jprichardson/node-fs-extra) from 10.1.0 to 11.1.1.
- [Changelog](https://github.com/jprichardson/node-fs-extra/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jprichardson/node-fs-extra/compare/10.1.0...11.1.1)

---
updated-dependencies:
- dependency-name: fs-extra
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-24 14:25:55 +03:00
dependabot[bot] 0f915e1e75 chore: bump @types/react-dom from 18.0.5 to 18.2.7 in /offlinedocs (#8671)
Bumps [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) from 18.0.5 to 18.2.7.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

---
updated-dependencies:
- dependency-name: "@types/react-dom"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-24 11:21:33 +00:00
dependabot[bot] 625eef93c5 chore: bump next from 12.1.6 to 13.4.12 in /offlinedocs (#8669)
Bumps [next](https://github.com/vercel/next.js) from 12.1.6 to 13.4.12.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v12.1.6...v13.4.12)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-24 14:17:08 +03:00
dependabot[bot] c136fa9e52 chore: bump @types/react from 18.0.14 to 18.2.15 in /offlinedocs (#8672)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 18.0.14 to 18.2.15.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-24 14:16:57 +03:00
dependabot[bot] d54bc60842 chore: bump react-icons from 4.4.0 to 4.10.1 in /offlinedocs (#8673)
Bumps [react-icons](https://github.com/react-icons/react-icons) from 4.4.0 to 4.10.1.
- [Release notes](https://github.com/react-icons/react-icons/releases)
- [Commits](https://github.com/react-icons/react-icons/compare/v4.4.0...v4.10.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-24 14:16:44 +03:00
Muhammad Atif Ali 757ea68d4a ci: fmt, lint and build offlinedocs (#8642)
Co-authored-by: Dean Sheather <dean@deansheather.com>
2023-07-24 10:54:08 +00:00
Muhammad Atif Ali 87f07b9f2f ci: reduce duplication (#8692)
* ci: reduce duplication

* typo
2023-07-24 10:51:43 +00:00
Muhammad Atif Ali 196b29983b docs: fix a broken link (#8691) 2023-07-24 09:13:59 +00:00
Ammar Bandukwala c3b8898f0a fix(coderd/unhanger): de-duplicate logs (#8686)
The final warning just repeated previous lines.
2023-07-24 14:28:09 +10:00
Ammar Bandukwala c9ade6f6c5 chore: enforce that site icons are .svg (#8684) 2023-07-23 16:27:11 -05:00
Ammar Bandukwala d56bf52141 chore(scripts): support Sapling in project_root (#8685) 2023-07-23 15:59:49 -05:00
Ammar Bandukwala 9833cd32b3 Revert "chore(scripts): support sappling in project_root"
This reverts commit 6855735085.

This was pushed accidentally.
2023-07-23 19:40:28 +00:00
Ammar Bandukwala 6855735085 chore(scripts): support sappling in project_root 2023-07-23 19:34:44 +00:00
Muhammad Atif Ali 43cc5449a1 fix: fix a typo in deploy-pr.yaml (#8678) 2023-07-23 16:21:29 +00:00
Muhammad Atif Ali 30e86e0058 chore: do not deploy a PR on deleted comment (#8677)
* chore: improve deploy-pr.yaml

* Update pr-deploy.yaml

* make fmt
2023-07-23 19:06:24 +03:00
Muhammad Atif Ali e7330436c9 chore: insatll coder-logstream-kube for PR deployments (#8676) 2023-07-23 15:35:53 +00:00
Muhammad Atif Ali ec880f26b4 chore: enable deployments for PR deplyments (#8674) 2023-07-23 17:32:17 +03:00
Sebastiaan ten Pas a28883d9f0 fix: do not wait for devcontainer template volume claim bound (#8539) 2023-07-23 06:06:23 +00:00
dependabot[bot] 1cadfa4e4f chore: bump the storybook group in /site with 6 updates (#8635)
Bumps the storybook group in /site with 6 updates:

| Package | Update |
| --- | --- |
| [@storybook/addon-actions](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/actions) | 7.0.26 to 7.1.0 |
| [@storybook/addon-essentials](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/essentials) | 7.0.26 to 7.1.0 |
| [@storybook/addon-links](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/links) | 7.0.26 to 7.1.0 |
| [@storybook/addon-mdx-gfm](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/gfm) | 7.0.26 to 7.1.0 |
| [@storybook/react-vite](https://github.com/storybookjs/storybook/tree/HEAD/code/frameworks/react-vite) | 7.0.26 to 7.1.0 |
| [storybook](https://github.com/storybookjs/storybook/tree/HEAD/code/lib/cli) | 7.0.26 to 7.1.0 |


Updates `@storybook/addon-actions` from 7.0.26 to 7.1.0
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v7.1.0/code/addons/actions)

Updates `@storybook/addon-essentials` from 7.0.26 to 7.1.0
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v7.1.0/code/addons/essentials)

Updates `@storybook/addon-links` from 7.0.26 to 7.1.0
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v7.1.0/code/addons/links)

Updates `@storybook/addon-mdx-gfm` from 7.0.26 to 7.1.0
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v7.1.0/code/addons/gfm)

Updates `@storybook/react-vite` from 7.0.26 to 7.1.0
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v7.1.0/code/frameworks/react-vite)

Updates `storybook` from 7.0.26 to 7.1.0
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v7.1.0/code/lib/cli)

---
updated-dependencies:
- dependency-name: "@storybook/addon-actions"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: storybook
- dependency-name: "@storybook/addon-essentials"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: storybook
- dependency-name: "@storybook/addon-links"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: storybook
- dependency-name: "@storybook/addon-mdx-gfm"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: storybook
- dependency-name: "@storybook/react-vite"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: storybook
- dependency-name: storybook
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: storybook
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-23 01:00:42 +03:00
dependabot[bot] 9a7729d60f chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0 in /site (#8670)
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.16.5 to 5.17.0.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v5.16.5...v5.17.0)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-22 21:46:39 +00:00
dependabot[bot] 82310896ad chore: bump eslint from 8.17.0 to 8.45.0 in /offlinedocs (#8631)
Bumps [eslint](https://github.com/eslint/eslint) from 8.17.0 to 8.45.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.17.0...v8.45.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-22 21:39:59 +00:00
dependabot[bot] aae7175a83 chore: bump @chakra-ui/react from 2.7.1 to 2.8.0 in /offlinedocs (#8633)
Bumps [@chakra-ui/react](https://github.com/chakra-ui/chakra-ui/tree/HEAD/packages/components/react) from 2.7.1 to 2.8.0.
- [Release notes](https://github.com/chakra-ui/chakra-ui/releases)
- [Changelog](https://github.com/chakra-ui/chakra-ui/blob/main/packages/components/react/CHANGELOG.md)
- [Commits](https://github.com/chakra-ui/chakra-ui/commits/@chakra-ui/react@2.8.0/packages/components/react)

---
updated-dependencies:
- dependency-name: "@chakra-ui/react"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-23 00:34:23 +03:00
dependabot[bot] e056d0243a chore: bump @tanstack/react-query from 4.29.19 to 4.32.0 in /site (#8668)
Bumps [@tanstack/react-query](https://github.com/tanstack/query) from 4.29.19 to 4.32.0.
- [Release notes](https://github.com/tanstack/query/releases)
- [Commits](https://github.com/tanstack/query/compare/v4.29.19...v4.32.0)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-22 21:33:18 +00:00
dependabot[bot] a9036ed02b chore: bump chromatic from 6.19.9 to 6.20.0 in /site (#8667)
Bumps [chromatic](https://github.com/chromaui/chromatic-cli) from 6.19.9 to 6.20.0.
- [Changelog](https://github.com/chromaui/chromatic-cli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chromaui/chromatic-cli/compare/v6.19.9...v6.20.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-22 21:31:50 +00:00
dependabot[bot] b7db4ee69f chore: bump eslint-config-next from 12.1.6 to 13.4.10 in /offlinedocs (#8629)
Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 12.1.6 to 13.4.10.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v13.4.10/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-23 00:28:56 +03:00
Mathias Fredriksson e0c1aacac1 fix(coderd): fix template insight intervals (#8662) 2023-07-21 20:51:35 +00:00
Jyotirmoy Bandyopadhyaya 65583eca47 feat(cli): check if dotfiles install script is executable (#8588)
* feat(cli): check if dotfiles install script is executable

* feat(docs): add section for dotfiles setup and document executable fix

---------

Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
Co-authored-by: Muhammad Atif Ali <matifali@live.com>
2023-07-21 18:39:50 +00:00
Bruno Quaresma 7f67000b90 fix(site): send build parameters over the confirmation dialog on restart (#8660) 2023-07-21 12:35:28 -06:00
Mathias Fredriksson 30fe153296 feat(coderd): add user latency and template insights endpoints (#8519)
Part of #8514
Refs #8109
2023-07-21 18:00:19 +00:00
Cian Johnston 539fcf9e6b chore(coderd/util/tz): skip flaky test (#8652) 2023-07-21 16:31:52 +00:00
Eric Paulsen 001670cc13 docs: add steps for postgres SSL cert config (#8648)
* docs: add steps for postgres SSL cert config

* make fmt

* Update docs/install/kubernetes.md

Co-authored-by: Cian Johnston <cian@coder.com>

* fixup! Update docs/install/kubernetes.md

---------

Co-authored-by: Cian Johnston <cian@coder.com>
2023-07-21 10:36:11 -04:00
Ben Potter 1a915f5b30 chore: improve workspace proxy copy (#8427)
* chore: workspace proxy copy

* fmt
2023-07-21 13:34:29 +00:00
Ben Potter 12b51ca804 ci: fix release notes compare link (#8581)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2023-07-21 15:22:54 +03:00
Marcin Tojek c8772fba82 chore: update nix flake to include sqlc v1.19.0 (#8645) 2023-07-21 14:21:44 +02:00
Cian Johnston fd372f6735 fix(cli/clistat): improve detection of container environment (#8643)
Use the presence of /var/run/secrets/kubernetes.io/serviceaccount/token to determine if we are in a container in addition to sniffing /proc/1/cgroup
2023-07-21 10:18:56 +00:00
dependabot[bot] 87d5cdaf58 chore: bump ts-proto from 1.153.2 to 1.156.0 in /site (#8637)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-21 10:49:25 +03:00
Colin Adler 2901147ae3 test: fix race in templateedit_test.go (#8641) 2023-07-21 03:26:55 +00:00
Jon Ayers b47d076756 feat: add deleting_at column to workspaces (#8333) 2023-07-20 22:01:11 -05:00
Muhammad Atif Ali 0c73164f15 chore: add offlinedocs to ts filter in ci.yaml (#8616) 2023-07-20 20:33:50 +00:00
dependabot[bot] 2d3d78b5b1 chore: bump google.golang.org/api from 0.131.0 to 0.132.0 (#8596)
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.131.0 to 0.132.0.
- [Release notes](https://github.com/googleapis/google-api-go-client/releases)
- [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md)
- [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.131.0...v0.132.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-20 23:13:07 +03:00
Muhammad Atif Ali cc659163ef chore: add offlinedocs to depemdabot.yaml (#8617)
Add offlinedocs to depemdabot.yaml
2023-07-20 23:12:08 +03:00
Colin Adler 830020df15 fix(site): don't mark metadata with interval: 0 as stale (#8627) 2023-07-20 15:05:32 -05:00
Colin Adler 1cb39fc65d test: ignore more spurious pgcoord errors (#8628) 2023-07-20 19:55:25 +00:00
Colin Adler 71d4e4e6e8 fix(agent): check agent metadata every second instead of minute (#8614) 2023-07-20 14:02:58 -05:00
Colin Adler c8d65de4b7 test(agent): fix TestAgent_Metadata/Once flake (#8613) 2023-07-20 18:49:44 +00:00
Muhammad Atif Ali deb9261232 ci: allow running other jobs in the matrix if one of them fails (#8624) 2023-07-20 15:56:47 +00:00
Muhammad Atif Ali fd5d26d385 chore: prevent running chromatic on all merged PRs (#8623) 2023-07-20 15:18:02 +00:00
Bruno Quaresma 6de95f4f32 chore(site): remove build logs from experimental (#8619) 2023-07-20 15:02:19 +00:00
Dean Sheather dc8b73168e feat: add user quiet hours schedule and restart requirement feature flag (#8115) 2023-07-20 23:35:41 +10:00
Ben Potter 4821e2e6d8 chore: add jq to base image (#8563)
* chore: add jq to base image

* fix formatting
2023-07-20 07:04:56 -05:00
Cian Johnston 68a46198d3 fix(scaletest): deploy external provisionerd (#8618)
* scaletest: stop kubernetes_secret from being constantly recreated
* scaletest: ensure we do not get auto-upgraded
* scaletest: add external provisionerd deployment, the lazy way
2023-07-20 11:38:46 +01:00
Marcin Tojek 9689bca5d2 feat(cli): implement ssh remote forward (#8515) 2023-07-20 12:05:39 +02:00
dependabot[bot] c68e80970d chore: bump semver from 6.3.0 to 6.3.1 in /offlinedocs (#8600)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-20 10:14:09 +03:00
dependabot[bot] bd5eb9dc1b chore: bump word-wrap from 1.2.3 to 1.2.4 in /offlinedocs (#8598)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-20 10:14:00 +03:00
dependabot[bot] b16901485e chore: bump json5 from 1.0.1 to 1.0.2 in /offlinedocs (#8599)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-20 10:13:27 +03:00
Colin Adler 39889179ea test(coderd): fix TestWorkspaceWatcher flake (#8612) 2023-07-19 18:03:56 -05:00
Jon Ayers 80b940c556 feat: support localhost apps running https (#8585) 2023-07-19 17:33:07 -05:00
Colin Adler 00b9a3ce58 fix: prevent error log when pgcoord query is canceled (#8609) 2023-07-19 16:40:57 -05:00
Steven Masley aceedefce3 chore: add template_with_user view to include user contextual data (#8568)
* chore: Refactor template sql queries to use new view
* TemplateWithUser -> Template
* Add unit test to enforce good view
2023-07-19 16:07:33 -04:00
Ben Potter cdbae29a83 fix: agent health copy (#8608)
* fix: agent health copy

* remove restart in place of upcoming button
2023-07-19 19:56:23 +00:00
Bruno Quaresma d96a6575af docs: add link to CODER_DOCS_URL (#8606) 2023-07-19 19:47:38 +00:00
Bruno Quaresma 16cd1a675c docs: add documentation about the offline docs (#8603) 2023-07-19 13:51:44 -05:00
Bruno Quaresma 0e52461624 feat(site): add support for custom docs url (#8601) 2023-07-19 15:27:38 -03:00
Colin Adler f8a2bd0f4d chore: remove duplicate logger name in wsproxy ServerTailnet (#8604) 2023-07-19 18:06:26 +00:00
Ben Potter b4bb77f2f3 hotfix: change permissions for /deploy-pr comment (#8597) 2023-07-19 17:02:51 +00:00
Bruno Quaresma acc12d3426 feat: add offline docs (#8527) 2023-07-19 13:57:57 -03:00
Marcin Tojek 164672e5d3 feat: expose docs URL via static meta (#8594) 2023-07-19 13:57:43 -03:00
Colin Adler 517fb19474 feat: add single tailnet support to moons (#8587) 2023-07-19 11:11:11 -05:00
Colin Adler cc8d0af027 fix(enterprise): avoid initial license reconfig if feature isn't enabled (#8586) 2023-07-19 10:32:29 -05:00
Cian Johnston 0965a2de70 fix(cli/clistat): read from alternate cgroup path (#8591)
* Attempts reading cgroupv1 quota, period, usage from /sys/fs/cgroup/cpu,cpuacct by default
* Fall back to /sys/fs/cgroup/cpu for v1 quota and period
* Fall back to /sys/fs/cgroup/cpuacct for v1 usage

Fixes https://github.com/coder/coder/issues/8468
2023-07-19 16:25:26 +01:00
Josh Vawdrey 6318c4c09f feat(site): increase TTL and max TTL validation to 30 days (#8258)
Co-authored-by: Dean Sheather <dean@deansheather.com>
2023-07-19 23:43:10 +10:00
Marcin Tojek 4232a2eb96 feat: add custom docs URL to deployment config (#8590) 2023-07-19 08:31:17 -03:00
Eric Paulsen 164b816679 feat(helm): add commandArgs for custom entrypoint (#8567) 2023-07-19 00:50:52 +00:00
Mathias Fredriksson 611fbd8215 test(coderd): fix TestWorkspaceWatcher skipped pending state (#8584) 2023-07-18 21:58:50 +03:00
Bruno Quaresma d12221c782 feat(site): add build parameters option when starting or restarting a workspace (#8524) 2023-07-18 14:53:26 -03:00
Mathias Fredriksson 2fae9b0a69 chore(scripts/dbgen): only add arg validation for dbfake (#8578) 2023-07-18 17:26:03 +00:00
Kyle Carberry 98164f687e fix!: remove startup logs eof for streaming (#8528)
* fix: remove startup logs eof for streaming

We have external utilities like logstream-kube that may send
logs after an agent shuts down unexpectedly to report additional
information. In a recent change we stopped accepting these logs,
which broke these utilities.

In the future we'll rename startup logs to agent logs or something
more generalized so this is less confusing in the future.

* fix(cli/cliui): handle never ending startup log stream in Agent

---------

Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2023-07-18 09:57:29 -06:00
Mathias Fredriksson 58265881af test(testutil): increase wait times to reduce flakes (#8576) 2023-07-18 17:25:54 +03:00
Muhammad Atif Ali 9e170a7332 chore: add filebrowser to dogfood (#8535) 2023-07-18 17:04:00 +03:00
Cian Johnston 278527cff4 feat(scaletest): add option to send traffic over SSH (#8521)
- Refactors the metrics logic to avoid needing to pass in a whole prometheus registry
- Adds an --ssh option to the workspace-traffic command to send SSH traffic

Fixes #8242
2023-07-18 12:17:11 +01:00
Marcin Tojek ab54008c94 feat: make ephemeral parameters optional (#8571) 2023-07-18 11:56:41 +02:00
Muhammad Atif Ali 7d92537af5 chore: fix a typo dogfood.yaml (#8572) 2023-07-18 09:51:46 +00:00
Muhammad Atif Ali b012b4a810 chore: use commit title as template message in dogfood (#8570)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2023-07-18 09:08:58 +00:00
Mathias Fredriksson d467160581 feat(cli): show workspace health in show (#8548) 2023-07-18 11:28:47 +03:00
dependabot[bot] 6b978bef71 chore: bump the github-actions group with 1 update (#8544)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-17 19:58:03 +00:00
Colin Adler fd7c019fff chore(coderd): require api key to access derpmap (#8569) 2023-07-17 19:40:12 +00:00
Bruno Quaresma a823ce7337 refactor(site): add sticky option to the build logs (#8565) 2023-07-17 14:58:25 -03:00
Bruno Quaresma 0bf1b01a9d fix(site): remove extra spacing on template variables page 2023-07-17 14:58:06 -03:00
Cian Johnston 56889d6cd4 fix(cli): unskip TestStatCPUCmd/JSON and explicitly set --host in test cmd invocation (#8558)
- Un-skips TestStatCPUCmd/JSON
- Explicitly sets --host flag when running the cli tests for the stat command as when these are invoked inside a container without a CPU or memory limit set, these tests may fail.
2023-07-17 17:24:46 +01:00
Mathias Fredriksson 854643236a chore(go.mod): update gvisor via github.com/coder/gvisor fork (#8561)
Fixes #7388
2023-07-17 16:09:26 +00:00
Muhammad Atif Ali bf5e0675fe chore: allow running multiple preview deployments in parallel (#8562) 2023-07-17 16:06:29 +00:00
Muhammad Atif Ali 0a56ea6916 chore: do not cancel in progress runs in pr-deploy.yaml (#8559) 2023-07-17 18:40:25 +03:00
Muhammad Atif Ali db8ba6cb68 chore: remove buggy comment reaction from deploy-pr.yaml (#8555) 2023-07-17 18:29:40 +03:00
Bruno Quaresma c83975cee0 fix(site): fix bottom border on build logs (#8554) 2023-07-17 11:57:24 -03:00
Bruno Quaresma 2218160e35 refactor(site): redirect the user to the workspace page after update parameters (#8553) 2023-07-17 11:26:24 -03:00
Steven Masley 67494a3012 chore: push GetUsers authorization filter to SQL (#8497)
* feat: push GetUsers filter to SQL
* Remove GetAuthorizedUserFilter
* Remove GetFilteredUserCount
* remove GetUsersWithCount
2023-07-17 09:44:58 -04:00
Mathias Fredriksson dfac0745f3 feat(cli): show workspace health in list (#8541)
Ref #6461
2023-07-17 15:04:06 +03:00
Alexander Hansen 616e1d7e9a Remove unmaintained 8Bitz0/coder-rust-template template (#8534) 2023-07-16 09:03:48 +00:00
sharkymark 3ea9699908 docs: added kubernetes option to workspace proxies (#8533) 2023-07-15 14:54:52 -05:00
Sebastiaan ten Pas dbc422f29e fix: devcontainer template workspace directory (#8530) 2023-07-15 09:04:51 +00:00
Muhammad Atif Ali ce114a7f9f chore: upgrade sqlc to 1.19.1 (#8511) 2023-07-15 09:07:19 +03:00
Dean Sheather 7a62534359 fix: allow unhanger to unhang canceling jobs (#8529) 2023-07-15 01:54:57 +00:00
Asher 7ed17b2605 fix: add some missing workspace updates (#7790)
* Standardize on function to get workspace channel name

There were two, now there is one.

* Add some missing workspace updates

There are some failure cases where we do not set the type as a workspace
build which causes the workspace update to never be published.

* Make build failures warnings

Otherwise the associated test fails due to the logger fataling on
error messages.
2023-07-14 15:07:48 -08:00
Ammar Bandukwala 970a829939 ci: relax stalebot (#8522)
\#6243 and others suggest the stalebot is too aggressive.
2023-07-14 16:38:04 +00:00
Cian Johnston 52b4563106 fix(scripts/develop.sh): use correct variables-file arg for template create (#8520) 2023-07-14 16:31:29 +00:00
Steven Masley 4c4d966c7b feat: add ability to make workspace for other user from cli (#8481)
* feat: add ability to make workspace for other user from cli
* Add example to show functionality
2023-07-14 09:48:02 -04:00
Mathias Fredriksson 5fd77ad7cf test(agent): fix service banner and metadata intervals (#8516) 2023-07-14 16:10:26 +03:00
Marcin Tojek b7806fd216 fix(site): paginate audit logs (#8513) 2023-07-14 12:24:30 +02:00
dependabot[bot] 9aae983821 chore: bump @fontsource/ibm-plex-mono from 4.5.10 to 5.0.5 in /site (#8512)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-14 01:38:45 +03:00
Muhammad Atif Ali be1013899f chore: use names for all GitHub actions steps and use sqlc setup action (#8495) 2023-07-13 22:46:18 +03:00
Colin Adler 132f1d0eb5 chore: fuzzy match alpine terraform version (#8506) 2023-07-13 14:36:45 -05:00
Bruno Quaresma dc52df940b chore: update coder tf provider on dogfood (#8503) 2023-07-13 14:50:23 -03:00
Dean Sheather ecee3ceeec chore: improve listener algorithm in apptest (#8502) 2023-07-13 17:34:14 +00:00
Steven Masley c2acc35d45 feat: add custom coder bin path for ProxyCommand (#8425)
* feat: add custom coder bin path for ProxyCommand
* force cli path to be absolute (on non-windows)
2023-07-13 13:17:39 -04:00
dependabot[bot] 47d5806e2c chore: bump monaco-editor from 0.39.0 to 0.40.0 in /site (#8475)
Bumps [monaco-editor](https://github.com/microsoft/monaco-editor) from 0.39.0 to 0.40.0.
- [Changelog](https://github.com/microsoft/monaco-editor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/microsoft/monaco-editor/compare/v0.39.0...v0.40.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-13 11:13:26 -06:00
Steven Masley 3b433181be chore: add custom querier functions to dbgen (#8496)
* chore: add custom querier functions to dbgen
* chore: parse package was missing some imports, so force them
2023-07-13 13:12:29 -04:00
dependabot[bot] b650ab40f0 chore: bump @tanstack/react-query from 4.22.4 to 4.29.19 in /site (#8488)
Bumps [@tanstack/react-query](https://github.com/tanstack/query) from 4.22.4 to 4.29.19.
- [Release notes](https://github.com/tanstack/query/releases)
- [Commits](https://github.com/tanstack/query/compare/v4.22.4...v4.29.19)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-13 10:58:36 -06:00
Muhammad Atif Ali 9249309d8e docs: add docs to use remote Docker host (#8479)
Co-authored-by: Ben Potter <ben@coder.com>
2023-07-13 14:41:15 +00:00
Bruno Quaresma b833861960 feat(site): display version message (#8435) 2023-07-13 10:36:10 -03:00
Ben Potter 44e25185ff docs: add custom API use cases (#8445) 2023-07-13 13:24:09 +00:00
Marcin Tojek ebdc510f12 fix(site): delete workspace action in audit log (#8494) 2023-07-13 14:43:10 +02:00
dependabot[bot] 24ec05b5c5 chore: bump prettier from 2.8.1 to 3.0.0 in /site (#8477)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Cian Johnston <cian@coder.com>
Co-authored-by: Muhammad Atif Ali <atif@coder.com>
2023-07-13 14:44:12 +03:00
Marcin Tojek 3727e02bbf feat(cli): add --create flag to templates push (#8454) 2023-07-13 12:58:34 +02:00
Marcin Tojek 5432c3f5ea feat(cli): support ephemeral parameters (#8415) 2023-07-13 12:56:02 +02:00
Cian Johnston cdf9b9045f fix(scaletest/terraform): fix prometheus namespace deps, disable auto-upgrade (#8490)
* hotfix(scaletest/terraform): fix prometheus namespace deps, disable auto-upgrade

* fixup! hotfix(scaletest/terraform): fix prometheus namespace deps, disable auto-upgrade
2023-07-13 10:54:57 +01:00
Mathias Fredriksson 70047ffd15 chore(scripts): use slim and always run make in coder-dev.sh (#8418) 2023-07-13 11:51:53 +03:00
Muhammad Atif Ali f21541c0e4 chore: update gh cli in dogfood Dockerfile (#8489) 2023-07-13 11:49:43 +03:00
dependabot[bot] 837f88d38e chore: bump ts-proto from 1.152.0 to 1.153.2 in /site (#8486)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-13 09:07:48 +03:00
Colin Adler d7cbdbd9c6 chore: replace wsconncache with a single tailnet (#8176) 2023-07-12 17:38:25 -05:00
Colin Adler c47b78c44b chore: replace wsconncache with a single tailnet (#8176) 2023-07-12 17:37:31 -05:00
dependabot[bot] 0a37dd20d6 chore: bump google.golang.org/api from 0.130.0 to 0.131.0 (#8482)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-13 00:15:04 +03:00
dependabot[bot] ecda87060a chore: bump @octokit/types from 10.0.0 to 11.1.0 in /site (#8483)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-12 23:26:34 +03:00
dependabot[bot] 1bd188db66 chore: bump @emoji-mart/data from 1.0.5 to 1.1.2 in /site (#8484)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-12 23:22:56 +03:00
dependabot[bot] 79772f2f3f chore: bump @playwright/test from 1.35.1 to 1.36.0 in /site (#8476)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-12 22:45:24 +03:00
dependabot[bot] 7fc5cce356 chore: bump cronstrue from 2.27.0 to 2.28.0 in /site (#8478)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-12 22:44:35 +03:00
Colin Adler b5c17048c6 chore: upgrade github.com/codeclysm/extract to v3.1.1 (#8480) 2023-07-12 14:33:46 -05:00
Muhammad Atif Ali a0fedebd62 chore: rename dependabot groups (#8473) 2023-07-12 17:01:57 +00:00
ElioDiNino d502de8ca8 docs: fix git credentials file name (#8406) 2023-07-12 20:00:15 +03:00
Bruno Quaresma f8f4ae8e2c refactor(site): update build log to be displayed only on active statuses (#8459) 2023-07-12 13:34:07 -03:00
Mathias Fredriksson 29e4205abf ci(scripts): add support for --force in release.sh (#8469) 2023-07-12 16:31:13 +00:00
dependabot[bot] ffc28e6b93 chore: bump xterm in /site from 5.1.0 to 5.2.1 (#8465)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-12 18:35:32 +03:00
Colin Adler 1c3bfacca3 fix(cli): ensure cliui.Agent doesn't fetch infinitely (#8446) 2023-07-12 10:21:54 -05:00
dependabot[bot] 14caa9b7c1 chore: bump the npm-eslint group in /site with 3 updates (#8464)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-12 18:21:02 +03:00
dependabot[bot] b62798303c chore: bump just-debounce-it from 3.1.1 to 3.2.0 in /site (#8466)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-12 18:11:36 +03:00
dependabot[bot] d7eeb4118e chore: bump the npm-mui group in /site with 3 updates (#8460)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-12 15:04:08 +00:00
dependabot[bot] 49081e0383 chore: bump @emotion/react from 11.10.8 to 11.11.1 in /site (#8380)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-12 17:58:24 +03:00
dependabot[bot] 861efe7b50 chore: bump the npm-xstate group in /site with 3 updates (#8461)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-12 17:56:52 +03:00
Muhammad Atif Ali 765fd29336 chore: upgrade go version to 1.20.6 (#8457) 2023-07-12 17:27:17 +03:00
Ben Potter 280112a366 docs: add devcontainer templates (#8256)
* start

* start a template

* basic structure

* Update docs/templates/devcontainers.md

Co-authored-by: Sebastiaan ten Pas <sebastiaan@diggimedia.nl>

* Update examples/templates/devcontainer-docker/main.tf

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

* add kubernetes template

* mention parameters

* fixups

* fixes from feedback

---------

Co-authored-by: Sebastiaan ten Pas <sebastiaan@diggimedia.nl>
Co-authored-by: Muhammad Atif Ali <atif@coder.com>
2023-07-12 10:22:39 -04:00
Muhammad Atif Ali 19f58350f2 chore: group various npm dependencies (#8453) 2023-07-12 16:45:50 +03:00
Bruno Quaresma bff73ade6a feat(site): add warning for unhealthy workspace (#8422) 2023-07-12 10:27:25 -03:00
dependabot[bot] 86f89892c8 chore: bump msw from 1.1.0 to 1.2.2 in /site (#8440)
Bumps [msw](https://github.com/mswjs/msw) from 1.1.0 to 1.2.2.
- [Release notes](https://github.com/mswjs/msw/releases)
- [Changelog](https://github.com/mswjs/msw/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mswjs/msw/compare/v1.1.0...v1.2.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-12 10:26:51 -03:00
Mathias Fredriksson f6a8a5f7be fix(site): prevent ExtractAPIKey from dirtying the HTML output (#8450)
If `httpmw.ExtractAPIKey` fails when we are rendering an HTML page, the
HTML output will be dirtied with the error repsonse and the HTTP status
will also be wrong.

The use of this function in the `renderHTMLWithState` is additive, and
failure means we simply can't embed static data. To fix this, we can
simply pass a `http.ResponseWriter` that is no-op.

Fixes #8351
2023-07-12 15:38:30 +03:00
Steven Masley e9d7a230fa chore: server header specifies if wsproxy (#8432) 2023-07-12 12:07:36 +00:00
Steven Masley 928091aa05 feat!: add table format to 'coder license ls', 'license_expires' --> 'license_expires_human' (#8421)
* feat: add table format to 'coder license ls'
* feat: license expires_at to table view
* change: `license_expires` to `license_expires_human` and `license_expires` is unix timestamp
2023-07-12 08:06:18 -04:00
dependabot[bot] 2c2dd0eb83 chore: bump crate-ci/typos from v1.14.12 to v1.16.0 and fix typos (#8455)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Muhammad Atif Ali <atif@coder.com>
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2023-07-12 12:01:30 +00:00
Muhammad Atif Ali 7a210d941e chore: remove invalid dependabot config for example templates (#8452) 2023-07-12 14:33:57 +03:00
Spike Curtis b4057bd74a feat: make pgCoordinator generally available (#8419)
* pgCoord to GA, fix tests

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

* Fix generation and coordinator delete RBAC

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

* Fix fakeQuerier -> FakeQuerier

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-07-12 13:35:29 +04:00
dependabot[bot] 2e9f3e0a6b chore: bump the go-otel group with 4 updates (#8437)
Co-authored-by: Colin Adler <colin1adler@gmail.com>
2023-07-11 17:12:59 -05:00
dependabot[bot] 13b9ed3240 chore: bump google.golang.org/api from 0.129.0 to 0.130.0 (#8438)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-11 20:50:23 +00:00
Colin Adler 2c0f653aa8 chore(dogfood): give Asher more ram (#8442) 2023-07-11 15:24:29 -05:00
Marcin Tojek bc835dbf69 fix: use numeric comparison to check monotonicity (#8436) 2023-07-11 22:08:41 +02:00
Eric Paulsen d8d8eb2c8d docs: add note on recommended Azure LB (#8431) 2023-07-11 18:19:45 +00:00
Colin Adler 21597e2d69 chore: upgrade Go to 1.20.6 (#8433) 2023-07-11 13:06:34 -05:00
goodspark dd4aafb350 feat: add template info tags to coderd_agents_up metric (#7942)
Co-authored-by: Colin Adler <colin1adler@gmail.com>
2023-07-11 12:39:14 -05:00
Mathias Fredriksson 398e8fdf89 fix(coderd/httpmw): handle oauth config removed for existing auth (#8420)
This commit fixes an edge case tied to unexpired oauth logins where the
oauth provider is removed, the server restarted, and the users auth
expiring after the fact.

Refs #8351, #8352, #8390
2023-07-11 20:29:29 +03:00
Mathias Fredriksson e508d9aa6e fix(agent/usershell): check shell on darwin via dscl (#8366) 2023-07-11 20:27:50 +03:00
Mathias Fredriksson de1d04d7bb fix(dogfood): use gcr.io/coder-dev-1/alpine mirror to fix rate limit (#8424) 2023-07-11 20:27:07 +03:00
Steven Masley 704212e271 feat: expand wsproxy settings page by default (#8428)
* feat: wsproxy settings page expanded by default
* chore: warnings & defaults on proxy setting page start open
* always expanded
2023-07-11 13:15:11 -04:00
Steven Masley 0a911d58f9 chore: change workspace proxy badge names (#8429)
* chore: "Not Reachable" --> "Not Dialable"
* chore: "Not registered" --> "Never seen"
2023-07-11 13:05:42 -04:00
Steven Masley f67ccc9bc0 feat: add resource_target to audit log search (#8423)
* feat: add resource_target to audit log search
2023-07-11 13:05:21 -04:00
Muhammad Atif Ali 700ec966e3 chore: use dogfood-oss image in .devcontainer (#8231)
* use dogfood-oss image in devcontainer

* fix image name

* use Dockerfile

* Discard changes to dogfood/Dockerfile

* Use features to get docker in docker

* use previleged container

* use codercom/oss-dogfood image

* add `--priviliged` to fix GitHub Codespaces builds

* remove default options
2023-07-11 10:48:34 -04:00
Steven Masley bc102d6bd7 feat: add cli first class validation (#8374)
* feat: add cli first class validation
* feat: add required flag to cli options
* Add unit test to catch invalid and missing flag
2023-07-11 09:59:55 -04:00
Cian Johnston 3f6a158016 chore: enable exhaustruct linter (#8403)
* chore: enable exhaustruct linter

* add exlusion rules

* move to allowlist instead

* exhaustruct httpmw package

* fixup! exhaustruct httpmw package

* make lint

* address PR comments
2023-07-11 14:30:33 +01:00
Mathias Fredriksson 75f62dc39d feat: add support for template version messages in api and cli (#8336) 2023-07-11 13:11:08 +03:00
Kyle Carberry b4a7fe3221 fix: use expires_in field for git device refresh (#8411)
This was causing git auth to never refresh after the token
became expired after 8hrs.
2023-07-11 09:28:52 +00:00
Colin Adler 9df80530d7 chore(cli): unhide netcheck command (#8412) 2023-07-10 21:38:02 -05:00
Bruno Quaresma d896b74fa2 feat(site): display build logs on workspace transitioning statuses (#8397) 2023-07-10 17:47:39 -03:00
dependabot[bot] b7641b219e chore: bump the golang-x group with 4 updates (#8400)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-10 15:33:17 -05:00
dependabot[bot] ff62f70f55 chore: bump ts-proto from 1.150.0 to 1.152.0 in /site (#8402)
Bumps [ts-proto](https://github.com/stephenh/ts-proto) from 1.150.0 to 1.152.0.
- [Release notes](https://github.com/stephenh/ts-proto/releases)
- [Changelog](https://github.com/stephenh/ts-proto/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stephenh/ts-proto/compare/v1.150.0...v1.152.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-10 17:28:52 -03:00
dependabot[bot] 000d538ace chore: bump react-router-dom from 6.13.0 to 6.14.1 in /site (#8398)
Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 6.13.0 to 6.14.1.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@6.14.1/packages/react-router-dom)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-10 17:28:40 -03:00
Bruno Quaresma ecb99717ef refactor(site) re-design proxies table (#8410) 2023-07-10 17:01:59 -03:00
Bruno Quaresma 3c2ce4f52a refactor(site): adjust the proxy menu design (#8409) 2023-07-10 17:01:09 -03:00
Colin Adler ef836de330 fix: document workspace filter query param correctly (#8408) 2023-07-10 18:57:09 +00:00
dependabot[bot] cee8fde93d chore: bump tough-cookie from 4.1.2 to 4.1.3 in /site (#8384)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-10 13:36:48 -05:00
Muhammad Atif Ali bf8af90d5a chore: group golang.org/x/* dependencies (#8392) 2023-07-10 18:07:14 +03:00
Steven Masley 2ee406d7b6 feat: log out and redirect user when converting to oidc (#8347)
* feat: log out user on conver to oidc

Log out user and redirect to login page and log out user when
they convert to oidc.
2023-07-10 10:25:41 -04:00
Mathias Fredriksson 90a3debe3f fix(coderd): pass oauth configs to site (#8390) 2023-07-10 17:23:41 +03:00
dependabot[bot] 9a3d9053a0 chore: bump @types/uuid from 8.3.4 to 9.0.2 in /site (#8377)
Bumps [@types/uuid](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/uuid) from 8.3.4 to 9.0.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/uuid)

---
updated-dependencies:
- dependency-name: "@types/uuid"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-10 11:11:54 -03:00
dependabot[bot] 19ec5789a4 chore: bump vite from 4.3.9 to 4.4.2 in /site (#8381)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.3.9 to 4.4.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.4.2/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-10 11:11:10 -03:00
Mathias Fredriksson 9a72ddf7d4 fix(enterprise/coderd): add missing fields to extract api key config (#8393) 2023-07-10 16:51:50 +03:00
Marcin Tojek 9f2a931eb8 feat(coderd): support ephemeral parameters (#8367) 2023-07-10 13:44:03 +02:00
Marcin Tojek 8f4157c28d chore: go mod tidy (#8389) 2023-07-10 11:27:24 +00:00
Mathias Fredriksson b73f9d8e86 feat: add computed workspace and agent health fields to the api (#8280) 2023-07-10 12:40:11 +03:00
dependabot[bot] eabf929676 chore: bump actions/checkout from 2 to 3 (#8382) 2023-07-09 01:43:44 +03:00
Ammar Bandukwala 0115adf9d7 chore(.golangci.yaml): make gocyclo slightly more sensitive (#8383) 2023-07-08 22:43:34 +00:00
Muhammad Atif Ali d19e679c20 chore: use dependabot groups (#8372) 2023-07-08 17:29:47 +03:00
Steven Masley a297a014d6 chore: improve error message for incorrect login type (#8349)
* chore: add better error in wrong login type
2023-07-07 11:33:31 -04:00
Cian Johnston c1ab5cf0ba fix(cli/clistat): better handle cgroups with no limits (#8373) 2023-07-07 15:49:36 +01:00
Steven Masley f75d497c41 chore: touch ups to wsproxy UX (#8350)
* chore: update wording on wsproxy help
* chore: show help if no fields specified in wsproxy edit
* chore: Add run command example to wsproxy create
* chore: remove localhost warning
* chore: navbar match page title
* chore: Add helper text to latency picker
* chore: add confirm delete to workspace proxy delete cli
* chore: add errors + warnings to workspace proxy table
2023-07-07 10:06:11 -04:00
Cian Johnston 396e5e9a60 chore(database): fix test flake in TestUserLastSeenFilter (#8369) 2023-07-07 14:28:45 +01:00
Bruno Quaresma 2baa34364a fix(site): resize terminal when alert is dismissed (#8368) 2023-07-07 10:19:39 -03:00
Marcin Tojek e088303382 feat!: drop LegacyVariableName from coder parameter (#8360) 2023-07-07 14:14:29 +02:00
Cian Johnston 4e42c33769 chore(Makefile): specify count=1 instead of cleaning test cache (#8359)
* chore(Makefile): specify count=1 instead of cleaning test cache

* Update Makefile

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

* add comment to test-clean target

---------

Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2023-07-07 12:16:16 +01:00
Mathias Fredriksson 34c3f919dc fix(agent/agentssh): check for hushlogin via afero fs (#8358) 2023-07-07 13:30:23 +03:00
Cian Johnston 259453007e chore(Makefile): add test-race target (#8357)
* chore(Makefile): add test-race target

* fixup! chore(Makefile): add test-race target
2023-07-07 10:15:50 +00:00
Marcin Tojek 64687631aa feat: enable Terraform template-wide variables by default (#8334) 2023-07-07 11:49:02 +02:00
Cian Johnston 435c67ab75 refactor(cli)!: move scaletest to exp/scaletest (#8339)
* refactor(cli): mv scaletest exp/scaletest

* make gen
2023-07-07 09:10:14 +01:00
Colin Adler ccea595b39 chore: temporarily skip prisma scan (#8355) 2023-07-07 00:48:29 +00:00
Colin Adler 6079cda851 chore(dogfood): add docker compose (#8354) 2023-07-06 19:46:40 -05:00
Colin Adler 9a0ba1bdc3 fix(coderd): remove CREATE INDEX CONCURRENTLY from migrations (#8353) 2023-07-06 16:44:29 -05:00
Steven Masley 2ebd0ec6c5 fix: resolve nil pointer dereference on missing oauth config (#8352) 2023-07-06 16:46:22 -04:00
Steven Masley 9f5bc7c10b feat: add --branch option to clone or checkout different dotfiles branch (#8331)
* feat: --branch option to clone different dotfiles branch
* chore: checkout specified branch if dotfiles already exist
2023-07-06 20:24:04 +00:00
Muhammad Atif Ali 5bb6bc5571 chore: update fly.io example to remove deprecated parameters (#8194)
* chore: update fly.io example to remove deprecated parameters

* `make fmt`

* update provider version
2023-07-06 10:20:23 -04:00
Bruno Quaresma a33a255885 chore: upgrade node version on CI to match workspace version (#8340) 2023-07-06 14:04:29 +00:00
Colin Adler 33bdc23e1d chore(site): fix node vulnerabilities (#8330)
* chore: fix node vulnerabilities

https://github.com/advisories/GHSA-c2qf-rxjj-qqgw
https://github.com/advisories/GHSA-j8xg-fqg3-53r7

* fixup! chore: fix node vulnerabilities

* fixup! chore: fix node vulnerabilities

* fixup! chore: fix node vulnerabilities

* fixup! chore: fix node vulnerabilities

* fixup! chore: fix node vulnerabilities
2023-07-06 10:26:55 -03:00
Mathias Fredriksson 4a13c58077 test(pty/ptytest): fix error message on deadline exceeded (#8337) 2023-07-06 16:18:03 +03:00
Mathias Fredriksson 814534d6b7 fix(enterprise/audit): improve error message for missing action (#8335) 2023-07-06 15:43:32 +03:00
Cian Johnston 7fcf319e01 fix(cli)!: protect client Logger and refactor cli scaletest tests (#8317)
- (breaking) Protects Logger and LogBodies fields of codersdk.Client with its mutex. This addresses a data race in cli/scaletest.
- Fillets the existing cli/createworkspaces unit test and moves the testing logic there into the tests under scaletest/createworkspaces.
- Adds testutil.RaceEnabled bool const and conditionaly skips previously-skipped tests under scaletest/ if the race detector is enabled. This is unfortunate and sad, but I would prefer to have these tests at least running without the race detector than not running at all.
- Adds IgnoreErrors option to fake in-memory agent loggers; having the agents fail the test immediately when they encounter any sort of error isn't really helpful.
2023-07-06 09:43:39 +01:00
Mathias Fredriksson 1d746b901b fix(Makefile): add dbmock to swagger deps and improve performance (#8332) 2023-07-06 10:59:08 +03:00
Mathias Fredriksson 3f058f28e7 test(agent): use afero for motd tests to allow parallel execution (#8329) 2023-07-06 10:57:51 +03:00
Marcin Tojek c6fcd7ee93 fix: report failed CompletedJob (#8318) 2023-07-06 07:26:33 +00:00
Marcin Tojek 45eca671fe feat: delay pending-in-queue banner (#8309) 2023-07-06 09:13:25 +02:00
Ben Potter d843735c02 docs: explain Template inheritance with Terraform modules (#8328)
* docs: explain Template inheritance with Terraform modules

* make fmt & title renaming

---------

Co-authored-by: Eric <ericpaulsen@coder.com>
2023-07-05 15:06:16 -04:00
Colin Adler c0835c443d chore: pin terraform to 1.5.2 (#8322)
The Alpine ARM builders are no longer behind on releases.
2023-07-05 12:59:58 -05:00
Steven Masley 418c9b8743 docs: update template acl example params (#8320)
Param examples were unhelpful
2023-07-05 17:24:21 +00:00
Steven Masley 4a008a8f34 chore: prevent nil dereferences on cmd handlers (#8319)
* chore: detect nil cmd handlers

Prevent nil panic dereferences on cmd handlers. Add a unit test
to prevent future mistakes
2023-07-05 17:20:12 +00:00
Steven Masley d70e2d97de chore: use correct prettier version in ci (#8321)
Install prettier in package.json + yarn.lock. Global install was using incorrect version
2023-07-05 13:11:08 -04:00
Steven Masley 0f5a1ad480 feat: add spinner to latencies when refetching (#8278)
* feat: add spinner to latencies when refetching
2023-07-05 13:06:09 -04:00
Steven Masley 9a7705c656 feat: generate a new session with coder login --token (#8275)
* feat: coder login --token generates a new session

Makes sure /logout does not delete the inputted token
* flag to enable previous behavior if needed
2023-07-05 08:59:38 -04:00
Mathias Fredriksson 88c35d3f04 fix(pty): close output writer before reader on Windows to unblock close (#8299) 2023-07-05 15:25:07 +03:00
Colin Adler 59246e054f chore(dogfood): add libssl-dev to image (#8312) 2023-07-05 00:24:32 -05:00
sharkymark 65112ecc04 docs: update examples of IDEs: remove JetBrains Projector and add VS Code Server (#8310)
* docs: remove mention of jetbrains projector; add vs code server web IDE example

* fmt

---------

Co-authored-by: Ben <me@bpmct.net>
2023-07-04 13:27:49 +00:00
dependabot[bot] 647fd7699b chore: bump github.com/prometheus/client_golang from 1.15.1 to 1.16.0 (#8300)
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.15.1 to 1.16.0.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.15.1...v1.16.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-03 13:00:37 -05:00
dependabot[bot] 865f49af35 chore: bump github.com/gohugoio/hugo from 0.114.1 to 0.115.0 (#8301)
Bumps [github.com/gohugoio/hugo](https://github.com/gohugoio/hugo) from 0.114.1 to 0.115.0.
- [Release notes](https://github.com/gohugoio/hugo/releases)
- [Changelog](https://github.com/gohugoio/hugo/blob/master/hugoreleaser.toml)
- [Commits](https://github.com/gohugoio/hugo/compare/v0.114.1...v0.115.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-03 13:00:31 -05:00
dependabot[bot] 2fc8a275fe chore: bump google.golang.org/api from 0.128.0 to 0.129.0 (#8303)
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.128.0 to 0.129.0.
- [Release notes](https://github.com/googleapis/google-api-go-client/releases)
- [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md)
- [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.128.0...v0.129.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-03 13:00:15 -05:00
dependabot[bot] 82a6405bfa chore: bump golang.org/x/tools from 0.9.3 to 0.10.0 (#8304)
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.9.3 to 0.10.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.9.3...v0.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-03 13:00:05 -05:00
Jon Ayers 4a9c8f407a feat: add auto-locking/deleting workspace based on template config (#8240) 2023-07-02 21:29:52 -05:00
dependabot[bot] 818c4a7f23 chore: bump yup from 0.32.11 to 1.2.0 in /site (#8287)
Bumps [yup](https://github.com/jquense/yup) from 0.32.11 to 1.2.0.
- [Release notes](https://github.com/jquense/yup/releases)
- [Changelog](https://github.com/jquense/yup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jquense/yup/compare/v0.32.11...v1.2.0)

---
updated-dependencies:
- dependency-name: yup
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-02 10:32:43 -05:00
dependabot[bot] 2af41a439b chore: bump @vitejs/plugin-react from 2.1.0 to 4.0.1 in /site (#8288)
Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 2.1.0 to 4.0.1.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@4.0.1/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-02 10:32:36 -05:00
dependabot[bot] 2899b9431d chore: bump monaco-editor from 0.38.0 to 0.39.0 in /site (#8289)
Bumps [monaco-editor](https://github.com/microsoft/monaco-editor) from 0.38.0 to 0.39.0.
- [Changelog](https://github.com/microsoft/monaco-editor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/microsoft/monaco-editor/compare/v0.38.0...v0.39.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-02 10:32:30 -05:00
dependabot[bot] c19541e28f chore: bump eslint-plugin-unicorn from 44.0.0 to 47.0.0 in /site (#8291)
Bumps [eslint-plugin-unicorn](https://github.com/sindresorhus/eslint-plugin-unicorn) from 44.0.0 to 47.0.0.
- [Release notes](https://github.com/sindresorhus/eslint-plugin-unicorn/releases)
- [Commits](https://github.com/sindresorhus/eslint-plugin-unicorn/compare/v44.0.0...v47.0.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-unicorn
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-02 10:32:19 -05:00
Jon Ayers 681e2db9ec Revert "fix: add front-end fixes for minor workspace action bugs (#8252)" (#8286)
This reverts commit c569528fb7.
2023-06-30 20:26:38 -05:00
Muhammad Atif Ali 83bee35c36 chore: prevent running ci when other workflows are updated (#8285) 2023-06-30 22:38:50 +00:00
Muhammad Atif Ali dad4876e90 chore: react to deploy-pr comment (#8284) 2023-06-30 22:25:23 +00:00
Asher 6015319e9d feat: show service banner in SSH/TTY sessions (#8186)
* Allow workspace agents to get appearance
* Poll for service banner every two minutes
* Show service banner before MOTD if not quiet
2023-06-30 10:41:29 -08:00
Steven Masley eb0497ff82 feat: fetch proxy latencies at most once per 30s (#8277)
* feat: fetch proxy latencies at most once per 30s
2023-06-30 12:45:02 -04:00
Steven Masley f0bd258ff1 feat: move proxy settings page to deployment options (#8246)
* feat: Move workspace proxy page to deployment options

Workspace proxy settings page is now an admin feature

* WorkspaceProxy response extends region
2023-06-30 11:32:35 -04:00
Cian Johnston 1e8cc2ca8d feat: scaletest: scale down nodegroups by default (#8276)
* feat: allow scaling down scaletest environments

* fix bugged namespace deletion

* misc fixes to scaletest.sh

* destroy namespaces is a no-op as the cluster will be gone anyway
2023-06-30 16:07:47 +01:00
Cian Johnston a6bd85df38 feat: scaletest: add Grafana dashboard for scale testing (#8274)
* feat: scaletest: add Grafana dashboard for scale testing

Fixes #7600.

* make fmt
2023-06-30 14:04:46 +00:00
Kira Pilot c569528fb7 fix: add front-end fixes for minor workspace action bugs (#8252)
* fix: incorrect copy on inactivity_ttl field

* disabling locked fields unless inactivity TTL is set

* scoping inactivity dialog message to template

* fixed pluralization for inactivity dialog

* amending logic gate to show inactivity dialog

* fixed pagination bug
2023-06-30 09:45:44 -04:00
Steven Masley 9f76dab348 chore: Bump migration to remove conflict (#8273) 2023-06-30 13:12:40 +00:00
Cian Johnston 8b2d4b87fc fix: examples/lima: use new parameter name when creating template (#8272) 2023-06-30 12:45:47 +00:00
Steven Masley b5f26d9bdf feat: add ability for users to convert their password login type to oauth/github login (#8105)
* Currently toggled by experiment flag

---------

Co-authored-by: Bruno Quaresma <bruno@coder.com>
2023-06-30 08:38:48 -04:00
Cian Johnston 357f3b38f7 fix: scaletest: mount CODER_CACHE volume under /tmp (#8271)
Mounting the CODER_CACHE volume under /tmp/coder causes
template creation to fail due to read-only tmp dir.
2023-06-30 12:48:34 +01:00
Marcin Tojek 6639c69fad feat: add "display_order" column to coder_parameter to keep parameters sorted in UI (#8227) 2023-06-30 12:41:55 +02:00
Colin Adler 72a48bee52 fix: pin datadog-ci version (#8270) 2023-06-30 03:42:13 +00:00
Kyle Carberry 34467a3289 feat: add github device flow for authentication (#8232)
* feat: add github device flow for authentication

This will allow us to add a GitHub OAuth provider out-of-the-box
to reduce setup requirements.

* Improve askpass view

* Add routes to improve clarity of git auth

* Redesign the git auth page

* Refactor to add a page view

* Fix sideways layout

* Remove legacy notify

* Fix git auth redirects

* Add E2E tests

* Fix route documentation

* Fix imports

* Remove unused imports

* Fix E2E web test

* Fix friendly message appearance

* Fix layout shifting for full-screen sign-in

* Fix height going to 100%

* Fix comments
2023-06-29 18:58:01 +00:00
Eric Paulsen 7f82eca64a docs: note SSH key location in workspaces (#8264) 2023-06-29 12:01:07 -04:00
Cian Johnston 7072b8eff5 chore: update scaletest terraform with latest findings (#8249)
Updates scaletest terraform with learnings from #8213:

- Increase max pods per node to 256
- Decrease CPU requests for test workspace to allow maxing out workspaces per node
- Explicitly set CODER_ACCESS_URL for ssh to work
- Explicitly disable rate limits in coderd
- Increase DB size for medium and large scenarios
- Mount cache volume directly under /tmp/coder instead of /tmp.
- Plumb through requests and limits for workspaces
- Plumb through requests for coderd
2023-06-29 14:03:11 +00:00
Marcin Tojek 83fee4b192 feat: enable Terraform debug mode via deployment configuration (#8260) 2023-06-29 15:22:21 +02:00
Kira Pilot 7ca624eceb fix: removing prettier path in vscode config (#8255) 2023-06-29 09:00:51 -04:00
Jon Ayers 749307ef08 feat: provide endpoint to lock/unlock workspace (#8239) 2023-06-28 16:12:49 -05:00
Steven Masley 72e83df578 hotfix: fix broken link in the group sync docs (#8251) 2023-06-28 13:55:50 -04:00
Marcin Tojek 3dbc96d597 feat: show queue position of pending workspace build (#8244) 2023-06-28 14:48:58 +02:00
Mathias Fredriksson d3c39b60c9 feat: add agent log streaming and follow provisioner format (#8170) 2023-06-28 10:54:13 +02:00
Spike Curtis c0a01ec81c fix: fix TestPGCoordinatorDual_Mainline flake (#8228)
* fix TestPGCoordinatorDual_Mainline flake

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

* use slices.Contains instead of local function

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-06-28 11:37:45 +04:00
Eric Paulsen df95cf7ab2 feat: helm rbac perms for deployments (#8233) 2023-06-27 15:14:39 -04:00
Colin Adler b8a143566b fix: use *string instead of error in healthcheck response (#8234) 2023-06-27 19:13:54 +00:00
Asher e2e07b01e9 fix: revert to canvas renderer for xterm (#8138)
The default changed from canvas to DOM in the last update.  The canvas
renderer resolves line spacing issues and has better performance, as
well as bringing us back to the previous status quo.

In the new version the canvas is enabled via an addon.
2023-06-27 09:57:16 -08:00
Mathias Fredriksson b4751c72d8 fix(cli/agent): wrap lumberjack logger to prevent re-open (#8229) 2023-06-27 12:49:44 +00:00
Mathias Fredriksson 6d176aee5d test(agent): fix lifecycle test flakeyness (#8230) 2023-06-27 12:44:16 +00:00
Muhammad Atif Ali 25decc152a chore: fix broken docs links (#8198)
* chore: add examples templates to docs filter

* chore: fix broken docs links

* Discard changes to .github/workflows/ci.yaml
2023-06-27 08:20:57 -04:00
Ben Potter 70cd87140d docs: mark agent metadata as GA (#8111)
* docs: agent metadata is now GA

* fmt
2023-06-27 07:35:01 -04:00
Muhammad Atif Ali 9cefd46bcf chore: delete skipped workflow runs of pr-deploy.yaml (#8225) 2023-06-27 14:26:21 +03:00
Marcin Tojek b83cdfe428 chore: update nix flake to include sqlc v1.18.0 (#8226) 2023-06-27 12:50:04 +02:00
Spike Curtis 7943a5b85e fix PG coordinator context and RBAC subject (#8223)
Signed-off-by: Spike Curtis <spike@coder.com>
2023-06-27 10:14:31 +00:00
Spike Curtis 5d26637686 feat(scaletest): add license and experiment to scaletest (#8222)
* add license and experiment to scaletest

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

* appease lint & fmt

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-06-27 10:13:36 +00:00
Muhammad Atif Ali 96435ee2ce chore: fix condition in pr-deploy.yaml (#8224) 2023-06-27 09:59:15 +00:00
dependabot[bot] c969da469d chore: bump semver from 7.3.7 to 7.5.2 in /site (#8162)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-27 12:54:19 +03:00
Muhammad Atif Ali b4ca285582 chore: ignore sub directories in examples for go files check (#8197)
There are no go files in any of the subdirectories in examples directory.
2023-06-27 00:35:09 +03:00
Steven Masley 140683813d chore: reword wsproxy help (#8168) 2023-06-26 12:32:47 -05:00
Mathias Fredriksson 31076ad665 fix(enterprise/coderd): prevent deadlock during entitlements update (#8215) 2023-06-26 20:22:28 +03:00
Steven Masley 56395410bd chore: disable auto proxy selection based on latency (#8137)
* chore: disable auto pick proxy based on latency

* Remove latency pulled from storage
2023-06-26 13:08:32 -04:00
Steven Masley 82415a6541 chore: add documentation to address common group sync errors (#8212)
* chore: Add documentation to address common group sync errors
2023-06-26 13:08:20 -04:00
Cian Johnston 80ef147060 fix(cli): stat: explicitly specify resource SI prefix (#8206)
* fix(cli): move to explicitly specifying units

* make gen
2023-06-26 18:06:38 +01:00
dependabot[bot] 1558ef52f1 chore: bump github.com/gohugoio/hugo from 0.113.0 to 0.114.1 (#8201)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-26 10:48:39 -05:00
dependabot[bot] 98457e993b chore: bump github.com/valyala/fasthttp from 1.47.0 to 1.48.0 (#8204)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-26 15:40:21 +00:00
Kyle Carberry f40865bc2f chore: use mutex around blockEndpoints (#8209)
https://github.com/coder/coder/actions/runs/5378950122/jobs/9759972142
2023-06-26 10:01:50 -05:00
dependabot[bot] 4539ce7db6 chore: bump golang.org/x/oauth2 from 0.8.0 to 0.9.0 (#8203)
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.8.0 to 0.9.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.8.0...v0.9.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-26 10:01:10 -05:00
Cian Johnston b8437ce453 fix(scaletest): adjust sessionAffinity and scenario resources (#8205)
* scaletest: adjust scenario resources
* scaletest: set sessionAffinity=None for coder service
2023-06-26 15:54:05 +01:00
Muhammad Atif Ali cfbe96a379 chore: return success if pr-cleanup.yaml faisl to find a PR image tag (#8208) 2023-06-26 14:47:10 +00:00
Marcin Tojek b4f14cc6a9 feat: include coder_parameters from external modules (#8195) 2023-06-26 13:46:30 +00:00
Muhammad Atif Ali 0a6e6442b8 chore: add examples templates to docs filter (#8196) 2023-06-26 09:48:23 +00:00
野漫横江 bdecf53413 Update additional-clusters.md (#8190)
Domain name should not be localhost
2023-06-25 15:41:17 +00:00
野漫横江 e27cfe6e6a docs: fix incorrect command in project documentation (#8191) 2023-06-25 15:12:10 +00:00
Dean Sheather 98a5ae7f48 feat: add provisioner job hang detector (#7927) 2023-06-25 13:17:00 +00:00
Eric Paulsen 3671846b1b docs: add default dir for VS Code Desktop (#8184) 2023-06-23 17:52:01 -05:00
Eric Paulsen 46df6c58bc docs: add max lifetime docs and refactor UI helper text (#8185) 2023-06-23 17:51:42 -05:00
Spike Curtis 5d48122f12 fix: fix PG Coordinator to update when heartbeats (re)start (#8178)
* fix: fix PG Coordinator to update when heartbeats (re)start

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

* rename resetExpiryTimer(WithLock)

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-06-23 10:38:58 +00:00
Spike Curtis ba9d038d42 feat: add periodic cleanup of PG Coordinator state (#8142)
* PG Coordinator cleans orphaned state

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

* Don't need pubsub

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-06-23 13:23:28 +04:00
Rodrigo Maia c594f02948 fix(site): display correct user_limit on license ui (#8118) 2023-06-23 08:38:36 +02:00
Steven Masley 797e91d4c6 feat: add flag for Windows to create unix compatible filepaths (#8164)
* feat: add flag for Windows to create unix compatible filepaths
2023-06-22 17:08:12 -05:00
Marcin Tojek 5d45218a5d docs: application logs (#8166) 2023-06-22 23:25:17 +02:00
Mathias Fredriksson 3b9b06fe5a feat(codersdk/agentsdk): add StartupLogsSender and StartupLogsWriter (#8129)
This commit adds two new `agentsdk` functions, `StartupLogsSender` and
`StartupLogsWriter` that can be used by any client looking to send
startup logs to coderd.

We also refactor the `agent` to use these new functions.

As a bonus, agent startup logs are separated into "info" and "error"
levels to separate stdout and stderr.

---------

Co-authored-by: Marcin Tojek <mtojek@users.noreply.github.com>
2023-06-22 23:28:59 +03:00
Steven Masley f56db1b41b feat: add user search query param on last_seen (#8139)
* feat: add sql filter for before/after on last_seen column
2023-06-22 15:24:48 -05:00
Colin Adler 97945ae272 fix(scripts): properly shift for --build-base option (#8167) 2023-06-22 15:24:03 -05:00
Colin Adler 41e7216df7 chore: upgrade terraform to 1.5.1 (#8160) 2023-06-22 19:08:40 +00:00
Marcin Tojek 8e2422d42c feat: use named loggers in coderd (#8148) 2023-06-22 20:09:33 +02:00
Muhammad Atif Ali 1417c12229 ci: get PR number correctly from pull_request event (#8158) 2023-06-22 17:19:14 +00:00
Muhammad Atif Ali 2c046ef475 ci: use single quotes when we have backticks in title (#8159) 2023-06-22 16:57:44 +00:00
Muhammad Atif Ali f4a712b271 ci: delete old workflow runs for pr-cleanup.yaml(#8146) 2023-06-22 19:16:06 +03:00
Muhammad Atif Ali ea4b9a4354 ci: fix syntax issue in pr-deploy.yaml (#8154) 2023-06-22 16:06:15 +00:00
Muhammad Atif Ali eee4f835ec ci: fix wrong trigger pr-deploy.yaml (#8151) 2023-06-22 18:29:27 +03:00
Muhammad Atif Ali 1ffc8058c2 ci: deploy automatic PR deployments on request (#8121) 2023-06-22 14:35:58 +00:00
Spike Curtis e856491476 feat: enable PG Coordinator as experiment (#8144)
Signed-off-by: Spike Curtis <spike@coder.com>
2023-06-22 13:12:29 +04:00
Spike Curtis e738123a9c chore: log ssh connection disconnects with errors (#8143)
Signed-off-by: Spike Curtis <spike@coder.com>
2023-06-22 11:37:50 +04:00
Jon Ayers 1b0124ecdb feat: automatically stop workspaces based on failure_ttl (#7989) 2023-06-22 00:33:22 -04:00
Kyle Carberry d434181941 feat: add cohesive e2e tests for the web terminal, apps, and workspaces (#8140)
* feat: add cohesive e2e tests for the web terminal, apps, and workspaces

* Fix web terminal flake
2023-06-22 00:21:40 +00:00
Kyle Carberry 2a492b7008 chore: improve the setup experience with various fixes (#8130)
* Move updatecheck logs to debug mode

This was causing logs to be emitted immediately after
initial setup, which seemed odd for the user.

* Fix setup page text to say "Create" instead of "Setup" account

* Improve copy on the empty workspaces page

- This view can be seen after the user creates their first workspace, so saying first is bad.
- It should describe what a workspace is, so I modified the description.
- The create from template button wasn't helpful!

* Improve the copy for the empty templates view

- This didn't describe what a template actually is.
- The title had the same problem as workspaces, where first makes no sense.

* Improve text consistency on the Create Template page

* Fix "View activity" displaying for non-Enterprise users

This was causing an exception to be thrown.

* Improve messaging of empty groups view

* Appropriately capitalize Workspace and Template

* Improve Docker template taglines

* Fix types
2023-06-21 18:23:59 -05:00
Dean Sheather a28d422c35 feat: add flag to disable all direct connections (#7936) 2023-06-21 22:02:05 +00:00
Asher 96f9e61ca1 Strip CORS headers from applications (#8057)
The problem is that the headers get doubled up (not overwritten) and
browsers do not like multiple values for the allowed origin even though
it appears the spec allows for it.

We could prefer the application's headers instead of ours but since we
control OPTIONS I think preferring ours will by the more consistent
experience and also aligns with the original RFC.
2023-06-21 13:41:27 -08:00
Dean Sheather 24b95e16c4 feat: add --disable-direct flag to CLI (#8131) 2023-06-21 20:22:43 +00:00
Colin Adler bc739bdfce feat(cli): add hidden netcheck command (#8136) 2023-06-21 14:33:19 -05:00
Colin Adler 7703bb77a7 ci: update sqlc version in security scan (#8135) 2023-06-21 13:53:41 -05:00
Eric Paulsen a0d0e0e72d docs: add coder ping example for troubleshooting (#8133) 2023-06-21 18:07:24 +00:00
Bruno Quaresma 1faed639c4 fix(site): show error when parameter is invalid (#8125) 2023-06-21 11:39:28 -03:00
Bruno Quaresma bce8a983fe chore(site): remove new filter from experimental (#8112) 2023-06-21 10:20:37 -03:00
Spike Curtis cc17d2feea refactor: add postgres tailnet coordinator (#8044)
* postgres tailnet coordinator

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

* Fix db migration; tests

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

* Add fixture, regenerate

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

* Fix fixtures

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

* review comments, run clean gen

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

* Rename waitForConn -> cleanupConn

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

* code review updates

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

* db migration order

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

* fix log field name last_heartbeat

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

* fix heartbeat_from log field

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

* fix slog fields for linting

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-06-21 16:20:58 +04:00
Marcin Tojek 4fb4c9b270 chore: add more rules to ensure logs consistency (#8104) 2023-06-21 12:00:38 +02:00
Spike Curtis 1c8f564fdb feat: add logging of ssh connections to agent (#8096)
* feat: adds logging of ssh connections to agent

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

* code review improvements

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-06-21 13:49:58 +04:00
Colin Adler 4a431b6eb8 feat(helm): add coder.command to control pod command (#8116) 2023-06-20 22:59:01 +00:00
Colin Adler 15cba05dfc ci: scan images with sysdig (#8037) 2023-06-20 22:58:17 +00:00
Eric Paulsen b398814ab0 docs: add prometheus scrape_config example (#8113)
* docs: add prometheus scrape_config example

* make: fmt

* add: label

* fix: spelling

* scheme: http
2023-06-20 21:09:35 +00:00
Colin Adler 7d43f4ceee chore: update dogfood image (#8114) 2023-06-20 15:08:15 -05:00
Kyle Carberry 69f911dfd5 feat: add queue_position and queue_size to provisioner jobs (#8074) 2023-06-20 15:07:18 -05:00
Kyle Carberry bbb0fab1de chore: merge database gen scripts (#8073)
* chore: merge database gen scripts

* Fix type params gen

* Merge enum into dbgen
2023-06-20 16:24:33 +00:00
Kyle Carberry f3b2009499 chore: reorder helm chart to improve ux (#8108)
It makes no sense that the primary option `env` was so far
below infrequently used options.
2023-06-20 16:23:56 +00:00
Kyle Carberry 467b0a1004 chore: restyle provisioner job output format (#8106)
Reduce colors and formatting to reduce bugs and increase UNIX-feel.
2023-06-20 16:16:27 +00:00
Colin Adler 823127e761 feat: add healthcheck database section (#8060) 2023-06-20 15:13:22 +00:00
Bruno Quaresma 2db4488337 fix(site): fix filters errors display (#8103) 2023-06-20 12:08:05 -03:00
Mathias Fredriksson ea4b7d60d7 fix(agent): refactor trackScriptLogs to avoid deadlock (#8084)
During agent close it was possible for the startup script logs consumer
to enter a deadlock state where by agent close was waiting via
`a.trackConnGoroutine` and the log reader for a flush event.

This refactor removes the mutex in favor of channel communication and
relies on two goroutines without shared state.
2023-06-20 18:05:11 +03:00
Colin Adler adf14f1917 chore(cli): warn on template push or create when no lockfile present (#8059) 2023-06-20 15:02:44 +00:00
Bruno Quaresma a47a9b1cfe fix(site): prevent filter input update when focused (#8102) 2023-06-20 13:46:04 +00:00
Mathias Fredriksson b8ba287128 fix: disable websocket compression for startup logs in Safari (#8087) 2023-06-20 16:29:32 +03:00
Bruno Quaresma c3781d95b4 fix(site): fix build logs route (#8100) 2023-06-20 13:24:48 +00:00
Cian Johnston eb0783ff24 chore(cli): do not assert non-zero host cpu usage (#8097) 2023-06-20 13:03:56 +00:00
Cian Johnston 0f754f0bb9 fix(clistat): do not include buffers/cache for host memory used (#8095) 2023-06-20 13:29:27 +01:00
Marcin Tojek 06a5e24f5b skip: TestStatCPUCmd/JSON (#8093) 2023-06-20 12:08:11 +00:00
Mathias Fredriksson 8dac0356ed refactor: replace startup script logs EOF with starting/ready time (#8082)
This commit reverts some of the changes in #8029 and implements an
alternative method of keeping track of when the startup script has ended
and there will be no more logs.

This is achieved by adding new agent fields for tracking when the agent
enters the "starting" and "ready"/"start_error" lifecycle states. The
timestamps simplify logic since we don't need understand if the current
state is before or after the state we're interested in. They can also be
used to show data like how long the startup script took to execute. This
also allowed us to remove the EOF field from the logs as the
implementation was problematic when we returned the EOF log entry in the
response since requesting _after_ that ID would give no logs and the API
would thus lose track of EOF.
2023-06-20 14:41:55 +03:00
Marcin Tojek b1d1b63113 chore: ensure logs consistency across Coder (#8083) 2023-06-20 12:30:45 +02:00
Cian Johnston d6f8bd7847 feat(cli): add coder stat command (#8005) 2023-06-20 11:16:57 +02:00
Jon Ayers c3aef9363b feat: add locked TTL field to template meta (#8020) 2023-06-19 22:37:55 -04:00
dependabot[bot] 1ecc371ade chore: bump google.golang.org/grpc from 1.55.0 to 1.56.0 (#8079)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.55.0 to 1.56.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.55.0...v1.56.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-19 20:48:05 -05:00
Mathias Fredriksson af45e64b1d chore(coderd/database/gen): improve generated fake stub (#8088)
* chore(coderd/database/gen): generate arg validation where applicable
* fix(coderd/database/gen): support pointers and slices as return types
2023-06-20 00:05:26 +03:00
Kyle Carberry f444100aa6 feat: add region querying to pre-fetched html (#8077) 2023-06-19 11:23:26 -05:00
dependabot[bot] e083cbca6a chore: bump golang.org/x/mod from 0.10.0 to 0.11.0 (#8078)
Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.10.0 to 0.11.0.
- [Commits](https://github.com/golang/mod/compare/v0.10.0...v0.11.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-19 10:00:37 -05:00
dependabot[bot] 020c6fb71e chore: bump google.golang.org/api from 0.127.0 to 0.128.0 (#8080)
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.127.0 to 0.128.0.
- [Release notes](https://github.com/googleapis/google-api-go-client/releases)
- [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md)
- [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.127.0...v0.128.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-19 10:00:21 -05:00
dependabot[bot] 456ad85ce3 chore: bump golang.org/x/crypto from 0.9.0 to 0.10.0 (#8081)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.9.0 to 0.10.0.
- [Commits](https://github.com/golang/crypto/compare/v0.9.0...v0.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-19 10:00:13 -05:00
Kyle Carberry 5b9c3784bc docs: add steps for registering a github app with coder (#7976)
Co-authored-by: Muhammad Atif Ali <atif@coder.com>
2023-06-18 14:01:37 -05:00
Kyle Carberry 9df9ad4503 feat: embed common client requests into the template html (#8076)
This should reduce the number of API requests a client makes
when loading the dashboard dramatically!
2023-06-18 13:57:27 -05:00
Kyle Carberry 2a10c9127f fix: show git provider id instead of type (#8075)
Fixes #6343.
2023-06-18 17:01:44 +00:00
Ammar Bandukwala ca6b9e9368 chore: use robust RNG in cryptorand (#8040) 2023-06-17 17:33:40 -05:00
Montasser abd majid ZEHRI c8e67833f5 docs: add oidc keycloak docs (#8042)
* adding docs for keycloak to solve the problem with the refraish token

* fmt
2023-06-16 10:18:26 -05:00
Mathias Fredriksson 0c5077464b fix: avoid missed logs when streaming startup logs (#8029)
* feat(coderd,agent): send startup log eof at the end

* fix(coderd): fix edge case in startup log pubsub

* fix(coderd): ensure startup logs are closed on lifecycle state change (fallback)

* fix(codersdk): fix startup log channel shared memory bug

* fix(site): remove the EOF log line
2023-06-16 17:14:22 +03:00
Marcin Tojek 247f8a973f feat: replace ssh maxTimeout with keep-alive mechanism (#8062)
* Bump up coder/ssh

* feat: Set default agent timeout to ~72h

* Address PR comments

* Fix
2023-06-16 15:22:18 +02:00
Cian Johnston 751c0505bf chore: add benchmark for prometheusmetrics.MetricsAggregator (#8066)
* add benchmark for prom metrics aggregator

* fixup! add benchmark for prom metrics aggregator

* make fmt
2023-06-16 14:04:28 +01:00
Colin Adler 3ec2e96ff4 chore: tidy up grafana example dashboard (#8056) 2023-06-15 14:22:31 -05:00
dependabot[bot] 137b51e2c7 chore: bump cronstrue from 2.21.0 to 2.27.0 in /site (#7851)
Bumps [cronstrue](https://github.com/bradymholt/cronstrue) from 2.21.0 to 2.27.0.
- [Release notes](https://github.com/bradymholt/cronstrue/releases)
- [Changelog](https://github.com/bradymholt/cRonstrue/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bradymholt/cronstrue/compare/v2.21.0...v2.27.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Muhammad Atif Ali <atif@coder.com>
2023-06-15 14:34:00 -03:00
Bruno Quaresma e4d23ff5fe fix(site): add test and fix username params in terminal (#8052) 2023-06-15 13:18:19 -03:00
Bruno Quaresma f61001d049 chore(docs): optimize images (#8047) 2023-06-15 10:49:47 -03:00
dependabot[bot] 33c0674d76 chore: bump react-router-dom from 6.4.1 to 6.12.1 in /site (#7950)
* chore: bump react-router-dom from 6.4.1 to 6.12.1 in /site

Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 6.4.1 to 6.12.1.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@6.12.1/packages/react-router-dom)

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

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

* fix what needs to be fixed

* fix storybook

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: BrunoQuaresma <bruno_nonato_quaresma@hotmail.com>
2023-06-15 10:34:49 -03:00
Cian Johnston 6e598234b6 fix: only collect prometheus database metrics when explicitly enabled (#8045)
* fix: only collect prometheus database metrics when explicitly enabled

* add missing test

* de-duplicate wrapping
2023-06-15 12:34:16 +01:00
Mathias Fredriksson 3f81f38fcc ci: fix commit type prefix regexp in release script (#8046)
Previously we were incorrectly categorizing `fix(a/b): ...` as other
because the regexp only expected letters, not `/`. Now we accept any
input within the parenthesis.

---------

Co-authored-by: Cian Johnston <cian@coder.com>
2023-06-15 10:57:18 +00:00
Cian Johnston b1588fa596 fix(helm): use /healthz for liveness and readiness probes instead of /api/v2/buildinfo (#8035) 2023-06-15 10:08:48 +01:00
Eric Paulsen 977e9ef505 docs: add steps for configuring trusted headers & origins in Helm chart (#8031) 2023-06-14 16:20:40 -04:00
Kyle Carberry 8ac68c95a4 chore: lazily initialize find commands in makefile (#8030)
Improves make performance for small targets!
2023-06-14 13:17:16 -05:00
Bruno Quaresma af06e80949 fix(site): fix footer year (#8036) 2023-06-14 14:59:33 -03:00
Steven Masley 6c4c3d6ce5 feat: add login type 'none' to prevent password login (#8009)
* feat: add login type 'none' to prevent login

Users with this login type must use tokens to authenticate.
Tokens must come from some other source, not a /login with password
authentication
2023-06-14 12:48:43 -05:00
Bruno Quaresma cbd49abfcd fix(site): resize terminal when dismissing warning (#8028) 2023-06-14 14:40:55 -03:00
Steven Masley 3619a3a6dd feat: add disabling of default 'everyone' group access to template (#7982)
* feat: add disabling of default 'everyone' group access to template
* add FE to disable everyone group
* require entitlement to uncheck box
2023-06-14 11:08:58 -05:00
Kyle Carberry e4b6f5695b chore: separate pubsub into a new package (#8017)
* chore: rename store to dbmock for consistency

* chore: remove redundant dbtype package

This wasn't necessary and forked how we do DB types.

* chore: separate pubsub into a new package

This didn't need to be in database and was bloating it.
2023-06-14 15:34:54 +00:00
Dean Sheather 2c843f4011 fix: fix --header flag in CLI (#8023) 2023-06-14 21:52:01 +10:00
Cian Johnston df842b31e8 chore: fix miscellaneous issues in scaletest scripts (#8006)
* chore: scaletest: plumb through more options

* bump terraform version

* scaletest.sh: pprof during traffic gen

* cli/scaletest: actually wait for prometheus metrics to be scraped

* increase prometheus wait
2023-06-14 09:38:04 +01:00
Rodrigo Maia 1da2570e14 feat(site): add documentation links to webterminal notifications (#8019)
* feat(site): add documentation links to webterminal notifications

* change button variant
2023-06-14 00:27:40 +00:00
dependabot[bot] 7a7ee63225 chore: bump github.com/hashicorp/terraform-json from 0.16.0 to 0.17.0 (#7972)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-13 22:19:01 +00:00
dependabot[bot] af50ef76f9 chore: bump google.golang.org/api from 0.125.0 to 0.127.0 (#8018)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-14 01:15:08 +03:00
dependabot[bot] 299260361a chore: bump github.com/gohugoio/hugo from 0.111.3 to 0.113.0 (#7969)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-13 16:50:45 -05:00
Kyle Carberry 8c4b7c01e2 chore: remove redundant dbtype package (#8014)
* chore: rename store to dbmock for consistency

* chore: remove redundant dbtype package

This wasn't necessary and forked how we do DB types.
2023-06-13 19:14:55 +00:00
Mathias Fredriksson 4f512fb230 chore: use gcr.io for postgres image in OpenContainerized (#8015)
Refs #7935
2023-06-13 22:03:33 +03:00
Kyle Carberry 7d54fd5e0d chore: rename store to dbmock for consistency (#8013) 2023-06-13 13:57:43 -05:00
Kyle Carberry a658c94a67 chore: automatically generate dbauthz when new queries are added (#8007)
* chore: automatically generate dbauthz when new queries are added

* Merge system functions
2023-06-13 13:40:46 -05:00
Mathias Fredriksson c12c9f1f4e chore(go.mod): update cdr.dev/slog (#7994)
* chore(mod): update cdr.dev/slog

* fix: change uses of []slog.Field to []any to match new API
2023-06-13 18:17:04 +00:00
Mathias Fredriksson 74fdcb1ace fix(agent/agentssh): wait for sessions to exit (#8008) 2023-06-13 17:52:31 +00:00
Kyle Carberry 794a551176 chore: generate dbmetrics automatically when adding new queries (#7999)
* chore: rename dbgen package files and remove small file

* chore: automatically generate dbmetrics when new queries are added
2023-06-13 12:35:12 -05:00
Colin Adler f2758fcb33 feat: add example grafana dashboard (#7987)
* feat: add example grafana dashboard

* fixup! feat: add example grafana dashboard

* fixup! feat: add example grafana dashboard

* fixup! feat: add example grafana dashboard
2023-06-13 12:26:42 -05:00
Mathias Fredriksson 51226c55ab test(coderd): close metricscache and avoid background context (#7996) 2023-06-13 20:18:31 +03:00
Kyle Carberry 2e7e99b135 fix: scope errors to subtests for flake (#8004)
See: https://github.com/coder/coder/actions/runs/5257676837/jobs/9500882394
2023-06-13 11:52:17 -05:00
Kyle Carberry 24953869a6 chore: remove key comparison check to fix gitsshkey flake (#8002)
In tests we use weak randomness and the same key was generated which
caused a test flake here.
2023-06-13 15:51:45 +00:00
Kyle Carberry 75078fafa2 chore: try removing cache from fmt and lint (#8001) 2023-06-13 10:48:53 -05:00
Steven Masley 9cf3c582cb test: add unit test that deletes abandoned workspace (#7990)
* test: add unit test that deletes abandoned workspace

This is to ensure we do not break this functionality in future.
This is important incase this edge case happens, an admin can
clean up the abandoned resources.
2023-06-13 10:42:18 -05:00
Mathias Fredriksson c916a9e67f fix(agent): guard against multiple rpty race for same id (#7998)
* fix(agent): guard against multiple rpty race for same id
* fix(agent): ensure pty is closed on error
2023-06-13 15:14:07 +00:00
Kyle Carberry 9440b3da66 chore: rename dbgen package files and remove small file (#7997) 2023-06-13 09:21:12 -05:00
Mathias Fredriksson 1d0fae83a2 fix(coderd): prevent lost messages in watchWorkspaceAgentMetadata (#7934)
* fix(codersdk): wait for subscription in WatchWorkspaceAgentMetadata
* fix(coderd): subscribe before sending initial metadata event
* test(coderd): add retries to TestWorkspaceAgent_Metadata to avoid flake
2023-06-13 12:21:06 +00:00
Mathias Fredriksson 518300a26c fix(coderd/database): improve pubsub closure and context cancellation (#7993) 2023-06-13 15:19:56 +03:00
Ben Potter aba5cb8377 fix(docs): capture and export audit logs (#7991)
* hotfix(docs): Capturing/exporting audit logs

We frequntly get questions about this!

* fmt
2023-06-12 23:06:58 -05:00
Jon Ayers 31bd2b0bdc chore: update legacy parameters in envbox template (#7898) 2023-06-12 20:53:12 -05:00
Colin Adler ac605bad3d ci: scan images with prisma (#7988) 2023-06-12 23:11:45 +00:00
Kyle Carberry 28f35393e7 chore: generate and order dbfake funcs automatically (#7986)
* chore: rename `databasefake` to `dbfake`

* Remove unused method

* chore: add generation to dbfake

* Auto gen on make gen

* Fix preserving imports

* gen dbfake

* Add goimports

* Fix the makefile

* Make it panic instead of a comment
2023-06-12 17:40:58 -05:00
Kyle Carberry 685abfc6d7 chore: rename databasefake to dbfake (#7979)
* chore: rename `databasefake` to `dbfake`

* Remove unused method
2023-06-12 16:05:37 -05:00
Asher 4a0ac13bb7 docs: document CORS behavior between forwarded apps (#7944)
* docs: document CORS behavior

* docs: move CORS documentation under dashboard section

None of this applies to ports forwarded in other ways.
2023-06-12 20:59:47 +00:00
Kyle Carberry 6ac7d86c93 chore: prevent gen from rerunning every time (#7985) 2023-06-12 14:45:17 -05:00
Steven Masley b70d2b1767 fix: correct the correct cli output for 'template version list --output=json' (#7984)
* fix: correct 'template version list --output=json' cli cmd
2023-06-12 14:35:51 -05:00
Kira Pilot f13632cea8 feat: add impending deletion filter to workspaces page (#7860)
* add workspace deletion dialog

* add deleting_by query param

* added test

* filtering on workspaces to be deleted

* cleaned up form

* added story

* added banner filter

* PR feedback

* fix lint and stories

* PR feedback

* added enterprise test

* added unit tests in search_test.go

* remove unused fn

* unstaged changes
2023-06-12 11:55:51 -07:00
Mathias Fredriksson 4068f70d2b fix(coderd): avoid deadlock in (*logFollower).follow (#7983) 2023-06-12 18:38:46 +00:00
Ammar Bandukwala aa9dbf2eb3 ci: reduce Windows parallelism (#7981)
We have seen an uptick in Windows failures.
2023-06-12 18:24:54 +00:00
dependabot[bot] 0d71314ae1 chore: Bump aquasecurity/trivy-action from 0.10.0 to 0.11.2 (#7967)
Bumps [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action) from 0.10.0 to 0.11.2.
- [Release notes](https://github.com/aquasecurity/trivy-action/releases)
- [Commits](https://github.com/aquasecurity/trivy-action/compare/e5f43133f6e8736992c9f3c1b3296e24b37e17f2...41f05d9ecffa2ed3f1580af306000f734b733e54)

---
updated-dependencies:
- dependency-name: aquasecurity/trivy-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-12 21:22:10 +03:00
Ammar Bandukwala a540e629e5 ci: improve caching (#7954) 2023-06-12 12:09:54 -05:00
Mathias Fredriksson 4bc4e63637 fix(cli/ssh): fix lint error (#7974) 2023-06-12 16:17:41 +00:00
Muhammad Atif Ali 68ebf93fdb ci: add fmt and lint to required tests (#7975) 2023-06-12 10:43:45 -05:00
Ammar Bandukwala 5de1084639 feat(cli/ssh): simplify log file flags (#7863)
And, fix a race condition.
2023-06-12 09:18:33 +04:00
Dean Sheather ec7b117878 chore: add gen and test-go-race to required check (#7952) 2023-06-10 04:07:17 +00:00
dependabot[bot] 1ec463d74d chore: bump i18next from 21.9.1 to 22.5.0 in /site (#7812)
* chore: Bump i18next from 21.9.1 to 22.5.0 in /site

Bumps [i18next](https://github.com/i18next/i18next) from 21.9.1 to 22.5.0.
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v21.9.1...v22.5.0)

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

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

* fix i18n types

* prettier

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Rodrigo Maia <rodrigo.maia.pereira@gmail.com>
2023-06-09 18:54:17 -05:00
Dean Sheather 634e146032 ci: skip ts or go tests based on changed files (#7134)
* chore: skip ts or go tests based on files changed

* comments

* chore: add 'required' job

* fixup! chore: add 'required' job

* fake go modification

* merge main into dean/skip-tests-fe-be

* fix typo

* remove x_test.go

* remove duplicate setup-node action

* condition test-go-race too

* skip ete on docs changes

* allows tests to run if changes are made .github directory

---------

Co-authored-by: Atif Ali <atif@coder.com>
2023-06-10 02:53:16 +03:00
Ammar Bandukwala 71c908974c ci: improve caching (#7943) 2023-06-09 16:46:44 -05:00
Cian Johnston 2bbe650eb0 chore: scaletest: collect database metrics using prometheus-postgres-exporter (#7945)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2023-06-09 21:21:08 +00:00
Muhammad Atif Ali 175561bf36 docs: update CONTRIBUTING.md (#7937) 2023-06-09 19:38:16 +03:00
Steven Masley 065206345e test: add golden files to enterprise cli (#7924)
* test: Add golden files to enterprise cli
2023-06-09 11:35:20 -05:00
Dean Sheather 4f9d3155c9 ci: use macos-latest-xl runners (#7938) 2023-06-09 11:22:29 -05:00
Muhammad Atif Ali 16ebe10ff2 docs(admin/workspace-proxies): add instructions to run using the Docker image (#7915)
* docs(admin/workspace-proxies): add instructions to run using the Docker image.

* make fmt
2023-06-09 10:53:04 -05:00
Eric Paulsen 53969415ec docs: add multi git providers to enterprise chart (#7941)
* docs: add multi git providers to enterprise chart

* make: fmt
2023-06-09 14:52:51 +00:00
Dean Sheather c60ea38691 chore: use gcr.io for postgres image in CI (#7935) 2023-06-09 13:39:55 +00:00
Mathias Fredriksson e4744686ec fix(codersdk): handle API older than client for startup script behavior (#7933) 2023-06-09 13:01:56 +00:00
Colin Adler 30a635aa5f fix(enterprise): ensure scim usernames are validated (#7925) 2023-06-08 17:59:49 -05:00
Steven Masley a4cc883be1 chore: add proxy health interval flag/env config (#7919)
* chore: plumbing for proxy health interval from flag/env
2023-06-08 12:58:24 -05:00
Steven Masley fa8153a0fd chore: make default workspace proxy editable (#7903)
* chore: add editing the default workspace proxy
2023-06-08 10:30:15 -05:00
Steven Masley 5e647ba07a chore: improve workspace proxy create cli flow (#7907)
* chore: improve workspace proxy create cli flow
2023-06-08 10:02:35 -05:00
Muhammad Atif Ali 26f575e284 ci: prevent running nightly-gauntlet.yaml on PRs (#7920) 2023-06-08 14:57:39 +00:00
Mathias Fredriksson 976d23e3dc docs(templates): fix docs for updated --wait flag (#7918) 2023-06-08 14:31:22 +00:00
Mathias Fredriksson 03034dc508 chore(cli/configssh): add comment about wait and skip proxy (#7917) 2023-06-08 14:23:03 +00:00
Mathias Fredriksson fa91e11105 fix(cli/help): show deprecation notice properly for flags (#7904) 2023-06-08 14:14:42 +00:00
Mathias Fredriksson a1c32954d9 feat(cli/configssh): add support for wait yes/no/auto (#7893)
Refs #7768
2023-06-08 14:06:50 +00:00
Mathias Fredriksson 94aa9be33a feat(cli/ssh): implement wait options and deprecate no-wait (#7894)
Fixes #7768
Refs #7893
2023-06-08 16:52:44 +03:00
Steven Masley b2324325fa chore: add warning log if misconfigured groups oidc (#7874)
* chore: add warning log if misconfigured groups oidc

This is not perfect, but if we find a 'groups' claim and it is not
configured, put out a warning log to give some information
2023-06-08 08:51:59 -05:00
Montasser abd majid ZEHRI 479467473e docs: add RStudio community template without requiring a subdomain (#7909)
* adding a comunity template

* make fmt

* fixing typo in url

---------

Co-authored-by: Atif Ali <atif@coder.com>
2023-06-08 13:07:20 +00:00
Cian Johnston efbb55803b chore: add scaletest convenience script (#7819)
- Adds a convenience script `scaletest.sh` to automate process of running scale tests
- Enables pprof endpoint by default, and captures pprof traces before tearing down infra.
- Improves idempotency of coder_init.sh
- Removes the promtest.Float64 invocations in workspacetraffic runner, these metrics will be in prometheus.
- Increases default workspace traffic output to 40KB/s/workspace.
2023-06-08 09:30:02 +01:00
Ammar Bandukwala 9ec1fcf1a7 ci: move timing tests to nightly gauntlet (#7910)
Test_Runner_Timing was one of our flakiest tests before.
2023-06-08 04:03:03 +00:00
Ammar Bandukwala fcca639d38 test(agent/agentssh): close SSH servers in all tests (#7911)
Potentially solves the flake seen here:

https://github.com/coder/coder/actions/runs/5167029213/jobs/9307647816.
2023-06-07 23:43:38 +00:00
Ammar Bandukwala f6db95e71c test(coderd/database/migrations): increase fixture test timeout 2023-06-07 23:17:29 +00:00
Technofab 52ead3d933 feat(provisioner): add support for .tf.json templates (#7835)
Co-authored-by: Colin Adler <colin1adler@gmail.com>
2023-06-08 08:06:50 +10:00
Asher f0c5201617 feat: allow cross-origin requests between users' own apps (#7688) 2023-06-07 11:08:14 -08:00
Asher 125e9ef00e fix: validate that parameter names are unique (#7882) 2023-06-07 09:44:50 -08:00
Bruno Quaresma fbdbc8a6c5 fix(site): return correct useer when using me on filter menu (#7906) 2023-06-07 14:32:52 -03:00
Bruno Quaresma 5be2b77830 fix(site): format numbers in pagination text (#7900) 2023-06-07 15:25:03 +00:00
Bruno Quaresma df82f29945 fix(site): improve filter on smaller screens (#7899) 2023-06-07 15:01:49 +00:00
Bruno Quaresma 2a5025c84c feat(site): add view activity on user context menu (#7897) 2023-06-07 11:58:40 -03:00
Mathias Fredriksson eb11b50bd1 docs(templates): improve workspace incomplete and session docs (#7896) 2023-06-07 17:34:49 +03:00
Bruno Quaresma d793564937 feat(site): add new filter to audit logs (#7878) 2023-06-07 14:10:58 +00:00
Mathias Fredriksson ab3a649520 docs(templates): improve startup_script_behavior docs (#7892)
Refs #7857
2023-06-07 13:39:44 +00:00
Steven Masley 1376ba3724 chore: use a cache to select best latency (#7879)
* chore: Testing using a cache to choose the best latency
* Allow storing more latencies if needed

This PR enables a feature to record and save latencies to local storage for later analysis
2023-06-07 08:29:18 -05:00
Kira Pilot 74ffd2756a fix: respect uppercase letters in username filter for audit (#7880)
* fix: respect uppercase letters in username filter for audit

* updated documentation
2023-06-07 05:48:08 -07:00
Bruno Quaresma 91dd3fbfab feat(site): add presets back to the filters (#7876) 2023-06-07 09:46:16 -03:00
Mathias Fredriksson a77b48a5e3 docs(templates): document startup_script_behavior in-depth (#7857)
Fixes #7759

---------

Co-authored-by: Cian Johnston <cian@coder.com>
2023-06-07 12:29:19 +03:00
Cian Johnston 4f29f9abe3 fixup! fix(cli): speed up CLI over SSH (#7885) (#7888) 2023-06-07 09:13:22 +00:00
Muhammad Atif Ali 8c4b6b02e7 ci: rename cron-weekly.yaml to weekly-docs.yaml (#7886)
Renames `cron-weekly.yaml` to `weekly-docs.yaml`
2023-06-07 07:56:19 +00:00
Ammar Bandukwala 5eaf809851 fix(cli): speed up CLI over SSH (#7885)
By caching the terminal's color profile, we avoid myriad round trips during command execution.
2023-06-07 05:22:58 +00:00
Colin Adler 1288a83e42 chore: upgrade openssl (#7883) 2023-06-06 16:59:56 -05:00
goodspark 0665a6c2f2 feat: add metric for provisioner daemons (#7858) 2023-06-06 16:50:11 -05:00
Muhammad Atif Ali 4954052ff6 ci: use builder runners to build dogfood image (#7881) 2023-06-06 16:11:25 -05:00
Colin Adler 003120882f chore: update Go to 1.20.5 (#7877) 2023-06-06 14:05:09 -05:00
Steven Masley 72f59950f2 chore: add prometheus timing to latency endpoint (#7742)
* chore: Prometheus timing to latency endpoint
2023-06-06 10:26:13 -05:00
Bruno Quaresma 944c42dcb6 docs: add docs for the available query filter for workspaces and users (#7875)
* Add docs for user filtering

* Add filter for workspace

* Apply suggestions from code review

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

---------

Co-authored-by: Muhammad Atif Ali <atif@coder.com>
2023-06-06 15:12:10 +00:00
Bruno Quaresma 3121ccc7ff fix(site): Update the template form when a value is changed (#7873) 2023-06-06 11:25:25 -03:00
Bruno Quaresma 6d3dc30207 refactor(site): Remove terminal padding and add border to bottom bar (#7871) 2023-06-06 11:08:07 -03:00
Bruno Quaresma baa9922a82 feat(site): add new filter to the users page (#7818) 2023-06-06 09:59:15 -03:00
Marcin Tojek ee45b3df77 fix: ignore case while sorting usernames (#7870) 2023-06-06 12:37:41 +02:00
Mathias Fredriksson 660bbb8d38 refactor: deprecate login_before_ready in favor of startup_script_behavior (#7837)
Fixes #7758
2023-06-06 11:58:07 +03:00
Marcin Tojek 93378daeb3 feat: sort users by username (#7838) 2023-06-06 08:47:59 +02:00
Muhammad Atif Ali 2ad1308450 ci: run dogfood.yaml action when the action is updated (#7866) 2023-06-06 06:34:20 +00:00
Muhammad Atif Ali 2be7ef88e8 ci: use coder binary from the server (#7865) 2023-06-06 09:13:25 +03:00
dependabot[bot] 7332acc935 chore: bump vite from 4.3.0 to 4.3.9 in /site (#7864)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.3.0 to 4.3.9.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.3.9/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-05 21:57:53 -05:00
sharkymark 8e655b42b1 chore(docs): Added macOS self-signed cert add step and moved plugin docs to top (#7859) 2023-06-05 19:59:01 -04:00
Steven Masley fa8f50a169 fix: fix workspace status filter returning more statuses that requested (#7732) 2023-06-05 18:12:10 -05:00
Bruno Quaresma b9e3226612 refactor(site): give default error message when description is missing (#7846) 2023-06-05 17:43:24 -03:00
Bruno Quaresma 7ec16cf779 feat(site): add latency to the terminal (#7801) 2023-06-05 18:32:49 +00:00
dependabot[bot] 0413ed0178 chore: update xterm, xterm-addon-fit, and xterm-addon-web-links in /site (#7765)
* chore: bump xterm, xterm-addon-fit and xterm-addon-web-links in /site

Bumps [xterm](https://github.com/xtermjs/xterm.js), [xterm-addon-fit](https://github.com/xtermjs/xterm.js) and [xterm-addon-web-links](https://github.com/xtermjs/xterm.js). These dependencies needed to be updated together.

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

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

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

---
updated-dependencies:
- dependency-name: xterm
  dependency-type: direct:production
  update-type: version-update:semver-major
- dependency-name: xterm-addon-fit
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: xterm-addon-web-links
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

* Remove rendererType

This option no longer exists.  Renderers other than the DOM renderer
have been split into addons.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Asher <ash@coder.com>
2023-06-05 13:53:27 -03:00
dependabot[bot] a2e129083e chore: bump github.com/hashicorp/hcl/v2 from 2.14.0 to 2.17.0 (#7853)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-05 11:30:20 -05:00
dependabot[bot] 3a7c92a56f chore: bump go.uber.org/atomic from 1.10.0 to 1.11.0 (#7854)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-05 11:27:57 -05:00
dependabot[bot] b79bfb2874 chore: bump alpine from 3.16 to 3.18 in /dogfood (#7847)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-05 11:26:58 -05:00
Colin Adler 1ab2450250 fix(provisioner/terraform): ensure ordering rich parameters produces no nil values (#7824) 2023-06-05 11:26:04 -05:00
dependabot[bot] 5a3d6b589a chore: bump eslint-plugin-jsx-a11y from 6.6.1 to 6.7.1 in /site (#7850)
Bumps [eslint-plugin-jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y) from 6.6.1 to 6.7.1.
- [Release notes](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/releases)
- [Changelog](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/compare/v6.6.1...v6.7.1)

---
updated-dependencies:
- dependency-name: eslint-plugin-jsx-a11y
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-05 08:33:38 -07:00
Muhammad Atif Ali 466c530525 ci: add dogfood to dependabot (#7844) 2023-06-05 18:12:15 +03:00
dependabot[bot] 3fa7ee969d chore: bump github.com/elastic/go-sysinfo from 1.9.0 to 1.11.0 (#7841)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-05 09:56:41 -05:00
dependabot[bot] 82c709b1ba chore: bump google.golang.org/api from 0.122.0 to 0.125.0 (#7843)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-05 09:54:44 -05:00
dependabot[bot] cb42396596 chore: bump github.com/ory/dockertest/v3 from 3.9.1 to 3.10.0 (#7840)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-05 09:52:26 -05:00
sharkymark 88dc66ae47 chore(docs): Add clarifying language that resetting a user password must run on the coderd host (#7845) 2023-06-05 13:37:12 +00:00
Bruno Quaresma 6fde86d1ac fix(site): remove tar file type limitation (#7817) 2023-06-05 10:34:36 -03:00
dependabot[bot] cdba0744a2 chore: Bump jest-runner-eslint from 2.0.0 to 2.1.0 in /site (#7810)
Bumps [jest-runner-eslint](https://github.com/jest-community/jest-runner-eslint) from 2.0.0 to 2.1.0.
- [Release notes](https://github.com/jest-community/jest-runner-eslint/releases)
- [Changelog](https://github.com/jest-community/jest-runner-eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jest-community/jest-runner-eslint/compare/v2.0.0...v2.1.0)

---
updated-dependencies:
- dependency-name: jest-runner-eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-05 06:08:25 -07:00
Steven Masley bbecff28ae feat: return better error if file size is too big to upload (#7775)
* feat: return better error if file size is too big to upload
* Use a limit writer to capture actual tar size
2023-06-05 11:19:25 +00:00
Spike Curtis e016c307ae Fix postgres ephemeral ports; don't use for TestPubsub_Disconnect (#7798)
Signed-off-by: Spike Curtis <spike@coder.com>
2023-06-05 09:24:44 +04:00
dependabot[bot] f14f011abf chore: Bump github.com/hashicorp/yamux (#7808) 2023-06-04 07:30:36 +03:00
dependabot[bot] 53e5625292 chore: Bump @typescript-eslint/parser from 5.45.1 to 5.59.8 in /site (#7807) 2023-06-04 07:29:30 +03:00
dependabot[bot] 4d5f723740 chore: Bump golangci/golangci-lint-action from 3.3.1 to 3.5.0 (#7805)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 3.3.1 to 3.5.0.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v3.3.1...v3.5.0)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-04 03:37:18 +00:00
dependabot[bot] 2bf3cae3cb chore: Bump crate-ci/typos from 1.14.11 to 1.14.12 (#7804)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.14.11 to 1.14.12.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.14.11...v1.14.12)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-04 03:31:41 +00:00
dependabot[bot] e10644f86a chore: Bump monaco-editor from 0.37.1 to 0.38.0 in /site (#7814)
Bumps [monaco-editor](https://github.com/microsoft/monaco-editor) from 0.37.1 to 0.38.0.
- [Changelog](https://github.com/microsoft/monaco-editor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/microsoft/monaco-editor/compare/v0.37.1...v0.38.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-03 22:40:59 +00:00
goodspark e2613c91a7 feat(cli): support non-active template version pushes (#7488)
So that we can push template updates for testing without impacting
normal users of the template.

---------

Co-authored-by: Ammar Bandukwala <ammar@ammar.io>
Co-authored-by: Muhammad Atif Ali <matifali@live.com>
Co-authored-by: Atif Ali <atif@coder.com>
2023-06-03 22:39:00 +00:00
Muhammad Atif Ali 1f8e817da7 ci: delete CODEOWNERS (#7830) 2023-06-03 17:08:55 -05:00
Ammar Bandukwala 38d0f6fb74 perf(provisioner/terraform): upgrade hc-install (#7832)
By upgrading hc-install, we benefit from increased performance
in checksum verification introduced here:

- https://github.com/hashicorp/hc-install/commit/858962c71d47df1ea505b9031bccd5fdf7c8e07f

Also:

- Made minor improvements to tests
- Excluded TestInstall from the race detector
2023-06-03 22:03:41 +00:00
Ammar Bandukwala b62e22766f ci: skip DataDog upload for dependabot (#7831) 2023-06-03 20:47:09 +00:00
Marcin Tojek b26dd4b802 docs: remove unsupported legacy parameters (#7799)
* docs: unsupported legacy parameters

* Update docs/templates/parameters.md

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

---------

Co-authored-by: Muhammad Atif Ali <atif@coder.com>
2023-06-03 06:05:06 +02:00
Colin Adler 14a63d7b4a revert(provisionersdk): remove support for .tf.json files (#7829) 2023-06-02 23:13:41 +00:00
Colin Adler c6cd042e80 fix(clitest): use slogtest for logging stdin and stdout (#7828) 2023-06-02 22:38:31 +00:00
Ammar Bandukwala a4424205c7 ci: truly fix repo name detection 2023-06-02 22:03:21 +00:00
Muhammad Atif Ali 887bbea72a hotfix(docs): fix broken link reported by ci (#7825)
Resolves
https://github.com/coder/coder/actions/runs/5159895963/jobs/9295371489#step:4:188
2023-06-03 00:53:06 +03:00
Ammar Bandukwala f37e007b8e ci: fix repo name detection 2023-06-02 21:40:18 +00:00
dependabot[bot] ea03c7276b chore: Bump github.com/golang-migrate/migrate/v4 from 4.15.2 to 4.16.0 (#7811)
Bumps [github.com/golang-migrate/migrate/v4](https://github.com/golang-migrate/migrate) from 4.15.2 to 4.16.0.
- [Release notes](https://github.com/golang-migrate/migrate/releases)
- [Changelog](https://github.com/golang-migrate/migrate/blob/master/.goreleaser.yml)
- [Commits](https://github.com/golang-migrate/migrate/compare/v4.15.2...v4.16.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-03 00:12:20 +03:00
Eric Paulsen 6715cb6a95 docs: resolve README 404s (#7822) 2023-06-02 20:56:06 +00:00
Colin Adler edccd2de73 fix(agent): configure logger for both reaper and agent (#7823) 2023-06-02 15:51:10 -05:00
Ammar Bandukwala 97fb5020cf hotfix(ci): correctly detect repo name (#7820) 2023-06-02 20:18:09 +00:00
Eric Paulsen d082ab59b4 docs: add note on log truncation (#7821) 2023-06-02 19:36:43 +00:00
Ammar Bandukwala 2631a8436c hotfix: disable nightly-flake on all pushes 2023-06-02 18:11:04 +00:00
Ammar Bandukwala 6d8caf6f14 ci: add nightly flake workflow (#7803) 2023-06-02 12:48:51 -05:00
Ammar Bandukwala 902265bdd7 hotfix(ci): skip DataDog upload on forks (#7802)
* hotfix(ci): skip DataDog upload on forks

* fixup! hotfix(ci): skip DataDog upload on forks
2023-06-02 17:47:14 +00:00
Colin Adler 6fdebd8a07 hotfix(agent): only retain 1 logfile at a time (#7815) 2023-06-02 12:28:38 -05:00
Colin Adler 8f736fe5f5 fix(prometheusmetrics): ensure periodic metrics tick on startup (#7585) 2023-06-02 11:56:37 -05:00
Muhammad Atif Ali 9f810b1964 chore(dependabot): assign frontend team to review dependabot PRs automatically (#7800)
* chore(dependabot): assign frontend tam to review dependabot PRs automatically

* fix team name
2023-06-02 16:34:40 +00:00
Spike Curtis 078926f612 fix: select on context to avoid leak in logFollower (#7792)
Signed-off-by: Spike Curtis <spike@coder.com>
2023-06-02 11:25:46 -05:00
dependabot[bot] 004ad1755f chore: bump formik from 2.2.9 to 2.4.1 in /site (#7763)
* chore: bump formik from 2.2.9 to 2.4.1 in /site

Bumps [formik](https://github.com/formium/formik) from 2.2.9 to 2.4.1.
- [Release notes](https://github.com/formium/formik/releases)
- [Commits](https://github.com/formium/formik/compare/formik@2.2.9...formik@2.4.1)

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

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

* Fix ESLINT

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: BrunoQuaresma <bruno_nonato_quaresma@hotmail.com>
2023-06-02 09:58:59 -03:00
Marcin Tojek 10036ab5fb fix: use minDisabled, maxDisabled for parameter validation (#7755) 2023-06-02 11:43:48 +02:00
Marcin Tojek a7366a8b76 feat!: drop support for legacy parameters (#7663) 2023-06-02 11:16:46 +02:00
Colin Adler 2b63492649 feat(healthcheck): add failing sections to report (#7789) 2023-06-01 19:21:24 -05:00
Colin Adler 9b8e5c2d8a feat(provisionersdk): add support for .tf.json templates (#7744) 2023-06-02 00:03:59 +00:00
Bruno Quaresma 9dbbe82cf8 fix(site): proxy menu not showing latency (#7785) 2023-06-01 20:03:27 -03:00
Asher 9704945428 fix: standardize on CODER_PG_CONNECTION_URL (#7781)
Fixes #7513.
2023-06-01 15:02:29 -08:00
Ammar Bandukwala 613f90cf2b ci: send test statistics to DataDog (#7786) 2023-06-01 16:25:52 -05:00
dependabot[bot] 083449c438 chore: bump github.com/gin-gonic/gin from 1.9.0 to 1.9.1 (#7787)
Bumps [github.com/gin-gonic/gin](https://github.com/gin-gonic/gin) from 1.9.0 to 1.9.1.
- [Release notes](https://github.com/gin-gonic/gin/releases)
- [Changelog](https://github.com/gin-gonic/gin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/gin-gonic/gin/compare/v1.9.0...v1.9.1)

---
updated-dependencies:
- dependency-name: github.com/gin-gonic/gin
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-01 16:25:09 -05:00
sharkymark 49ae49095c docs: make template git auth example in sync with git auth setup example (#7784) 2023-06-01 19:34:52 +00:00
Eric Paulsen 4acf36b84a docs: describe gateway and internal certs (#7747)
* docs: gateway & self-signed certs

* docs: update trust store locations

* toolbox paths

* fix: windows cacerts location

---------

Co-authored-by: Ammar Bandukwala <ammar@ammar.io>
2023-06-01 14:49:30 -04:00
Bruno Quaresma 62b2560bd0 chore(site): add type check on lint (#7778) 2023-06-01 15:45:15 -03:00
Bruno Quaresma 1a615778e6 chore(site): add workspace filter experiments into all (#7779) 2023-06-01 15:45:04 -03:00
dependabot[bot] 0bed8b57bc chore: bump eslint from 8.35.0 to 8.41.0 in /site (#7761)
Bumps [eslint](https://github.com/eslint/eslint) from 8.35.0 to 8.41.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.35.0...v8.41.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-01 15:44:33 -03:00
dependabot[bot] 33f1e468cb chore: bump @fontsource/inter from 4.5.11 to 5.0.2 in /site (#7762)
Bumps [@fontsource/inter](https://github.com/fontsource/font-files/tree/HEAD/fonts/google/inter) from 4.5.11 to 5.0.2.
- [Changelog](https://github.com/fontsource/font-files/blob/main/fonts/google/inter/CHANGELOG.md)
- [Commits](https://github.com/fontsource/font-files/commits/HEAD/fonts/google/inter)

---
updated-dependencies:
- dependency-name: "@fontsource/inter"
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-01 15:44:22 -03:00
dependabot[bot] 93f4a5008e chore: bump @types/jest from 29.4.0 to 29.5.2 in /site (#7764)
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 29.4.0 to 29.5.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

---
updated-dependencies:
- dependency-name: "@types/jest"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-01 15:43:26 -03:00
Bruno Quaresma 0f439d5374 hotfix: initial options as empty array (#7782) 2023-06-01 18:25:54 +00:00
Ammar Bandukwala ba3e6cea51 hotfix: remove cryptorand.Bool (#7780)
It caused a CI flake and we we weren't using it anywhere.

`cryptorand.Float64() < 0.5` can be used in the future too.
2023-06-01 17:59:08 +00:00
Bruno Quaresma 332362cf4b chore(site): simplify the logic to load workspace initial data (#7772) 2023-06-01 14:20:17 -03:00
Bruno Quaresma 4de4e8ee21 feat(site): Show total of items in the workspaces table (#7774) 2023-06-01 13:21:16 -03:00
Bruno Quaresma 80b60e158d refactor(site): make vscode dropdown button thiner (#7773) 2023-06-01 16:17:56 +00:00
Bruno Quaresma e80e88e019 refactor(site): add default owner:me filter to workspaces (#7738) 2023-06-01 12:04:10 -03:00
Bruno Quaresma 1952c042e1 fix(site): Fix navbar alignment on small screens (#7739)
Co-authored-by: Ammar Bandukwala <ammar@ammar.io>
2023-06-01 13:49:20 +00:00
Steven Masley b32ed2d97e chore: add function to refetch latencies to ProxyContext (#7769)
* Allow refetching of proxy latencies
* Pass refetch funtion up the context stack
* Add to menu bar
2023-06-01 13:37:20 +00:00
Steven Masley cf8d2bc096 feat: use tz_offset when doing deployment dau query (#7736)
* Have FE query correct tz_offset data
2023-06-01 09:23:50 -04:00
Ammar Bandukwala 555ea64669 ci: deduplicate language setup (#7753)
* Deduplicate actions

* Split up go-race and go-pg
2023-06-01 08:20:49 +00:00
Muhammad Atif Ali 80269ba1b9 hotfix(.github): fix dependency in contrib.yaml (#7754)
`title` job was removed in #7743, so this job was falling.
2023-06-01 10:29:07 +03:00
Spike Curtis a46e8f2e49 fix: concurrent writes to executor stats (#7731)
Signed-off-by: Spike Curtis <spike@coder.com>
2023-06-01 05:23:51 +00:00
Ammar Bandukwala 9810339fd6 ci: use Buildjet and faster Windows runners (#7751) 2023-06-01 00:06:37 -05:00
Ben Potter 36f4e0dc35 hotfix(docs): minor updates to workspace proxies (#7735) 2023-05-31 21:32:26 +00:00
Colin Adler f1d27ba42d hotfix(healthcheck): properly calculate healthy status (#7746) 2023-05-31 20:17:33 +00:00
Ammar Bandukwala 828f33ac7b hotfix(.github): remove legacy title validator (#7743)
* hotfix(.github): remove legacy title validator

`cdr-bot` centralizes the behavior and can enforce stricter
standards.

* Increase jest timeout
2023-05-31 19:39:41 +00:00
Muhammad Atif Ali b80756e4f5 feat(site): Add support for insiders channel to "VS Code Desktop" button (#7730) 2023-05-31 16:28:27 +00:00
Cian Johnston 784696dfa5 feat: add prometheus metrics to database.Store (#7713)
* Adds dbmetrics package and wraps database.Store with a Prometheus HistogramVec of timings.
* Adds Wrappers method to database.Store to avoid double-wrapping interfaces
* Fixes test flake in TestLicensesListFake
2023-05-31 14:55:57 +01:00
Bruno Quaresma 00a30775bc refactor(site): Improve and fix loading buttons (#7719) 2023-05-31 10:00:30 -03:00
Bruno Quaresma b34894977d fix(site): Confirm delete on press Enter in delete dialog (#7720) 2023-05-31 09:59:36 -03:00
Bruno Quaresma be2067bb0d fix(site): Make current user first in the list and fix search (#7722) 2023-05-31 09:59:25 -03:00
Muhammad Atif Ali ce6b698048 hotfix(ci): fix a syntax issue in contrib.yaml (#7727) 2023-05-31 07:21:25 +00:00
Spike Curtis 7c3dbbbe93 fix: stream provisioner logs (#7712)
* stream provisioner logs

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

* Fix imports

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

* Better logging, naming, arg order

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-05-31 06:15:58 +00:00
Spike Curtis 583b777251 fix: wildcard dns docs (#7709)
* Fix wildcard dns docs

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

* Clarify

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-05-31 10:10:30 +04:00
dependabot[bot] ec3ef167d2 chore: bump eslint-config-prettier from 8.5.0 to 8.8.0 in /site (#7556)
Bumps [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) from 8.5.0 to 8.8.0.
- [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-config-prettier/compare/v8.5.0...v8.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-30 23:30:13 -05:00
dependabot[bot] ae82a5985d chore: bump xstate from 4.33.5 to 4.37.2 in /site (#7560)
Bumps [xstate](https://github.com/statelyai/xstate) from 4.33.5 to 4.37.2.
- [Release notes](https://github.com/statelyai/xstate/releases)
- [Commits](https://github.com/statelyai/xstate/compare/xstate@4.33.5...xstate@4.37.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-30 23:29:23 -05:00
Ammar Bandukwala 43eee35ae8 chore(cli): correctly report telemetry even when transport replaced (#7670)
By introducing the "ExtraHeaders" map, we can apply headers even when
handlers replace the transport, as in the case of our scaletests.

Also, only send telemetry header when it's small.
2023-05-31 00:11:56 +00:00
Ammar Bandukwala 867996aa18 chore(.github): specify PR types 2023-05-30 23:57:45 +00:00
Colin Adler 43e0968dac feat: format healthcheck responses (#7723) 2023-05-30 16:59:39 -05:00
Steven Masley 73d795f426 chore: Revert to only using 1 timezone support for template DAUs (#7721)
* chore: Revert to only using 1 timezone support for template DAUs

Keeping the logic to support more in case we optimize later
2023-05-30 15:37:07 -04:00
Colin Adler 022372dd73 feat(healthcheck): add websocket report (#7689) 2023-05-30 14:22:32 -05:00
Bruno Quaresma 77b0ca0b53 refactor(site): Improve workspaces filtering (#7681) 2023-05-30 14:52:13 -03:00
Steven Masley c795a0e500 feat: Fix Deployment DAUs to work with local timezones (#7647)
* chore: Add timezone param to DAU SQL query
* Merge DAUs response
* Pass time offsets to metricscache
2023-05-30 13:18:27 -04:00
dependabot[bot] 68658b5197 chore: bump github.com/valyala/fasthttp from 1.44.0 to 1.47.0 (#7703)
Co-authored-by: Colin Adler <colin1adler@gmail.com>
2023-05-30 11:48:56 -05:00
dependabot[bot] 2aa48d4143 chore: bump crate-ci/typos from 1.14.10 to 1.14.11 (#7701)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-30 11:38:33 -05:00
dependabot[bot] 115f80737a chore: bump github.com/coder/retry (#7705)
Bumps [github.com/coder/retry](https://github.com/coder/retry) from 1.3.1-0.20230210155434-e90a2e1e091d to 1.4.0.
- [Commits](https://github.com/coder/retry/commits/v1.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-30 11:09:12 -05:00
Bruno Quaresma c2d2751897 feat(site): Add proxy menu into navbar (#7715) 2023-05-30 11:41:23 -03:00
Ammar Bandukwala e8eb28adb4 chore(.github): remove stalebot messages 2023-05-30 14:10:39 +00:00
Marcin Tojek 702c9081e0 fix: do not skip parameter validation if min or max = 0 (#7707) 2023-05-30 14:57:06 +02:00
Marcin Tojek 4eb0baa849 chore: update protoc-gen-go (#7708) 2023-05-30 10:24:34 +02:00
Rodrigo Maia 61dc875454 feat(cli): show license_expires as rfc3339 date instead of unix time (#7687)
* feat(licenses): show license_expires time as rfc3339 date

* fix review comments
2023-05-26 14:36:26 +00:00
Mathias Fredriksson 66f5d39ff5 test(agent): Improve use of inv.Context() (#7696)
This will ensure tests timeout instead of hang indefinitely since
`inv.Context()` is assigned a timeout by default.
2023-05-26 13:48:39 +00:00
Mathias Fredriksson 05efd64320 test: Skip tests that send os.Interrupt to test pid (#7695)
This can cause test flakes due to clitest commands running in memory and
listening to interrupt signals.
2023-05-26 16:17:09 +03:00
Cian Johnston 795050bba3 chore: add prometheus monitoring of workspace traffic generation (#7583)
- Exposes reads/writes from scaletest traffic generation (default: 0.0.0.0:21112)
- Adds self-hosted prometheus with remote_write to loadtest terraform
- Adds convenience script to run a traffic generation test
2023-05-26 13:53:35 +01:00
Mathias Fredriksson 0fd2ea4044 test(coderd/rbac): Increase TestFilter timeout (#7694) 2023-05-26 15:44:05 +03:00
Muhammad Atif Ali e5add7292f chore: Only run deploy_template after deploy_image has been completed (#7693) 2023-05-26 12:20:14 +00:00
Bruno Quaresma 79ab72fbb1 fix(site): Fix retry on debug mode alert (#7686) 2023-05-25 17:22:57 -03:00
Kira Pilot 4d4ffa2d99 feat: add expiration indicators to license card (#7684)
* sorting licenses; add expiration badge

* updated story
2023-05-25 11:37:51 -07:00
Kira Pilot 0c665235ff feat: add impending deletion banner to workspace page (#7634)
* add banner to workspace page

* fix prettier and lint

* color-code banner

* using warning instead

* improve prop name for clarity
2023-05-25 10:55:41 -07:00
Marcin Tojek 5d2b2c577b fix: add mockgen to nix env (#7682) 2023-05-25 12:44:32 -04:00
Rodrigo Maia d9299caa12 feat: order workspaces by running first (#7656)
* wip

* use updated sql

* wip

* Implement sorting in databasefake.go

* More fixes

* sql fmt

---------

Co-authored-by: Marcin Tojek <marcin@coder.com>
2023-05-25 13:35:47 -03:00
Spike Curtis 96a2e63809 chore: upgrade to golangci-lint 1.52.2 (#7674)
* Disable ioutilDeprecated lint

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

* Upgrade to golangci-lint 1.52.2

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-05-25 14:56:31 +04:00
Marcin Tojek 14efdadd3c feat: Collect agent SSH metrics (#7584) 2023-05-25 12:52:36 +02:00
Spike Curtis 05da1e94a2 Fix pubsub goroutines in tests (#7677)
Signed-off-by: Spike Curtis <spike@coder.com>
2023-05-25 14:46:32 +04:00
Spike Curtis 67cc196c92 feat: pubsub reports dropped messages (#7660)
* Implementation; need linux tests

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

* Pubsub with errors tests and fixes

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

* Deal with test goroutines

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-05-25 06:22:30 +00:00
Spike Curtis 6a1e7ee1d0 feat: add file logger to coder ssh (#7646)
* coder ssh can log to file

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

* Update golden file

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

* generate CLI docs

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

* Fix imports, typo

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

* log more things!

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-05-25 05:07:39 +00:00
Colin Adler a903d7c249 fix(ci): use env for Go version in release workflow (#7673) 2023-05-24 22:52:18 +00:00
Colin Adler dd69cc9148 fix: use different terraform cache dir (#7672) 2023-05-24 22:35:00 +00:00
Colin Adler 5eb41e8794 feat(cli): allow specifying the listen address in coder port-forward (#7635) 2023-05-24 15:38:40 -04:00
Ammar Bandukwala d413b261d8 chore: deduplicate CLI telemetry reports (#7669) 2023-05-24 12:45:41 -05:00
Ammar Bandukwala ec117e841a chore: add CLI invokation telemetry (#7589) 2023-05-24 11:08:03 -05:00
sharkymark b6604e8845 chore(docs): AWS EKS load balancer setup steps (#7665) 2023-05-24 10:19:45 -05:00
goodspark 71c52ea93c feat: Add logging options for coder agent (#7474)
Similar to logging options in the coderd server, but for the agent
running in workspaces. Meant to make hollistic log collection and
querying simpler.
2023-05-24 14:32:40 +00:00
Mathias Fredriksson b6c8e5be48 fix(cli/ssh): Fetch up-to-date build info to avoid ws has no agents (#7650)
Fixes #5836
2023-05-24 12:37:22 +03:00
Cian Johnston f47ecc29f0 chore(dogfood): Update sqlc version in dogfood image to match CI (#7661) 2023-05-24 09:30:56 +00:00
Colin Adler 0c074742a5 fix(ci): manually select Go version (#7654) 2023-05-24 07:47:44 +00:00
Colin Adler f72d8e95a4 fix(ci): add mockgen to trivy scan (#7655) 2023-05-23 17:09:16 -04:00
Colin Adler 02cc325047 chore(mod): symbolically bump tailscale (#7653) 2023-05-23 15:20:22 -04:00
dependabot[bot] 8f13c145c4 chore: bump github.com/moby/moby from 20.10.23+incompatible to 24.0.1+incompatible (#7622)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-23 19:04:00 +00:00
Colin Adler c8ea76c7fa fix(cli): correctly use cache dir for provisionerd work dir (#7651) 2023-05-23 13:37:27 -04:00
Bruno Quaresma ba02dc8f3a chore(site): Force exact deps (#7649) 2023-05-23 07:43:46 -08:00
Steven Masley d203f5259d fix: Port forward should ignore coder ports (#7645)
Ports opened by coder agent should be ignored in the listening ports
map.
2023-05-23 17:14:05 +02:00
Spike Curtis cd416c86dd refactor: workspace builds (#7541)
* refactor workspace builds

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

* make gen

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

* Remove ParameterResolver from typescript

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

* rename conversion -> database/db2sdk

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

* tests for db2sdk

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

* Tests for ParameterResolver

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

* wsbuilder tests

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

* Move parameter validation tests to richparameters_test.go

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

* Fix CI generation; rename mock->dbmock

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

* Fix test imports

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-05-23 12:06:33 +04:00
Spike Curtis 622456faf8 docs: Add autoscale recommendations docs (#7617)
* Add autoscale recommendations

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

* review updates

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-05-23 09:22:29 +04:00
Rodrigo Maia 4a320617b4 feat(workspaces): change sorting order of the workspace list (#7594)
* feat(workspaces): implement sorting order on workspace list

* split slice into its own function and test it

* use require instead of assert

* Update coderd/workspaces_internal_test.go

Co-authored-by: Cian Johnston <cian@coder.com>

* refactor tests into table tests

* fix test

---------

Co-authored-by: Cian Johnston <cian@coder.com>
2023-05-22 20:51:29 +00:00
Steven Masley 5d711fc95a chore: CORs option for yarn dev server (#7630)
* chore: Yarn dev servers require CORs headers for external proxies

Adds a flag to set CORs headers to `*` for yarn dev servers
2023-05-22 20:02:39 +02:00
dependabot[bot] 1f4f0efed6 chore: bump github.com/prometheus/client_golang from 1.14.0 to 1.15.1 (#7620)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-22 13:18:44 -04:00
dependabot[bot] 808e83f87d chore: bump golang.org/x/tools from 0.6.0 to 0.9.1 (#7619)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-22 13:16:27 -04:00
dependabot[bot] 7d1ca7c12f chore: bump crate-ci/typos from 1.14.9 to 1.14.10 (#7618)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-22 13:13:03 -04:00
dependabot[bot] 9c923d71ca chore: bump github.com/hashicorp/terraform-json from 0.14.0 to 0.16.0 (#7621)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-22 13:10:11 -04:00
Steven Masley b8c07ff014 feat: Auto select workspace proxy based on lowest latency (#7515)
* feat: Proxy auto select and user selection state
* chore: Auto select based on latency
* Add extra test for unknown latencies
* Mock latencies for unit tests
2023-05-22 16:56:41 +02:00
Marcin Tojek f9a97c25dc fix: disable TCP SACK (#7624) 2023-05-22 13:37:05 +00:00
dependabot[bot] b38fb90e06 chore: bump @emotion/styled from 11.10.8 to 11.11.0 in /site (#7561)
Bumps [@emotion/styled](https://github.com/emotion-js/emotion) from 11.10.8 to 11.11.0.
- [Release notes](https://github.com/emotion-js/emotion/releases)
- [Changelog](https://github.com/emotion-js/emotion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/emotion-js/emotion/compare/@emotion/styled@11.10.8...@emotion/styled@11.11.0)

---
updated-dependencies:
- dependency-name: "@emotion/styled"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-22 09:23:33 -03:00
dependabot[bot] 1bd3ed9ecd chore: bump chromatic from 6.15.0 to 6.17.4 in /site (#7554)
Bumps [chromatic](https://github.com/chromaui/chromatic-cli) from 6.15.0 to 6.17.4.
- [Changelog](https://github.com/chromaui/chromatic-cli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chromaui/chromatic-cli/compare/v6.15.0...v6.17.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-21 13:12:03 -05:00
Muhammad Atif Ali 51130540af docs: Add steps to migrate from the built-in database to an external database (#7614) 2023-05-21 10:31:06 -05:00
Colin Adler ef2411ef17 fix(provisionersdk): allow .terraform.lock.hcl files to be archived (#7604) 2023-05-19 13:22:59 -04:00
Cian Johnston a29c4c543d chore(dogfood): fix dogfood image (#7603)
* chore(dogfood): update keys, add script to do so

* chore(dogfood): fix urls in Dockerfile

* fmt
2023-05-19 15:09:34 +01:00
Muhammad Atif Ali e375169ac0 chore: remove CPU usage calculation for hosts on cgroup v1 (#7602) 2023-05-19 14:08:20 +00:00
Muhammad Atif Ali ef635f12ea chore(dogfood): update the dogfood template to add workspace CPU and RAM usage from cgroup (#7529)
Co-authored-by: Cian Johnston <cian@coder.com>
2023-05-19 15:15:48 +03:00
Colin Adler dd5b0b2721 fix(scim): ensure scim users aren't created with their own org (#7595) 2023-05-18 20:54:45 -04:00
Kira Pilot 0b15b1bcd1 feat: add impending deletion indicators to the workspace page (#7588)
* created WorkspaceDeletion directory

* remove commented code

* attempting to fix workspace stories

* fix lint

* fix the rest of the stories

* fix right stories

* PR comments

* fix lint
2023-05-18 11:08:55 -07:00
Bruno Quaresma 8e31ed4072 refactor(site): Refactor alerts (#7587) 2023-05-18 13:17:16 -03:00
Muhammad Atif Ali 63a9e34381 fix: correct free disk space reported by agent metadata in fly.io example (#7592) 2023-05-18 11:42:04 +03:00
Jon Ayers ec5ef51b49 feat: add session token injection to provisioner (#7461) 2023-05-17 23:29:22 -05:00
Jon Ayers 00a2413c03 feat: add telemetry support for workspace agent subsystem (#7579) 2023-05-17 22:49:25 -05:00
dependabot[bot] 52bb84a26f chore: bump google.golang.org/api from 0.108.0 to 0.122.0 (#7558)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-17 14:06:17 -04:00
Bruno Quaresma 12f87cb98d refactor(site): Show update notification as snackbar (#7546) 2023-05-17 13:56:26 -03:00
Bruno Quaresma a7f14f89e3 fix(site): Fix loading buttons (#7549) 2023-05-17 13:56:15 -03:00
dependabot[bot] 119098a1eb chore: bump github.com/coreos/go-oidc/v3 from 3.4.0 to 3.6.0 (#7553)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-17 16:29:42 +00:00
Kyle Carberry 6e7175b589 chore: fix lengthy tests in psql (#7545)
* chore: fix lengthy tests in psql

This was adding at a minimum 3mins to our psql tests!

* fix: automatically cancel tests on cleanup
2023-05-17 11:26:53 -05:00
dependabot[bot] d1b1122690 chore: bump github.com/gohugoio/hugo from 0.110.0 to 0.111.3 (#7562)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-17 12:14:38 -04:00
dependabot[bot] bfa136276c chore: bump crate-ci/typos from 1.14.8 to 1.14.9 (#7552)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-17 12:11:56 -04:00
dependabot[bot] ce91009d6b chore: bump beatlabs/delete-old-branches-action from 0.0.9 to 0.0.10 (#7551)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-17 12:11:43 -04:00
Ben Potter e951778147 fix: indentation in rootless podman docs (#7577)
* fix: indentation in rootless podman docs

* fmt
2023-05-17 01:26:13 +00:00
Colin Adler 75b0643623 chore: enable dependabot for coder base image (#7550) 2023-05-16 19:09:45 +00:00
Colin Adler e3b324a1a1 chore(dogfood): update dependency versions (#7548) 2023-05-16 18:26:06 +00:00
Steve Coder 04f68e8f15 doc:fixed enterprise feature 404s. (#7547) 2023-05-16 12:32:54 -05:00
Pfau, Sascha ad9a29dfa6 feat(site): added URL param name to allow setting workspace name (#7528)
* feat: added URL param name to allow setting workspace name

* fix: format

---------

Co-authored-by: SaschaPfau <Sascha.Pfau@ilabnordlb.onmicrosoft.com>
2023-05-16 13:26:13 -03:00
Kira Pilot dca77ba487 feat: add workspaces banner for impending deletion (#7538)
* feat: add workspaces banner for impending deletion

* added storybook

* remove storybook - cannot add because of hook used in badge component
2023-05-16 07:01:22 -07:00
Marcin Tojek 97b4743a47 Revert "fix: set default sshMaxTimeout to 60s (#7532)" (#7544)
This reverts commit 049e557675.
2023-05-16 15:28:15 +02:00
Kyle Carberry c6055c64be chore: improve input hover color (#7534) 2023-05-16 09:00:20 -03:00
dependabot[bot] a1853f2fa2 chore: bump date-fns from 2.29.3 to 2.30.0 in /site (#7335)
Bumps [date-fns](https://github.com/date-fns/date-fns) from 2.29.3 to 2.30.0.
- [Release notes](https://github.com/date-fns/date-fns/releases)
- [Changelog](https://github.com/date-fns/date-fns/blob/v2.30.0/CHANGELOG.md)
- [Commits](https://github.com/date-fns/date-fns/compare/v2.29.3...v2.30.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-15 16:05:52 -03:00
dependabot[bot] 4997b86548 chore: bump eslint-plugin-compat from 4.0.2 to 4.1.4 in /site (#7339)
Bumps [eslint-plugin-compat](https://github.com/amilajack/eslint-plugin-compat) from 4.0.2 to 4.1.4.
- [Release notes](https://github.com/amilajack/eslint-plugin-compat/releases)
- [Changelog](https://github.com/amilajack/eslint-plugin-compat/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amilajack/eslint-plugin-compat/compare/v4.0.2...v4.1.4)

---
updated-dependencies:
- dependency-name: eslint-plugin-compat
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-15 15:45:54 -03:00
Ben Potter 78f862eedc chore: rename "external workspace proxies" and "external provisioners" (#7454) 2023-05-15 21:38:49 +03:00
Bruno Quaresma 11b7732720 feat(site): Show confirmation dialog on restart (#7531) 2023-05-15 14:43:51 -03:00
Bruno Quaresma d9fc94f9a9 fix(site): Fix focus state on buttons (#7526) 2023-05-15 14:43:16 -03:00
Spike Curtis 0b156420de Make coder dotfiles symlinking idempotent (#7525)
Signed-off-by: Spike Curtis <spike@coder.com>
2023-05-15 21:00:50 +04:00
Kyle Carberry 424f954b91 fix: use password style for session token auth (#7533) 2023-05-15 11:27:39 -05:00
Ben Potter 2aea5b76aa docs: parameters are not alpha (#7487) 2023-05-15 16:14:59 +00:00
Bruno Quaresma 6f62204d38 feat(site): Add template embed page (#7501) 2023-05-15 13:07:39 -03:00
Marcin Tojek 049e557675 fix: set default sshMaxTimeout to 60s (#7532)
* fix: set default sshMaxTimeout to 60s

* fix
2023-05-15 10:40:19 -05:00
Kira Pilot 224d25d4e1 feat: add 'impending deletion' badges to workspaces page (#7530)
* update deleting logic

* added status badge on workspaces page

* licensing and feature flagging

* preset filter for failed workspaces

* remove comment

* PR feedback

* Revert "PR feedback"

This reverts commit 2dfbb50acd.

* PR feedback 2
2023-05-15 07:59:17 -07:00
Cian Johnston 854e974bb4 chore: add terraform for spinning up load test cluster (#7504)
Adds terraform configs for spinning up loadtest environments
2023-05-15 15:56:47 +01:00
Bruno Quaresma dab1d1fe20 fix(site): Fix terminal elements size (#7527) 2023-05-15 10:48:41 -03:00
Marcin Tojek 87784493e8 fix: use mtojek/gvisor to tweak RTO (#7500)
* fix: use mtojek/gvisor to tweak RTO

* fix
2023-05-15 09:12:45 +02:00
Kyle Carberry a08e8a16ab chore: switch make test to use the standard go format (#7519)
I find it easier to read.
2023-05-14 21:20:19 -05:00
Kyle Carberry 70d2203b9e chore: reduce the log output of skipped tests (#7520)
With the introduction of the workspace proxy tests there was a lot
of output if a test was eventually skipped.
2023-05-14 19:37:00 -05:00
Ammar Bandukwala 9bb0253290 chore: update various dependencies (#7503)
This will help us pass the security scanners.
2023-05-14 20:37:50 +00:00
Kyle Carberry 26490aecca chore: improve postgres test time by removing cleanup (#7522)
We don't need to delete databases on cleanup... and we don't need to
always run without a cache either!
2023-05-14 15:32:44 -05:00
Kyle Carberry 778cb7494a chore: adjust the style and wording of the license page (#7396)
- Reduce the size of the card to improve the use of space
- Adjust the language of the add page to remove word redundancy
- Change from using background colors to text colors
2023-05-14 15:31:54 -05:00
Kyle Carberry 8e1da5e628 chore: run yarn-deduplicate to speed up yarn install (#7523) 2023-05-14 15:28:07 -05:00
Ammar Bandukwala f05f12231d feat: update slog to use logfmt (#7477) 2023-05-14 20:23:13 +00:00
Ammar Bandukwala 0f4717e471 hotfix(.github): only push dogfood template when /dogfood changes 2023-05-14 20:13:37 +00:00
Kyle Carberry 50f2d0c7e9 fix: add a mutex around reading logs from scaletests (#7521) 2023-05-14 12:16:00 -05:00
Ammar Bandukwala fcde77b35f chore(.github): only deploy dogfood on main (#7518) 2023-05-14 11:32:22 -05:00
Ben Potter 6bb4a4c3c5 chore: add latest dogfood template (#7321)
Co-authored-by: Muhammad Atif Ali <matifali@live.com>
2023-05-13 15:42:10 +03:00
Muhammad Atif Ali 8d4a8fde66 docs: add instructions for connecting via RDP (#7510) 2023-05-12 11:41:10 -05:00
Marcin Tojek cd38cb8290 feat: Add template version warnings (#7483) 2023-05-12 09:21:49 +00:00
Steven Masley 8f768f8276 feat: Workspace Proxy picker show latency to each proxy (#7486)
* chore: Add cors to workspace proxies to allow for latency checks
* Add latency check to wsproxy

Use performance API timings.
- Fix cors and timing headers
- Accept custom headers
2023-05-11 15:42:30 -05:00
Ammar Bandukwala 640fcf450c chore: improve terraform install performance (#7478) 2023-05-11 19:30:51 +00:00
Eric Paulsen 0e6361383c fix(helm): change proxy to wsproxy (#7499) 2023-05-11 17:55:04 +00:00
Kira Pilot ae3473dc1b feat: add deleted_at field to workspace model (#7475)
* added impending_deletion workspace field

* gen docs

* update golden files

* added test

* PR comments
2023-05-11 08:47:53 -07:00
Eric Paulsen fe0e94ece9 docs: add docs for disabling email/pass auth (#7494) 2023-05-11 10:09:14 -05:00
Ben Potter 892bddfb7b fix: scroll to reveal template create errors (#7464)
Co-authored-by: Kira Pilot <kira@coder.com>
2023-05-11 14:47:51 +03:00
Steven Masley b7f4f3a771 chore: Implement workspace proxy going away (graceful shutdown) (#7459)
* chore: Implement workspace proxy going away

When a workspace proxy shuts down, the health status of that
proxy should immediately be updated. This is purely a courtesy
and technically not required
2023-05-10 19:23:16 -05:00
Kira Pilot a42a36a474 feat: add workspace actions cleanup configuration flags to CLI template create and edit (#7453)
* added cleanup flags on template create

* added cleanup flags on template edit

* fixed tests

* added to tests
2023-05-10 12:57:11 -07:00
Bruno Quaresma 816c37dd0d chore(site): Upgrade to MUI v5 (#7437) 2023-05-10 19:42:40 +00:00
Steven Masley fb86ac2608 chore: If selecting an unhealthy proxy, default to primary (#7472)
* chore: If selecting an unhealthy proxy, default to primary
2023-05-10 13:10:06 -05:00
Steven Masley 3f9af6f5e7 chore: Allow cors requests to workspace proxies for latency checks (#7484)
* CSP addition for web requests
* chore: Add cors to workspace proxies to allow for latency checks
2023-05-10 17:19:55 +00:00
Muhammad Atif Ali d17ea84b4a docs: change --ttl to --default-ttl (#7479) 2023-05-10 07:03:50 -05:00
Spike Curtis d35a458767 feat: Windows on Azure example template (#7469)
* WIP Azure template for windows machine

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

* WIP windows uses data disk

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

* Data drive working

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

* Add az cli commands to start & stop

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

* Remove commented line

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

* Prettierify

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-05-10 05:56:19 +00:00
Steven Masley b5ad628460 chore: Allow editing proxy fields via api. (#7435)
* chore: Add ability to update workspace proxy fields
2023-05-09 13:46:50 -05:00
Steven Masley fc1bc374cb chore: Drop application host call, default to regions for info (#7457)
* chore: Drop application host call, default to regions for info
2023-05-09 09:28:25 -05:00
Ben Potter 2d62cbc83a chore: minor change to workspace actions copy (#7458) 2023-05-09 08:31:07 -05:00
Eric Paulsen 503e94a5c2 docs: fix proxy cmd ref (#7460) 2023-05-08 19:22:35 -04:00
Kira Pilot 0755ff32ef label change (#7455) 2023-05-08 16:13:14 -03:00
Steven Masley 1aac8200fa chore: Fix column name in proxy ls command (#7450) 2023-05-08 12:23:12 -05:00
Ben Potter 3931a6c7bc fix: accurate list of experiments in docs (#7449) 2023-05-08 14:58:07 +00:00
Steven Masley c3e60dcdfb chore: Fix proxy description wording on workspace proxy page (#7448) 2023-05-08 14:41:14 +00:00
Ben Potter 8f9a3ea1a7 docs: add docs for selecting a proxy (#7444) 2023-05-08 14:37:56 +00:00
Ammar Bandukwala 8899dd89ca chore: add global caching to rbac (#7439)
Co-authored-by: Steven Masley <stevenmasley@coder.com>
2023-05-08 08:59:01 -05:00
Bruno Quaresma 643a9efea9 chore(site): Add type checking (#7441) 2023-05-05 18:46:51 -03:00
Steven Masley 52d2bc930b chore: Make better error message if missing license (#7440)
* chore: Make better error message if missing license

* Change word to proxies
2023-05-05 14:22:56 -05:00
Steven Masley 07608fc3fb chore: fix proxy 404 page (#7421)
* chore: fix proxy 404 page

---------

Co-authored-by: Kyle Carberry <kyle@coder.com>
2023-05-05 13:53:19 -05:00
Steven Masley 2624ee8f12 chore: Remove extra opt and fix 'proxy' alias (#7413)
* chore: Remove extra opt and fix 'proxy' alias
* proxy -> wsproxy
2023-05-05 13:09:04 -05:00
Bruno Quaresma ec11405208 chore(site): Remove dead code (#7436) 2023-05-05 16:30:08 +00:00
Spike Curtis dc3d39baf8 fix: agent disconnects from coordinator (#7430)
* work around websocket deadline bug

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

* Use test context to hold websocket open

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

* Fix race creating test websocket

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

* set write deadline to time.Time zero

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-05-05 20:29:03 +04:00
Kira Pilot 5ffa6dae50 feat: add inactivity cleanup and failure cleanup configuration fields to Template Schedule Form (#7402)
* added workspace actions entitlement

* added workspace actions experiment

* added new route for template enterprise meta

* removing new route; repurposing old

* add new fields to get endpoints

* removed workspace actions experiment

* added logic to enterprise template store

* added new form fields

* feature flagged new fields

* fix validation

* fixed submit btn

* fix tests

* changed ttl defaults

* added FE tests

* added BE tests

* fixed lint

* adjusted comment language

* fixing unstaged changes check

* fix test

* Update coderd/database/migrations/000122_add_template_cleanup_ttls.down.sql

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

* Update coderd/database/migrations/000122_add_template_cleanup_ttls.up.sql

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

---------

Co-authored-by: Dean Sheather <dean@deansheather.com>
2023-05-05 08:19:26 -07:00
Bruno Quaresma 3632ac8c01 refactor(site): Update workspace header (#7433) 2023-05-05 15:02:21 +00:00
Cian Johnston 7f02fa696a chore(docs): update openshift documentation re psp (#7432) 2023-05-05 13:00:07 +01:00
Cian Johnston 08fb9a6f1b feat(cli): add trafficgen command for load testing (#7307)
This PR adds a scaletest workspace-traffic command for load testing. This opens a
ReconnectingPTY connection to each scaletest workspace (via coderd) and 
concurrently writes and reads random data to/from the PTY. Payloads are of the
form #${RANDOM_ALPHANUMERIC_STRING}, which essentially drops garbage
comments in the remote shell, and should not result in any commands being executed.
2023-05-05 10:34:58 +01:00
Cian Johnston a172e073e3 chore(ci): update golden files (#7431) 2023-05-05 09:05:45 +00:00
Spike Curtis a6a44896bf fix: pubsub ordering (#7404)
* fix: pubsub sends messages in order

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

* Drop messages rather than block

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-05-05 09:39:07 +04:00
Dean Sheather 667d9a7557 chore: add workspaceProxy to helm chart (#7398) 2023-05-05 07:07:49 +10:00
Bruno Quaresma 164146c462 feat(site): Show update confirmation dialog (#7420) 2023-05-04 15:40:41 -03:00
Steven Masley 6d24f7c894 feat: Add documentation for workspace proxy creation (#7377)
* feat: Add documentation for workspace proxy creation
2023-05-04 13:30:06 -05:00
Bruno Quaresma 8909110f58 fix(site): Fix template icon field validation (#7394) 2023-05-04 14:30:48 -03:00
Jon Ayers 614bdfbf3c fix: update envbox template to use hostname (#7417) 2023-05-04 12:21:20 -05:00
Kyle Carberry 11ac3be4d8 fix: redirect to login on gitauth invalid session (#7416) 2023-05-04 17:19:44 +00:00
Steven Masley 9908c84b2a test: Return early and avoid using nil handler (#7411)
* test: Return early and avoid using nil handler
2023-05-04 11:14:53 -05:00
Cian Johnston b4d913e24f fix(helm): explode verbs instead of wildcarding (#7405)
Updates the Helm chart role specification for Coder to explicitly list required verbs instead of requesting wildcard.
2023-05-04 10:45:51 +00:00
Spike Curtis b3689c8f64 Only send tailnet nodes updates with preferred DERP (#7387)
Signed-off-by: Spike Curtis <spike@coder.com>
2023-05-04 14:30:57 +04:00
Colin Adler 4e52112112 fix: template versions test flake (#7400) 2023-05-03 20:19:49 -05:00
Colin Adler 8bd9f9c351 feat: unified tracing between coderd<->provisionerd (#7370) 2023-05-03 23:02:35 +00:00
Steven Masley 3368b8b65f chore: Minor rbac memory optimization (#7391)
* test: Add benchmark for static rbac roles
* static roles should only be allocated once
* A unit test that modifies the ast value should not mess with the globals
* Cache subject AST values to avoid reallocating slices
2023-05-03 14:42:24 -05:00
Kira Pilot 2e9310b203 chore: add workspace actions entitlement and experiment (#7361)
* added workspace actions entitlement

* added workspace actions experiment
2023-05-03 11:34:43 -07:00
Dean Sheather 5961cf900d chore: bump terraform from 1.3.4-r3 to r4 in image (#7393)
Looks like 1.3.4-r3 isn't available anymore, and 1.3.4-r4 is available
instead.
2023-05-03 18:21:11 +00:00
Bruno Quaresma 2ea438cf4f refactor(site): Show immutable parameters in the settings (#7383) 2023-05-03 14:40:47 -03:00
Steven Masley 434c4be9f1 chore: Add listing proxies to cli 'coder proxy ls' (#7376)
* feat: Add listing proxies to cli 'coder proxy ls'

* Add unit test

* Ignore errors

* Make gen and update golden files

* Update golden files
2023-05-03 10:12:56 -05:00
Kyle Carberry 90c57a538c fix: make telemetry source a string not an enum (#7390) 2023-05-03 14:33:51 +00:00
Spike Curtis 9c030a8888 fix: pty.Start respects context on Windows too (#7373)
* fix: pty.Start respects context on Windows too

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

* Fix windows imports; rename ToExec -> AsExec

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

* Fix import in windows test

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-05-03 11:43:05 +04:00
Bruno Quaresma e6931d6920 refactor(site): Remove optimistic workspace action (#7385) 2023-05-02 18:38:53 -03:00
Steven Masley dd67283323 chore: Adjust wording to mention only browser connections (#7384) 2023-05-02 13:39:23 -05:00
Bruno Quaresma 730039f35f feat(site): Show warning if startup script is running (#7326) 2023-05-02 14:49:16 -03:00
Colin Adler 75ad72de56 fix(server): prevent otel tracer provider from immediately being closed (#7369) 2023-05-02 12:06:58 -05:00
Spike Curtis bd630113b2 fix: coordinator node update race (#7345)
* fix: coordinator node update race

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

* Lint fixes, make core private

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

* Don't log broken connections as errors

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-05-02 20:58:21 +04:00
Bruno Quaresma 0e78d0a502 fix(site): Remove extra spacing between ssh button (#7380) 2023-05-02 13:54:52 -03:00
dependabot[bot] 6dfce5a2c9 chore: bump jest-runner-eslint from 1.1.0 to 2.0.0 in /site (#7343)
Bumps [jest-runner-eslint](https://github.com/jest-community/jest-runner-eslint) from 1.1.0 to 2.0.0.
- [Release notes](https://github.com/jest-community/jest-runner-eslint/releases)
- [Changelog](https://github.com/jest-community/jest-runner-eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jest-community/jest-runner-eslint/compare/v1.1.0...v2.0.0)

---
updated-dependencies:
- dependency-name: jest-runner-eslint
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-02 09:56:58 -05:00
Muhammad Atif Ali f5ce911b8d docs: make use of display_name and name in Open with Coder (#7372)
This PR removed the spaces from `name` and makes it equal to the resource name as we now have a sperate field `display_name` https://github.com/coder/coder/pull/6919

The docs references https://github.com/bpmct/coder-templates/tree/main/kubernetes-open-in-coder example which does not yet makes use of  `display name` and needs updating.
2023-05-02 09:51:50 -05:00
Steven Masley a1db82582f chore: Dynamic CSP connect-src to support terminals connecting to workspace proxies (#7352)
* chore: Expose proxy hostnames to csp header
2023-05-02 08:30:44 -05:00
Ammar Bandukwala 465fe8658d chore: skip timing-sensistive AgentMetadata test in the standard suite (#7237)
* chore: skip timing-sensistive AgentMetadata test in the standard suite

* Add test-timing target

* fix windows?

* Works on my Windows desktop?

* Use tag system

* fixup! Use tag system
2023-05-02 10:41:41 +00:00
Dean Sheather 398d08a0cf chore: fix ci.yaml deploy step for other regions 2 (#7368) 2023-05-02 01:34:21 +00:00
Dean Sheather 41726a785e chore: fix ci.yaml deploy step for other regions (#7367) 2023-05-02 01:00:55 +00:00
Dean Sheather 140637448c chore: fix nfpm.yaml (#7366) 2023-05-01 22:44:11 +00:00
Dean Sheather 3b15234660 chore: add continuous deployment for workspace proxies (#7364) 2023-05-02 08:02:51 +10:00
Ammar Bandukwala 4b9621f9ae fix(coderd): don't hang on first gitauth clone (#7331)
Previously, the `coder git ssh` command would hang on the API, which was endlessly polling the database for oauth tokens that expire in the future.

Some oAuth implementations (including GitHub by default) will not give back a token expiry date, and the absence of such a date was represented as a zero data in the database as opposed to a null value.

Follow-up calls to `git clone` would succeed because this hot path doesn't check expiry, perhaps originally by mistake.

In addition to fixing the zero date issue, this PR removes all gitauth PubSub, which added too much complexity when the polling interval is 1 second.
2023-05-01 19:19:41 +00:00
Steven Masley 55824986bc chore: 404 Requests to workspace proxy direct back to the primary (#7353)
* chore: 404 Requests to workspace proxy direct back to the primary

* Remove unnecessary sprintf
2023-05-01 13:58:36 -05:00
dependabot[bot] 04f9ca824f chore: bump golang.org/x/mod from 0.8.0 to 0.10.0 (#7338)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-01 16:51:28 +00:00
dependabot[bot] 6030847c94 chore: bump golang.org/x/crypto from 0.7.0 to 0.8.0 (#7336)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-01 11:39:40 -05:00
dependabot[bot] 4dec828c88 chore: bump tj-actions/branch-names from 6.4 to 6.5 (#7334)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-01 16:26:52 +00:00
dependabot[bot] 38fd4c0820 chore: bump gopkg.in/natefinch/lumberjack.v2 from 2.0.0 to 2.2.1 (#7337)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-01 11:15:38 -05:00
dependabot[bot] 0bf00d6122 chore: bump aquasecurity/trivy-action from 0.9.2 to 0.10.0 (#7333)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-01 11:15:09 -05:00
dependabot[bot] a3f3d7e682 chore: bump github.com/hashicorp/hc-install from 0.4.1-0.20220912074615-4487b02cbcbb to 0.5.1 (#7342)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-01 16:14:43 +00:00
dependabot[bot] 97c8bb5c1d chore: bump crate-ci/typos from 1.14.3 to 1.14.8 (#7332)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-01 11:14:15 -05:00
Steven Masley 079d2821f5 chore: Set proxy health checks to 1 minute intervals (#7351) 2023-05-01 16:06:29 +00:00
Rodrigo Maia d3a9d7c497 chore: minor tweaks to license ui (#7314)
* chore: minor tweaks to license ui

* minor license ui tweaks

* rename stories
2023-04-28 21:49:54 +00:00
Steven Masley 4a9d1c16c7 chore: UI/UX for regions (#7283)
* chore: Allow regular users to query for all workspaces
* FE to add workspace proxy options to account settings
* WorkspaceProxy context syncs with coderd on region responses

---------

Co-authored-by: Dean Sheather <dean@deansheather.com>
2023-04-28 16:04:52 -05:00
Colin Adler c00f5e499a fix(server): retry initial connection to postgres (#7325) 2023-04-28 13:51:31 -05:00
Kira Pilot a2ff674158 fix(UI): workspace restart button stops build before starting a new one (#7301)
* feat(UI): add workspace restart button (#7137)

* Refactor primary buttons

* refactor(site): Always show the main actions

* Remove tests that are testes on Storybook

* Fix tests

* Fix keys

* added restart btn

---------

Co-authored-by: BrunoQuaresma <bruno_nonato_quaresma@hotmail.com>

* added restart hook

* added error handling

* going back to chaining in success callback

* add restarting btn

* added test

* PR feedback

---------

Co-authored-by: BrunoQuaresma <bruno_nonato_quaresma@hotmail.com>
2023-04-28 11:16:04 -07:00
Eric Paulsen 3078cd3d98 fix: envbox template 404 (#7324) 2023-04-28 12:49:26 -05:00
Ben Potter 8d1f163cae chore: remove login_before_ready from example templates (#7322) 2023-04-28 14:59:50 +00:00
Bruno Quaresma 88c362dfdc refactor(site): Refactor error state (#7313) 2023-04-28 13:03:01 +00:00
Marley e747aad2b6 docs: added additional documentation for azure devops git provider (#6923)
Co-authored-by: Ben Potter <me@bpmct.net>
Co-authored-by: Atif Ali <matifali@live.com>
2023-04-28 11:41:47 +00:00
Colin Adler 59efa4a528 fix(audit): ensure template creation errors are audited (#7315) 2023-04-27 18:55:34 -05:00
Marcin Tojek 77d9937dc4 fix: vite fatals on receiving HTTP4xx (#7306)
* fix: vite fatals on receiving HTTP4xx

* tune Vite

* fmt

* rewrite

* fmt
2023-04-27 19:04:24 +02:00
Eric Paulsen ed1b33acd6 docs: clarify quota allocation (#7310) 2023-04-27 16:51:05 +00:00
Ben Potter fe323a159e fix: keep "workspace create" form when rendering errors (#7289)
* fix: keep "workspace create" form when rendering errors

* fmt

* scroll to top if errors are present
2023-04-27 16:31:42 +00:00
Marcin Tojek bb0a38b161 feat: Implement aggregator for agent metrics (#7259) 2023-04-27 12:34:00 +02:00
Spike Curtis b6666cf1cf chore: tailnet debug logging (#7260)
* Enable discovery (disco) debug

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

* Better debug on reconnectingPTY

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

* Agent logging in appstest

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

* More reconnectingPTY logging

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

* Add logging to coordinator

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

* Update agent/agent.go

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

* Update agent/agent.go

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

* Update agent/agent.go

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

* Update agent/agent.go

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

* Clarify logs; remove unrelated changes

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2023-04-27 13:59:01 +04:00
winter0mute 7aa07cfc8d docs: Fix relay link in HA doc (#7159)
Co-authored-by: Muhammad Atif Ali <atif@coder.com>
2023-04-27 08:25:15 +00:00
Jon Ayers f1763f2aa5 chore: add envbox documentation (#7198) 2023-04-26 16:42:33 -05:00
Rodrigo Maia 87b7537878 feat: add license settings UI (#7210)
* wip: license page

* WIP

* WIP

* wip

* wip

* wip

* wip

* wip

* wip

* Apply suggestions from code review

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

* wip: ui improvements

* wip: extract components

* wip: stories

* wip: stories

* fixes from PR reviews

* fix stories

* fix empty license page

* fix copy

* fix

* wip

* add golang test

---------

Co-authored-by: Ben Potter <ben@coder.com>
2023-04-26 17:47:46 -03:00
Rodrigo Maia c3fe2515a7 feat: add license expiration warning (#7264)
* wip: add expiration warning

* Use GraceAt

* show expiration warning for trial accounts

* fix test

* only show license banner for users with deployment permission

---------

Co-authored-by: Marcin Tojek <marcin@coder.com>
2023-04-26 16:39:39 -03:00
Colin Adler 3eb7f06bf1 feat(agent): add http debug routes for magicsock (#7287) 2023-04-26 13:01:49 -05:00
Ben Potter 272573e9f0 chore: change some wording in the dashboard (#7293) 2023-04-26 11:34:17 -05:00
Bruno Quaresma 0e469031b8 fix(site): Fix secondary buttons with popovers (#7296) 2023-04-26 15:33:23 +00:00
Bruno Quaresma 218d6a92d4 docs(site): Mention template editor in template edit docs (#7261) 2023-04-26 12:11:12 +00:00
Spike Curtis 29cbc5404a Reconnecting PTY waits for command output or EOF (#7279)
Signed-off-by: Spike Curtis <spike@coder.com>
2023-04-26 09:02:06 +04:00
Dean Sheather f1dfeb03db chore: fix flake in apptest reconnecting-pty test (#7281) 2023-04-26 00:31:41 +00:00
Bruno Quaresma 35b3ed255c fix(site): Fix default value for options (#7265) 2023-04-25 17:26:42 -03:00
Bruno Quaresma 1134e78b7b fix(site): Do not show template params if there is no param to be displayed (#7263) 2023-04-25 19:54:33 +00:00
Cian Johnston 9afad8241b chore: add security advisories to docs (#7282)
* chore: add security advisories to docs

* Update docs/security/0001_user_apikeys_invalidation.md

Co-authored-by: Ammar Bandukwala <ammar@ammar.io>

---------

Co-authored-by: Ammar Bandukwala <ammar@ammar.io>
2023-04-25 16:56:09 +01:00
Colin Adler b62b6af0eb fix(healthcheck): don't allow panics to exit coderd (#7276) 2023-04-25 15:11:45 +00:00
Dean Sheather a98341612c feat: add regions endpoint for proxies feature (#7277)
* feat: add regions endpoint for proxies feature
2023-04-25 09:37:52 -05:00
Spike Curtis 6e8ff2d95c Fix macOS pty race with dropped output (#7278)
Signed-off-by: Spike Curtis <spike@coder.com>
2023-04-25 12:32:28 +04:00
Colin Adler e2d8bda246 chore(dogfood): remove unnecessary docker host replace (#7269) 2023-04-25 04:58:09 +00:00
Cian Johnston 8fc8559076 fix(coderd): ensure that user API keys are deleted when a user is (#7270)
Fixes an issue where API tokens belonging to a deleted user were
not invalidated:
- Adds a trigger to delete rows from the api_key stable when the
  column deleted is set to true in the users table.
- Adds a trigger to the api_keys table to ensure that new rows
  may not be added where user_id corresponds to a deleted user.
- Adds a migration to delete all API keys from deleted users.
- Adds tests + dbfake implementation for the above.
2023-04-24 21:48:26 +01:00
Bruno Quaresma ad82a60806 refactor(site): Group app and agent actions together (#7267) 2023-04-24 16:59:52 -03:00
Kira Pilot 96a12d17ef Revert "feat(UI): add workspace restart button (#7137)" (#7268)
This reverts commit 9ec16d4454.
2023-04-24 12:44:22 -07:00
Steven Masley 3129741e08 chore: Proxy health status checks + endpoint (#7233)
* chore: Implement workspace proxy health check cron

At a given interval will check the reachability of workspace proxies.

* Proxyhealth is an enterprise feature
* Start proxyhealth go routine on enterprise coder
2023-04-24 10:25:35 -05:00
Nathanial Spearing 63e68c11d1 feat(community-templates): Added vscode-server-template (#7219)
Co-authored-by: Kyle Carberry <kyle@carberry.com>
2023-04-24 15:22:18 +03:00
Spike Curtis daee91c6dc refactor: PTY & SSH (#7100)
* Add ssh tests for longoutput, orphan

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

* PTY/SSH tests & improvements

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

* Fix some tests

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

* Fix linting

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

* fmt

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

* Fix windows test

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

* Windows copy test

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

* WIP Windows pty handling

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

* Fix truncation tests

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

* Appease linter/fmt

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

* Fix typo

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

* Rework truncation test to not assume OS buffers

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

* Disable orphan test on Windows --- uses sh

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

* agent_test running SSH in pty use ptytest.Start

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

* More detail about closing pseudoconsole on windows

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

* Code review fixes

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

* Rearrange ptytest method order

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

* Protect pty.Resize on windows from races

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

* Fix windows bugs

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

* PTY doesn't extend PTYCmd

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

* Fix windows types

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

---------

Signed-off-by: Spike Curtis <spike@coder.com>
2023-04-24 14:53:57 +04:00
Marcin Tojek c000f2ec28 fix: coder_parameter fallbacks to default (#7240) 2023-04-24 09:51:34 +02:00
sharkymark ab077d1f15 docs: fixed broken podman template link and added already-installed ide gateway doc (#6352)
Co-authored-by: Eric Paulsen <ericpaulsen@coder.com>
2023-04-21 20:43:19 +00:00
Mathias Fredriksson 712098fa2b test(agent): Increase the time to wait for agent reachable (#7245) 2023-04-21 19:40:17 +00:00
Colin Adler 501dfeedf7 fix: invalid version for github.com/coder/ssh (#7250) 2023-04-21 18:10:35 +00:00
Colin Adler 1fc32b1c3d ci: disable caching in setup/go action (#7251) 2023-04-21 13:01:42 -05:00
Kyle Carberry fd84df769d fix: add DISPLAY env var for X11 connections (#7248)
* fix: add `DISPLAY` env var for X11 connections

* Update agent/agentssh/agentssh.go

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

---------

Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2023-04-21 16:43:49 +00:00
Mathias Fredriksson f9da2631e9 test(cli): Fix portforward test timeouts (#7241) 2023-04-21 11:40:31 -05:00
Kyle Carberry f39e6a79de feat: add support for X11 forwarding (#7205)
* feat: add support for X11 forwarding

* Only run X forwarding on Linux

* Fix piping

* Fix comments
2023-04-21 15:52:40 +00:00
Mathias Fredriksson 6f06f8dadb test: Fix test timeouts due to contexts created too early (#7242) 2023-04-21 17:32:25 +03:00
Mathias Fredriksson ea78ca5dff test(coderd/workspaceapps): Fix incorrect use of testing.T (#7243) 2023-04-21 17:32:10 +03:00
Dean Sheather 3680e158d9 chore: fix lint in main caused by incompatible merge (#7239) 2023-04-21 10:27:56 +00:00
Steven Masley 4353ad7940 chore: Remove url and wildcard url from moon create (#7224)
* chore: Remove url and wildcard url from moon create
2023-04-20 21:09:14 -05:00
Dean Sheather 68667323f3 chore: support signed token query param for web terminal (#7197)
* chore: add endpoint to get token for web terminal

* chore: support signed token query param for web terminal
2023-04-20 23:59:45 +00:00
Colin Adler ac3c530283 fix(cli/clitest): race between Start/StartWithWaiter cleanup order (#7232) 2023-04-20 23:41:56 +00:00
Colin Adler 745868fd8a revert: chore: upgrade tailscale (#7236) 2023-04-20 17:58:22 -05:00
Mathias Fredriksson 300ae4a6bf test(agent): Fix TestAgent_UnixRemoteForwarding timeout (#7235) 2023-04-21 01:35:51 +03:00
Mathias Fredriksson 38a6d546ab fix(enterprise/replicasync): Avoid deadlock during Close^2 (#7230) 2023-04-20 22:48:44 +03:00
Colin Adler 34c6ad671c fix(clitest): use separate channel when waiting for exit (#7231) 2023-04-20 14:37:44 -05:00
Mathias Fredriksson ad0070354f test: Fix generated workspace name length (#7228) 2023-04-20 18:40:36 +00:00
Bruno Quaresma d8eda97dbe refactor(site): Redesign the agent row (#7226) 2023-04-20 18:37:10 +00:00
Colin Adler a86830a283 chore: upgrade tailscale (#7207) 2023-04-20 13:29:56 -05:00
Kira Pilot e2a7448cc8 fix: allow update of empty template description (#7225) 2023-04-20 10:59:57 -07:00
Colin Adler ea27129348 chore: upgrade github.com/open-policy-agent/opa (#7204) 2023-04-20 12:50:21 -05:00
Bruno Quaresma e90a076fad fix(site): Fix websocket connections (#7187) 2023-04-20 17:01:08 +00:00
Steven Masley 7fa1112958 chore: Enforce workspace proxy unique name case insensitive (#7202)
* chore: Enforce workspace proxy unique name case insensitive
2023-04-20 11:30:52 -05:00
Bruno Quaresma 9abfe97dcc Revert "fix(site): Fix update when missing parameters (#7221)" (#7223)
This reverts commit 7d9a7636e9.
2023-04-20 12:40:31 -03:00
Steven Masley a5a5c4d400 chore: Add workspace proxy enterprise cli commands (#7176)
* feat: Add workspace proxy enterprise cli commands
* chore: Handle custom workspace proxy options. Remove excess
* chore: Add endpoint to register workspace proxies
2023-04-20 09:48:47 -05:00
Muhammad Atif Ali 8926c10b7d chore: remove disk metadata from Kubernetes examples (#7215) 2023-04-20 09:42:35 -05:00
Bruno Quaresma 7d9a7636e9 fix(site): Fix update when missing parameters (#7221) 2023-04-20 09:32:20 -05:00
Bruno Quaresma 93d0956465 chore(site): Upgrade Vite to 4.3 (#7222) 2023-04-20 14:28:56 +00:00
Mathias Fredriksson 8d27978760 fix(enterprise/replicasync): Avoid deadlock during Close (#7220) 2023-04-20 15:37:30 +03:00
Dean Sheather 528a0686c0 chore: fix deadlock in dbfake and incorrect lock types (#7218)
I manually went through every single dbfake function and ensured it has
the correct lock type depending on whether it writes or only reads.
There were a surprising amount of methods that had the wrong lock type
(Lock when only reading, or RLock when writing (!!!)).

This also manually fixes every method that acquires a RLock and then
calls a method that also acquires it's own RLock to use noLock methods
instead. You cannot rely on acquiring a RLock twice in the same
goroutine as RWMutex prioritizes any waiting Lock calls.

I tried writing a ruleguard rule for this but because of limitations in
ruleguard it doesn't seem possible.
2023-04-20 21:53:34 +10:00
Colin Adler 5f5edb18b0 chore(healthcheck): fix DERP test flakes (#7211) 2023-04-19 20:03:05 -05:00
Colin Adler f60b5579a7 chore: remove usage of k8s.io/utils/pointer (#7209) 2023-04-19 15:53:47 -05:00
Mathias Fredriksson c2871e12aa fix(cli/ssh): Avoid connection hang when workspace is stopped (#7201)
* fix(cli/ssh): Avoid connection hang when workspace is stopped

Two issues are addressed here:
1. We were not detecting disconnects due to waiting for Stdin to close
   (disconnect would only propagate after entering input and failing to
   write to the connection).
2. In other scenarios, where the connection drop is not detected, we now
   also watch workspace status and drop the connection when a workspace
   reaches the stopped state.

Fixes: https://github.com/coder/jetbrains-coder/issues/199

Refs: #6180, #6175
2023-04-19 21:32:28 +03:00
Marc fff2b1dc90 fix(cli): Fix postgres TDE failing version check (#7203) 2023-04-19 11:59:56 -05:00
Ammar Bandukwala 2b9d12828a cli: add --debug-http flag (#7192)
This makes it easier to help debug client issues.
2023-04-19 11:07:53 -05:00
Marcin Tojek f94ac55f02 feat(agent): Expose magicsock metrics (#7183)
* feat: Expose magicsock metrics

* golden-files
2023-04-19 09:09:23 +02:00
Colin Adler fbf329fbb7 fix(tailnet): set TCP keepalive idle to 72 hours for SSH conns (#7196) 2023-04-18 17:53:11 -05:00
Colin Adler 57c4de4647 feat(healthcheck): add accessurl check (#7193) 2023-04-18 15:52:00 -05:00
Dean Sheather fa5387ce07 chore: fix make gen (#7190) 2023-04-18 17:56:52 +00:00
Kyle Carberry 888eb238ec fix: use CODER_HTTP_ADDRESS in docker-compose.yml (#7185)
Fixes #7184.
2023-04-18 11:44:41 -05:00
Steven Masley 7f041fecd8 test: Enable filter test with cancelled context (#7189)
* test: Enable filter test with cancelled context

* fixup! test: Enable filter test with cancelled context
2023-04-18 11:06:10 -05:00
Cian Johnston b26826ee3f chore(docs): update advice for cache dir (#7182)
* Adds a note in openshift documentation regarding CODER_CACHE_DIRECTORY with readOnlyRootFS=true
2023-04-18 14:13:45 +01:00
Kyle Carberry 693e5d94bc fix: add global headers to vscodessh command (#7181) 2023-04-18 08:07:10 -05:00
Bruno Quaresma 56bf9cfdbe fix(site): Handle carriage on logs output (#7172) 2023-04-17 20:45:48 +00:00
Colin Adler b44e6e6711 fix(healthcheck): remove t.Parallel() from healthcheck tests (#7174) 2023-04-17 15:01:47 -05:00
Steven Masley 658246d5f2 chore: add workspace proxies to the backend (#7032)
Co-authored-by: Dean Sheather <dean@deansheather.com>
2023-04-17 19:57:21 +00:00
Kira Pilot dc5e16ae22 feat: add success modal with token value to create token page (#7170)
* added token success modal

* added a test for new modal
2023-04-17 11:54:43 -07:00
Bruno Quaresma 76b5deea78 chore(site): Remove template editor out of experimental (#7165) 2023-04-17 14:40:21 -03:00
Ammar Bandukwala 80bf042528 chore(coderd): remove timing check (#7144) 2023-04-17 17:40:02 +00:00
Mathias Fredriksson b3b26a62f2 test(agent/reaper): Fix restructure issue (#7168)
In #7164 we accidentally removed the "in CI" check, this fixes it.
2023-04-17 17:39:10 +00:00
Bruno Quaresma bb3b8bab78 fix(site): Reset delete modal data when selected user changes (#7167) 2023-04-17 12:29:08 -05:00
Mathias Fredriksson bf0fed4f3f chore: Update pion/udp and improve parallel/non-parallel tests (#7164)
* test(all): Improve and fix subtests with parallell/nonparallel parents

* chore: Update pion/udp to fix buffer close
2023-04-17 20:23:10 +03:00
Ammar Bandukwala c6b2861493 feat: allow disabling stun addresses via env (#7066)
* feat: allow disabling stun addresses via env

Resolves #6791

* Specify a dummy access URL so the tunnel wouldn't start

* Document

---------

Co-authored-by: Kyle Carberry <kyle@carberry.com>
2023-04-17 17:20:26 +00:00
Cian Johnston 25c1e45930 chore(helm): add unit tests for setting labels / annotations on serviceaccount / deployment / pod (#7156)
* chore(helm): add unit tests for setting sa annotations

* chore(cli): also add test for labels / annotations / podLabels / podAnnotations
2023-04-17 16:49:25 +01:00
Kyle Carberry 26cfc18b74 fix: use proper deployment access setting name (#7162) 2023-04-17 10:01:51 -05:00
Mathias Fredriksson 51841e9e11 test(cli): Fix TestTemplatePush/UseWorkingDir bad use of chdir (#7160) 2023-04-17 17:58:25 +03:00
Bruno Quaresma 53f521aaf9 chore(site): Make requests when offline (#7161) 2023-04-17 14:44:38 +00:00
Muhammad Atif Ali 4061a254a5 docs: fix a broken link in docs/README.md (#7150) 2023-04-17 13:05:12 +00:00
Mathias Fredriksson 8b2aead7f4 test(coderd/rbac): Use static name for tests (#7155) 2023-04-17 12:17:18 +00:00
Bruno Quaresma d639e6b898 feat(site): Ask for missing template variables in the template editor (#7108) 2023-04-17 09:12:28 -03:00
Marcin Tojek c39c0dcc7c fix: select default option value (#7152) 2023-04-17 12:55:28 +02:00
Ammar Bandukwala 285646bf24 docs: disclaim write load for agent metadata (#7048)
* docs: disclaim write load for agent metadata

* Update docs/templates/agent-metadata.md

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

* Update docs/templates/agent-metadata.md

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

---------

Co-authored-by: Muhammad Atif Ali <atif@coder.com>
2023-04-16 05:46:04 +00:00
Kyle Carberry d3356fa48e fix: upgrade terraform in apk (#7146) 2023-04-14 20:10:17 -05:00
Eric Paulsen c37f22ba78 feat(helm): add labels for coder pod (#7139)
* feat(helm): add labels for coder pod

* feedback & syntax fix

* ordering

* fix: notation
2023-04-14 18:55:38 +00:00
am009 706326715d fix(docs): fix a wrong word. (#7113) 2023-04-14 18:32:38 +00:00
Ben Potter 9afbd152fc chore: fix one broken link (#7143)
* fix one broken link

* Fix the hopefully last broken link

---------

Co-authored-by: Muhammad Atif Ali <atif@coder.com>
2023-04-14 18:28:58 +00:00
Steven Masley 2137db0445 test: Handle Filter flake with ctx errors (#7119)
* test: Handle Fitler flake with ctx errors
* Add unit test to check filter for proper error
* Correctly return category of errors
2023-04-14 12:30:35 -05:00
Ammar Bandukwala c87ec484ff docs: restore supportLinks (#7142)
* docs: restore supportLinks

* fixup! docs: restore supportLinks
2023-04-14 17:28:34 +00:00
Kyle Carberry 7f12020229 fix: remove irrelevant help info from coder tokens create (#7135)
The additional text didn't help much, and made it much harder to script around.
2023-04-14 12:18:01 -05:00
Ammar Bandukwala f36a4a0b07 chore: fix race check for AgentMetadata test (#7141) 2023-04-14 20:02:44 +03:00
Muhammad Atif Ali 0e50afe4cf fix(docs): fix broken docs (#7138)
* fixing a lot of broken links

* more fixes

* add 0 as valid status codes

https://tools.ietf.org/html/rfc7231#section-6.3.1 --> 0 as status code
2023-04-14 11:11:51 -05:00
Kira Pilot 9ec16d4454 feat(UI): add workspace restart button (#7137)
* Refactor primary buttons

* refactor(site): Always show the main actions

* Remove tests that are testes on Storybook

* Fix tests

* Fix keys

* added restart btn

---------

Co-authored-by: BrunoQuaresma <bruno_nonato_quaresma@hotmail.com>
2023-04-14 08:48:05 -07:00
Bruno Quaresma 7bbbb91df5 refactor(site): Refactor workspace actions (#7124) 2023-04-14 15:28:52 +00:00
Muhammad Atif Ali 9c0f59e3e5 chore: Also check README.md for dead links (#7130) 2023-04-14 14:46:16 +00:00
Muhammad Atif Ali 69797add49 chore: turn on experiments while developing (#7121) 2023-04-14 17:42:03 +03:00
Kyle Carberry 0bbe1562f3 fix: adjust manifest for networking doc (#7136) 2023-04-14 09:36:23 -05:00
Muhammad Atif Ali bb43713d38 fix: VSCode desktop connection (#7120)
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2023-04-14 17:32:18 +03:00
Marcin Tojek 942aba3a66 feat: expose agent stats via Prometheus endpoint (#7115)
* WIP

* WIP

* WIP

* Agents

* fix

* 1min

* fix

* WIP

* Test

* docs

* fmt

* Add timer to measure the metrics collection

* Use CachedGaugeVec

* Unit tests

* WIP

* WIP

* db: GetWorkspaceAgentStatsAndLabels

* fmt

* WIP

* gauges

* feat: collect

* fix

* fmt

* minor fixes

* Prometheus flag

* fix

* WIP

* fix tests

* WIP

* fix json

* Rx Tx bytes

* CloseFunc

* fix

* fix

* Fixes

* fix

* fix: IgnoreErrors

* Fix: Windows

* fix

* reflect.DeepEquals
2023-04-14 16:14:52 +02:00
Mathias Fredriksson e0689456cb fix(scripts): Use gh api to fetch ci logs more reliably (#7133) 2023-04-14 16:29:13 +03:00
Bruno Quaresma dafb7663c5 fix(site): Fix button text overflow (#7132) 2023-04-14 15:13:55 +02:00
Bruno Quaresma ad665cef91 fix(site): Load monaco locally (#7092) 2023-04-14 13:11:28 +00:00
Bruno Quaresma 17bb1a1ff1 chore(site): Upgrade storybook to 7.0.4 (#7127) 2023-04-13 21:36:20 +00:00
Colin Adler 085330ad96 fix(provisionerd): only heartbeat when logs aren't being flushed (#7110) 2023-04-13 14:02:10 -05:00
Colin Adler f5a8a27714 fix(healthcheck): prevent infinite loop if DERP message exchange fails (#7112) 2023-04-13 13:46:16 -05:00
Steven Masley 38e5b9679b chore: Rbac errors should be returned, and not hidden behind 404 (#7122)
* chore: Rbac errors should be returned, and not hidden behind 404

SqlErrNoRows was hiding actual errors
* Replace sql.ErrNoRow checks
* Remove sql err no rows check from dbauthz test
* Fix to use dbauthz system user
2023-04-13 13:06:16 -05:00
Steven Masley fa64c58e56 chore: Export all functions used by server cmd (#7118)
* chore: Export all functions used by server cmd

Required to make workspace proxy cmd
* Factor out httpservers and tracer
2023-04-13 09:07:19 -05:00
Cian Johnston 87fe16cde9 feat(cli): add --id parameter to templates init command (#7116)
This PR makes the following changes:

- Adds an --id parameter to coder templates init so that you can non-interactively initialize a specific example template by ID (e.g. folder name)
- Updates develop.sh and lima/coder.yaml to use this parameter to select the docker example template.
2023-04-13 15:02:49 +01:00
Colin Adler 17f692a89a fix(scaletest): correctly validate configs using SessionToken (#7111) 2023-04-12 17:36:05 -05:00
Ben Potter f517724caa chore: reorganize some docs (#7062)
* chore: move docs pages into corresponding folder

* chore: prioritize clouds in platforms list

* chore: fix networking docs links

* chore: rename to "docker in workspaces"

* mention CLI documentation

* fmt
2023-04-12 21:47:25 +00:00
Muhammad Atif Ali 114d4aac23 chore: update example templates to use display_name (#7001)
* add `display_name`

* fix typos and `terraform validate`

* update display_name

* Update examples/templates/aws-windows/main.tf

Co-authored-by: Ben Potter <me@bpmct.net>

* Update examples/templates/aws-linux/main.tf

Co-authored-by: Ben Potter <me@bpmct.net>

* Update examples/templates/azure-linux/main.tf

Co-authored-by: Ben Potter <me@bpmct.net>

* Update examples/templates/azure-linux/main.tf

Co-authored-by: Ben Potter <me@bpmct.net>

* Update examples/templates/do-linux/main.tf

Co-authored-by: Ben Potter <me@bpmct.net>

* Update examples/templates/do-linux/main.tf

Co-authored-by: Ben Potter <me@bpmct.net>

* Update main.tf

* Update main.tf

* Update main.tf

* Update main.tf

* Update main.tf

---------

Co-authored-by: Ben Potter <me@bpmct.net>
2023-04-12 16:33:38 -05:00
Kira Pilot f6c89a2615 feat: differentiate new user registration from user login in the audit log (#7096)
* auditing register events

* fix tests

* update docs

* update comments

* Update coderd/audit/request.go

Co-authored-by: Colin Adler <colin1adler@gmail.com>

---------

Co-authored-by: Colin Adler <colin1adler@gmail.com>
2023-04-12 11:46:16 -07:00
Rodrigo Maia d1d459cb79 feat(site): add advanced schedule settings to UI (#7061)
* feat: add advanced schedule settings

* Update site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx

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

* rename variable

* fix optionText css

---------

Co-authored-by: Dean Sheather <dean@deansheather.com>
2023-04-12 17:47:48 +00:00
Muhammad Atif Ali 264093a874 chore: add agent metadata to example templates (#7044)
* add `display_name`

* fix typos and `terraform validate`

* update display_name

* Add agent metadata

* add % to cpu usage

* update disk usage check script

* remove metadata from containers and pods workspaces

* removes disk metadata from container workspaces

---------

Co-authored-by: Ben <me@bpmct.net>
2023-04-12 12:05:16 -05:00
Ben Potter af24aea04f chore: reduce docker examples (#6849) 2023-04-12 16:26:03 +00:00
Mathias Fredriksson 770712e1f5 ci: Add script for fetching past test stats from CI (#7086)
Refs: #6677, #6675
2023-04-12 10:03:26 +00:00
Muhammad Atif Ali 531fd23fd2 fix(docs): fix broken markdown links (#7081)
* fix(docs): fix markdown docs links

More than half of them were actually valid links but, due to missing file extensions were flagged as invalid. I added a file extension, so it should pass now.

* Updated docgen to include extension
2023-04-11 15:05:51 -05:00
Colin Adler a44070e2ec feat(scaletest): allow scaletests to run using the host credentials (#7075) 2023-04-11 19:49:28 +00:00
Steven Masley 2585249014 chore: Move writeConfig to a cli middleware (#7093)
* chore: Move writeConfig to a cli middleware

Trying to reduce the server LoC, it's very long and unwieldy.
The workspace proxy code needs to replicate, and trying to reuse
as much as possible

* Move deprecation warnings too
2023-04-11 14:47:07 -05:00
Marcin Tojek e1db64a1e8 chore: bump up coder/tailscale (#7089) 2023-04-11 11:44:47 -05:00
Bruno Quaresma b86ed11bc0 refactor(site): Refactor the schedule controls in the workspace page (#7083) 2023-04-11 13:33:24 -03:00
Bruno Quaresma b92e7d4fab refactor(site): Refactor workspace schedule page (#7069) 2023-04-11 16:27:52 +00:00
Kevin Conley 4dd5d79412 fix: Parse CODER_GITAUTH_N_NO_REFRESH env var value instead of key (#7051) 2023-04-11 09:18:17 -05:00
Marcin Tojek 3b10e89047 Revert "fix: change auto-start to automatically update workspaces (#6053)" (#7084)
This reverts commit bdddc3e7ae.
2023-04-11 14:14:35 +00:00
Steven Masley 9d39371ee0 feat: Option to remove WorkspaceExec from owner role (#7050)
* chore: Add AllResources option for listing all RBAC objects
* Owners cannot do workspace exec site wide
* Fix FE authchecks to valid RBAC resources
2023-04-11 08:57:23 -05:00
Marcin Tojek ad2353c3d8 chore: improve syntax in metrics collector (#7085) 2023-04-11 13:50:34 +00:00
Muhammad Atif Ali 63af4c6ed1 chore: Add exceptions for markdown check (#7078)
* rename and run slack step on failure

* add exceptions
2023-04-11 13:15:44 +02:00
Ben Potter 45776e57f6 docs: remove RBAC section (#7040) 2023-04-10 21:57:00 -05:00
Muhammad Atif Ali fb44dc653e fix(ci): fix typo in cron-weekly.yaml (#7059)
Co-authored-by: Dean Sheather <dean@deansheather.com>
2023-04-11 01:58:37 +00:00
Kyle Carberry 81e2b2500a feat: add level support for startup logs (#7067)
This allows external services like our devcontainer support to display
errors and warnings with custom styles to indicate failures to users.
2023-04-10 14:29:59 -05:00
Bruno Quaresma aa2468b16e fix(site): Handle rename a folder to a file (#7063) 2023-04-10 13:30:21 -03:00
Eric Paulsen e5c6ebdc96 docs: bump tf version in offline docs (#7065) 2023-04-10 11:04:31 -04:00
Ammar Bandukwala 4b99e2d07e feat: add YAML support to server (#6934) 2023-04-07 22:58:21 +00:00
Colin Adler a3c6cb1768 fix: don't query workspace in UpdateWorkspaceAgentConnectionByID (#7042) 2023-04-07 15:21:52 -05:00
Muhammad Atif Ali 592b84984f chore: Update code-server in example templates (#6806)
Update code-server from `4.8.3` to `4.11.0`
2023-04-07 12:09:46 -05:00
Muhammad Atif Ali 7e530b0652 chore: check for invalid markdown links in docs weekly (#6795)
* chore(ci): add an automatic check to verify markdown links. 

As per #6793, there are multiple broken links in our docs. This action will check if all markdown links are valid.

cc: @ammario @bpmct

* Revert "chore(ci): add an automatic check to verify markdown links. "

This reverts commit 294767e056.

* Create cron-weekly.yaml

* add workflow dispatch for testing

* fmt

* Update cron-weekly.yaml

* remove slack notification for now

* Add slack notification

* Update cron-weekly.yaml

* Add logs URL

* fmt
2023-04-07 16:50:08 +00:00
Marcin Tojek 3e250c6609 chore: clidocgen: generate consistent docs (#7047)
* chore: clidocgen: ensure same generated docs

* fix
2023-04-07 18:32:06 +02:00
Marcin Tojek 0347231bb8 feat: expose agent metrics via Prometheus endpoint (#7011)
* WIP

* WIP

* WIP

* Agents

* fix

* 1min

* fix

* WIP

* Test

* docs

* fmt

* Add timer to measure the metrics collection

* Use CachedGaugeVec

* Unit tests

* Address PR comments
2023-04-07 17:48:52 +02:00
Ammar Bandukwala dd85ea8977 feat: tolerate disconnects in agent metadata frontend (#6939) 2023-04-06 19:51:52 -05:00
Dean Sheather 5398150c25 chore: move workspace apps tests to new package (#7025)
* chore: move workspace apps tests to new package
* chore: move reconnecting pty to apptest package
2023-04-06 16:35:27 -05:00
Steven Masley b2892c3d17 test: Increase test coverage on auditable resources (#7038)
* test: Increase test coverage on auditable resources

When adding a new audit resource, we also need to add it to the
function switch statements. This is a likely mistake, now a unit
test will check this for you
2023-04-06 16:16:53 -05:00
Ammar Bandukwala 24d8644c0b chore: de-flake TestWorkspaceAgent_Metadata (round 2) (#7039)
This time, we keep the timing / "racey" tests, but avoid running
them in the harsher CI conditions.
2023-04-06 21:10:13 +00:00
Kyle Carberry e1149992d8 feat: support ansi colors in startup script logs (#7037)
This will help with beautiful output for devcontainers!
2023-04-06 12:39:09 -05:00
Ammar Bandukwala 2637f5501b chore: de-flake TestWorkspaceAgent_Metadata (#7036) 2023-04-06 17:20:16 +00:00
Kyle Carberry c68ab7d9a8 fix: support X-Forwarded-Host with CODER_REDIRECT_TO_ACCESS_URL (#7035)
Fixes #7026.
2023-04-06 12:07:24 -05:00
Mathias Fredriksson aa660e0631 feat(agentssh): Gracefully close SSH sessions on Close (#7027)
By tracking and closing sessions manually before closing the underlying
connections, we ensure that the termination is propagated to SSH/SFTP
clients and they're not left waiting for a connection timeout.

Refs: #6177
2023-04-06 19:57:30 +03:00
Colin Adler f4f40d0ed2 fix: increase workspace name randomess in tests (#7018) 2023-04-06 11:53:02 -05:00
Mathias Fredriksson 0224426e5b refactor(agent): Move SSH server into agentssh package (#7004)
Refs: #6177
2023-04-06 19:39:22 +03:00
Kyle Carberry 3ff2ae1b1a fix: output askpass to stderr (#7034)
This fixes askpass not outputting a prompt.
2023-04-06 11:32:43 -05:00
Marcin Tojek 5bd02917ec docs: Update description for mutable parameters (#7003) 2023-04-06 18:23:47 +02:00
Mathias Fredriksson 121c2bcde8 test(agent): Fix tests without cmd.Wait() (#7029) 2023-04-06 16:45:53 +03:00
Eric Paulsen 2da0702009 fix(helm): missing templating for deployment labels (#6869)
Co-authored-by: Dean Sheather <dean@deansheather.com>
2023-04-06 17:03:21 +10:00
Colin Adler a32951c46a fix: reduce idle workspace queries (#7022) 2023-04-06 01:58:54 +00:00
Dean Sheather 63f9ef2480 chore: fix minor int coercion codeql vulnerability (#7024) 2023-04-05 21:43:24 +00:00
Dean Sheather c18ed6197c chore: fix trivy ci workflow (#7023) 2023-04-05 21:36:04 +00:00
Eric Paulsen 0167d76a7c docs: add helm example for OIDC_GROUP_MAPPING var (#7007)
* docs: add helm example for OIDC_GROU_MAPPING var

* fix: fmt
2023-04-05 21:05:48 +00:00
Colin Adler e0f7cf5ec6 chore: fix postGroupByOrganization swagger tag (#7021) 2023-04-05 20:17:36 +00:00
Colin Adler 391738cc25 chore: remove documented groups endpoint that doesn't exist (#7020) 2023-04-05 14:26:21 -05:00
Ben Potter 865c8b7c09 docs: additional kubernetes clusters (#7019)
* docs: additional kubernetes clusters

* fmt and change wording

* fmt
2023-04-05 14:03:36 -05:00
Dean Sheather eb66cc9f35 chore: move app proxying code to workspaceapps pkg (#6998)
* chore: move app proxying code to workspaceapps pkg

Moves path-app, subdomain-app and reconnecting PTY proxying to the new
workspaceapps.WorkspaceAppServer struct. This is in preparation for
external workspace proxies.

Updates app logout flow to avoid redirecting to coder-logout.${app_host}
on logout. Instead, all subdomain app tokens owned by the logging-out
user will be deleted every time you logout for simplicity sake.

Tests will remain in their original package, pending being moved to an
apptest package (or similar).

Co-authored-by: Steven Masley <stevenmasley@coder.com>
2023-04-05 13:41:55 -05:00
ElliotG 0069831e8d fix: use error log when failing provisioner job (#6812)
Co-authored-by: Colin Adler <colin1adler@gmail.com>
2023-04-05 13:30:53 -05:00
dependabot[bot] bf1a0791f7 chore: bump github.com/docker/docker from 20.10.17+incompatible to 20.10.24+incompatible (#7009)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-05 18:29:27 +00:00
Colin Adler 72c84c5b0a fix(loadtest): use cryptorand.String to generate user password (#7006) 2023-04-05 12:52:47 -05:00
Colin Adler 407f80a1ef fix(dbpurge): use dbauthz.AsSystemRestricted (#7017) 2023-04-05 17:48:39 +00:00
Steven Masley a361673ad8 fix: Include 'CODER' env var prefix on group mappings (#7015)
* fix: Include 'CODER' env var prefix on group mappings
* Unit test to force CODER prefix
2023-04-05 17:47:40 +00:00
Cian Johnston 00d468b964 feat(cli): add --output={text,json} to version cmd (#7010)
* feat(cliui): add TextFormat
* feat(cli): add --format={text,json} to version cmd
2023-04-05 13:16:05 +01:00
Cian Johnston 9c4ccd76a0 fix(coderd)!: add CODER_OIDC_IGNORE_USERINFO configuration option (#6922)
* add CODER_OIDC_IGNORE_USERINFO option
* chore: update docs for CODER_OIDC_IGNORE_USERINFO w.r.t ADFS
* fix!: codersdk: fix incorrectly named OIDC_GROUP_MAPPING -> CODER_OIDC_GROUP_MAPPING
2023-04-05 09:07:43 +01:00
sharkymark 929589ddfa docs: fixed incorrect agent metadata docs (#7005)
* docs: fixed incorrect agent metadata docs

* docs: prettier; fixed incorrect agent metadata docs
2023-04-04 15:50:27 -05:00
Steven Masley b4afbe7720 feat: Implement experiment gated CRUD for workspace proxies (#6928)
* feat: Implement basic moon crud
* chore: Implement enterprise endpoints for moons
2023-04-04 15:07:29 -05:00
Kyle Carberry 385a4262e2 fix: remove audit-logging flag from the server (#6991)
Co-authored-by: Colin Adler <colin1adler@gmail.com>
2023-04-04 12:11:10 -05:00
Bruno Quaresma 29e9b9e663 feat(site): Add change version for template admins (#6988) 2023-04-04 10:25:49 -03:00
Kyle Carberry c12bc39821 fix: always show a newly created workspace at the top of the list (#6984)
Fixes #5795.
2023-04-04 08:24:04 -05:00
Bruno Quaresma 46f42ea9ac fix(site): Prompting user for missing variables (#7002) 2023-04-04 13:15:35 +00:00
Dean Sheather e33941b7c2 feat: allow disabling autostart and custom autostop for template (#6933)
API only, frontend in upcoming PR.
2023-04-04 12:48:35 +00:00
Marcin Tojek 083fc89f93 feat: accept immutable parameters when used first time (#7000)
* Backend fixes

* CLI: adjust update flow
2023-04-04 14:22:46 +02:00
Josh Vawdrey e84061e2be feat(provisioner): bump the maximum terraform version (#6995) 2023-04-04 06:19:48 +00:00
Steven Masley a96376e02e chore: Add "required" to allow requring url params (#6994) 2023-04-03 20:59:59 -05:00
Dean Sheather 3cca30c770 chore: make version.sh work in forks (#6993) 2023-04-04 01:13:32 +00:00
Dean Sheather 34593e3944 chore: ticket provider interface (#6915) 2023-04-04 00:59:41 +00:00
Colin Adler e0f7f011d8 fix(healthcheck): ensure STUNOnly nodes aren't marked as unhealthy (#6990) 2023-04-03 16:14:48 -05:00
Rodrigo Maia a1371dbd60 feat(site): move template's readme to its own tab (#6863)
* feat(site): display template's readme first on template page

* chore: prettier

* move readme to a new docs tab

* test

* prettier

* fix tests

* prettier
2023-04-03 18:18:28 +00:00
Colin Adler 95e578ba10 fix: derp healthcheck test flake (#6982) 2023-04-03 11:36:26 -05:00
Kyle Carberry 861d4afdd8 fix: update tailscale to remove websocket logs (#6981) 2023-04-03 16:28:49 +00:00
Kyle Carberry bc18f6c113 fix: add CODER_AGENT_TAILNET_LISTEN_PORT for specifying a static tailnet port (#6980)
Fixes #5175.
2023-04-03 16:20:19 +00:00
Kyle Carberry 4ee01dc95c fix: remove orphan rbac warning message (#6979)
The request will fail if the user doesn't have permissions, so it seems
useless to output this.
2023-04-03 11:15:56 -05:00
Kyle Carberry 55c0b26977 fix: update status text for the workspace progress bar (#6977)
Fixes #4691.
2023-04-03 11:00:56 -05:00
dependabot[bot] 8e69f02695 chore: bump axios from 0.26.1 to 1.3.4 in /site (#6954)
Bumps [axios](https://github.com/axios/axios) from 0.26.1 to 1.3.4.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.26.1...v1.3.4)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-03 10:30:39 -05:00
Ben Potter ded931f0f6 docs: rename quickstart to platforms + Kubernetes guide (#6972)
* rename quickstart to platforms + Kubernetes guide

* fmt
2023-04-03 09:51:07 -05:00
Kyle Carberry 2d051094e7 fix: ignore deleted workspaces in global stats (#6973)
Fixes #6568.
2023-04-03 14:46:09 +00:00
Bruno Quaresma aa43f998d4 feat(site): Promote template version (#6929) 2023-04-03 14:27:17 +00:00
Steven Masley fab8da633b chore: Merge more rbac files (#6927)
* chore: Merge more rbac files

- Remove cache.go -> authz.go
- Remove query.go -> authz.go
- Remove role.go -> roles.go

* Order imports

* fmt
2023-04-03 09:05:06 -05:00
Ben Potter 333718d1fa docs: remove mention of support links (#6970)
* docs: remove mention of support links

* fmt
2023-04-03 13:18:28 +00:00
Marcin Tojek 633dfbdb2e feat: rich parameters: introduce display_name (#6919)
* model

* DB

* fix: DisplayName

* proto

* Proto

* Update go dep

* fixme

* fix format

* config

* fmt

* fix

* Fix

* fix

* chore(UI): redirecting from workspace page if 404 (#6880)

* model

* CLI: Display parameter

* fix

* update dep

* fix

* fix

* fix

* UI changes

* fmt

---------

Co-authored-by: Kira Pilot <kira@coder.com>
2023-04-03 14:37:47 +02:00
Mathias Fredriksson d9d44c1188 ci: Print go test stats (#6855)
Fixes #6676
2023-04-03 11:07:25 +00:00
Colin Adler 7738274b3e feat(coderd): add DERP healthcheck (#6936) 2023-04-03 06:28:42 +00:00
Colin Adler f4d16a1ae5 fix: add span.SetStatus to codersdk.Request (#6964) 2023-04-03 05:52:42 +00:00
dependabot[bot] 47afafa4d4 chore: bump github.com/klauspost/compress from 1.15.15 to 1.16.3 (#6948)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-03 00:35:35 -05:00
Colin Adler a29fc7dd6f chore: update otel to v1.14.0 (#6963) 2023-04-03 00:31:39 -05:00
dependabot[bot] f6f927e44f chore: bump github.com/go-playground/validator/v10 from 10.11.0 to 10.12.0 (#6950)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-03 04:59:49 +00:00
Colin Adler 8683169e71 fix: agent metadata flake (#6962) 2023-04-03 04:45:23 +00:00
dependabot[bot] d2bfa2b9a0 chore: bump aquasecurity/trivy-action from 0.9.1 to 0.9.2 (#6945)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-02 23:37:06 -05:00
dependabot[bot] 390f29cf8c chore: bump actions/stale from 7.0.0 to 8.0.0 (#6944)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-02 23:32:22 -05:00
Eric Paulsen caec0b8aae rm: mac from docker quickstart (#6958) 2023-04-01 22:31:53 -05:00
dependabot[bot] 31690c4b3d chore: bump @xstate/react from 3.0.1 to 3.2.1 in /site (#6955)
Bumps [@xstate/react](https://github.com/statelyai/xstate) from 3.0.1 to 3.2.1.
- [Release notes](https://github.com/statelyai/xstate/releases)
- [Commits](https://github.com/statelyai/xstate/compare/@xstate/react@3.0.1...@xstate/react@3.2.1)

---
updated-dependencies:
- dependency-name: "@xstate/react"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-01 16:37:57 -05:00
Ammar Bandukwala 512fdbf634 chore: debounce agent watch-metadata stream (#6940) 2023-04-01 16:36:21 -05:00
dependabot[bot] e40b0778e9 chore: bump crate-ci/typos from 1.13.14 to 1.14.3 (#6942)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.13.14 to 1.14.3.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.13.14...v1.14.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-01 16:35:19 -05:00
dependabot[bot] e60460b120 chore: bump jaxxstorm/action-install-gh-release from 1.9.0 to 1.10.0 (#6943)
Bumps [jaxxstorm/action-install-gh-release](https://github.com/jaxxstorm/action-install-gh-release) from 1.9.0 to 1.10.0.
- [Release notes](https://github.com/jaxxstorm/action-install-gh-release/releases)
- [Commits](https://github.com/jaxxstorm/action-install-gh-release/compare/v1.9.0...v1.10.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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-01 16:35:11 -05:00
dependabot[bot] 5200591264 chore: bump actions/setup-go from 3 to 4 (#6946)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3 to 4.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-01 16:34:58 -05:00
dependabot[bot] 87d64baf7a chore: bump golang.org/x/crypto from 0.6.0 to 0.7.0 (#6949)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/golang/crypto/releases)
- [Commits](https://github.com/golang/crypto/compare/v0.6.0...v0.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-01 16:34:50 -05:00
Ammar Bandukwala 34debbf837 fix(agent): prevent goroutine pile up in reportMetadataLoop (#6957) 2023-04-01 16:34:42 -05:00
Ammar Bandukwala ccadd0f286 docs: tweak some words and fix a typo in agent-metadata.md 2023-03-31 23:20:30 +00:00
Kyle Carberry 84956c16cc fix: reduce spacing when agent metadata doesn't exist (#6937) 2023-03-31 22:08:42 +00:00
Ammar Bandukwala ca4fa81570 feat: add agent metadata (#6614) 2023-03-31 15:26:19 -05:00
Ammar Bandukwala c191692751 chore(cli): send help to stdout (#6865)
Minimizes pesky `2>&1` when working with help.
2023-03-31 18:51:55 +00:00
Ammar Bandukwala c2a96bdc7c fix: compilation failure with (*OptionSet).SetDefaults
This slipped through CI due to a merge race.
2023-03-31 18:02:55 +00:00
Ammar Bandukwala 599699b3a9 fix: truly allow overridding default string array (#6874) 2023-03-31 12:12:03 -05:00
Cian Johnston 96ff400587 chore(clibase): add some more tests for parsing options from env (#6930) 2023-03-31 17:21:56 +01:00
Steven Masley ce51435507 chore: Merge some rbac files, move some functions around (#6916)
* chore: Merge some rbac files, move some functions around

* Fix imports
2023-03-31 09:46:38 -05:00
Steven Masley 27e17ff2c3 chore: Remove unused Experimental field (#6924) 2023-03-31 09:44:29 -05:00
Kyle Carberry cd807bc0c8 fix: delete old addresses from the active derp mesh mapping (#6926) 2023-03-31 14:25:01 +00:00
Cian Johnston 334d9820fa fix(coderd): update provisionderd authz policy to allow updating user data (#6925) 2023-03-31 15:11:07 +01:00
Bruno Quaresma 90e2bab078 feat(site): Display workspace build error + option to retry in debug mode (#6903) 2023-03-31 14:01:12 +00:00
Steven Masley 901045a95f fix: FE show correct config-ssh prefix (#6904)
* fix: Push correct ssh prefix to FE
2023-03-31 08:48:44 -05:00
Kira Pilot a364318462 chore(UI): redirecting from workspace page if 404 (#6880) 2023-03-31 06:31:48 -07:00
Marcin Tojek 56f00a82e1 chore: update sqlc to v1.17.2 (#6920)
* WIP

* chore: Update sqlc to v1.17.2
2023-03-31 14:05:20 +02:00
Marcin Tojek 2612e32bac chore: image for support links (#6902) 2023-03-30 18:07:27 +00:00
Kira Pilot 3b52d4f336 chore(UI): remove template link from workspaces page row (#6882)
* chore(UI): remove template link from workspaces page row

* remove stack and span as per PR comment
2023-03-30 10:32:23 -07:00
Kyle Carberry b5f5740d0b chore: ensure agent conn routine is closed before exit (#6900)
This caused a leak in `main`!
2023-03-30 17:20:14 +00:00
Kyle Carberry e496bdb687 chore: update slog to fix local leak with http.Transport (#6899) 2023-03-30 12:13:15 -05:00
Kyle Carberry a63c97b8de chore: wrap audit logs in a mutex to fix data race (#6898)
This was seen in `main`!
2023-03-30 12:13:03 -05:00
Kyle Carberry 5780006adb chore: use http-swagger/v2 for go:embed to reduce binary size (#6897)
I'm testing how much this will reduce the size, but it should
be by a few MB!
2023-03-30 16:46:58 +00:00
Cian Johnston afcc179244 chore: fix duplicated sentence in docs/auth.md (#6895) 2023-03-30 16:29:57 +00:00
Kyle Carberry 8f55f5c28b chore: ensure go.mod has proper references to forked deps (#6893)
There were a few that have been merged upstream!
2023-03-30 11:19:11 -05:00
Ben Potter 5953a46785 docs: Open in Coder (#6859)
* docs: git auth via template

* add page

* docs: Open in Coder

* fmt
2023-03-30 16:12:47 +00:00
Kyle Carberry a6b7e8c43a chore: use an older version of fastclock to resolve leak (#6892) 2023-03-30 10:58:56 -05:00
Kyle Carberry 04e404e448 chore: dial the remote socket continually until connect (#6891)
It's possible that the command starts but the socket isn't ready
even when the file exists.
2023-03-30 15:36:23 +00:00
Kyle Carberry 5686fc9983 chore: skip chdir template push test on windows (#6890)
See https://github.com/coder/coder/actions/runs/4565958214/jobs/8057842941
2023-03-30 10:21:13 -05:00
Kyle Carberry 401b9276ae chore: clone styles to prevent race (#6888)
See https://github.com/coder/coder/actions/runs/4565541977/jobs/8056830691?pr=6885
2023-03-30 10:18:24 -05:00
Kyle Carberry 5c1dc1b7fe fix: iterate through all workspace updates on logs overflow (#6885)
This was causing some flakes!
2023-03-30 10:05:45 -05:00
Kyle Carberry e470162305 chore: move away from set-output in GitHub Actions (#6884)
This is deprecated! See:
https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/
2023-03-30 09:58:56 -05:00
Kyle Carberry 1f600fc526 chore: use defer instead of t.Cleanup for chdir (#6887)
This _might_ resolve the races in `TestTemplatePush`.
2023-03-30 09:48:03 -05:00
Bruno Quaresma b26f30688f feat(site): Duplicate template (#6853) 2023-03-30 11:42:29 -03:00
Kyle Carberry 6378294071 chore: upgrade from deprecated protobuf import (#6883)
This was displaying a warning when running `go get ./...`.
2023-03-30 14:17:44 +00:00
Marcin Tojek 0ba200c2a1 feat: Enable workspace debug logging (#6838)
* feat: Enable workspace debug logging

* Fix

* Fix

* Fix

* fix

* fix

* Enable RBAC

* unit tests

* Fix

* fix

* fix

* fix

* more tests

* fix: workspacebuild_test use roles

* fix: swagger comment

* fix: ctx.Done

* fix: address PR comments

* break loop
2023-03-30 16:00:33 +02:00
Dean Sheather 665b84de0d feat: use app tickets for web terminal (#6628) 2023-03-30 23:24:51 +10:00
Muhammad Atif Ali a07209efa1 fix: very small typo (#6877) 2023-03-30 07:56:19 -05:00
Bruno Quaresma 7d7aa789b3 fix(site): Only patch version name if name is changed (#6878) 2023-03-30 12:22:55 +00:00
Bruno Quaresma d8762c676f feat(site): Show main.tf file first on template files page (#6854) 2023-03-30 11:33:42 +00:00
Marcin Tojek b120247213 fix: extend regex for template version name (#6876) 2023-03-30 13:27:58 +02:00
Cian Johnston 563c3ade06 feat: allow configuring OIDC email claim and OIDC auth url parameters (#6867)
This commit:

- Allows configuring the OIDC claim Coder uses for email addresses (by default, this is still email)
- Allows customising the parameters sent to the upstream identity provider when requesting a token. This is still access_type=offline by default.
- Updates documentation related to the above.
2023-03-30 09:36:57 +01:00
Ammar Bandukwala 6981f89cd8 Revert "fix: allow overridding default string array (#6873)"
This reverts commit 58d650c2bb.
2023-03-29 20:24:07 -05:00
Ammar Bandukwala 58d650c2bb fix: allow overridding default string array (#6873)
* fix: allow overridding default string array

* Cleanup code

* fixup! Cleanup code

* fixup! Cleanup code

* fixup! Cleanup code

* fixup! Cleanup code
2023-03-30 01:09:20 +00:00
dependabot[bot] 1c7adc0ebd chore: bump github.com/fatih/color from 1.14.1 to 1.15.0 (#6868)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-29 21:42:35 +00:00
Rodrigo Maia 3d91fe8895 fix(site): fix redirection to login after logout/change password (#6870)
* fix(site): fix redirection to login after logout/change password

* chore: add login verification assert

* prettier
2023-03-29 21:39:56 +00:00
Steven Masley 90da09bc2c chore: Make deployment admin page show better durations (#6856)
* chore: Make deployment admin page show better durations

Also fix group mappings
2023-03-29 16:26:20 -05:00
Colin Adler 872037bf85 feat: allow http2 in coder server (#6871) 2023-03-29 14:45:57 -05:00
Bruno Quaresma 175dde1c52 chore(site): Try to fix flake test (#6848) 2023-03-29 16:10:04 -03:00
Mathias Fredriksson 90d18dd2e5 fix(agent): Close stdin and stdout separately to fix pty output loss (#6862)
Fixes #6656
Closes #6840
2023-03-29 21:58:38 +03:00
dependabot[bot] 349bfad2e9 chore: bump github.com/opencontainers/runc from 1.1.2 to 1.1.5 (#6864)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-29 17:46:54 +00:00
Ben Potter 311327cb11 docs: git auth via template (#6850) 2023-03-28 17:11:30 -05:00
Steven Masley a8346bd8ea feat: Allow unsetting ssh config options from deployment (#6847)
This allows deleting ssh config options
2023-03-28 11:06:42 -05:00
Ammar Bandukwala 1176256a44 feat: improve CLI error messages (#6778) 2023-03-28 16:03:34 +00:00
Cian Johnston b38d1ed4a5 Revert "fix(UI): redirect if user is not permissioned to see workspace (#6786)" (#6836)
This reverts commit fc21e159b8.
2023-03-28 09:38:39 +00:00
Mathias Fredriksson 891bbda995 fix(agent): More protection for lost output of SSH PTY commands (#6833)
Fixes #6656 (part 2)
2023-03-28 09:11:15 +00:00
Ammar Bandukwala 164528176a fix(clibase): allow empty values to unset defaults (#6832) 2023-03-28 01:58:06 +00:00
Ammar Bandukwala 773580c7c9 fix: correct minor formatting issues in CLI (#6813)
* fix: remove excess newlines from server startup

* Don't log benign closed pipe errors
2023-03-28 01:01:25 +00:00
Ammar Bandukwala 42b3d90221 chore: rename "InsertOrUpdate" to Upsert around the codebase (#6823)
* chore: rename "InsertOrUpdate" to Upsert around the codebase

The shorter name uses up less line width, is easier to read
and is used more often.

* make gen
2023-03-28 00:55:10 +00:00
1937 changed files with 171869 additions and 69652 deletions
-83
View File
@@ -1,83 +0,0 @@
FROM ubuntu
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
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
# configure locales to UTF8
RUN apt-get install locales && locale-gen en_US.UTF-8
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' LC_ALL='en_US.UTF-8'
# configure direnv
RUN direnv hook bash >> $HOME/.bashrc
# install nix
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
# 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
# install golang and language tooling
ENV GO_VERSION=1.20
ENV GOPATH=$HOME/go-packages
ENV GOROOT=$HOME/go
ENV PATH=$GOROOT/bin:$GOPATH/bin:$PATH
RUN curl -fsSL https://dl.google.com/go/go$GO_VERSION.linux-amd64.tar.gz | tar xzs
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
RUN bash -c "$(curl -fsSL https://deb.nodesource.com/setup_14.x)" \
&& apt-get install -y nodejs
# install zstd
RUN bash -c "$(curl -fsSL https://raw.githubusercontent.com/horta/zstd.install/main/install)"
# install nfpm
RUN echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | sudo tee /etc/apt/sources.list.d/goreleaser.list \
&& apt update \
&& apt install nfpm
+8 -20
View File
@@ -1,24 +1,12 @@
// For format details, see https://aka.ms/devcontainer.json
{
"name": "Development environments on your infrastructure",
"image": "codercom/oss-dogfood:latest",
// 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",
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
"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"
]
"features": {
// See all possible options here https://github.com/devcontainers/features/tree/main/src/docker-in-docker
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
},
// SYS_PTRACE to enable go debugging
// without --priviliged the Github Codespace build fails (not required otherwise)
"runArgs": ["--cap-add=SYS_PTRACE", "--privileged"]
}
+3
View File
@@ -1,5 +1,7 @@
# Generated files
coderd/apidoc/docs.go linguist-generated=true
docs/api/*.md linguist-generated=true
docs/cli/*.md linguist-generated=true
coderd/apidoc/swagger.json linguist-generated=true
coderd/database/dump.sql linguist-generated=true
peerbroker/proto/*.go linguist-generated=true
@@ -9,3 +11,4 @@ provisionersdk/proto/*.go linguist-generated=true
*.tfstate.json linguist-generated=true
*.tfstate.dot linguist-generated=true
*.tfplan.dot linguist-generated=true
site/src/api/typesGenerated.ts linguist-generated=true
-3
View File
@@ -1,3 +0,0 @@
docs/ @coder/docs
README.md @coder/docs
ADOPTERS.md @coder/docs
+70
View File
@@ -0,0 +1,70 @@
name: "Setup Go"
description: |
Sets up the Go environment for tests, builds, etc.
inputs:
version:
description: "The Go version to use."
default: "1.20.6"
runs:
using: "composite"
steps:
- name: Cache go toolchain
uses: buildjet/cache@v3
with:
path: |
${{ runner.tool_cache }}/go/${{ inputs.version }}
key: gotoolchain-${{ runner.os }}-${{ inputs.version }}
restore-keys: |
gotoolchain-${{ runner.os }}-
- name: Setup Go
uses: buildjet/setup-go@v4
with:
# We do our own caching for implementation clarity.
cache: false
go-version: ${{ inputs.version }}
- name: Get cache dirs
shell: bash
run: |
set -x
echo "GOMODCACHE=$(go env GOMODCACHE)" >> $GITHUB_ENV
echo "GOCACHE=$(go env GOCACHE)" >> $GITHUB_ENV
# We split up GOMODCACHE from GOCACHE because the latter must be invalidated
# on code change, but the former can be kept.
- name: Cache $GOMODCACHE
uses: buildjet/cache@v3
with:
path: |
${{ env.GOMODCACHE }}
key: gomodcache-${{ runner.os }}-${{ hashFiles('**/go.sum') }}-${{ github.job }}
# restore-keys aren't used because it causes the cache to grow
# infinitely. go.sum changes very infrequently, so rebuilding from
# scratch every now and then isn't terrible.
- name: Cache $GOCACHE
uses: buildjet/cache@v3
with:
path: |
${{ env.GOCACHE }}
# Job name must be included in the key for effective test cache reuse.
# The key format is intentionally different than GOMODCACHE, because any
# time a Go file changes we invalidate this cache, whereas GOMODCACHE is
# only invalidated when go.sum changes.
# The number in the key is incremented when the cache gets too large,
# since this technically grows without bound.
key: gocache2-${{ runner.os }}-${{ github.job }}-${{ hashFiles('**/*.go', 'go.**') }}
restore-keys: |
gocache2-${{ runner.os }}-${{ github.job }}-
gocache2-${{ runner.os }}-
- name: Install gotestsum
shell: bash
run: go install gotest.tools/gotestsum@latest
# It isn't necessary that we ever do this, but it helps
# separate the "setup" from the "run" times.
- name: go mod download
shell: bash
run: go mod download -x
+31
View File
@@ -0,0 +1,31 @@
name: "Setup Node"
description: |
Sets up the node environment for tests, builds, etc.
inputs:
directory:
description: |
The directory to run the setup in.
required: false
default: "site"
runs:
using: "composite"
steps:
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
- name: Setup Node
uses: buildjet/setup-node@v3
with:
node-version: 18.17.0
# See https://github.com/actions/setup-node#caching-global-packages-data
cache: "pnpm"
cache-dependency-path: ${{ inputs.directory }}/pnpm-lock.yaml
- name: Install root node_modules
shell: bash
run: ./scripts/pnpm_install.sh
- name: Install node_modules
shell: bash
run: ../scripts/pnpm_install.sh
working-directory: ${{ inputs.directory }}
+10
View File
@@ -0,0 +1,10 @@
name: Setup sqlc
description: |
Sets up the sqlc environment for tests, builds, etc.
runs:
using: "composite"
steps:
- name: Setup sqlc
uses: sqlc-dev/setup-sqlc@v3
with:
sqlc-version: "1.19.1"
+11
View File
@@ -0,0 +1,11 @@
name: "Setup Terraform"
description: |
Sets up Terraform for tests, builds, etc.
runs:
using: "composite"
steps:
- name: Install Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: ~1.5
terraform_wrapper: false
@@ -0,0 +1,27 @@
name: Upload tests to datadog
if: always()
inputs:
api-key:
description: "Datadog API key"
required: true
runs:
using: "composite"
steps:
- shell: bash
run: |
owner=${{ github.repository_owner }}
echo "owner: $owner"
if [[ $owner != "coder" ]]; then
echo "Not a pull request from the main repo, skipping..."
exit 0
fi
if [[ -z "${{ inputs.api-key }}" ]]; then
# This can happen for dependabot.
echo "No API key provided, skipping..."
exit 0
fi
npm install -g @datadog/datadog-ci@2.10.0
datadog-ci junit upload --service coder ./gotests.xml \
--tags os:${{runner.os}} --tags runner_name:${{runner.name}}
env:
DATADOG_API_KEY: ${{ inputs.api-key }}
+105 -5
View File
@@ -3,7 +3,7 @@ updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
interval: "weekly"
time: "06:00"
timezone: "America/Chicago"
labels: []
@@ -24,11 +24,15 @@ updates:
update-types:
- version-update:semver-minor
- version-update:semver-patch
groups:
github-actions:
patterns:
- "*"
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "monthly"
interval: "weekly"
time: "06:00"
timezone: "America/Chicago"
commit-message:
@@ -39,6 +43,33 @@ updates:
- dependency-name: "*"
update-types:
- version-update:semver-patch
groups:
otel:
patterns:
- "go.nhat.io/otelsql"
- "go.opentelemetry.io/otel*"
golang-x:
patterns:
- "golang.org/x/*"
# Update our Dockerfile.
- package-ecosystem: "docker"
directory: "/scripts/"
schedule:
interval: "weekly"
time: "06:00"
timezone: "America/Chicago"
commit-message:
prefix: "chore"
labels: []
ignore:
# We need to coordinate terraform updates with the version hardcoded in
# our Go code.
- dependency-name: "terraform"
groups:
scripts-docker:
patterns:
- "*"
- package-ecosystem: "npm"
directory: "/site/"
@@ -46,6 +77,60 @@ updates:
interval: "monthly"
time: "06:00"
timezone: "America/Chicago"
reviewers:
- "coder/ts"
commit-message:
prefix: "chore"
labels: []
ignore:
# Ignore patch updates for all dependencies
- dependency-name: "*"
update-types:
- version-update:semver-patch
# Ignore major updates to Node.js types, because they need to
# correspond to the Node.js engine version
- dependency-name: "@types/node"
update-types:
- version-update:semver-major
groups:
react:
patterns:
- "react*"
- "@types/react*"
xterm:
patterns:
- "xterm*"
xstate:
patterns:
- "xstate"
- "@xstate*"
mui:
patterns:
- "@mui*"
storybook:
patterns:
- "@storybook*"
- "storybook*"
eslint:
patterns:
- "eslint*"
- "@eslint*"
- "@typescript-eslint/eslint-plugin"
- "@typescript-eslint/parser"
jest:
patterns:
- "jest*"
- "@swc/jest"
- "@types/jest"
- package-ecosystem: "npm"
directory: "/offlinedocs/"
schedule:
interval: "monthly"
time: "06:00"
timezone: "America/Chicago"
reviewers:
- "coder/ts"
commit-message:
prefix: "chore"
labels: []
@@ -60,10 +145,25 @@ updates:
update-types:
- version-update:semver-major
- package-ecosystem: "terraform"
directory: "/examples/templates"
# Update dogfood.
- package-ecosystem: "docker"
directory: "/dogfood/"
schedule:
interval: "monthly"
interval: "weekly"
time: "06:00"
timezone: "America/Chicago"
commit-message:
prefix: "chore"
labels: []
groups:
dogfood-docker:
patterns:
- "*"
- package-ecosystem: "terraform"
directory: "/dogfood/"
schedule:
interval: "weekly"
time: "06:00"
timezone: "America/Chicago"
commit-message:
+433 -340
View File
@@ -6,7 +6,6 @@ on:
- main
pull_request:
workflow_dispatch:
permissions:
@@ -15,7 +14,7 @@ permissions:
contents: read
deployments: none
issues: none
packages: none
packages: write
pull-requests: none
repository-projects: none
security-events: none
@@ -28,22 +27,120 @@ concurrency:
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
lint:
runs-on: ${{ github.repository_owner == 'coder' && 'ubuntu-latest-8-cores' || 'ubuntu-latest' }}
changes:
runs-on: ubuntu-latest
outputs:
docs-only: ${{ steps.filter.outputs.docs_count == steps.filter.outputs.all_count }}
go: ${{ steps.filter.outputs.go }}
ts: ${{ steps.filter.outputs.ts }}
k8s: ${{ steps.filter.outputs.k8s }}
ci: ${{ steps.filter.outputs.ci }}
offlinedocs-only: ${{ steps.filter.outputs.offlinedocs_count == steps.filter.outputs.all_count }}
offlinedocs: ${{ steps.filter.outputs.offlinedocs }}
steps:
- name: Checkout
uses: actions/checkout@v3
# Install Go!
- uses: actions/setup-go@v3
with:
go-version: "~1.20"
fetch-depth: 1
# For pull requests it's not necessary to checkout the code
- name: check changed files
uses: dorny/paths-filter@v2
id: filter
with:
filters: |
all:
- "**"
docs:
- "docs/**"
- "README.md"
- "examples/templates/**"
- "examples/web-server/**"
- "examples/monitoring/**"
- "examples/lima/**"
go:
- "**.sql"
- "**.go"
- "**.golden"
- "go.mod"
- "go.sum"
# Other non-Go files that may affect Go code:
- "**.rego"
- "**.sh"
- "**.tpl"
- "**.gotmpl"
- "**.gotpl"
- "Makefile"
- "site/static/error.html"
# Main repo directories for completeness in case other files are
# touched:
- "agent/**"
- "cli/**"
- "cmd/**"
- "coderd/**"
- "enterprise/**"
- "examples/*"
- "provisioner/**"
- "provisionerd/**"
- "provisionersdk/**"
- "pty/**"
- "scaletest/**"
- "tailnet/**"
- "testutil/**"
ts:
- "site/**"
- "Makefile"
k8s:
- "helm/**"
- "scripts/Dockerfile"
- "scripts/Dockerfile.base"
- "scripts/helm.sh"
ci:
- ".github/actions/**"
- ".github/workflows/ci.yaml"
offlinedocs:
- "offlinedocs/**"
# Check for any typos!
- id: debug
run: |
echo "${{ toJSON(steps.filter )}}"
lint:
needs: changes
if: needs.changes.outputs.offlinedocs-only == 'false' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 1
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Setup Go
uses: ./.github/actions/setup-go
- name: Get golangci-lint cache dir
run: |
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.53.2
dir=$(golangci-lint cache status | awk '/Dir/ { print $2 }')
echo "LINT_CACHE_DIR=$dir" >> $GITHUB_ENV
- name: golangci-lint cache
uses: buildjet/cache@v3
with:
path: |
${{ env.LINT_CACHE_DIR }}
key: golangci-lint-${{ runner.os }}-${{ hashFiles('**/*.go') }}
restore-keys: |
golangci-lint-${{ runner.os }}-
# Check for any typos
- name: Check for typos
uses: crate-ci/typos@v1.13.14
uses: crate-ci/typos@v1.16.1
with:
config: .github/workflows/typos.toml
- name: Fix the typos
if: ${{ failure() }}
run: |
@@ -51,138 +148,43 @@ jobs:
cargo install typos-cli
typos -c .github/workflows/typos.toml -w"
# Check for Go linting errors!
- name: Lint Go
uses: golangci/golangci-lint-action@v3.3.1
with:
version: v1.51.0
- name: Lint shell scripts
uses: ludeeus/action-shellcheck@2.0.0
env:
SHELLCHECK_OPTS: --external-sources
with:
ignore: node_modules
# Lint our dashboard!
- name: Cache node_modules
id: cache-node
uses: actions/cache@v3
with:
path: |
**/node_modules
.eslintcache
key: js-${{ runner.os }}-test-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
js-${{ runner.os }}-
- name: Install node_modules
run: ./scripts/yarn_install.sh
- name: Lint TypeScript
run: yarn lint
working-directory: site
# Make sure the Helm chart is linted!
# Needed for helm chart linting
- name: Install helm
uses: azure/setup-helm@v3
with:
version: v3.9.2
- name: Lint Helm chart
run: |
cd helm
make lint
# Ensure AGPL and Enterprise are separated!
- name: Check for AGPL code importing Enterprise...
run: ./scripts/check_enterprise_imports.sh
changes:
runs-on: ubuntu-latest
outputs:
docs-only: ${{ steps.filter.outputs.docs_count == steps.filter.outputs.all_count }}
sh: ${{ steps.filter.outputs.sh }}
ts: ${{ steps.filter.outputs.ts }}
k8s: ${{ steps.filter.outputs.k8s }}
steps:
- uses: actions/checkout@v3
# For pull requests it's not necessary to checkout the code
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
all:
- '**'
docs:
- 'docs/**'
# For testing:
# - '.github/**'
sh:
- "**.sh"
ts:
- 'site/**'
k8s:
- 'helm/**'
- scripts/Dockerfile
- scripts/Dockerfile.base
- scripts/helm.sh
- id: debug
- name: make lint
run: |
echo "${{ toJSON(steps.filter )}}"
make --output-sync=line -j lint
gen:
timeout-minutes: 8
runs-on: ${{ github.repository_owner == 'coder' && 'ubuntu-latest-8-cores' || 'ubuntu-latest' }}
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
needs: changes
if: needs.changes.outputs.docs-only == 'false'
if: needs.changes.outputs.docs-only == 'false' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Cache Node
id: cache-node
uses: actions/cache@v3
- name: Checkout
uses: actions/checkout@v3
with:
path: |
**/node_modules
.eslintcache
key: js-${{ runner.os }}-test-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
js-${{ runner.os }}-
fetch-depth: 1
- name: Install node_modules
run: ./scripts/yarn_install.sh
- name: Setup Node
uses: ./.github/actions/setup-node
- uses: actions/setup-go@v3
with:
go-version: "~1.20"
- name: Setup Go
uses: ./.github/actions/setup-go
- name: Echo Go Cache Paths
id: go-cache-paths
- name: Setup sqlc
uses: ./.github/actions/setup-sqlc
- name: go install tools
run: |
echo "GOCACHE=$(go env GOCACHE)" >> $GITHUB_OUTPUT
echo "GOMODCACHE=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT
- name: Go Build Cache
uses: actions/cache@v3
with:
path: ${{ steps.go-cache-paths.outputs.GOCACHE }}
key: ${{ github.job }}-go-build-${{ hashFiles('**/go.sum', '**/**.go') }}
- name: Go Mod Cache
uses: actions/cache@v3
with:
path: ${{ steps.go-cache-paths.outputs.GOMODCACHE }}
key: ${{ github.job }}-go-mod-${{ hashFiles('**/go.sum') }}
- name: Install sqlc
run: |
curl -sSL https://github.com/kyleconroy/sqlc/releases/download/v1.16.0/sqlc_1.16.0_linux_amd64.tar.gz | sudo tar -C /usr/bin -xz sqlc
- name: Install protoc-gen-go
run: go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
- name: Install protoc-gen-go-drpc
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
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30
go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.33
go install golang.org/x/tools/cmd/goimports@latest
go install github.com/mikefarah/yq/v4@v4.30.6
go install github.com/golang/mock/mockgen@v1.6.0
- name: Install Protoc
run: |
@@ -204,28 +206,25 @@ jobs:
run: ./scripts/check_unstaged.sh
fmt:
runs-on: ubuntu-latest
timeout-minutes: 5
needs: changes
if: needs.changes.outputs.offlinedocs-only == 'false' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
timeout-minutes: 7
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
submodules: true
fetch-depth: 1
- name: Cache Node
id: cache-node
uses: actions/cache@v3
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Setup Go
uses: buildjet/setup-go@v4
with:
path: |
**/node_modules
.eslintcache
key: js-${{ runner.os }}-test-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
js-${{ runner.os }}-
- name: Install node_modules
run: ./scripts/yarn_install.sh
# This doesn't need caching. It's super fast anyways!
cache: false
go-version: 1.20.6
- name: Install shfmt
run: go install mvdan.cc/sh/v3/cmd/shfmt@v3.5.0
@@ -239,54 +238,28 @@ jobs:
run: ./scripts/check_unstaged.sh
test-go:
runs-on: ${{ matrix.os == 'ubuntu-latest' && github.repository_owner == 'coder' && 'ubuntu-latest-8-cores' || matrix.os == 'windows-2022' && github.repository_owner == 'coder' && 'windows-latest-8-cores'|| matrix.os }}
runs-on: ${{ matrix.os == 'ubuntu-latest' && github.repository_owner == 'coder' && 'buildjet-4vcpu-ubuntu-2204' || matrix.os == 'macos-latest' && github.repository_owner == 'coder' && 'macos-latest-xl' || matrix.os == 'windows-2019' && github.repository_owner == 'coder' && 'windows-latest-8-cores' || matrix.os }}
needs: changes
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macos-latest
- windows-2022
- windows-2019
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- name: Checkout
uses: actions/checkout@v3
with:
go-version: "~1.20"
fetch-depth: 1
# Sadly the new "set output" syntax (of writing env vars to
# $GITHUB_OUTPUT) does not work on both powershell and bash so we use the
# deprecated syntax here.
- name: Echo Go Cache Paths
id: go-cache-paths
run: |
echo "::set-output name=GOCACHE::$(go env GOCACHE)"
echo "::set-output name=GOMODCACHE::$(go env GOMODCACHE)"
- name: Setup Go
uses: ./.github/actions/setup-go
- name: Go Build Cache
uses: actions/cache@v3
with:
path: ${{ steps.go-cache-paths.outputs.GOCACHE }}
key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.**', '**.go') }}
- name: Go Mod Cache
uses: actions/cache@v3
with:
path: ${{ steps.go-cache-paths.outputs.GOMODCACHE }}
key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}
- name: Install gotestsum
uses: jaxxstorm/action-install-gh-release@v1.9.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
repo: gotestyourself/gotestsum
tag: v1.9.0
- uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.1.9
terraform_wrapper: false
- name: Setup Terraform
uses: ./.github/actions/setup-tf
- name: Test with Mock Database
id: test
@@ -302,16 +275,29 @@ jobs:
echo "cover=false" >> $GITHUB_OUTPUT
fi
gotestsum --junitfile="gotests.xml" --packages="./..." -- -parallel=8 -timeout=7m -short -failfast $COVERAGE_FLAGS
# By default Go will use the number of logical CPUs, which
# is a fine default.
PARALLEL_FLAG=""
- uses: actions/upload-artifact@v3
export TS_DEBUG_DISCO=true
gotestsum --junitfile="gotests.xml" --jsonfile="gotests.json" \
--packages="./..." -- $PARALLEL_FLAG -short -failfast $COVERAGE_FLAGS
- name: Print test stats
if: success() || failure()
run: |
# Artifacts are not available after rerunning a job,
# so we need to print the test stats to the log.
go run ./scripts/ci-report/main.go gotests.json | tee gotests_stats.json
- name: Upload test stats to Datadog
uses: ./.github/actions/upload-datadog
if: success() || failure()
with:
name: gotests-${{ matrix.os }}.xml
path: ./gotests.xml
retention-days: 30
api-key: ${{ secrets.DATADOG_API_KEY }}
- uses: codecov/codecov-action@v3
- name: Check code coverage
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
# that is no guarantee, see:
@@ -323,63 +309,47 @@ jobs:
files: ./gotests.coverage
flags: unittest-go-${{ matrix.os }}
test-go-psql:
runs-on: ${{ github.repository_owner == 'coder' && 'ubuntu-latest-8-cores' || 'ubuntu-latest' }}
test-go-pg:
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
needs: changes
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
# This timeout must be greater than the timeout set by `go test` in
# `make test-postgres` to ensure we receive a trace of running
# goroutines. Setting this to the timeout +5m should work quite well
# even if some of the preceding steps are slow.
timeout-minutes: 25
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- name: Checkout
uses: actions/checkout@v3
with:
go-version: "~1.20"
fetch-depth: 1
- name: Echo Go Cache Paths
id: go-cache-paths
run: |
echo "GOCACHE=$(go env GOCACHE)" >> $GITHUB_OUTPUT
echo "GOMODCACHE=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT
- name: Setup Go
uses: ./.github/actions/setup-go
- name: Go Build Cache
uses: actions/cache@v3
with:
path: ${{ steps.go-cache-paths.outputs.GOCACHE }}
key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum', '**/**.go') }}
- name: Go Mod Cache
uses: actions/cache@v3
with:
path: ${{ steps.go-cache-paths.outputs.GOMODCACHE }}
key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}
- name: Install gotestsum
uses: jaxxstorm/action-install-gh-release@v1.9.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
repo: gotestyourself/gotestsum
tag: v1.9.0
- uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.1.9
terraform_wrapper: false
- name: Setup Terraform
uses: ./.github/actions/setup-tf
- name: Test with PostgreSQL Database
run: |
export TS_DEBUG_DISCO=true
make test-postgres
- uses: actions/upload-artifact@v3
- name: Print test stats
if: success() || failure()
run: |
# Artifacts are not available after rerunning a job,
# so we need to print the test stats to the log.
go run ./scripts/ci-report/main.go gotests.json | tee gotests_stats.json
- name: Upload test stats to Datadog
uses: ./.github/actions/upload-datadog
if: success() || failure()
with:
name: gotests-postgres.xml
path: ./gotests.xml
retention-days: 30
api-key: ${{ secrets.DATADOG_API_KEY }}
- uses: codecov/codecov-action@v3
- name: Check code coverage
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
# that is no guarantee, see:
@@ -391,9 +361,36 @@ jobs:
files: ./gotests.coverage
flags: unittest-go-postgres-linux
test-go-race:
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
needs: changes
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 1
- name: Setup Go
uses: ./.github/actions/setup-go
- name: Setup Terraform
uses: ./.github/actions/setup-tf
- name: Run Tests
run: |
gotestsum --junitfile="gotests.xml" -- -race ./...
- name: Upload test stats to Datadog
uses: ./.github/actions/upload-datadog
if: always()
with:
api-key: ${{ secrets.DATADOG_API_KEY }}
deploy:
name: "deploy"
runs-on: ${{ github.repository_owner == 'coder' && 'ubuntu-latest-8-cores' || 'ubuntu-latest' }}
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
timeout-minutes: 30
needs: changes
if: |
@@ -403,7 +400,8 @@ jobs:
contents: read
id-token: write
steps:
- uses: actions/checkout@v3
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
@@ -416,38 +414,11 @@ jobs:
- name: Set up Google Cloud SDK
uses: google-github-actions/setup-gcloud@v1
- uses: actions/setup-go@v3
with:
go-version: "~1.20"
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Echo Go Cache Paths
id: go-cache-paths
run: |
echo "GOCACHE=$(go env GOCACHE)" >> $GITHUB_OUTPUT
echo "GOMODCACHE=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT
- name: Go Build Cache
uses: actions/cache@v3
with:
path: ${{ steps.go-cache-paths.outputs.GOCACHE }}
key: ${{ runner.os }}-release-go-build-${{ hashFiles('**/go.sum') }}
- name: Go Mod Cache
uses: actions/cache@v3
with:
path: ${{ steps.go-cache-paths.outputs.GOMODCACHE }}
key: ${{ runner.os }}-release-go-mod-${{ hashFiles('**/go.sum') }}
- name: Cache Node
id: cache-node
uses: actions/cache@v3
with:
path: |
**/node_modules
.eslintcache
key: js-${{ runner.os }}-release-node-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
js-${{ runner.os }}-
- name: Setup Go
uses: ./.github/actions/setup-go
- name: Install goimports
run: go install golang.org/x/tools/cmd/goimports@latest
@@ -470,16 +441,39 @@ jobs:
- name: Install Release
run: |
set -euo pipefail
regions=(
# gcp-region-id instance-name systemd-service-name
"us-central1-a coder coder"
"australia-southeast1-b coder-sydney coder-workspace-proxy"
"europe-west3-c coder-europe coder-workspace-proxy"
"southamerica-east1-b coder-brazil coder-workspace-proxy"
)
deb_pkg="./build/coder_$(./scripts/version.sh)_linux_amd64.deb"
if [ ! -f "$deb_pkg" ]; then
echo "deb package not found: $deb_pkg"
ls -l ./build
exit 1
fi
gcloud config set project coder-dogfood
gcloud config set compute/zone us-central1-a
gcloud compute scp ./build/coder_*_linux_amd64.deb coder:/tmp/coder.deb
gcloud compute ssh coder -- sudo dpkg -i --force-confdef /tmp/coder.deb
gcloud compute ssh coder -- sudo systemctl daemon-reload
for region in "${regions[@]}"; do
echo "::group::$region"
set -- $region
- name: Start
run: gcloud compute ssh coder -- sudo service coder restart
set -x
gcloud config set compute/zone "$1"
gcloud compute scp "$deb_pkg" "${2}:/tmp/coder.deb"
gcloud compute ssh "$2" -- /bin/sh -c "set -eux; sudo dpkg -i --force-confdef /tmp/coder.deb; sudo systemctl daemon-reload; sudo service '$3' restart"
set +x
- uses: actions/upload-artifact@v3
echo "::endgroup::"
done
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: coder
path: |
@@ -489,33 +483,24 @@ jobs:
retention-days: 7
test-js:
runs-on: ${{ github.repository_owner == 'coder' && 'ubuntu-latest-8-cores' || 'ubuntu-latest' }}
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
needs: changes
if: needs.changes.outputs.ts == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
timeout-minutes: 20
steps:
- uses: actions/checkout@v3
- name: Cache Node
id: cache-node
uses: actions/cache@v3
- name: Checkout
uses: actions/checkout@v3
with:
path: |
**/node_modules
.eslintcache
key: js-${{ runner.os }}-test-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
js-${{ runner.os }}-
fetch-depth: 1
- uses: actions/setup-node@v3
with:
node-version: "16.16.0"
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Install node_modules
run: ./scripts/yarn_install.sh
- run: yarn test:ci --max-workers ${{ steps.cpu-cores.outputs.count }}
- run: pnpm test:ci --max-workers $(nproc)
working-directory: site
- uses: codecov/codecov-action@v3
- name: Check code coverage
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
# that is no guarantee, see:
@@ -528,63 +513,33 @@ jobs:
flags: unittest-js
test-e2e:
needs:
- changes
if: needs.changes.outputs.docs-only == 'false'
runs-on: ${{ github.repository_owner == 'coder' && 'ubuntu-latest-8-cores' || 'ubuntu-latest' }}
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
needs: changes
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ts == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
timeout-minutes: 20
steps:
- uses: actions/checkout@v3
- name: Cache Node
id: cache-node
uses: actions/cache@v3
- name: Checkout
uses: actions/checkout@v3
with:
path: |
**/node_modules
.eslintcache
key: js-${{ runner.os }}-e2e-${{ hashFiles('**/yarn.lock') }}
fetch-depth: 1
- uses: actions/setup-go@v3
with:
go-version: "~1.20"
- name: Setup Node
uses: ./.github/actions/setup-node
- uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.1.9
terraform_wrapper: false
- name: Setup Go
uses: ./.github/actions/setup-go
- uses: actions/setup-node@v3
with:
node-version: "16.16.0"
- name: Echo Go Cache Paths
id: go-cache-paths
run: |
echo "GOCACHE=$(go env GOCACHE)" >> $GITHUB_OUTPUT
echo "GOMODCACHE=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT
- name: Go Build Cache
uses: actions/cache@v3
with:
path: ${{ steps.go-cache-paths.outputs.GOCACHE }}
key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }}
- name: Go Mod Cache
uses: actions/cache@v3
with:
path: ${{ steps.go-cache-paths.outputs.GOMODCACHE }}
key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}
- name: Setup Terraform
uses: ./.github/actions/setup-tf
- name: Build
run: |
sudo npm install -g prettier
make -B site/out/index.html
- run: yarn playwright:install
- run: pnpm playwright:install
working-directory: site
- run: yarn playwright:test
- run: pnpm playwright:test
env:
DEBUG: pw:api
working-directory: site
@@ -600,22 +555,18 @@ jobs:
chromatic:
# REMARK: this is only used to build storybook and deploy it to Chromatic.
runs-on: ubuntu-latest
needs:
- changes
if: needs.changes.outputs.ts == 'true'
needs: changes
if: needs.changes.outputs.ts == 'true' || needs.changes.outputs.ci == 'true'
steps:
- uses: actions/checkout@v3
- name: Checkout
uses: actions/checkout@v3
with:
# Required by Chromatic for build-over-build history, otherwise we
# only get 1 commit on shallow checkout.
fetch-depth: 0
- uses: actions/setup-node@v3
with:
node-version: "16.16.0"
- name: Install dependencies
run: cd site && yarn
- name: Setup Node
uses: ./.github/actions/setup-node
# This step is not meant for mainline because any detected changes to
# storybook snapshots will require manual approval/review in order for
@@ -623,13 +574,23 @@ jobs:
- name: Publish to Chromatic (non-mainline)
if: github.ref != 'refs/heads/main' && github.repository_owner == 'coder'
uses: chromaui/action@v1
env:
NODE_OPTIONS: "--max_old_space_size=4096"
STORYBOOK: true
with:
buildScriptName: "storybook:build"
exitOnceUploaded: true
# This will prevent CI from failing when Chromatic detects visual changes
exitZeroOnChanges: true
# Chromatic states its fine to make this token public. See:
# https://www.chromatic.com/docs/github-actions#forked-repositories
projectToken: 695c25b6cb65
workingDir: "./site"
# Prevent excessive build runs on minor version changes
skip: "@(renovate/**|dependabot/**)"
# Run TurboSnap to trace file dependencies to related stories
# and tell chromatic to only take snapshots of relevent stories
onlyChanged: true
# This is a separate step for mainline only that auto accepts and changes
# instead of holding CI up. Since we squash/merge, this is defensive to
@@ -640,8 +601,140 @@ jobs:
- name: Publish to Chromatic (mainline)
if: github.ref == 'refs/heads/main' && github.repository_owner == 'coder'
uses: chromaui/action@v1
env:
NODE_OPTIONS: "--max_old_space_size=4096"
STORYBOOK: true
with:
autoAcceptChanges: true
# This will prevent CI from failing when Chromatic detects visual changes
exitZeroOnChanges: true
buildScriptName: "storybook:build"
projectToken: 695c25b6cb65
workingDir: "./site"
# Run TurboSnap to trace file dependencies to related stories
# and tell chromatic to only take snapshots of relevent stories
onlyChanged: true
offlinedocs:
name: offlinedocs
needs: changes
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
if: needs.changes.outputs.offlinedocs == 'true' || needs.changes.outputs.ci == 'true'
steps:
- name: Checkout
uses: actions/checkout@v3
with:
# 0 is required here for version.sh to work.
fetch-depth: 0
- name: Setup Node
uses: ./.github/actions/setup-node
with:
directory: offlinedocs
- name: Setup Go
uses: ./.github/actions/setup-go
- name: Install go tools
run: |
go install github.com/golang/mock/mockgen@v1.6.0
- name: Setup sqlc
uses: sqlc-dev/setup-sqlc@v3
with:
sqlc-version: "1.19.1"
- name: Format
run: |
cd offlinedocs
pnpm format:check
- name: Lint
run: |
cd offlinedocs
pnpm lint
- name: Build
run: |
make -j build/coder_docs_"$(./scripts/version.sh)".tgz
required:
runs-on: ubuntu-latest
needs:
- fmt
- lint
- gen
- test-go
- test-go-pg
- test-go-race
- test-js
- offlinedocs
# Allow this job to run even if the needed jobs fail, are skipped or
# cancelled.
if: always()
steps:
- name: Ensure required checks
run: |
echo "Checking required checks"
echo "- fmt: ${{ needs.fmt.result }}"
echo "- lint: ${{ needs.lint.result }}"
echo "- gen: ${{ needs.gen.result }}"
echo "- test-go: ${{ needs.test-go.result }}"
echo "- test-go-pg: ${{ needs.test-go-pg.result }}"
echo "- test-go-race: ${{ needs.test-go-race.result }}"
echo "- test-js: ${{ needs.test-js.result }}"
echo
# We allow skipped jobs to pass, but not failed or cancelled jobs.
if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" || "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "One of the required checks has failed or has been cancelled"
exit 1
fi
echo "Required checks have passed"
build-main-image:
# This build and publihes ghcr.io/coder/coder-preview:main for each merge commit to main branch.
# We are only building this for amd64 plateform. (>95% pulls are for amd64)
needs: changes
if: github.ref == 'refs/heads/main' && needs.changes.outputs.docs-only == 'false'
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
env:
DOCKER_CLI_EXPERIMENTAL: "enabled"
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Setup Go
uses: ./.github/actions/setup-go
- name: Setup sqlc
uses: ./.github/actions/setup-sqlc
- name: GHCR Login
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Linux amd64 Docker image
run: |
set -euxo pipefail
go mod download
make gen/mark-fresh
export DOCKER_IMAGE_NO_PREREQUISITES=true
version="$(./scripts/version.sh)"
export CODER_IMAGE_BUILD_BASE_TAG="$(CODER_IMAGE_BASE=coder-base ./scripts/image_tag.sh --version "$version")"
make -j build/coder_linux_amd64
./scripts/build_docker.sh \
--arch amd64 \
--target ghcr.io/coder/coder-preview:main \
--version $version \
--push \
build/coder_linux_amd64
+4 -15
View File
@@ -25,7 +25,8 @@ jobs:
permissions:
pull-requests: write
steps:
- uses: hmarr/auto-approve-action@v3
- name: auto-approve dependabot
uses: hmarr/auto-approve-action@v3
if: github.actor == 'dependabot[bot]'
cla:
@@ -47,25 +48,13 @@ jobs:
branch: "main"
allowlist: dependabot*
title:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request_target'
steps:
- name: Validate PR title
uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
requireScope: false
release-labels:
runs-on: ubuntu-latest
# Depend on lint so that title is Conventional Commits-compatible.
needs: [title]
# Skip tagging for draft PRs.
if: ${{ github.event_name == 'pull_request_target' && success() && !github.event.pull_request.draft }}
steps:
- uses: actions/github-script@v6
- name: release-labels
uses: actions/github-script@v6
with:
# This script ensures PR title and labels are in sync:
#
+2 -1
View File
@@ -31,7 +31,8 @@ jobs:
runs-on: ubuntu-latest
if: github.repository_owner == 'coder'
steps:
- uses: actions/checkout@v3
- name: Checkout
uses: actions/checkout@v3
- name: Docker login
uses: docker/login-action@v2
+23 -10
View File
@@ -6,18 +6,21 @@ on:
- main
paths:
- "dogfood/**"
pull_request:
paths:
- "dogfood/**"
- ".github/workflows/dogfood.yaml"
# Uncomment these lines when testing with CI.
# pull_request:
# paths:
# - "dogfood/**"
# - ".github/workflows/dogfood.yaml"
workflow_dispatch:
jobs:
deploy_image:
runs-on: ubuntu-latest
runs-on: buildjet-4vcpu-ubuntu-2204
steps:
- name: Get branch name
id: branch-name
uses: tj-actions/branch-names@v6.4
uses: tj-actions/branch-names@v6.5
- name: "Branch name to Docker tag name"
id: docker-tag-name
@@ -43,26 +46,35 @@ jobs:
uses: docker/build-push-action@v4
with:
context: "{{defaultContext}}:dogfood"
pull: true
push: true
tags: "codercom/oss-dogfood:${{ steps.docker-tag-name.outputs.tag }},codercom/oss-dogfood:latest"
cache-from: type=registry,ref=codercom/oss-dogfood:latest
cache-to: type=inline
deploy_template:
needs: deploy_image
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Get short commit SHA
id: vars
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: "Install latest Coder"
- name: Get latest commit title
id: message
run: echo "pr_title=$(git log --format=%s -n 1 ${{ github.sha }})" >> $GITHUB_OUTPUT
- name: "Get latest Coder binary from the server"
run: |
curl -L https://coder.com/install.sh | sh
# env:
# VERSION: 0.x
curl -fsSL "https://dev.coder.com/bin/coder-linux-amd64" -o "./coder"
chmod +x "./coder"
- name: "Push template"
run: |
coder templates push $CODER_TEMPLATE_NAME --directory $CODER_TEMPLATE_DIR --yes --name=$CODER_TEMPLATE_VERSION
./coder templates push $CODER_TEMPLATE_NAME --directory $CODER_TEMPLATE_DIR --yes --name=$CODER_TEMPLATE_VERSION --message="$CODER_TEMPLATE_MESSAGE"
env:
# Consumed by Coder CLI
CODER_URL: https://dev.coder.com
@@ -71,3 +83,4 @@ jobs:
CODER_TEMPLATE_NAME: ${{ secrets.CODER_TEMPLATE_NAME }}
CODER_TEMPLATE_VERSION: ${{ steps.vars.outputs.sha_short }}
CODER_TEMPLATE_DIR: ./dogfood
CODER_TEMPLATE_MESSAGE: ${{ steps.message.outputs.pr_title }}
+2 -1
View File
@@ -18,5 +18,6 @@
{
"pattern": "tailscale.com"
}
]
],
"aliveStatusCodes": [200, 0]
}
+60
View File
@@ -0,0 +1,60 @@
# The nightly-gauntlet runs tests that are either too flaky or too slow to block
# every PR.
name: nightly-gauntlet
on:
schedule:
# Every day at midnight
- cron: "0 0 * * *"
workflow_dispatch:
jobs:
go-race:
# While GitHub's toaster runners are likelier to flake, we want consistency
# between this environment and the regular test environment for DataDog
# statistics and to only show real workflow threats.
runs-on: "buildjet-8vcpu-ubuntu-2204"
# This runner costs 0.016 USD per minute,
# so 0.016 * 240 = 3.84 USD per run.
timeout-minutes: 240
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Go
uses: ./.github/actions/setup-go
- name: Setup Terraform
uses: ./.github/actions/setup-tf
- name: Run Tests
run: |
# -race is likeliest to catch flaky tests
# due to correctness detection and its performance
# impact.
gotestsum --junitfile="gotests.xml" -- -timeout=240m -count=10 -race ./...
- name: Upload test results to DataDog
uses: ./.github/actions/upload-datadog
if: always()
with:
api-key: ${{ secrets.DATADOG_API_KEY }}
go-timing:
# We run these tests with p=1 so we don't need a lot of compute.
runs-on: "buildjet-2vcpu-ubuntu-2204"
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Go
uses: ./.github/actions/setup-go
- name: Run Tests
run: |
gotestsum --junitfile="gotests.xml" -- --tags="timing" -p=1 -run='_Timing/' ./...
- name: Upload test results to DataDog
uses: ./.github/actions/upload-datadog
if: always()
with:
api-key: ${{ secrets.DATADOG_API_KEY }}
+2 -1
View File
@@ -13,4 +13,5 @@ jobs:
assign-author:
runs-on: ubuntu-latest
steps:
- uses: toshimaru/auto-author-assign@v1.6.2
- name: Assign author
uses: toshimaru/auto-author-assign@v1.6.2
+73
View File
@@ -0,0 +1,73 @@
name: Cleanup PR deployment and image
on:
pull_request:
types: closed
workflow_dispatch:
inputs:
pr_number:
description: "PR number"
required: true
permissions:
packages: write
jobs:
cleanup:
runs-on: "ubuntu-latest"
steps:
- name: Get PR number
id: pr_number
run: |
if [ -n "${{ github.event.pull_request.number }}" ]; then
echo "PR_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
else
echo "PR_NUMBER=${{ github.event.inputs.pr_number }}" >> $GITHUB_OUTPUT
fi
- name: Delete image
continue-on-error: true
uses: bots-house/ghcr-delete-image-action@v1.1.0
with:
owner: coder
name: coder-preview
token: ${{ secrets.GITHUB_TOKEN }}
tag: pr${{ steps.pr_number.outputs.PR_NUMBER }}
- name: Set up kubeconfig
run: |
set -euxo pipefail
mkdir -p ~/.kube
echo "${{ secrets.PR_DEPLOYMENTS_KUBECONFIG }}" > ~/.kube/config
export KUBECONFIG=~/.kube/config
- name: Delete helm release
run: |
set -euxo pipefail
helm delete --namespace "pr${{ steps.pr_number.outputs.PR_NUMBER }}" "pr${{ steps.pr_number.outputs.PR_NUMBER }}" || echo "helm release not found"
- name: "Remove PR namespace"
run: |
kubectl delete namespace "pr${{ steps.pr_number.outputs.PR_NUMBER }}" || echo "namespace not found"
- name: "Remove DNS records"
run: |
set -euxo pipefail
# Get identifier for the record
record_id=$(curl -X GET "https://api.cloudflare.com/client/v4/zones/${{ secrets.PR_DEPLOYMENTS_ZONE_ID }}/dns_records?name=%2A.pr${{ steps.pr_number.outputs.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}" \
-H "Authorization: Bearer ${{ secrets.PR_DEPLOYMENTS_CLOUDFLARE_API_TOKEN }}" \
-H "Content-Type:application/json" | jq -r '.result[0].id') || echo "DNS record not found"
echo "::add-mask::$record_id"
# Delete the record
(
curl -X DELETE "https://api.cloudflare.com/client/v4/zones/${{ secrets.PR_DEPLOYMENTS_ZONE_ID }}/dns_records/$record_id" \
-H "Authorization: Bearer ${{ secrets.PR_DEPLOYMENTS_CLOUDFLARE_API_TOKEN }}" \
-H "Content-Type:application/json" | jq -r '.success'
) || echo "DNS record not found"
- name: "Delete certificate"
if: ${{ github.event.pull_request.merged == true }}
run: |
set -euxo pipefail
kubectl delete certificate "pr${{ steps.pr_number.outputs.PR_NUMBER }}-tls" -n pr-deployment-certs || echo "certificate not found"
+494
View File
@@ -0,0 +1,494 @@
# This action will trigger when
# 1. when the workflow is manually triggered
# 2. ./scripts/deploy_pr.sh is run locally
# 3. when a PR is updated
name: Deploy PR
on:
pull_request:
types: synchronize
workflow_dispatch:
inputs:
pr_number:
description: "PR number"
type: number
required: true
skip_build:
description: "Skip build job"
required: false
type: boolean
default: false
experiments:
description: "Experiments to enable"
required: false
type: string
default: "*"
env:
REPO: ghcr.io/coder/coder-preview
permissions:
contents: read
packages: write
pull-requests: write
concurrency:
group: ${{ github.workflow }}-PR-${{ github.event.pull_request.number || github.event.inputs.pr_number }}
cancel-in-progress: true
jobs:
get_info:
if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request'
outputs:
PR_NUMBER: ${{ steps.pr_info.outputs.PR_NUMBER }}
PR_TITLE: ${{ steps.pr_info.outputs.PR_TITLE }}
PR_URL: ${{ steps.pr_info.outputs.PR_URL }}
PR_BRANCH: ${{ steps.pr_info.outputs.PR_BRANCH }}
CODER_BASE_IMAGE_TAG: ${{ steps.set_tags.outputs.CODER_BASE_IMAGE_TAG }}
CODER_IMAGE_TAG: ${{ steps.set_tags.outputs.CODER_IMAGE_TAG }}
NEW: ${{ steps.check_deployment.outputs.new }}
BUILD: ${{ steps.filter.outputs.all_count > steps.filter.outputs.ignored_count || steps.check_deployment.outputs.new }}
runs-on: "ubuntu-latest"
steps:
- name: Get PR number, title, and branch name
id: pr_info
run: |
set -euxo pipefail
PR_NUMBER=${{ github.event.inputs.pr_number || github.event.pull_request.number }}
PR_TITLE=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/repos/coder/coder/pulls/$PR_NUMBER | jq -r '.title')
PR_BRANCH=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/repos/coder/coder/pulls/$PR_NUMBER | jq -r '.head.ref')
echo "PR_URL=https://github.com/coder/coder/pull/$PR_NUMBER" >> $GITHUB_OUTPUT
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "PR_TITLE=$PR_TITLE" >> $GITHUB_OUTPUT
echo "PR_BRANCH=$PR_BRANCH" >> $GITHUB_OUTPUT
- name: Set required tags
id: set_tags
run: |
set -euxo pipefail
echo "CODER_BASE_IMAGE_TAG=$CODER_BASE_IMAGE_TAG" >> $GITHUB_OUTPUT
echo "CODER_IMAGE_TAG=$CODER_IMAGE_TAG" >> $GITHUB_OUTPUT
env:
CODER_BASE_IMAGE_TAG: ghcr.io/coder/coder-preview-base:pr${{ steps.pr_info.outputs.PR_NUMBER }}
CODER_IMAGE_TAG: ghcr.io/coder/coder-preview:pr${{ steps.pr_info.outputs.PR_NUMBER }}
- name: Set up kubeconfig
run: |
set -euxo pipefail
mkdir -p ~/.kube
echo "${{ secrets.PR_DEPLOYMENTS_KUBECONFIG }}" > ~/.kube/config
export KUBECONFIG=~/.kube/config
- name: Check if the helm deployment already exists
id: check_deployment
run: |
set -euxo pipefail
if helm status "pr${{ steps.pr_info.outputs.PR_NUMBER }}" --namespace "pr${{ steps.pr_info.outputs.PR_NUMBER }}" > /dev/null 2>&1; then
echo "Deployment already exists. Skipping deployment."
new=false
else
echo "Deployment doesn't exist."
new=true
fi
echo "new=$new" >> $GITHUB_OUTPUT
- name: Find Comment
uses: peter-evans/find-comment@v2
if: github.event_name == 'workflow_dispatch' || steps.check_deployment.outputs.NEW == 'false'
id: fc
with:
issue-number: ${{ steps.pr_info.outputs.PR_NUMBER }}
comment-author: "github-actions[bot]"
body-includes: ":rocket:"
direction: last
- name: Comment on PR
id: comment_id
if: github.event_name == 'workflow_dispatch' || steps.check_deployment.outputs.NEW == 'false'
uses: peter-evans/create-or-update-comment@v3
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ steps.pr_info.outputs.PR_NUMBER }}
edit-mode: replace
body: |
---
:rocket: Deploying PR ${{ steps.pr_info.outputs.PR_NUMBER }} ...
---
reactions: eyes
reactions-edit-mode: replace
- name: Checkout
if: github.event_name == 'workflow_dispatch' || steps.check_deployment.outputs.NEW == 'false'
uses: actions/checkout@v3
with:
ref: ${{ steps.pr_info.outputs.PR_BRANCH }}
fetch-depth: 0
- name: Check changed files
if: github.event_name == 'workflow_dispatch' || steps.check_deployment.outputs.NEW == 'false'
uses: dorny/paths-filter@v2
id: filter
with:
filters: |
all:
- "**"
ignored:
- "docs/**"
- "README.md"
- "examples/web-server/**"
- "examples/monitoring/**"
- "examples/lima/**"
- ".github/**"
- "offlinedocs/**"
- ".devcontainer/**"
- "helm/**"
- "*[^g][^o][^.][^s][^u][^m]*"
- "*[^g][^o][^.][^m][^o][^d]*"
- "*[^M][^a][^k][^e][^f][^i][^l][^e]*"
- "scripts/**/*[^D][^o][^c][^k][^e][^r][^f][^i][^l][^e]*"
- "scripts/**/*[^D][^o][^c][^k][^e][^r][^f][^i][^l][^e][.][b][^a][^s][^e]*"
- name: Print number of changed files
if: github.event_name == 'workflow_dispatch' || steps.check_deployment.outputs.NEW == 'false'
run: |
set -euxo pipefail
echo "Total number of changed files: ${{ steps.filter.outputs.all_count }}"
echo "Number of ignored files: ${{ steps.filter.outputs.ignored_count }}"
build:
needs: get_info
# Skips the build job if the workflow was triggered by a workflow_dispatch event and the skip_build input is set to true
# or if the workflow was triggered by an issue_comment event and the comment body contains --skip-build
# always run the build job if a pull_request event triggered the workflow
if: |
(github.event_name == 'workflow_dispatch' && github.event.inputs.skip_build == 'false') ||
(github.event_name == 'pull_request' && needs.get_info.result == 'success' && needs.get_info.outputs.NEW == 'false')
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
env:
DOCKER_CLI_EXPERIMENTAL: "enabled"
CODER_IMAGE_TAG: ${{ needs.get_info.outputs.CODER_IMAGE_TAG }}
PR_NUMBER: ${{ needs.get_info.outputs.PR_NUMBER }}
PR_BRANCH: ${{ needs.get_info.outputs.PR_BRANCH }}
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ env.PR_BRANCH }}
fetch-depth: 0
- name: Setup Node
if: needs.get_info.outputs.BUILD == 'true'
uses: ./.github/actions/setup-node
- name: Setup Go
if: needs.get_info.outputs.BUILD == 'true'
uses: ./.github/actions/setup-go
- name: Setup sqlc
if: needs.get_info.outputs.BUILD == 'true'
uses: ./.github/actions/setup-sqlc
- name: GHCR Login
if: needs.get_info.outputs.BUILD == 'true'
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Linux amd64 Docker image
if: needs.get_info.outputs.BUILD == 'true'
run: |
set -euxo pipefail
go mod download
make gen/mark-fresh
export DOCKER_IMAGE_NO_PREREQUISITES=true
version="$(./scripts/version.sh)"
export CODER_IMAGE_BUILD_BASE_TAG="$(CODER_IMAGE_BASE=coder-base ./scripts/image_tag.sh --version "$version")"
make -j build/coder_linux_amd64
./scripts/build_docker.sh \
--arch amd64 \
--target ${{ env.CODER_IMAGE_TAG }} \
--version $version \
--push \
build/coder_linux_amd64
deploy:
needs: [build, get_info]
# Run deploy job only if build job was successful or skipped
if: |
always() && (needs.build.result == 'success' || needs.build.result == 'skipped') &&
(github.event_name == 'workflow_dispatch' || needs.get_info.outputs.NEW == 'false')
runs-on: "ubuntu-latest"
env:
CODER_IMAGE_TAG: ${{ needs.get_info.outputs.CODER_IMAGE_TAG }}
PR_NUMBER: ${{ needs.get_info.outputs.PR_NUMBER }}
PR_TITLE: ${{ needs.get_info.outputs.PR_TITLE }}
PR_URL: ${{ needs.get_info.outputs.PR_URL }}
PR_BRANCH: ${{ needs.get_info.outputs.PR_BRANCH }}
PR_DEPLOYMENT_ACCESS_URL: "pr${{ needs.get_info.outputs.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}"
steps:
- name: Set up kubeconfig
run: |
set -euxo pipefail
mkdir -p ~/.kube
echo "${{ secrets.PR_DEPLOYMENTS_KUBECONFIG }}" > ~/.kube/config
export KUBECONFIG=~/.kube/config
- name: Check if image exists
if: needs.get_info.outputs.NEW == 'true'
run: |
set -euxo pipefail
foundTag=$(curl -fsSL https://github.com/coder/coder/pkgs/container/coder-preview | grep -o ${{ env.CODER_IMAGE_TAG }} | head -n 1)
if [ -z "$foundTag" ]; then
echo "Image not found"
echo "${{ env.CODER_IMAGE_TAG }} not found in ghcr.io/coder/coder-preview"
echo "Please remove --skip-build from the comment and try again"
exit 1
fi
- name: Add DNS record to Cloudflare
if: needs.get_info.outputs.NEW == 'true'
run: |
curl -X POST "https://api.cloudflare.com/client/v4/zones/${{ secrets.PR_DEPLOYMENTS_ZONE_ID }}/dns_records" \
-H "Authorization: Bearer ${{ secrets.PR_DEPLOYMENTS_CLOUDFLARE_API_TOKEN }}" \
-H "Content-Type:application/json" \
--data '{"type":"CNAME","name":"*.${{ env.PR_DEPLOYMENT_ACCESS_URL }}","content":"${{ env.PR_DEPLOYMENT_ACCESS_URL }}","ttl":1,"proxied":false}'
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ env.PR_BRANCH }}
- name: Create PR namespace
if: needs.get_info.outputs.NEW == 'true'
run: |
set -euxo pipefail
# try to delete the namespace, but don't fail if it doesn't exist
kubectl delete namespace "pr${{ env.PR_NUMBER }}" || true
kubectl create namespace "pr${{ env.PR_NUMBER }}"
- name: Check and Create Certificate
if: needs.get_info.outputs.NEW == 'true'
run: |
# Using kubectl to check if a Certificate resource already exists
# we are doing this to avoid letsenrypt rate limits
if ! kubectl get certificate pr${{ env.PR_NUMBER }}-tls -n pr-deployment-certs > /dev/null 2>&1; then
echo "Certificate doesn't exist. Creating a new one."
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: pr${{ env.PR_NUMBER }}-tls
namespace: pr-deployment-certs
spec:
secretName: pr${{ env.PR_NUMBER }}-tls
issuerRef:
name: letsencrypt
kind: ClusterIssuer
dnsNames:
- "${{ env.PR_DEPLOYMENT_ACCESS_URL }}"
- "*.${{ env.PR_DEPLOYMENT_ACCESS_URL }}"
EOF
else
echo "Certificate exists. Skipping certificate creation."
fi
echo "Copy certificate from pr-deployment-certs to pr${{ env.PR_NUMBER }} namespace"
until kubectl get secret pr${{ env.PR_NUMBER }}-tls -n pr-deployment-certs &> /dev/null
do
echo "Waiting for secret pr${{ env.PR_NUMBER }}-tls to be created..."
sleep 5
done
(
kubectl get secret pr${{ env.PR_NUMBER }}-tls -n pr-deployment-certs -o json |
jq 'del(.metadata.namespace,.metadata.creationTimestamp,.metadata.resourceVersion,.metadata.selfLink,.metadata.uid,.metadata.managedFields)' |
kubectl -n pr${{ env.PR_NUMBER }} apply -f -
)
- name: Set up PostgreSQL database
if: needs.get_info.outputs.NEW == 'true'
run: |
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install coder-db bitnami/postgresql \
--namespace pr${{ env.PR_NUMBER }} \
--set auth.username=coder \
--set auth.password=coder \
--set auth.database=coder \
--set persistence.size=10Gi
kubectl create secret generic coder-db-url -n pr${{ env.PR_NUMBER }} \
--from-literal=url="postgres://coder:coder@coder-db-postgresql.pr${{ env.PR_NUMBER }}.svc.cluster.local:5432/coder?sslmode=disable"
- name: Create values.yaml
if: github.event_name == 'workflow_dispatch'
run: |
cat <<EOF > pr-deploy-values.yaml
coder:
image:
repo: ${{ env.REPO }}
tag: pr${{ env.PR_NUMBER }}
pullPolicy: Always
service:
type: ClusterIP
ingress:
enable: true
className: traefik
host: ${{ env.PR_DEPLOYMENT_ACCESS_URL }}
wildcardHost: "*.${{ env.PR_DEPLOYMENT_ACCESS_URL }}"
tls:
enable: true
secretName: pr${{ env.PR_NUMBER }}-tls
wildcardSecretName: pr${{ env.PR_NUMBER }}-tls
env:
- name: "CODER_ACCESS_URL"
value: "https://${{ env.PR_DEPLOYMENT_ACCESS_URL }}"
- name: "CODER_WILDCARD_ACCESS_URL"
value: "*.${{ env.PR_DEPLOYMENT_ACCESS_URL }}"
- name: "CODER_EXPERIMENTS"
value: "${{ github.event.inputs.experiments }}"
- name: CODER_PG_CONNECTION_URL
valueFrom:
secretKeyRef:
name: coder-db-url
key: url
- name: "CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS"
value: "true"
- name: "CODER_OAUTH2_GITHUB_CLIENT_ID"
value: "${{ secrets.PR_DEPLOYMENTS_GITHUB_OAUTH_CLIENT_ID }}"
- name: "CODER_OAUTH2_GITHUB_CLIENT_SECRET"
value: "${{ secrets.PR_DEPLOYMENTS_GITHUB_OAUTH_CLIENT_SECRET }}"
- name: "CODER_OAUTH2_GITHUB_ALLOWED_ORGS"
value: "coder"
EOF
- name: Install/Upgrade Helm chart
run: |
set -euxo pipefail
if [[ ${{ github.event_name }} == "workflow_dispatch" ]]; then
helm upgrade --install "pr${{ env.PR_NUMBER }}" ./helm \
--namespace "pr${{ env.PR_NUMBER }}" \
--values ./pr-deploy-values.yaml \
--force
else
if [[ ${{ needs.get_info.outputs.BUILD }} == "true" ]]; then
helm upgrade --install "pr${{ env.PR_NUMBER }}" ./helm \
--namespace "pr${{ env.PR_NUMBER }}" \
--reuse-values \
--force
else
echo "Skipping helm upgrade, as there is no new image to deploy"
fi
fi
- name: Install coder-logstream-kube
if: needs.get_info.outputs.NEW == 'true'
run: |
helm repo add coder-logstream-kube https://helm.coder.com/logstream-kube
helm upgrade --install coder-logstream-kube coder-logstream-kube/coder-logstream-kube \
--namespace "pr${{ env.PR_NUMBER }}" \
--set url="https://pr${{ env.PR_NUMBER }}.${{ secrets.PR_DEPLOYMENTS_DOMAIN }}"
- name: Get Coder binary
if: needs.get_info.outputs.NEW == 'true'
run: |
set -euxo pipefail
DEST="${HOME}/coder"
URL="https://${{ env.PR_DEPLOYMENT_ACCESS_URL }}/bin/coder-linux-amd64"
mkdir -p "$(dirname ${DEST})"
COUNT=0
until $(curl --output /dev/null --silent --head --fail "$URL"); do
printf '.'
sleep 5
COUNT=$((COUNT+1))
if [ $COUNT -ge 60 ]; then
echo "Timed out waiting for URL to be available"
exit 1
fi
done
curl -fsSL "$URL" -o "${DEST}"
chmod +x "${DEST}"
"${DEST}" version
mv "${DEST}" /usr/local/bin/coder
- name: Create first user, template and workspace
if: needs.get_info.outputs.NEW == 'true'
id: setup_deployment
run: |
set -euxo pipefail
# Create first user
# create a masked random password 12 characters long
password=$(openssl rand -base64 16 | tr -d "=+/" | cut -c1-12)
# add mask so that the password is not printed to the logs
echo "::add-mask::$password"
echo "password=$password" >> $GITHUB_OUTPUT
coder login \
--first-user-username test \
--first-user-email pr${{ env.PR_NUMBER }}@coder.com \
--first-user-password $password \
--first-user-trial \
--use-token-as-session \
https://${{ env.PR_DEPLOYMENT_ACCESS_URL }}
# Create template
coder templates init --id kubernetes && cd ./kubernetes/ && coder templates create -y --variable namespace=pr${{ env.PR_NUMBER }}
# Create workspace
cat <<EOF > workspace.yaml
cpu: "2"
memory: "4"
home_disk_size: "2"
EOF
coder create --template="kubernetes" test --rich-parameter-file ./workspace.yaml -y
coder stop test -y
- name: Send Slack notification
if: needs.get_info.outputs.NEW == 'true'
run: |
curl -s -o /dev/null -X POST -H 'Content-type: application/json' \
-d \
'{
"pr_number": "'"${{ env.PR_NUMBER }}"'",
"pr_url": "'"${{ env.PR_URL }}"'",
"pr_title": "'"${{ env.PR_TITLE }}"'",
"pr_access_url": "'"https://${{ env.PR_DEPLOYMENT_ACCESS_URL }}"'",
"pr_username": "'"test"'",
"pr_email": "'"pr${{ env.PR_NUMBER }}@coder.com"'",
"pr_password": "'"${{ steps.setup_deployment.outputs.password }}"'",
"pr_actor": "'"${{ github.actor }}"'"
}' \
${{ secrets.PR_DEPLOYMENTS_SLACK_WEBHOOK }}
echo "Slack notification sent"
- name: Find Comment
uses: peter-evans/find-comment@v2
id: fc
with:
issue-number: ${{ env.PR_NUMBER }}
comment-author: "github-actions[bot]"
body-includes: ":rocket:"
direction: last
- name: Comment on PR
uses: peter-evans/create-or-update-comment@v3
env:
STATUS: ${{ needs.get_info.outputs.NEW == 'true' && 'Created' || 'Updated' }}
with:
issue-number: ${{ env.PR_NUMBER }}
edit-mode: replace
comment-id: ${{ steps.fc.outputs.comment-id }}
body: |
---
:heavy_check_mark: PR ${{ env.PR_NUMBER }} ${{ env.STATUS }} successfully.
:rocket: Access the credentials [here](${{ secrets.PR_DEPLOYMENTS_SLACK_CHANNEL_URL }}).
---
cc: @${{ github.actor }}
reactions: rocket
reactions-edit-mode: replace
+22 -18
View File
@@ -28,18 +28,23 @@ env:
# https://github.blog/changelog/2022-06-10-github-actions-inputs-unified-across-manual-and-reusable-workflows/
CODER_RELEASE: ${{ !inputs.dry_run }}
CODER_DRY_RUN: ${{ inputs.dry_run }}
# For some reason, setup-go won't actually pick up a new patch version if
# it has an old one cached. We need to manually specify the versions so we
# can get the latest release. Never use "~1.xx" here!
CODER_GO_VERSION: "1.20.6"
jobs:
release:
name: Build and publish
runs-on: ${{ github.repository_owner == 'coder' && 'ubuntu-latest-8-cores' || 'ubuntu-latest' }}
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
env:
# Necessary for Docker manifest
DOCKER_CLI_EXPERIMENTAL: "enabled"
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v3
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
@@ -71,11 +76,11 @@ jobs:
set -euo pipefail
ref=HEAD
old_version="$(git describe --abbrev=0 "$ref^1")"
version="$(./scripts/version.sh)"
version="v$(./scripts/version.sh)"
# 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"
./scripts/release/generate_release_notes.sh --check-for-changelog --old-version "$old_version" --new-version "$version" --ref "$ref" >> "$release_notes_file"
echo CODER_RELEASE_NOTES_FILE="$release_notes_file" >> $GITHUB_ENV
- name: Show release notes
@@ -90,20 +95,11 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-go@v3
with:
go-version: "~1.20"
- name: Setup Go
uses: ./.github/actions/setup-go
- name: Cache Node
id: cache-node
uses: actions/cache@v3
with:
path: |
**/node_modules
.eslintcache
key: js-${{ runner.os }}-test-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
js-${{ runner.os }}-
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Install nsis and zstd
run: sudo apt-get install -y nsis zstd
@@ -255,6 +251,11 @@ jobs:
env:
CODER_BASE_IMAGE_TAG: ${{ steps.image-base-tag.outputs.tag }}
- name: Generate offline docs
run: |
version="$(./scripts/version.sh)"
make -j build/coder_docs_"$version".tgz
- name: ls build
run: ls -lh build
@@ -333,7 +334,8 @@ jobs:
runs-on: windows-latest
needs: release
steps:
- uses: actions/checkout@v3
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
@@ -397,6 +399,8 @@ jobs:
- name: Comment on PR
if: ${{ !inputs.dry_run }}
run: |
# wait 30 seconds
Start-Sleep -Seconds 30.0
# Find the PR that wingetcreate just made.
$version = "${{ needs.release.outputs.version }}".Trim('v')
$pr_list = gh pr list --repo microsoft/winget-pkgs --search "author:cdrci Coder.Coder version ${version}" --limit 1 --json number | `
+34 -46
View File
@@ -8,9 +8,12 @@ permissions:
on:
workflow_dispatch:
# Uncomment when testing.
# pull_request:
schedule:
# Run every 6 hours Monday-Friday!
- cron: "0 0,6,12,18 * * 1-5"
- cron: "0 0/6 * * 1-5"
# Cancel in-progress runs for pull requests when developers push
# additional changes
@@ -18,11 +21,15 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-security
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
CODER_GO_VERSION: "1.20.6"
jobs:
codeql:
runs-on: ${{ github.repository_owner == 'coder' && 'ubuntu-latest-8-cores' || 'ubuntu-latest' }}
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@v3
- name: Checkout
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
@@ -30,20 +37,7 @@ jobs:
languages: go, javascript
- name: Setup Go
uses: actions/setup-go@v3
with:
go-version: "~1.20"
- name: Go Cache Paths
id: go-cache-paths
run: |
echo "GOMODCACHE=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT
- name: Go Mod Cache
uses: actions/cache@v3
with:
path: ${{ steps.go-cache-paths.outputs.GOMODCACHE }}
key: ${{ runner.os }}-release-go-mod-${{ hashFiles('**/go.sum') }}
uses: ./.github/actions/setup-go
# Workaround to prevent CodeQL from building the dashboard.
- name: Remove Makefile
@@ -65,44 +59,30 @@ jobs:
"${{ secrets.SLACK_SECURITY_FAILURE_WEBHOOK_URL }}"
trivy:
runs-on: ${{ github.repository_owner == 'coder' && 'ubuntu-latest-8-cores' || 'ubuntu-latest' }}
runs-on: ${{ github.repository_owner == 'coder' && 'buildjet-8vcpu-ubuntu-2204' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@v3
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-go@v3
with:
go-version: "~1.20"
- name: Setup Go
uses: ./.github/actions/setup-go
- name: Go Cache Paths
id: go-cache-paths
run: |
echo "GOMODCACHE=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Go Mod Cache
uses: actions/cache@v3
with:
path: ${{ steps.go-cache-paths.outputs.GOMODCACHE }}
key: ${{ runner.os }}-release-go-mod-${{ hashFiles('**/go.sum') }}
- name: Cache Node
id: cache-node
uses: actions/cache@v3
with:
path: |
**/node_modules
.eslintcache
key: js-${{ runner.os }}-test-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
js-${{ runner.os }}-
- name: Setup sqlc
uses: ./.github/actions/setup-sqlc
- name: Install yq
run: go run github.com/mikefarah/yq/v4@v4.30.6
- name: Install mockgen
run: go install github.com/golang/mock/mockgen@v1.6.0
- name: Install protoc-gen-go
run: go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
run: go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30
- name: Install protoc-gen-go-drpc
run: go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.26
run: go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.33
- name: Install Protoc
run: |
# protoc must be in lockstep with our dogfood Dockerfile or the
@@ -136,8 +116,16 @@ jobs:
make -j "$image_job"
echo "image=$(cat "$image_job")" >> $GITHUB_OUTPUT
- name: Run Prisma Cloud image scan
uses: PaloAltoNetworks/prisma-cloud-scan@v1
with:
pcc_console_url: ${{ secrets.PRISMA_CLOUD_URL }}
pcc_user: ${{ secrets.PRISMA_CLOUD_ACCESS_KEY }}
pcc_pass: ${{ secrets.PRISMA_CLOUD_SECRET_KEY }}
image_name: ${{ steps.build.outputs.image }}
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@8bd2f9fbda2109502356ff8a6a89da55b1ead252
uses: aquasecurity/trivy-action@41f05d9ecffa2ed3f1580af306000f734b733e54
with:
image-ref: ${{ steps.build.outputs.image }}
format: sarif
@@ -160,7 +148,7 @@ jobs:
- name: Send Slack notification on failure
if: ${{ failure() }}
run: |
msg="❌ CodeQL Failed\n\nhttps://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
msg="❌ Trivy Failed\n\nhttps://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
curl \
-qfsSL \
-X POST \
+32 -13
View File
@@ -1,4 +1,4 @@
name: Stale Issue and Branch Cleanup
name: Stale Issue, Banch and Old Workflows Cleanup
on:
schedule:
# Every day at midnight
@@ -10,24 +10,22 @@ jobs:
permissions:
issues: write
pull-requests: write
actions: write
steps:
- uses: actions/stale@v7.0.0
- name: stale
uses: actions/stale@v8.0.0
with:
stale-issue-label: "stale"
stale-pr-label: "stale"
days-before-stale: 90
days-before-stale: 180
# Pull Requests become stale more quickly due to merge conflicts.
# Also, we promote minimizing WIP.
days-before-pr-stale: 7
days-before-pr-close: 3
stale-pr-message: >
This Pull Request is becoming stale. In order to minimize WIP,
prevent merge conflicts and keep the tracker readable, I'm going
close to this PR in 3 days if there isn't more activity.
stale-issue-message: >
This issue is becoming stale. In order to keep the tracker readable
and actionable, I'm going close to this issue in 7 days if there
isn't more activity.
# We rarely take action in response to the message, so avoid
# cluttering the issue and just close the oldies.
stale-pr-message: ""
stale-issue-message: ""
# Upped from 30 since we have a big tracker and was hitting the limit.
operations-per-run: 60
# Start with the oldest issues, always.
@@ -36,9 +34,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Run delete-old-branches-action
uses: beatlabs/delete-old-branches-action@v0.0.9
uses: beatlabs/delete-old-branches-action@v0.0.10
with:
repo_token: ${{ github.token }}
date: "6 months ago"
@@ -46,3 +44,24 @@ jobs:
delete_tags: false
# extra_protected_branch_regex: ^(foo|bar)$
exclude_open_pr_branches: true
del_runs:
runs-on: ubuntu-latest
steps:
- name: Delete PR Cleanup workflow runs
uses: Mattraks/delete-workflow-runs@v2
with:
token: ${{ github.token }}
repository: ${{ github.repository }}
retain_days: 1
keep_minimum_runs: 1
delete_workflow_pattern: pr-cleanup.yaml
- name: Delete PR Deploy workflow skipped runs
uses: Mattraks/delete-workflow-runs@v2
with:
token: ${{ github.token }}
repository: ${{ github.repository }}
retain_days: 0
keep_minimum_runs: 0
delete_run_by_conclusion_pattern: skipped
delete_workflow_pattern: pr-deploy.yaml
+13 -8
View File
@@ -4,6 +4,7 @@ Jetbrains = "JetBrains"
IST = "IST"
MacOS = "macOS"
AKS = "AKS"
O_WRONLY = "O_WRONLY"
[default.extend-words]
AKS = "AKS"
@@ -16,12 +17,16 @@ encrypter = "encrypter"
[files]
extend-exclude = [
"**.svg",
"**.png",
"**.lock",
"go.sum",
"go.mod",
# These files contain base64 strings that confuse the detector
"**XService**.ts",
"**identity.go",
"**.svg",
"**.png",
"**.lock",
"go.sum",
"go.mod",
# These files contain base64 strings that confuse the detector
"**XService**.ts",
"**identity.go",
"scripts/ci-report/testdata/**",
"**/*_test.go",
"**/*.test.tsx",
"**/pnpm-lock.yaml",
]
+32
View File
@@ -0,0 +1,32 @@
name: weekly-docs
# runs every monday at 9 am
on:
schedule:
- cron: "0 9 * * 1"
workflow_dispatch: # allows to run manually for testing
jobs:
check-docs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@master
- name: Check Markdown links
uses: gaurav-nelson/github-action-markdown-link-check@v1
id: markdown-link-check
# checks all markdown files from /docs including all subfolders
with:
use-quiet-mode: "yes"
use-verbose-mode: "yes"
config-file: ".github/workflows/mlc_config.json"
folder-path: "docs/"
file-path: "./README.md"
- name: Send Slack notification
if: failure()
run: |
curl -X POST -H 'Content-type: application/json' -d '{"msg":"Broken links found in the documentation. Please check the logs at ${{ env.LOGS_URL }}"}' ${{ secrets.DOCS_LINK_SLACK_WEBHOOK }}
echo "Sent Slack notification"
env:
LOGS_URL: https://github.com/coder/coder/actions/runs/${{ github.run_id }}
+14 -5
View File
@@ -6,7 +6,8 @@
**/*.swp
gotests.coverage
gotests.xml
gotestsum.json
gotests_stats.json
gotests.json
node_modules/
vendor/
yarn-error.log
@@ -26,12 +27,13 @@ site/storybook-static/
site/test-results/*
site/e2e/test-results/*
site/e2e/states/*.json
site/e2e/.auth.json
site/playwright-report/*
site/.swc
site/dist/
# Make target for updating golden files.
cli/testdata/.gen-golden
helm/tests/testdata/.gen-golden
# Make target for updating golden files (any dir).
.gen-golden
# Build
/build/
@@ -47,8 +49,15 @@ site/stats/
*.lock.hcl
.terraform/
/.coderv2/*
**/.coderv2/*
**/__debug_bin
# direnv
.envrc
*.test
# Loadtesting
./scaletest/terraform/.terraform
./scaletest/terraform/.terraform.lock.hcl
scaletest/terraform/secrets.tfvars
.terraform.tfstate.*
+13 -4
View File
@@ -2,6 +2,10 @@
# Over time we should try tightening some of these.
linters-settings:
exhaustruct:
include:
# Gradually extend to cover more of the codebase.
- 'httpmw\.\w+'
gocognit:
min-complexity: 46 # Min code complexity (def 30).
@@ -54,7 +58,6 @@ linters-settings:
# - importShadow
- indexAlloc
- initClause
- ioutilDeprecated
- mapKey
- methodExprCall
# - nestingReduce
@@ -116,7 +119,8 @@ linters-settings:
local-prefixes: coder.com,cdr.dev,go.coder.com,github.com/cdr,github.com/coder
gocyclo:
min-complexity: 50
# goal: 30
min-complexity: 47
importas:
no-unaliased: true
@@ -194,18 +198,22 @@ issues:
linters:
# We use assertions rather than explicitly checking errors in tests
- errcheck
- forcetypeassert
- exhaustruct # This is unhelpful in tests.
- path: scripts/*
linters:
- exhaustruct
fix: true
max-issues-per-linter: 0
max-same-issues: 0
run:
concurrency: 4
skip-dirs:
- node_modules
skip-files:
- scripts/rules.go
timeout: 5m
timeout: 10m
# Over time, add more and more linters from
# https://golangci-lint.run/usage/linters/ as the code improves.
@@ -219,6 +227,7 @@ linters:
- errcheck
- errname
- errorlint
- exhaustruct
- exportloopref
- forcetypeassert
- gocritic
+19 -5
View File
@@ -9,7 +9,8 @@
**/*.swp
gotests.coverage
gotests.xml
gotestsum.json
gotests_stats.json
gotests.json
node_modules/
vendor/
yarn-error.log
@@ -29,12 +30,13 @@ site/storybook-static/
site/test-results/*
site/e2e/test-results/*
site/e2e/states/*.json
site/e2e/.auth.json
site/playwright-report/*
site/.swc
site/dist/
# Make target for updating golden files.
cli/testdata/.gen-golden
helm/tests/testdata/.gen-golden
# Make target for updating golden files (any dir).
.gen-golden
# Build
/build/
@@ -50,11 +52,18 @@ site/stats/
*.lock.hcl
.terraform/
/.coderv2/*
**/.coderv2/*
**/__debug_bin
# direnv
.envrc
*.test
# Loadtesting
./scaletest/terraform/.terraform
./scaletest/terraform/.terraform.lock.hcl
scaletest/terraform/secrets.tfvars
.terraform.tfstate.*
# .prettierignore.include:
# Helm templates contain variables that are invalid YAML and can't be formatted
# by Prettier.
@@ -66,3 +75,8 @@ helm/templates/*.yaml
# Testdata shouldn't be formatted.
scripts/apitypings/testdata/**/*.ts
# Generated files shouldn't be formatted.
site/e2e/provisionerGenerated.ts
**/pnpm-lock.yaml
+5
View File
@@ -8,3 +8,8 @@ helm/templates/*.yaml
# Testdata shouldn't be formatted.
scripts/apitypings/testdata/**/*.ts
# Generated files shouldn't be formatted.
site/e2e/provisionerGenerated.ts
**/pnpm-lock.yaml
+2
View File
@@ -4,6 +4,8 @@
printWidth: 80
semi: false
trailingComma: all
useTabs: false
tabWidth: 2
overrides:
- files:
- README.md
+4 -9
View File
@@ -20,6 +20,8 @@
"codersdk",
"cronstrue",
"databasefake",
"dbfake",
"dbgen",
"dbtype",
"DERP",
"derphttp",
@@ -34,6 +36,7 @@
"Dsts",
"embeddedpostgres",
"enablements",
"enterprisemeta",
"errgroup",
"eventsourcemock",
"Failf",
@@ -90,7 +93,6 @@
"pqtype",
"prometheusmetrics",
"promhttp",
"promptui",
"protobuf",
"provisionerd",
"provisionerdserver",
@@ -209,12 +211,5 @@
"go.testFlags": ["-short", "-coverpkg=./..."],
// We often use a version of TypeScript that's ahead of the version shipped
// with VS Code.
"typescript.tsdk": "./site/node_modules/typescript/lib",
"grammarly.selectors": [
{
"language": "markdown",
"scheme": "file",
"pattern": "docs/contributing/frontend.md"
}
]
"typescript.tsdk": "./site/node_modules/typescript/lib"
}
+92 -34
View File
@@ -50,11 +50,11 @@ endif
# 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 \)
-not \( \( -path '*/.git/*' -o -path './build/*' -o -path './vendor/*' -o -path './.coderv2/*' -o -path '*/node_modules/*' -o -path '*/out/*' -o -path './coderd/apidoc/*' -o -path '*/.next/*' \) -prune \)
# Source files used for make targets, evaluated on use.
GO_SRC_FILES = $(shell find . $(FIND_EXCLUSIONS) -type f -name '*.go')
GO_SRC_FILES := $(shell find . $(FIND_EXCLUSIONS) -type f -name '*.go' -not -name '*_test.go')
# All the shell files in the repo, excluding ignored files.
SHELL_SRC_FILES = $(shell find . $(FIND_EXCLUSIONS) -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 := \
@@ -356,9 +356,17 @@ build/coder_helm_$(VERSION).tgz:
--output "$@"
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
../scripts/pnpm_install.sh
pnpm build
offlinedocs/out/index.html: $(shell find ./offlinedocs $(FIND_EXCLUSIONS) -type f) $(shell find ./docs $(FIND_EXCLUSIONS) -type f | sed 's: :\\ :g')
cd offlinedocs
../scripts/pnpm_install.sh
pnpm export
build/coder_docs_$(VERSION).tgz: offlinedocs/out/index.html
tar -czf "$@" -C offlinedocs/out .
install: build/coder_$(VERSION)_$(GOOS)_$(GOARCH)$(GOOS_BIN_EXT)
install_dir="$$(go env GOPATH)/bin"
@@ -382,9 +390,9 @@ fmt/prettier:
cd site
# Avoid writing files in CI to reduce file write activity
ifdef CI
yarn run format:check
pnpm run format:check
else
yarn run format:write
pnpm run format:write
endif
.PHONY: fmt/prettier
@@ -402,11 +410,22 @@ else
endif
.PHONY: fmt/shfmt
lint: lint/shellcheck lint/go
lint: lint/shellcheck lint/go lint/ts lint/helm lint/site-icons
.PHONY: lint
lint/site-icons:
./scripts/check_site_icons.sh
.PHONY: lint/site-icons
lint/ts:
cd site
pnpm i && pnpm lint
.PHONY: lint/ts
lint/go:
./scripts/check_enterprise_imports.sh
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.53.2
golangci-lint run
.PHONY: lint/go
@@ -416,13 +435,29 @@ lint/shellcheck: $(SHELL_SRC_FILES)
shellcheck --external-sources $(SHELL_SRC_FILES)
.PHONY: lint/shellcheck
lint/helm:
cd helm
make lint
.PHONY: lint/helm
# All files generated by the database should be added here, and this can be used
# as a target for jobs that need to run after the database is generated.
DB_GEN_FILES := \
coderd/database/querier.go \
coderd/database/unique_constraint.go \
coderd/database/dbfake/dbfake.go \
coderd/database/dbmetrics/dbmetrics.go \
coderd/database/dbauthz/dbauthz.go \
coderd/database/dbmock/dbmock.go
# all gen targets should be added here and to gen/mark-fresh
gen: \
coderd/database/dump.sql \
coderd/database/querier.go \
$(DB_GEN_FILES) \
provisionersdk/proto/provisioner.pb.go \
provisionerd/proto/provisionerd.pb.go \
site/src/api/typesGenerated.ts \
coderd/rbac/object_gen.go \
docs/admin/prometheus.md \
docs/cli.md \
docs/admin/audit-logs.md \
@@ -439,10 +474,11 @@ gen: \
gen/mark-fresh:
files="\
coderd/database/dump.sql \
coderd/database/querier.go \
$(DB_GEN_FILES) \
provisionersdk/proto/provisioner.pb.go \
provisionerd/proto/provisionerd.pb.go \
site/src/api/typesGenerated.ts \
coderd/rbac/object_gen.go \
docs/admin/prometheus.md \
docs/cli.md \
docs/admin/audit-logs.md \
@@ -471,9 +507,12 @@ coderd/database/dump.sql: coderd/database/gen/dump/main.go $(wildcard coderd/dat
go run ./coderd/database/gen/dump/main.go
# Generates Go code for querying the database.
coderd/database/querier.go: coderd/database/sqlc.yaml coderd/database/dump.sql $(wildcard coderd/database/queries/*.sql) coderd/database/gen/enum/main.go
coderd/database/querier.go: coderd/database/sqlc.yaml coderd/database/dump.sql $(wildcard coderd/database/queries/*.sql)
./coderd/database/generate.sh
coderd/database/dbmock/dbmock.go: coderd/database/db.go coderd/database/querier.go
go generate ./coderd/database/dbmock/
provisionersdk/proto/provisioner.pb.go: provisionersdk/proto/provisioner.proto
protoc \
--go_out=. \
@@ -493,38 +532,46 @@ provisionerd/proto/provisionerd.pb.go: provisionerd/proto/provisionerd.proto
site/src/api/typesGenerated.ts: scripts/apitypings/main.go $(shell find ./codersdk $(FIND_EXCLUSIONS) -type f -name '*.go')
go run scripts/apitypings/main.go > site/src/api/typesGenerated.ts
cd site
yarn run format:types
pnpm run format:types
coderd/rbac/object_gen.go: scripts/rbacgen/main.go coderd/rbac/object.go
go run scripts/rbacgen/main.go ./coderd/rbac > coderd/rbac/object_gen.go
docs/admin/prometheus.md: scripts/metricsdocgen/main.go scripts/metricsdocgen/metrics
go run scripts/metricsdocgen/main.go
cd site
yarn run format:write:only ../docs/admin/prometheus.md
pnpm run format:write:only ./docs/admin/prometheus.md
docs/cli.md: scripts/clidocgen/main.go $(GO_SRC_FILES) docs/manifest.json
docs/cli.md: scripts/clidocgen/main.go $(GO_SRC_FILES)
BASE_PATH="." go run ./scripts/clidocgen
cd site
yarn run format:write:only ../docs/cli.md ../docs/cli/*.md ../docs/manifest.json
pnpm run format:write:only ./docs/cli.md ./docs/cli/*.md ./docs/manifest.json
docs/admin/audit-logs.md: scripts/auditdocgen/main.go enterprise/audit/table.go
docs/admin/audit-logs.md: scripts/auditdocgen/main.go enterprise/audit/table.go coderd/rbac/object_gen.go
go run scripts/auditdocgen/main.go
cd site
yarn run format:write:only ../docs/admin/audit-logs.md
pnpm run format:write:only ./docs/admin/audit-logs.md
coderd/apidoc/swagger.json: $(shell find ./scripts/apidocgen $(FIND_EXCLUSIONS) -type f) $(wildcard coderd/*.go) $(wildcard enterprise/coderd/*.go) $(wildcard codersdk/*.go) .swaggo docs/manifest.json
coderd/apidoc/swagger.json: $(shell find ./scripts/apidocgen $(FIND_EXCLUSIONS) -type f) $(wildcard coderd/*.go) $(wildcard enterprise/coderd/*.go) $(wildcard codersdk/*.go) $(wildcard enterprise/wsproxy/wsproxysdk/*.go) $(DB_GEN_FILES) .swaggo docs/manifest.json coderd/rbac/object_gen.go
./scripts/apidocgen/generate.sh
yarn run --cwd=site format:write:only ../docs/api ../docs/manifest.json ../coderd/apidoc/swagger.json
pnpm run format:write:only ./docs/api ./docs/manifest.json ./coderd/apidoc/swagger.json
update-golden-files: cli/testdata/.gen-golden helm/tests/testdata/.gen-golden
update-golden-files: cli/testdata/.gen-golden helm/tests/testdata/.gen-golden scripts/ci-report/testdata/.gen-golden enterprise/cli/testdata/.gen-golden
.PHONY: update-golden-files
cli/testdata/.gen-golden: $(wildcard cli/testdata/*.golden) $(wildcard cli/*.tpl) $(GO_SRC_FILES)
go test ./cli -run=TestCommandHelp -update
cli/testdata/.gen-golden: $(wildcard cli/testdata/*.golden) $(wildcard cli/*.tpl) $(GO_SRC_FILES) $(wildcard cli/*_test.go)
go test ./cli -run="Test(CommandHelp|ServerYAML)" -update
touch "$@"
helm/tests/testdata/.gen-golden: $(wildcard helm/tests/testdata/*.golden) $(GO_SRC_FILES)
enterprise/cli/testdata/.gen-golden: $(wildcard enterprise/cli/testdata/*.golden) $(wildcard cli/*.tpl) $(GO_SRC_FILES) $(wildcard enterprise/cli/*_test.go)
go test ./enterprise/cli -run="TestEnterpriseCommandHelp" -update
touch "$@"
helm/tests/testdata/.gen-golden: $(wildcard helm/tests/testdata/*.yaml) $(wildcard helm/tests/testdata/*.golden) $(GO_SRC_FILES) $(wildcard helm/tests/*_test.go)
go test ./helm/tests -run=TestUpdateGoldenFiles -update
touch "$@"
scripts/ci-report/testdata/.gen-golden: $(wildcard scripts/ci-report/testdata/*) $(wildcard scripts/ci-report/*.go)
go test ./scripts/ci-report -run=TestOutputMatchesGoldenFile -update
touch "$@"
# 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.
@@ -585,22 +632,23 @@ site/.eslintignore site/.prettierignore: .prettierignore Makefile
echo "$${ignore}$${rule}" >> "$@"
done < "$<"
test: test-clean
gotestsum -- -v -short ./...
test:
gotestsum --format standard-quiet -- -v -short -count=1 ./...
.PHONY: test
# When updating -timeout for this test, keep in sync with
# test-go-postgres (.github/workflows/coder.yaml).
test-postgres: test-clean test-postgres-docker
# Do add coverage flags so that test caching works.
test-postgres: test-postgres-docker
# The postgres test is prone to failure, so we limit parallelism for
# more consistent execution.
DB=ci DB_FROM=$(shell go run scripts/migrate-ci/main.go) gotestsum \
--junitfile="gotests.xml" \
--jsonfile="gotests.json" \
--packages="./..." -- \
-covermode=atomic -coverprofile="gotests.coverage" -timeout=20m \
-parallel=4 \
-coverpkg=./... \
-count=1 -race -failfast
-timeout=20m \
-failfast \
-count=1
.PHONY: test-postgres
test-postgres-docker:
@@ -615,8 +663,10 @@ test-postgres-docker:
--name test-postgres-docker \
--restart no \
--detach \
postgres:13 \
gcr.io/coder-dev-1/postgres:13 \
-c shared_buffers=1GB \
-c work_mem=1GB \
-c effective_cache_size=1GB \
-c max_connections=1000 \
-c fsync=off \
-c synchronous_commit=off \
@@ -629,6 +679,14 @@ test-postgres-docker:
done
.PHONY: test-postgres-docker
# Make sure to keep this in sync with test-go-race from .github/workflows/ci.yaml.
test-race:
gotestsum --junitfile="gotests.xml" -- -race -count=1 ./...
.PHONY: test-race
# Note: we used to add this to the test target, but it's not necessary and we can
# achieve the desired result by specifying -count=1 in the go test invocation
# instead. Keeping it here for convenience.
test-clean:
go clean -testcache
.PHONY: test-clean
+1 -1
View File
@@ -84,7 +84,7 @@ coder server --postgres-url <url> --access-url <url>
> <sup>1</sup> For production deployments, set up an external PostgreSQL instance for reliability.
Use `coder --help` to get a list of flags and environment variables. Use our [quickstart guide](https://coder.com/docs/v2/latest/quickstart) for a full walkthrough.
Use `coder --help` to get a list of flags and environment variables. Use our [install guides](https://coder.com/docs/v2/latest/install) for a full walkthrough.
## Documentation
+673 -790
View File
File diff suppressed because it is too large Load Diff
+1005 -450
View File
File diff suppressed because it is too large Load Diff
+848
View File
@@ -0,0 +1,848 @@
package agentssh
import (
"bufio"
"context"
"crypto/rand"
"crypto/rsa"
"errors"
"fmt"
"io"
"net"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"github.com/gliderlabs/ssh"
"github.com/pkg/sftp"
"github.com/prometheus/client_golang/prometheus"
"github.com/spf13/afero"
"go.uber.org/atomic"
gossh "golang.org/x/crypto/ssh"
"golang.org/x/xerrors"
"cdr.dev/slog"
"github.com/coder/coder/agent/usershell"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/codersdk/agentsdk"
"github.com/coder/coder/pty"
)
const (
// MagicSessionErrorCode indicates that something went wrong with the session, rather than the
// command just returning a nonzero exit code, and is chosen as an arbitrary, high number
// unlikely to shadow other exit codes, which are typically 1, 2, 3, etc.
MagicSessionErrorCode = 229
// MagicSessionTypeEnvironmentVariable is used to track the purpose behind an SSH connection.
// This is stripped from any commands being executed, and is counted towards connection stats.
MagicSessionTypeEnvironmentVariable = "CODER_SSH_SESSION_TYPE"
// MagicSessionTypeVSCode is set in the SSH config by the VS Code extension to identify itself.
MagicSessionTypeVSCode = "vscode"
// MagicSessionTypeJetBrains is set in the SSH config by the JetBrains extension to identify itself.
MagicSessionTypeJetBrains = "jetbrains"
)
type Server struct {
mu sync.RWMutex // Protects following.
fs afero.Fs
listeners map[net.Listener]struct{}
conns map[net.Conn]struct{}
sessions map[ssh.Session]struct{}
closing chan struct{}
// Wait for goroutines to exit, waited without
// a lock on mu but protected by closing.
wg sync.WaitGroup
logger slog.Logger
srv *ssh.Server
x11SocketDir string
Env map[string]string
AgentToken func() string
Manifest *atomic.Pointer[agentsdk.Manifest]
ServiceBanner *atomic.Pointer[codersdk.ServiceBannerConfig]
connCountVSCode atomic.Int64
connCountJetBrains atomic.Int64
connCountSSHSession atomic.Int64
metrics *sshServerMetrics
}
func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prometheus.Registry, fs afero.Fs, maxTimeout time.Duration, x11SocketDir string) (*Server, error) {
// Clients' should ignore the host key when connecting.
// The agent needs to authenticate with coderd to SSH,
// so SSH authentication doesn't improve security.
randomHostKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
randomSigner, err := gossh.NewSignerFromKey(randomHostKey)
if err != nil {
return nil, err
}
if x11SocketDir == "" {
x11SocketDir = filepath.Join(os.TempDir(), ".X11-unix")
}
forwardHandler := &ssh.ForwardedTCPHandler{}
unixForwardHandler := &forwardedUnixHandler{log: logger}
metrics := newSSHServerMetrics(prometheusRegistry)
s := &Server{
listeners: make(map[net.Listener]struct{}),
fs: fs,
conns: make(map[net.Conn]struct{}),
sessions: make(map[ssh.Session]struct{}),
logger: logger,
x11SocketDir: x11SocketDir,
metrics: metrics,
}
srv := &ssh.Server{
ChannelHandlers: map[string]ssh.ChannelHandler{
"direct-tcpip": ssh.DirectTCPIPHandler,
"direct-streamlocal@openssh.com": directStreamLocalHandler,
"session": ssh.DefaultSessionHandler,
},
ConnectionFailedCallback: func(conn net.Conn, err error) {
s.logger.Warn(ctx, "ssh connection failed",
slog.F("remote_addr", conn.RemoteAddr()),
slog.F("local_addr", conn.LocalAddr()),
slog.Error(err))
metrics.failedConnectionsTotal.Add(1)
},
ConnectionCompleteCallback: func(conn *gossh.ServerConn, err error) {
s.logger.Info(ctx, "ssh connection complete",
slog.F("remote_addr", conn.RemoteAddr()),
slog.F("local_addr", conn.LocalAddr()),
slog.Error(err))
},
Handler: s.sessionHandler,
HostSigners: []ssh.Signer{randomSigner},
LocalPortForwardingCallback: func(ctx ssh.Context, destinationHost string, destinationPort uint32) bool {
// Allow local port forwarding all!
s.logger.Debug(ctx, "local port forward",
slog.F("destination_host", destinationHost),
slog.F("destination_port", destinationPort))
return true
},
PtyCallback: func(ctx ssh.Context, pty ssh.Pty) bool {
return true
},
ReversePortForwardingCallback: func(ctx ssh.Context, bindHost string, bindPort uint32) bool {
// Allow reverse port forwarding all!
s.logger.Debug(ctx, "local port forward",
slog.F("bind_host", bindHost),
slog.F("bind_port", bindPort))
return true
},
RequestHandlers: map[string]ssh.RequestHandler{
"tcpip-forward": forwardHandler.HandleSSHRequest,
"cancel-tcpip-forward": forwardHandler.HandleSSHRequest,
"streamlocal-forward@openssh.com": unixForwardHandler.HandleSSHRequest,
"cancel-streamlocal-forward@openssh.com": unixForwardHandler.HandleSSHRequest,
},
X11Callback: s.x11Callback,
ServerConfigCallback: func(ctx ssh.Context) *gossh.ServerConfig {
return &gossh.ServerConfig{
NoClientAuth: true,
}
},
SubsystemHandlers: map[string]ssh.SubsystemHandler{
"sftp": s.sessionHandler,
},
}
// The MaxTimeout functionality has been substituted with the introduction of the KeepAlive feature.
// In cases where very short timeouts are set, the SSH server will automatically switch to the connection timeout for both read and write operations.
if maxTimeout >= 3*time.Second {
srv.ClientAliveCountMax = 3
srv.ClientAliveInterval = maxTimeout / time.Duration(srv.ClientAliveCountMax)
srv.MaxTimeout = 0
} else {
srv.MaxTimeout = maxTimeout
}
s.srv = srv
return s, nil
}
type ConnStats struct {
Sessions int64
VSCode int64
JetBrains int64
}
func (s *Server) ConnStats() ConnStats {
return ConnStats{
Sessions: s.connCountSSHSession.Load(),
VSCode: s.connCountVSCode.Load(),
JetBrains: s.connCountJetBrains.Load(),
}
}
func (s *Server) sessionHandler(session ssh.Session) {
logger := s.logger.With(slog.F("remote_addr", session.RemoteAddr()), slog.F("local_addr", session.LocalAddr()))
logger.Info(session.Context(), "handling ssh session")
ctx := session.Context()
if !s.trackSession(session, true) {
// See (*Server).Close() for why we call Close instead of Exit.
_ = session.Close()
logger.Info(ctx, "unable to accept new session, server is closing")
return
}
defer s.trackSession(session, false)
extraEnv := make([]string, 0)
x11, hasX11 := session.X11()
if hasX11 {
handled := s.x11Handler(session.Context(), x11)
if !handled {
_ = session.Exit(1)
logger.Error(ctx, "x11 handler failed")
return
}
extraEnv = append(extraEnv, fmt.Sprintf("DISPLAY=:%d.0", x11.ScreenNumber))
}
switch ss := session.Subsystem(); ss {
case "":
case "sftp":
s.sftpHandler(session)
return
default:
logger.Warn(ctx, "unsupported subsystem", slog.F("subsystem", ss))
_ = session.Exit(1)
return
}
err := s.sessionStart(session, extraEnv)
var exitError *exec.ExitError
if xerrors.As(err, &exitError) {
logger.Info(ctx, "ssh session returned", slog.Error(exitError))
_ = session.Exit(exitError.ExitCode())
return
}
if err != nil {
logger.Warn(ctx, "ssh session failed", slog.Error(err))
// This exit code is designed to be unlikely to be confused for a legit exit code
// from the process.
_ = session.Exit(MagicSessionErrorCode)
return
}
logger.Info(ctx, "normal ssh session exit")
_ = session.Exit(0)
}
func (s *Server) sessionStart(session ssh.Session, extraEnv []string) (retErr error) {
ctx := session.Context()
env := append(session.Environ(), extraEnv...)
var magicType string
for index, kv := range env {
if !strings.HasPrefix(kv, MagicSessionTypeEnvironmentVariable) {
continue
}
magicType = strings.TrimPrefix(kv, MagicSessionTypeEnvironmentVariable+"=")
env = append(env[:index], env[index+1:]...)
}
switch magicType {
case MagicSessionTypeVSCode:
s.connCountVSCode.Add(1)
defer s.connCountVSCode.Add(-1)
case MagicSessionTypeJetBrains:
s.connCountJetBrains.Add(1)
defer s.connCountJetBrains.Add(-1)
case "":
s.connCountSSHSession.Add(1)
defer s.connCountSSHSession.Add(-1)
default:
s.logger.Warn(ctx, "invalid magic ssh session type specified", slog.F("type", magicType))
}
magicTypeLabel := magicTypeMetricLabel(magicType)
sshPty, windowSize, isPty := session.Pty()
cmd, err := s.CreateCommand(ctx, session.RawCommand(), env)
if err != nil {
ptyLabel := "no"
if isPty {
ptyLabel = "yes"
}
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, ptyLabel, "create_command").Add(1)
return err
}
if ssh.AgentRequested(session) {
l, err := ssh.NewAgentListener()
if err != nil {
ptyLabel := "no"
if isPty {
ptyLabel = "yes"
}
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, ptyLabel, "listener").Add(1)
return xerrors.Errorf("new agent listener: %w", err)
}
defer l.Close()
go ssh.ForwardAgentConnections(l, session)
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", "SSH_AUTH_SOCK", l.Addr().String()))
}
if isPty {
return s.startPTYSession(session, magicTypeLabel, cmd, sshPty, windowSize)
}
return s.startNonPTYSession(session, magicTypeLabel, cmd.AsExec())
}
func (s *Server) startNonPTYSession(session ssh.Session, magicTypeLabel string, cmd *exec.Cmd) error {
s.metrics.sessionsTotal.WithLabelValues(magicTypeLabel, "no").Add(1)
cmd.Stdout = session
cmd.Stderr = session.Stderr()
// This blocks forever until stdin is received if we don't
// use StdinPipe. It's unknown what causes this.
stdinPipe, err := cmd.StdinPipe()
if err != nil {
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "no", "stdin_pipe").Add(1)
return xerrors.Errorf("create stdin pipe: %w", err)
}
go func() {
_, err := io.Copy(stdinPipe, session)
if err != nil {
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "no", "stdin_io_copy").Add(1)
}
_ = stdinPipe.Close()
}()
err = cmd.Start()
if err != nil {
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "no", "start_command").Add(1)
return xerrors.Errorf("start: %w", err)
}
return cmd.Wait()
}
// ptySession is the interface to the ssh.Session that startPTYSession uses
// we use an interface here so that we can fake it in tests.
type ptySession interface {
io.ReadWriter
Context() ssh.Context
DisablePTYEmulation()
RawCommand() string
}
func (s *Server) startPTYSession(session ptySession, magicTypeLabel string, cmd *pty.Cmd, sshPty ssh.Pty, windowSize <-chan ssh.Window) (retErr error) {
s.metrics.sessionsTotal.WithLabelValues(magicTypeLabel, "yes").Add(1)
ctx := session.Context()
// Disable minimal PTY emulation set by gliderlabs/ssh (NL-to-CRNL).
// See https://github.com/coder/coder/issues/3371.
session.DisablePTYEmulation()
if isLoginShell(session.RawCommand()) {
serviceBanner := s.ServiceBanner.Load()
if serviceBanner != nil {
err := showServiceBanner(session, serviceBanner)
if err != nil {
s.logger.Error(ctx, "agent failed to show service banner", slog.Error(err))
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "yes", "service_banner").Add(1)
}
}
}
if !isQuietLogin(s.fs, session.RawCommand()) {
manifest := s.Manifest.Load()
if manifest != nil {
err := showMOTD(s.fs, session, manifest.MOTDFile)
if err != nil {
s.logger.Error(ctx, "agent failed to show MOTD", slog.Error(err))
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "yes", "motd").Add(1)
}
} else {
s.logger.Warn(ctx, "metadata lookup failed, unable to show MOTD")
}
}
cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", sshPty.Term))
// The pty package sets `SSH_TTY` on supported platforms.
ptty, process, err := pty.Start(cmd, pty.WithPTYOption(
pty.WithSSHRequest(sshPty),
pty.WithLogger(slog.Stdlib(ctx, s.logger, slog.LevelInfo)),
))
if err != nil {
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "yes", "start_command").Add(1)
return xerrors.Errorf("start command: %w", err)
}
defer func() {
closeErr := ptty.Close()
if closeErr != nil {
s.logger.Warn(ctx, "failed to close tty", slog.Error(closeErr))
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "yes", "close").Add(1)
if retErr == nil {
retErr = closeErr
}
}
}()
go func() {
for win := range windowSize {
resizeErr := ptty.Resize(uint16(win.Height), uint16(win.Width))
// If the pty is closed, then command has exited, no need to log.
if resizeErr != nil && !errors.Is(resizeErr, pty.ErrClosed) {
s.logger.Warn(ctx, "failed to resize tty", slog.Error(resizeErr))
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "yes", "resize").Add(1)
}
}
}()
go func() {
_, err := io.Copy(ptty.InputWriter(), session)
if err != nil {
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "yes", "input_io_copy").Add(1)
}
}()
// We need to wait for the command output to finish copying. It's safe to
// just do this copy on the main handler goroutine because one of two things
// will happen:
//
// 1. The command completes & closes the TTY, which then triggers an error
// after we've Read() all the buffered data from the PTY.
// 2. The client hangs up, which cancels the command's Context, and go will
// kill the command's process. This then has the same effect as (1).
n, err := io.Copy(session, ptty.OutputReader())
s.logger.Debug(ctx, "copy output done", slog.F("bytes", n), slog.Error(err))
if err != nil {
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "yes", "output_io_copy").Add(1)
return xerrors.Errorf("copy error: %w", err)
}
// We've gotten all the output, but we need to wait for the process to
// complete so that we can get the exit code. This returns
// immediately if the TTY was closed as part of the command exiting.
err = process.Wait()
var exitErr *exec.ExitError
// ExitErrors just mean the command we run returned a non-zero exit code, which is normal
// and not something to be concerned about. But, if it's something else, we should log it.
if err != nil && !xerrors.As(err, &exitErr) {
s.logger.Warn(ctx, "process wait exited with error", slog.Error(err))
s.metrics.sessionErrors.WithLabelValues(magicTypeLabel, "yes", "wait").Add(1)
}
if err != nil {
return xerrors.Errorf("process wait: %w", err)
}
return nil
}
func (s *Server) sftpHandler(session ssh.Session) {
s.metrics.sftpConnectionsTotal.Add(1)
ctx := session.Context()
// Typically sftp sessions don't request a TTY, but if they do,
// we must ensure the gliderlabs/ssh CRLF emulation is disabled.
// Otherwise sftp will be broken. This can happen if a user sets
// `RequestTTY force` in their SSH config.
session.DisablePTYEmulation()
var opts []sftp.ServerOption
// Change current working directory to the users home
// directory so that SFTP connections land there.
homedir, err := userHomeDir()
if err != nil {
s.logger.Warn(ctx, "get sftp working directory failed, unable to get home dir", slog.Error(err))
} else {
opts = append(opts, sftp.WithServerWorkingDirectory(homedir))
}
server, err := sftp.NewServer(session, opts...)
if err != nil {
s.logger.Debug(ctx, "initialize sftp server", slog.Error(err))
return
}
defer server.Close()
err = server.Serve()
if errors.Is(err, io.EOF) {
// Unless we call `session.Exit(0)` here, the client won't
// receive `exit-status` because `(*sftp.Server).Close()`
// calls `Close()` on the underlying connection (session),
// which actually calls `channel.Close()` because it isn't
// wrapped. This causes sftp clients to receive a non-zero
// exit code. Typically sftp clients don't echo this exit
// code but `scp` on macOS does (when using the default
// SFTP backend).
_ = session.Exit(0)
return
}
s.logger.Warn(ctx, "sftp server closed with error", slog.Error(err))
s.metrics.sftpServerErrors.Add(1)
_ = session.Exit(1)
}
// CreateCommand processes raw command input with OpenSSH-like behavior.
// If the script provided is empty, it will default to the users shell.
// This injects environment variables specified by the user at launch too.
func (s *Server) CreateCommand(ctx context.Context, script string, env []string) (*pty.Cmd, error) {
currentUser, err := user.Current()
if err != nil {
return nil, xerrors.Errorf("get current user: %w", err)
}
username := currentUser.Username
shell, err := usershell.Get(username)
if err != nil {
return nil, xerrors.Errorf("get user shell: %w", err)
}
manifest := s.Manifest.Load()
if manifest == nil {
return nil, xerrors.Errorf("no metadata was provided")
}
// OpenSSH executes all commands with the users current shell.
// We replicate that behavior for IDE support.
caller := "-c"
if runtime.GOOS == "windows" {
caller = "/c"
}
args := []string{caller, script}
// gliderlabs/ssh returns a command slice of zero
// when a shell is requested.
if len(script) == 0 {
args = []string{}
if runtime.GOOS != "windows" {
// On Linux and macOS, we should start a login
// shell to consume juicy environment variables!
args = append(args, "-l")
}
}
cmd := pty.CommandContext(ctx, shell, args...)
cmd.Dir = manifest.Directory
// If the metadata directory doesn't exist, we run the command
// in the users home directory.
_, err = os.Stat(cmd.Dir)
if cmd.Dir == "" || err != nil {
// Default to user home if a directory is not set.
homedir, err := userHomeDir()
if err != nil {
return nil, xerrors.Errorf("get home dir: %w", err)
}
cmd.Dir = homedir
}
cmd.Env = append(os.Environ(), env...)
executablePath, err := os.Executable()
if err != nil {
return nil, xerrors.Errorf("getting os executable: %w", err)
}
// Set environment variables reliable detection of being inside a
// Coder workspace.
cmd.Env = append(cmd.Env, "CODER=true")
cmd.Env = append(cmd.Env, fmt.Sprintf("USER=%s", username))
// Git on Windows resolves with UNIX-style paths.
// If using backslashes, it's unable to find the executable.
unixExecutablePath := strings.ReplaceAll(executablePath, "\\", "/")
cmd.Env = append(cmd.Env, fmt.Sprintf(`GIT_SSH_COMMAND=%s gitssh --`, unixExecutablePath))
// Specific Coder subcommands require the agent token exposed!
cmd.Env = append(cmd.Env, fmt.Sprintf("CODER_AGENT_TOKEN=%s", s.AgentToken()))
// Set SSH connection environment variables (these are also set by OpenSSH
// and thus expected to be present by SSH clients). Since the agent does
// networking in-memory, trying to provide accurate values here would be
// nonsensical. For now, we hard code these values so that they're present.
srcAddr, srcPort := "0.0.0.0", "0"
dstAddr, dstPort := "0.0.0.0", "0"
cmd.Env = append(cmd.Env, fmt.Sprintf("SSH_CLIENT=%s %s %s", srcAddr, srcPort, dstPort))
cmd.Env = append(cmd.Env, fmt.Sprintf("SSH_CONNECTION=%s %s %s %s", srcAddr, srcPort, dstAddr, dstPort))
// This adds the ports dialog to code-server that enables
// proxying a port dynamically.
cmd.Env = append(cmd.Env, fmt.Sprintf("VSCODE_PROXY_URI=%s", manifest.VSCodePortProxyURI))
// Hide Coder message on code-server's "Getting Started" page
cmd.Env = append(cmd.Env, "CS_DISABLE_GETTING_STARTED_OVERRIDE=true")
// Load environment variables passed via the agent.
// These should override all variables we manually specify.
for envKey, value := range manifest.EnvironmentVariables {
// Expanding environment variables allows for customization
// of the $PATH, among other variables. Customers can prepend
// or append to the $PATH, so allowing expand is required!
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", envKey, os.ExpandEnv(value)))
}
// Agent-level environment variables should take over all!
// This is used for setting agent-specific variables like "CODER_AGENT_TOKEN".
for envKey, value := range s.Env {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", envKey, value))
}
return cmd, nil
}
func (s *Server) Serve(l net.Listener) (retErr error) {
s.logger.Info(context.Background(), "started serving listener", slog.F("listen_addr", l.Addr()))
defer func() {
s.logger.Info(context.Background(), "stopped serving listener",
slog.F("listen_addr", l.Addr()), slog.Error(retErr))
}()
defer l.Close()
s.trackListener(l, true)
defer s.trackListener(l, false)
for {
conn, err := l.Accept()
if err != nil {
return err
}
go s.handleConn(l, conn)
}
}
func (s *Server) handleConn(l net.Listener, c net.Conn) {
logger := s.logger.With(
slog.F("remote_addr", c.RemoteAddr()),
slog.F("local_addr", c.LocalAddr()),
slog.F("listen_addr", l.Addr()))
defer c.Close()
if !s.trackConn(l, c, true) {
// Server is closed or we no longer want
// connections from this listener.
logger.Info(context.Background(), "received connection after server closed")
return
}
defer s.trackConn(l, c, false)
logger.Info(context.Background(), "started serving connection")
// note: srv.ConnectionCompleteCallback logs completion of the connection
s.srv.HandleConn(c)
}
// trackListener registers the listener with the server. If the server is
// closing, the function will block until the server is closed.
//
//nolint:revive
func (s *Server) trackListener(l net.Listener, add bool) {
s.mu.Lock()
defer s.mu.Unlock()
if add {
for s.closing != nil {
closing := s.closing
// Wait until close is complete before
// serving a new listener.
s.mu.Unlock()
<-closing
s.mu.Lock()
}
s.wg.Add(1)
s.listeners[l] = struct{}{}
return
}
s.wg.Done()
delete(s.listeners, l)
}
// trackConn registers the connection with the server. If the server is
// closed or the listener is closed, the connection is not registered
// and should be closed.
//
//nolint:revive
func (s *Server) trackConn(l net.Listener, c net.Conn, add bool) (ok bool) {
s.mu.Lock()
defer s.mu.Unlock()
if add {
found := false
for ll := range s.listeners {
if l == ll {
found = true
break
}
}
if s.closing != nil || !found {
// Server or listener closed.
return false
}
s.wg.Add(1)
s.conns[c] = struct{}{}
return true
}
s.wg.Done()
delete(s.conns, c)
return true
}
// trackSession registers the session with the server. If the server is
// closing, the session is not registered and should be closed.
//
//nolint:revive
func (s *Server) trackSession(ss ssh.Session, add bool) (ok bool) {
s.mu.Lock()
defer s.mu.Unlock()
if add {
if s.closing != nil {
// Server closed.
return false
}
s.wg.Add(1)
s.sessions[ss] = struct{}{}
return true
}
s.wg.Done()
delete(s.sessions, ss)
return true
}
// Close the server and all active connections. Server can be re-used
// after Close is done.
func (s *Server) Close() error {
s.mu.Lock()
// Guard against multiple calls to Close and
// accepting new connections during close.
if s.closing != nil {
s.mu.Unlock()
return xerrors.New("server is closing")
}
s.closing = make(chan struct{})
// Close all active sessions to gracefully
// terminate client connections.
for ss := range s.sessions {
// We call Close on the underlying channel here because we don't
// want to send an exit status to the client (via Exit()).
// Typically OpenSSH clients will return 255 as the exit status.
_ = ss.Close()
}
// Close all active listeners and connections.
for l := range s.listeners {
_ = l.Close()
}
for c := range s.conns {
_ = c.Close()
}
// Close the underlying SSH server.
err := s.srv.Close()
s.mu.Unlock()
s.wg.Wait() // Wait for all goroutines to exit.
s.mu.Lock()
close(s.closing)
s.closing = nil
s.mu.Unlock()
return err
}
// Shutdown gracefully closes all active SSH connections and stops
// accepting new connections.
//
// Shutdown is not implemented.
func (*Server) Shutdown(_ context.Context) error {
// TODO(mafredri): Implement shutdown, SIGHUP running commands, etc.
return nil
}
func isLoginShell(rawCommand string) bool {
return len(rawCommand) == 0
}
// isQuietLogin checks if the SSH server should perform a quiet login or not.
//
// https://github.com/openssh/openssh-portable/blob/25bd659cc72268f2858c5415740c442ee950049f/session.c#L816
func isQuietLogin(fs afero.Fs, rawCommand string) bool {
// We are always quiet unless this is a login shell.
if !isLoginShell(rawCommand) {
return true
}
// Best effort, if we can't get the home directory,
// we can't lookup .hushlogin.
homedir, err := userHomeDir()
if err != nil {
return false
}
_, err = fs.Stat(filepath.Join(homedir, ".hushlogin"))
return err == nil
}
// showServiceBanner will write the service banner if enabled and not blank
// along with a blank line for spacing.
func showServiceBanner(session io.Writer, banner *codersdk.ServiceBannerConfig) error {
if banner.Enabled && banner.Message != "" {
// The banner supports Markdown so we might want to parse it but Markdown is
// still fairly readable in its raw form.
message := strings.TrimSpace(banner.Message) + "\n\n"
return writeWithCarriageReturn(strings.NewReader(message), session)
}
return nil
}
// showMOTD will output the message of the day from
// the given filename to dest, if the file exists.
//
// https://github.com/openssh/openssh-portable/blob/25bd659cc72268f2858c5415740c442ee950049f/session.c#L784
func showMOTD(fs afero.Fs, dest io.Writer, filename string) error {
if filename == "" {
return nil
}
f, err := fs.Open(filename)
if err != nil {
if xerrors.Is(err, os.ErrNotExist) {
// This is not an error, there simply isn't a MOTD to show.
return nil
}
return xerrors.Errorf("open MOTD: %w", err)
}
defer f.Close()
return writeWithCarriageReturn(f, dest)
}
// writeWithCarriageReturn writes each line with a carriage return to ensure
// that each line starts at the beginning of the terminal.
func writeWithCarriageReturn(src io.Reader, dest io.Writer) error {
s := bufio.NewScanner(src)
for s.Scan() {
_, err := fmt.Fprint(dest, s.Text()+"\r\n")
if err != nil {
return xerrors.Errorf("write line: %w", err)
}
}
if err := s.Err(); err != nil {
return xerrors.Errorf("read line: %w", err)
}
return nil
}
// userHomeDir returns the home directory of the current user, giving
// priority to the $HOME environment variable.
func userHomeDir() (string, error) {
// First we check the environment.
homedir, err := os.UserHomeDir()
if err == nil {
return homedir, nil
}
// As a fallback, we try the user information.
u, err := user.Current()
if err != nil {
return "", xerrors.Errorf("current user: %w", err)
}
return u.HomeDir, nil
}
+197
View File
@@ -0,0 +1,197 @@
//go:build !windows
package agentssh
import (
"bufio"
"context"
"io"
"net"
"testing"
gliderssh "github.com/gliderlabs/ssh"
"github.com/prometheus/client_golang/prometheus"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/pty"
"github.com/coder/coder/testutil"
"cdr.dev/slog/sloggers/slogtest"
)
const longScript = `
echo "started"
sleep 30
echo "done"
`
// Test_sessionStart_orphan tests running a command that takes a long time to
// exit normally, and terminate the SSH session context early to verify that we
// return quickly and don't leave the command running as an "orphan" with no
// active SSH session.
func Test_sessionStart_orphan(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
defer cancel()
logger := slogtest.Make(t, nil)
s, err := NewServer(ctx, logger, prometheus.NewRegistry(), afero.NewMemMapFs(), 0, "")
require.NoError(t, err)
defer s.Close()
// Here we're going to call the handler directly with a faked SSH session
// that just uses io.Pipes instead of a network socket. There is a large
// variation in the time between closing the socket from the client side and
// the SSH server canceling the session Context, which would lead to a flaky
// test if we did it that way. So instead, we directly cancel the context
// in this test.
sessionCtx, sessionCancel := context.WithCancel(ctx)
toClient, fromClient, sess := newTestSession(sessionCtx)
ptyInfo := gliderssh.Pty{}
windowSize := make(chan gliderssh.Window)
close(windowSize)
// the command gets the session context so that Go will terminate it when
// the session expires.
cmd := pty.CommandContext(sessionCtx, "sh", "-c", longScript)
done := make(chan struct{})
go func() {
defer close(done)
// we don't really care what the error is here. In the larger scenario,
// the client has disconnected, so we can't return any error information
// to them.
_ = s.startPTYSession(sess, "ssh", cmd, ptyInfo, windowSize)
}()
readDone := make(chan struct{})
go func() {
defer close(readDone)
s := bufio.NewScanner(toClient)
assert.True(t, s.Scan())
txt := s.Text()
assert.Equal(t, "started", txt, "output corrupted")
}()
waitForChan(ctx, t, readDone, "read timeout")
// process is started, and should be sleeping for ~30 seconds
sessionCancel()
// now, we wait for the handler to complete. If it does so before the
// main test timeout, we consider this a pass. If not, it indicates
// that the server isn't properly shutting down sessions when they are
// disconnected client side, which could lead to processes hanging around
// indefinitely.
waitForChan(ctx, t, done, "handler timeout")
err = fromClient.Close()
require.NoError(t, err)
}
func waitForChan(ctx context.Context, t *testing.T, c <-chan struct{}, msg string) {
t.Helper()
select {
case <-c:
// OK!
case <-ctx.Done():
t.Fatal(msg)
}
}
type testSession struct {
ctx testSSHContext
// c2p is the client -> pty buffer
toPty *io.PipeReader
// p2c is the pty -> client buffer
fromPty *io.PipeWriter
}
type testSSHContext struct {
context.Context
}
func newTestSession(ctx context.Context) (toClient *io.PipeReader, fromClient *io.PipeWriter, s ptySession) {
toClient, fromPty := io.Pipe()
toPty, fromClient := io.Pipe()
return toClient, fromClient, &testSession{
ctx: testSSHContext{ctx},
toPty: toPty,
fromPty: fromPty,
}
}
func (s *testSession) Context() gliderssh.Context {
return s.ctx
}
func (*testSession) DisablePTYEmulation() {}
// RawCommand returns "quiet logon" so that the PTY handler doesn't attempt to
// write the message of the day, which will interfere with our tests. It writes
// the message of the day if it's a shell login (zero length RawCommand()).
func (*testSession) RawCommand() string { return "quiet logon" }
func (s *testSession) Read(p []byte) (n int, err error) {
return s.toPty.Read(p)
}
func (s *testSession) Write(p []byte) (n int, err error) {
return s.fromPty.Write(p)
}
func (testSSHContext) Lock() {
panic("not implemented")
}
func (testSSHContext) Unlock() {
panic("not implemented")
}
// User returns the username used when establishing the SSH connection.
func (testSSHContext) User() string {
panic("not implemented")
}
// SessionID returns the session hash.
func (testSSHContext) SessionID() string {
panic("not implemented")
}
// ClientVersion returns the version reported by the client.
func (testSSHContext) ClientVersion() string {
panic("not implemented")
}
// ServerVersion returns the version reported by the server.
func (testSSHContext) ServerVersion() string {
panic("not implemented")
}
// RemoteAddr returns the remote address for this connection.
func (testSSHContext) RemoteAddr() net.Addr {
panic("not implemented")
}
// LocalAddr returns the local address for this connection.
func (testSSHContext) LocalAddr() net.Addr {
panic("not implemented")
}
// Permissions returns the Permissions object used for this connection.
func (testSSHContext) Permissions() *gliderssh.Permissions {
panic("not implemented")
}
// SetValue allows you to easily write new values into the underlying context.
func (testSSHContext) SetValue(_, _ interface{}) {
panic("not implemented")
}
func (testSSHContext) KeepAlive() *gliderssh.SessionKeepAlive {
panic("not implemented")
}
+144
View File
@@ -0,0 +1,144 @@
// Package agentssh_test provides tests for basic functinoality of the agentssh
// package, more test coverage can be found in the `agent` and `cli` package(s).
package agentssh_test
import (
"bytes"
"context"
"net"
"strings"
"sync"
"testing"
"github.com/prometheus/client_golang/prometheus"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/atomic"
"go.uber.org/goleak"
"golang.org/x/crypto/ssh"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/agent/agentssh"
"github.com/coder/coder/codersdk/agentsdk"
"github.com/coder/coder/pty/ptytest"
)
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
func TestNewServer_ServeClient(t *testing.T) {
t.Parallel()
ctx := context.Background()
logger := slogtest.Make(t, nil)
s, err := agentssh.NewServer(ctx, logger, prometheus.NewRegistry(), afero.NewMemMapFs(), 0, "")
require.NoError(t, err)
defer s.Close()
// The assumption is that these are set before serving SSH connections.
s.AgentToken = func() string { return "" }
s.Manifest = atomic.NewPointer(&agentsdk.Manifest{})
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
done := make(chan struct{})
go func() {
defer close(done)
err := s.Serve(ln)
assert.Error(t, err) // Server is closed.
}()
c := sshClient(t, ln.Addr().String())
var b bytes.Buffer
sess, err := c.NewSession()
sess.Stdout = &b
require.NoError(t, err)
err = sess.Start("echo hello")
require.NoError(t, err)
err = sess.Wait()
require.NoError(t, err)
require.Equal(t, "hello", strings.TrimSpace(b.String()))
err = s.Close()
require.NoError(t, err)
<-done
}
func TestNewServer_CloseActiveConnections(t *testing.T) {
t.Parallel()
ctx := context.Background()
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})
s, err := agentssh.NewServer(ctx, logger, prometheus.NewRegistry(), afero.NewMemMapFs(), 0, "")
require.NoError(t, err)
defer s.Close()
// The assumption is that these are set before serving SSH connections.
s.AgentToken = func() string { return "" }
s.Manifest = atomic.NewPointer(&agentsdk.Manifest{})
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
err := s.Serve(ln)
assert.Error(t, err) // Server is closed.
}()
pty := ptytest.New(t)
doClose := make(chan struct{})
go func() {
defer wg.Done()
c := sshClient(t, ln.Addr().String())
sess, err := c.NewSession()
sess.Stdin = pty.Input()
sess.Stdout = pty.Output()
sess.Stderr = pty.Output()
assert.NoError(t, err)
err = sess.Start("")
assert.NoError(t, err)
close(doClose)
err = sess.Wait()
assert.Error(t, err)
}()
<-doClose
err = s.Close()
require.NoError(t, err)
wg.Wait()
}
func sshClient(t *testing.T, addr string) *ssh.Client {
conn, err := net.Dial("tcp", addr)
require.NoError(t, err)
t.Cleanup(func() {
_ = conn.Close()
})
sshConn, channels, requests, err := ssh.NewClientConn(conn, "localhost:22", &ssh.ClientConfig{
HostKeyCallback: ssh.InsecureIgnoreHostKey(), //nolint:gosec // This is a test.
})
require.NoError(t, err)
t.Cleanup(func() {
_ = sshConn.Close()
})
c := ssh.NewClient(sshConn, channels, requests)
t.Cleanup(func() {
_ = c.Close()
})
return c
}
+47
View File
@@ -0,0 +1,47 @@
package agentssh
import (
"context"
"io"
"sync"
)
// Bicopy copies all of the data between the two connections and will close them
// after one or both of them are done writing. If the context is canceled, both
// of the connections will be closed.
func Bicopy(ctx context.Context, c1, c2 io.ReadWriteCloser) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
defer func() {
_ = c1.Close()
_ = c2.Close()
}()
var wg sync.WaitGroup
copyFunc := func(dst io.WriteCloser, src io.Reader) {
defer func() {
wg.Done()
// If one side of the copy fails, ensure the other one exits as
// well.
cancel()
}()
_, _ = io.Copy(dst, src)
}
wg.Add(2)
go copyFunc(c1, c2)
go copyFunc(c2, c1)
// Convert waitgroup to a channel so we can also wait on the context.
done := make(chan struct{})
go func() {
defer close(done)
wg.Wait()
}()
select {
case <-ctx.Done():
case <-done:
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
package agent
package agentssh
import (
"context"
+82
View File
@@ -0,0 +1,82 @@
package agentssh
import (
"github.com/prometheus/client_golang/prometheus"
)
type sshServerMetrics struct {
failedConnectionsTotal prometheus.Counter
sftpConnectionsTotal prometheus.Counter
sftpServerErrors prometheus.Counter
x11HandlerErrors *prometheus.CounterVec
sessionsTotal *prometheus.CounterVec
sessionErrors *prometheus.CounterVec
}
func newSSHServerMetrics(registerer prometheus.Registerer) *sshServerMetrics {
failedConnectionsTotal := prometheus.NewCounter(prometheus.CounterOpts{
Namespace: "agent", Subsystem: "ssh_server", Name: "failed_connections_total",
})
registerer.MustRegister(failedConnectionsTotal)
sftpConnectionsTotal := prometheus.NewCounter(prometheus.CounterOpts{
Namespace: "agent", Subsystem: "ssh_server", Name: "sftp_connections_total",
})
registerer.MustRegister(sftpConnectionsTotal)
sftpServerErrors := prometheus.NewCounter(prometheus.CounterOpts{
Namespace: "agent", Subsystem: "ssh_server", Name: "sftp_server_errors_total",
})
registerer.MustRegister(sftpServerErrors)
x11HandlerErrors := prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: "agent",
Subsystem: "x11_handler",
Name: "errors_total",
},
[]string{"error_type"},
)
registerer.MustRegister(x11HandlerErrors)
sessionsTotal := prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: "agent",
Subsystem: "sessions",
Name: "total",
},
[]string{"magic_type", "pty"},
)
registerer.MustRegister(sessionsTotal)
sessionErrors := prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: "agent",
Subsystem: "sessions",
Name: "errors_total",
},
[]string{"magic_type", "pty", "error_type"},
)
registerer.MustRegister(sessionErrors)
return &sshServerMetrics{
failedConnectionsTotal: failedConnectionsTotal,
sftpConnectionsTotal: sftpConnectionsTotal,
sftpServerErrors: sftpServerErrors,
x11HandlerErrors: x11HandlerErrors,
sessionsTotal: sessionsTotal,
sessionErrors: sessionErrors,
}
}
func magicTypeMetricLabel(magicType string) string {
switch magicType {
case MagicSessionTypeVSCode:
case MagicSessionTypeJetBrains:
case "":
magicType = "ssh"
default:
magicType = "unknown"
}
return magicType
}
+200
View File
@@ -0,0 +1,200 @@
package agentssh
import (
"context"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"net"
"os"
"path/filepath"
"strconv"
"time"
"github.com/gliderlabs/ssh"
"github.com/gofrs/flock"
"github.com/spf13/afero"
gossh "golang.org/x/crypto/ssh"
"golang.org/x/xerrors"
"cdr.dev/slog"
)
// x11Callback is called when the client requests X11 forwarding.
// It adds an Xauthority entry to the Xauthority file.
func (s *Server) x11Callback(ctx ssh.Context, x11 ssh.X11) bool {
hostname, err := os.Hostname()
if err != nil {
s.logger.Warn(ctx, "failed to get hostname", slog.Error(err))
s.metrics.x11HandlerErrors.WithLabelValues("hostname").Add(1)
return false
}
err = s.fs.MkdirAll(s.x11SocketDir, 0o700)
if err != nil {
s.logger.Warn(ctx, "failed to make the x11 socket dir", slog.F("dir", s.x11SocketDir), slog.Error(err))
s.metrics.x11HandlerErrors.WithLabelValues("socker_dir").Add(1)
return false
}
err = addXauthEntry(ctx, s.fs, hostname, strconv.Itoa(int(x11.ScreenNumber)), x11.AuthProtocol, x11.AuthCookie)
if err != nil {
s.logger.Warn(ctx, "failed to add Xauthority entry", slog.Error(err))
s.metrics.x11HandlerErrors.WithLabelValues("xauthority").Add(1)
return false
}
return true
}
// x11Handler is called when a session has requested X11 forwarding.
// It listens for X11 connections and forwards them to the client.
func (s *Server) x11Handler(ctx ssh.Context, x11 ssh.X11) bool {
serverConn, valid := ctx.Value(ssh.ContextKeyConn).(*gossh.ServerConn)
if !valid {
s.logger.Warn(ctx, "failed to get server connection")
return false
}
// We want to overwrite the socket so that subsequent connections will succeed.
socketPath := filepath.Join(s.x11SocketDir, fmt.Sprintf("X%d", x11.ScreenNumber))
err := os.Remove(socketPath)
if err != nil && !errors.Is(err, os.ErrNotExist) {
s.logger.Warn(ctx, "failed to remove existing X11 socket", slog.Error(err))
return false
}
listener, err := net.Listen("unix", socketPath)
if err != nil {
s.logger.Warn(ctx, "failed to listen for X11", slog.Error(err))
return false
}
s.trackListener(listener, true)
go func() {
defer listener.Close()
defer s.trackListener(listener, false)
handledFirstConnection := false
for {
conn, err := listener.Accept()
if err != nil {
if errors.Is(err, net.ErrClosed) {
return
}
s.logger.Warn(ctx, "failed to accept X11 connection", slog.Error(err))
return
}
if x11.SingleConnection && handledFirstConnection {
s.logger.Warn(ctx, "X11 connection rejected because single connection is enabled")
_ = conn.Close()
continue
}
handledFirstConnection = true
unixConn, ok := conn.(*net.UnixConn)
if !ok {
s.logger.Warn(ctx, fmt.Sprintf("failed to cast connection to UnixConn. got: %T", conn))
return
}
unixAddr, ok := unixConn.LocalAddr().(*net.UnixAddr)
if !ok {
s.logger.Warn(ctx, fmt.Sprintf("failed to cast local address to UnixAddr. got: %T", unixConn.LocalAddr()))
return
}
channel, reqs, err := serverConn.OpenChannel("x11", gossh.Marshal(struct {
OriginatorAddress string
OriginatorPort uint32
}{
OriginatorAddress: unixAddr.Name,
OriginatorPort: 0,
}))
if err != nil {
s.logger.Warn(ctx, "failed to open X11 channel", slog.Error(err))
return
}
go gossh.DiscardRequests(reqs)
go Bicopy(ctx, conn, channel)
}
}()
return true
}
// addXauthEntry adds an Xauthority entry to the Xauthority file.
// The Xauthority file is located at ~/.Xauthority.
func addXauthEntry(ctx context.Context, fs afero.Fs, host string, display string, authProtocol string, authCookie string) error {
// Get the Xauthority file path
homeDir, err := os.UserHomeDir()
if err != nil {
return xerrors.Errorf("failed to get user home directory: %w", err)
}
xauthPath := filepath.Join(homeDir, ".Xauthority")
lock := flock.New(xauthPath)
defer lock.Close()
ok, err := lock.TryLockContext(ctx, 100*time.Millisecond)
if !ok {
return xerrors.Errorf("failed to lock Xauthority file: %w", err)
}
if err != nil {
return xerrors.Errorf("failed to lock Xauthority file: %w", err)
}
// Open or create the Xauthority file
file, err := fs.OpenFile(xauthPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o600)
if err != nil {
return xerrors.Errorf("failed to open Xauthority file: %w", err)
}
defer file.Close()
// Convert the authCookie from hex string to byte slice
authCookieBytes, err := hex.DecodeString(authCookie)
if err != nil {
return xerrors.Errorf("failed to decode auth cookie: %w", err)
}
// Write Xauthority entry
family := uint16(0x0100) // FamilyLocal
err = binary.Write(file, binary.BigEndian, family)
if err != nil {
return xerrors.Errorf("failed to write family: %w", err)
}
err = binary.Write(file, binary.BigEndian, uint16(len(host)))
if err != nil {
return xerrors.Errorf("failed to write host length: %w", err)
}
_, err = file.WriteString(host)
if err != nil {
return xerrors.Errorf("failed to write host: %w", err)
}
err = binary.Write(file, binary.BigEndian, uint16(len(display)))
if err != nil {
return xerrors.Errorf("failed to write display length: %w", err)
}
_, err = file.WriteString(display)
if err != nil {
return xerrors.Errorf("failed to write display: %w", err)
}
err = binary.Write(file, binary.BigEndian, uint16(len(authProtocol)))
if err != nil {
return xerrors.Errorf("failed to write auth protocol length: %w", err)
}
_, err = file.WriteString(authProtocol)
if err != nil {
return xerrors.Errorf("failed to write auth protocol: %w", err)
}
err = binary.Write(file, binary.BigEndian, uint16(len(authCookieBytes)))
if err != nil {
return xerrors.Errorf("failed to write auth cookie length: %w", err)
}
_, err = file.Write(authCookieBytes)
if err != nil {
return xerrors.Errorf("failed to write auth cookie: %w", err)
}
return nil
}
+100
View File
@@ -0,0 +1,100 @@
package agentssh_test
import (
"context"
"encoding/hex"
"net"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/gliderlabs/ssh"
"github.com/prometheus/client_golang/prometheus"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/atomic"
gossh "golang.org/x/crypto/ssh"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/agent/agentssh"
"github.com/coder/coder/codersdk/agentsdk"
"github.com/coder/coder/testutil"
)
func TestServer_X11(t *testing.T) {
t.Parallel()
if runtime.GOOS != "linux" {
t.Skip("X11 forwarding is only supported on Linux")
}
ctx := context.Background()
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
fs := afero.NewOsFs()
dir := t.TempDir()
s, err := agentssh.NewServer(ctx, logger, prometheus.NewRegistry(), fs, 0, dir)
require.NoError(t, err)
defer s.Close()
// The assumption is that these are set before serving SSH connections.
s.AgentToken = func() string { return "" }
s.Manifest = atomic.NewPointer(&agentsdk.Manifest{})
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
done := make(chan struct{})
go func() {
defer close(done)
err := s.Serve(ln)
assert.Error(t, err) // Server is closed.
}()
c := sshClient(t, ln.Addr().String())
sess, err := c.NewSession()
require.NoError(t, err)
reply, err := sess.SendRequest("x11-req", true, gossh.Marshal(ssh.X11{
AuthProtocol: "MIT-MAGIC-COOKIE-1",
AuthCookie: hex.EncodeToString([]byte("cookie")),
ScreenNumber: 0,
}))
require.NoError(t, err)
assert.True(t, reply)
err = sess.Shell()
require.NoError(t, err)
x11Chans := c.HandleChannelOpen("x11")
payload := "hello world"
require.Eventually(t, func() bool {
conn, err := net.Dial("unix", filepath.Join(dir, "X0"))
if err == nil {
_, err = conn.Write([]byte(payload))
assert.NoError(t, err)
_ = conn.Close()
}
return err == nil
}, testutil.WaitShort, testutil.IntervalFast)
x11 := <-x11Chans
ch, reqs, err := x11.Accept()
require.NoError(t, err)
go gossh.DiscardRequests(reqs)
got := make([]byte, len(payload))
_, err = ch.Read(got)
require.NoError(t, err)
assert.Equal(t, payload, string(got))
_ = ch.Close()
_ = s.Close()
<-done
// Ensure the Xauthority file was written!
home, err := os.UserHomeDir()
require.NoError(t, err)
_, err = fs.Stat(filepath.Join(home, ".Xauthority"))
require.NoError(t, err)
}
+222
View File
@@ -0,0 +1,222 @@
package agenttest
import (
"context"
"io"
"net"
"sync"
"testing"
"time"
"github.com/google/uuid"
"golang.org/x/exp/maps"
"golang.org/x/xerrors"
"cdr.dev/slog"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/codersdk/agentsdk"
"github.com/coder/coder/tailnet"
"github.com/coder/coder/testutil"
)
func NewClient(t testing.TB,
logger slog.Logger,
agentID uuid.UUID,
manifest agentsdk.Manifest,
statsChan chan *agentsdk.Stats,
coordinator tailnet.Coordinator,
) *Client {
if manifest.AgentID == uuid.Nil {
manifest.AgentID = agentID
}
return &Client{
t: t,
logger: logger.Named("client"),
agentID: agentID,
manifest: manifest,
statsChan: statsChan,
coordinator: coordinator,
derpMapUpdates: make(chan agentsdk.DERPMapUpdate),
}
}
type Client struct {
t testing.TB
logger slog.Logger
agentID uuid.UUID
manifest agentsdk.Manifest
metadata map[string]agentsdk.PostMetadataRequest
statsChan chan *agentsdk.Stats
coordinator tailnet.Coordinator
LastWorkspaceAgent func()
PatchWorkspaceLogs func() error
GetServiceBannerFunc func() (codersdk.ServiceBannerConfig, error)
mu sync.Mutex // Protects following.
lifecycleStates []codersdk.WorkspaceAgentLifecycle
startup agentsdk.PostStartupRequest
logs []agentsdk.Log
derpMapUpdates chan agentsdk.DERPMapUpdate
}
func (c *Client) Manifest(_ context.Context) (agentsdk.Manifest, error) {
return c.manifest, nil
}
func (c *Client) Listen(_ context.Context) (net.Conn, error) {
clientConn, serverConn := net.Pipe()
closed := make(chan struct{})
c.LastWorkspaceAgent = func() {
_ = serverConn.Close()
_ = clientConn.Close()
<-closed
}
c.t.Cleanup(c.LastWorkspaceAgent)
go func() {
_ = c.coordinator.ServeAgent(serverConn, c.agentID, "")
close(closed)
}()
return clientConn, nil
}
func (c *Client) ReportStats(ctx context.Context, _ slog.Logger, statsChan <-chan *agentsdk.Stats, setInterval func(time.Duration)) (io.Closer, error) {
doneCh := make(chan struct{})
ctx, cancel := context.WithCancel(ctx)
go func() {
defer close(doneCh)
setInterval(500 * time.Millisecond)
for {
select {
case <-ctx.Done():
return
case stat := <-statsChan:
select {
case c.statsChan <- stat:
case <-ctx.Done():
return
default:
// We don't want to send old stats.
continue
}
}
}
}()
return closeFunc(func() error {
cancel()
<-doneCh
close(c.statsChan)
return nil
}), nil
}
func (c *Client) GetLifecycleStates() []codersdk.WorkspaceAgentLifecycle {
c.mu.Lock()
defer c.mu.Unlock()
return c.lifecycleStates
}
func (c *Client) PostLifecycle(ctx context.Context, req agentsdk.PostLifecycleRequest) error {
c.mu.Lock()
defer c.mu.Unlock()
c.lifecycleStates = append(c.lifecycleStates, req.State)
c.logger.Debug(ctx, "post lifecycle", slog.F("req", req))
return nil
}
func (c *Client) PostAppHealth(ctx context.Context, req agentsdk.PostAppHealthsRequest) error {
c.logger.Debug(ctx, "post app health", slog.F("req", req))
return nil
}
func (c *Client) GetStartup() agentsdk.PostStartupRequest {
c.mu.Lock()
defer c.mu.Unlock()
return c.startup
}
func (c *Client) GetMetadata() map[string]agentsdk.PostMetadataRequest {
c.mu.Lock()
defer c.mu.Unlock()
return maps.Clone(c.metadata)
}
func (c *Client) PostMetadata(ctx context.Context, key string, req agentsdk.PostMetadataRequest) error {
c.mu.Lock()
defer c.mu.Unlock()
if c.metadata == nil {
c.metadata = make(map[string]agentsdk.PostMetadataRequest)
}
c.metadata[key] = req
c.logger.Debug(ctx, "post metadata", slog.F("key", key), slog.F("req", req))
return nil
}
func (c *Client) PostStartup(ctx context.Context, startup agentsdk.PostStartupRequest) error {
c.mu.Lock()
defer c.mu.Unlock()
c.startup = startup
c.logger.Debug(ctx, "post startup", slog.F("req", startup))
return nil
}
func (c *Client) GetStartupLogs() []agentsdk.Log {
c.mu.Lock()
defer c.mu.Unlock()
return c.logs
}
func (c *Client) PatchLogs(ctx context.Context, logs agentsdk.PatchLogs) error {
c.mu.Lock()
defer c.mu.Unlock()
if c.PatchWorkspaceLogs != nil {
return c.PatchWorkspaceLogs()
}
c.logs = append(c.logs, logs.Logs...)
c.logger.Debug(ctx, "patch startup logs", slog.F("req", logs))
return nil
}
func (c *Client) SetServiceBannerFunc(f func() (codersdk.ServiceBannerConfig, error)) {
c.mu.Lock()
defer c.mu.Unlock()
c.GetServiceBannerFunc = f
}
func (c *Client) GetServiceBanner(ctx context.Context) (codersdk.ServiceBannerConfig, error) {
c.mu.Lock()
defer c.mu.Unlock()
c.logger.Debug(ctx, "get service banner")
if c.GetServiceBannerFunc != nil {
return c.GetServiceBannerFunc()
}
return codersdk.ServiceBannerConfig{}, nil
}
func (c *Client) PushDERPMapUpdate(update agentsdk.DERPMapUpdate) error {
timer := time.NewTimer(testutil.WaitShort)
defer timer.Stop()
select {
case c.derpMapUpdates <- update:
case <-timer.C:
return xerrors.New("timeout waiting to push derp map update")
}
return nil
}
func (c *Client) DERPMapUpdates(_ context.Context) (<-chan agentsdk.DERPMapUpdate, io.Closer, error) {
closed := make(chan struct{})
return c.derpMapUpdates, closeFunc(func() error {
close(closed)
return nil
}), nil
}
type closeFunc func() error
func (c closeFunc) Close() error {
return c()
}
+130
View File
@@ -0,0 +1,130 @@
package agent
import (
"context"
"fmt"
"strings"
"github.com/prometheus/client_golang/prometheus"
prompb "github.com/prometheus/client_model/go"
"tailscale.com/util/clientmetric"
"cdr.dev/slog"
"github.com/coder/coder/codersdk/agentsdk"
)
type agentMetrics struct {
connectionsTotal prometheus.Counter
reconnectingPTYErrors *prometheus.CounterVec
}
func newAgentMetrics(registerer prometheus.Registerer) *agentMetrics {
connectionsTotal := prometheus.NewCounter(prometheus.CounterOpts{
Namespace: "agent", Subsystem: "reconnecting_pty", Name: "connections_total",
})
registerer.MustRegister(connectionsTotal)
reconnectingPTYErrors := prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: "agent",
Subsystem: "reconnecting_pty",
Name: "errors_total",
},
[]string{"error_type"},
)
registerer.MustRegister(reconnectingPTYErrors)
return &agentMetrics{
connectionsTotal: connectionsTotal,
reconnectingPTYErrors: reconnectingPTYErrors,
}
}
func (a *agent) collectMetrics(ctx context.Context) []agentsdk.AgentMetric {
var collected []agentsdk.AgentMetric
// Tailscale internal metrics
metrics := clientmetric.Metrics()
for _, m := range metrics {
if isIgnoredMetric(m.Name()) {
continue
}
collected = append(collected, agentsdk.AgentMetric{
Name: m.Name(),
Type: asMetricType(m.Type()),
Value: float64(m.Value()),
})
}
metricFamilies, err := a.prometheusRegistry.Gather()
if err != nil {
a.logger.Error(ctx, "can't gather agent metrics", slog.Error(err))
return collected
}
for _, metricFamily := range metricFamilies {
for _, metric := range metricFamily.GetMetric() {
labels := toAgentMetricLabels(metric.Label)
if metric.Counter != nil {
collected = append(collected, agentsdk.AgentMetric{
Name: metricFamily.GetName(),
Type: agentsdk.AgentMetricTypeCounter,
Value: metric.Counter.GetValue(),
Labels: labels,
})
} else if metric.Gauge != nil {
collected = append(collected, agentsdk.AgentMetric{
Name: metricFamily.GetName(),
Type: agentsdk.AgentMetricTypeGauge,
Value: metric.Gauge.GetValue(),
Labels: labels,
})
} else {
a.logger.Error(ctx, "unsupported metric type", slog.F("type", metricFamily.Type.String()))
}
}
}
return collected
}
func toAgentMetricLabels(metricLabels []*prompb.LabelPair) []agentsdk.AgentMetricLabel {
if len(metricLabels) == 0 {
return nil
}
labels := make([]agentsdk.AgentMetricLabel, 0, len(metricLabels))
for _, metricLabel := range metricLabels {
labels = append(labels, agentsdk.AgentMetricLabel{
Name: metricLabel.GetName(),
Value: metricLabel.GetValue(),
})
}
return labels
}
// isIgnoredMetric checks if the metric should be ignored, as Coder agent doesn't use related features.
// Expected metric families: magicsock_*, derp_*, tstun_*, netcheck_*, portmap_*, etc.
func isIgnoredMetric(metricName string) bool {
if strings.HasPrefix(metricName, "dns_") ||
strings.HasPrefix(metricName, "controlclient_") ||
strings.HasPrefix(metricName, "peerapi_") ||
strings.HasPrefix(metricName, "profiles_") ||
strings.HasPrefix(metricName, "tstun_") {
return true
}
return false
}
func asMetricType(typ clientmetric.Type) agentsdk.AgentMetricType {
switch typ {
case clientmetric.TypeGauge:
return agentsdk.AgentMetricTypeGauge
case clientmetric.TypeCounter:
return agentsdk.AgentMetricTypeCounter
default:
panic(fmt.Sprintf("unknown metric type: %d", typ))
}
}
+32 -35
View File
@@ -18,54 +18,51 @@ import (
"github.com/coder/coder/testutil"
)
//nolint:paralleltest // Non-parallel subtest.
// TestReap checks that's the reaper is successfully reaping
// exited processes and passing the PIDs through the shared
// channel.
//
//nolint:paralleltest
func TestReap(t *testing.T) {
// Don't run the reaper test in CI. It does weird
// things like forkexecing which may have unintended
// consequences in CI.
if _, ok := os.LookupEnv("CI"); ok {
if testutil.InCI() {
t.Skip("Detected CI, skipping reaper tests")
}
// OK checks that's the reaper is successfully reaping
// exited processes and passing the PIDs through the shared
// channel.
pids := make(reap.PidCh, 1)
err := reaper.ForkReap(
reaper.WithPIDCallback(pids),
// Provide some argument that immediately exits.
reaper.WithExecArgs("/bin/sh", "-c", "exit 0"),
)
require.NoError(t, err)
//nolint:paralleltest // Signal handling.
t.Run("OK", func(t *testing.T) {
pids := make(reap.PidCh, 1)
err := reaper.ForkReap(
reaper.WithPIDCallback(pids),
// Provide some argument that immediately exits.
reaper.WithExecArgs("/bin/sh", "-c", "exit 0"),
)
require.NoError(t, err)
cmd := exec.Command("tail", "-f", "/dev/null")
err = cmd.Start()
require.NoError(t, err)
cmd := exec.Command("tail", "-f", "/dev/null")
err = cmd.Start()
require.NoError(t, err)
cmd2 := exec.Command("tail", "-f", "/dev/null")
err = cmd2.Start()
require.NoError(t, err)
cmd2 := exec.Command("tail", "-f", "/dev/null")
err = cmd2.Start()
require.NoError(t, err)
err = cmd.Process.Kill()
require.NoError(t, err)
err = cmd.Process.Kill()
require.NoError(t, err)
err = cmd2.Process.Kill()
require.NoError(t, err)
err = cmd2.Process.Kill()
require.NoError(t, err)
expectedPIDs := []int{cmd.Process.Pid, cmd2.Process.Pid}
expectedPIDs := []int{cmd.Process.Pid, cmd2.Process.Pid}
for i := 0; i < len(expectedPIDs); i++ {
select {
case <-time.After(testutil.WaitShort):
t.Fatalf("Timed out waiting for process")
case pid := <-pids:
require.Contains(t, expectedPIDs, pid)
}
for i := 0; i < len(expectedPIDs); i++ {
select {
case <-time.After(testutil.WaitShort):
t.Fatalf("Timed out waiting for process")
case pid := <-pids:
require.Contains(t, expectedPIDs, pid)
}
})
}
}
//nolint:paralleltest // Signal handling.
@@ -73,7 +70,7 @@ func TestReapInterrupt(t *testing.T) {
// Don't run the reaper test in CI. It does weird
// things like forkexecing which may have unintended
// consequences in CI.
if _, ok := os.LookupEnv("CI"); ok {
if testutil.InCI() {
t.Skip("Detected CI, skipping reaper tests")
}
+20 -3
View File
@@ -1,8 +1,25 @@
package usershell
import "os"
import (
"os"
"os/exec"
"path/filepath"
"strings"
"golang.org/x/xerrors"
)
// Get returns the $SHELL environment variable.
func Get(_ string) (string, error) {
return os.Getenv("SHELL"), nil
func Get(username string) (string, error) {
// This command will output "UserShell: /bin/zsh" if successful, we
// can ignore the error since we have fallback behavior.
out, _ := exec.Command("dscl", ".", "-read", filepath.Join("/Users", username), "UserShell").Output()
s, ok := strings.CutPrefix(string(out), "UserShell: ")
if ok {
return strings.TrimSpace(s), nil
}
if s = os.Getenv("SHELL"); s != "" {
return s, nil
}
return "", xerrors.Errorf("shell for user %q not found via dscl or in $SHELL", username)
}
+4 -1
View File
@@ -27,5 +27,8 @@ func Get(username string) (string, error) {
}
return parts[6], nil
}
return "", xerrors.Errorf("user %q not found in /etc/passwd", username)
if s := os.Getenv("SHELL"); s != "" {
return s, nil
}
return "", xerrors.Errorf("shell for user %q not found in /etc/passwd or $SHELL", username)
}
-27
View File
@@ -1,27 +0,0 @@
//go:build !windows && !darwin
// +build !windows,!darwin
package usershell_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/agent/usershell"
)
func TestGet(t *testing.T) {
t.Parallel()
t.Run("Has", func(t *testing.T) {
t.Parallel()
shell, err := usershell.Get("root")
require.NoError(t, err)
require.NotEmpty(t, shell)
})
t.Run("NotFound", func(t *testing.T) {
t.Parallel()
_, err := usershell.Get("notauser")
require.Error(t, err)
})
}
+46
View File
@@ -0,0 +1,46 @@
package usershell_test
import (
"os/user"
"runtime"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/agent/usershell"
)
//nolint:paralleltest,tparallel // This test sets an environment variable.
func TestGet(t *testing.T) {
if runtime.GOOS == "windows" {
t.SkipNow()
}
t.Run("Fallback", func(t *testing.T) {
t.Setenv("SHELL", "/bin/sh")
t.Run("NonExistentUser", func(t *testing.T) {
shell, err := usershell.Get("notauser")
require.NoError(t, err)
require.Equal(t, "/bin/sh", shell)
})
})
t.Run("NoFallback", func(t *testing.T) {
// Disable env fallback for these tests.
t.Setenv("SHELL", "")
t.Run("NotFound", func(t *testing.T) {
_, err := usershell.Get("notauser")
require.Error(t, err)
})
t.Run("User", func(t *testing.T) {
u, err := user.Current()
require.NoError(t, err)
shell, err := usershell.Get(u.Username)
require.NoError(t, err)
require.NotEmpty(t, shell)
})
})
}
+190 -42
View File
@@ -18,23 +18,36 @@ import (
"cloud.google.com/go/compute/metadata"
"golang.org/x/xerrors"
"gopkg.in/natefinch/lumberjack.v2"
"tailscale.com/util/clientmetric"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/expfmt"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
"cdr.dev/slog/sloggers/slogjson"
"cdr.dev/slog/sloggers/slogstackdriver"
"github.com/coder/coder/agent"
"github.com/coder/coder/agent/reaper"
"github.com/coder/coder/buildinfo"
"github.com/coder/coder/cli/clibase"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/codersdk/agentsdk"
)
func (r *RootCmd) workspaceAgent() *clibase.Cmd {
var (
auth string
logDir string
pprofAddress string
noReap bool
sshMaxTimeout time.Duration
auth string
logDir string
pprofAddress string
noReap bool
sshMaxTimeout time.Duration
tailnetListenPort int64
prometheusAddress string
debugAddress string
slogHumanPath string
slogJSONPath string
slogStackdriverPath string
)
cmd := &clibase.Cmd{
Use: "agent",
@@ -45,19 +58,64 @@ func (r *RootCmd) workspaceAgent() *clibase.Cmd {
ctx, cancel := context.WithCancel(inv.Context())
defer cancel()
agentPorts := map[int]string{}
var (
ignorePorts = map[int]string{}
isLinux = runtime.GOOS == "linux"
isLinux := runtime.GOOS == "linux"
sinks = []slog.Sink{}
logClosers = []func() error{}
)
defer func() {
for _, closer := range logClosers {
_ = closer()
}
}()
addSinkIfProvided := func(sinkFn func(io.Writer) slog.Sink, loc string) error {
switch loc {
case "":
// Do nothing.
case "/dev/stderr":
sinks = append(sinks, sinkFn(inv.Stderr))
case "/dev/stdout":
sinks = append(sinks, sinkFn(inv.Stdout))
default:
fi, err := os.OpenFile(loc, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644)
if err != nil {
return xerrors.Errorf("open log file %q: %w", loc, err)
}
sinks = append(sinks, sinkFn(fi))
logClosers = append(logClosers, fi.Close)
}
return nil
}
if err := addSinkIfProvided(sloghuman.Sink, slogHumanPath); err != nil {
return xerrors.Errorf("add human sink: %w", err)
}
if err := addSinkIfProvided(slogjson.Sink, slogJSONPath); err != nil {
return xerrors.Errorf("add json sink: %w", err)
}
if err := addSinkIfProvided(slogstackdriver.Sink, slogStackdriverPath); err != nil {
return xerrors.Errorf("add stackdriver sink: %w", err)
}
// Spawn a reaper so that we don't accumulate a ton
// of zombie processes.
if reaper.IsInitProcess() && !noReap && isLinux {
logWriter := &lumberjack.Logger{
logWriter := &lumberjackWriteCloseFixer{w: &lumberjack.Logger{
Filename: filepath.Join(logDir, "coder-agent-init.log"),
MaxSize: 5, // MB
}
// Without this, rotated logs will never be deleted.
MaxBackups: 1,
}}
defer logWriter.Close()
logger := slog.Make(sloghuman.Sink(inv.Stderr), sloghuman.Sink(logWriter)).Leveled(slog.LevelDebug)
sinks = append(sinks, sloghuman.Sink(logWriter))
logger := slog.Make(sinks...).Leveled(slog.LevelDebug)
logger.Info(ctx, "spawning reaper process")
// Do not start a reaper on the child process. It's important
@@ -68,7 +126,7 @@ func (r *RootCmd) workspaceAgent() *clibase.Cmd {
reaper.WithCatchSignals(InterruptSignals...),
)
if err != nil {
logger.Error(ctx, "failed to reap", slog.Error(err))
logger.Error(ctx, "agent process reaper unable to fork", slog.Error(err))
return xerrors.Errorf("fork reap: %w", err)
}
@@ -87,28 +145,29 @@ func (r *RootCmd) workspaceAgent() *clibase.Cmd {
ctx, stopNotify := signal.NotifyContext(ctx, InterruptSignals...)
defer stopNotify()
// dumpHandler does signal handling, so we call it after the
// DumpHandler does signal handling, so we call it after the
// reaper.
go dumpHandler(ctx)
go DumpHandler(ctx)
ljLogger := &lumberjack.Logger{
logWriter := &lumberjackWriteCloseFixer{w: &lumberjack.Logger{
Filename: filepath.Join(logDir, "coder-agent.log"),
MaxSize: 5, // MB
}
defer ljLogger.Close()
logWriter := &closeWriter{w: ljLogger}
// Without this, rotated logs will never be deleted.
MaxBackups: 1,
}}
defer logWriter.Close()
logger := slog.Make(sloghuman.Sink(inv.Stderr), sloghuman.Sink(logWriter)).Leveled(slog.LevelDebug)
sinks = append(sinks, sloghuman.Sink(logWriter))
logger := slog.Make(sinks...).Leveled(slog.LevelDebug)
version := buildinfo.Version()
logger.Info(ctx, "starting agent",
logger.Info(ctx, "agent is starting now",
slog.F("url", r.agentURL),
slog.F("auth", auth),
slog.F("version", version),
)
client := agentsdk.New(r.agentURL)
client.SDK.Logger = logger
client.SDK.SetLogger(logger)
// Set a reasonable timeout so requests can't hang forever!
// The timeout needs to be reasonably long, because requests
// with large payloads can take a bit. e.g. startup scripts
@@ -118,11 +177,18 @@ func (r *RootCmd) workspaceAgent() *clibase.Cmd {
// Enable pprof handler
// This prevents the pprof import from being accidentally deleted.
_ = pprof.Handler
pprofSrvClose := serveHandler(ctx, logger, nil, pprofAddress, "pprof")
pprofSrvClose := ServeHandler(ctx, logger, nil, pprofAddress, "pprof")
defer pprofSrvClose()
// Do a best effort here. If this fails, it's not a big deal.
if port, err := urlPort(pprofAddress); err == nil {
agentPorts[port] = "pprof"
if port, err := extractPort(pprofAddress); err == nil {
ignorePorts[port] = "pprof"
}
if port, err := extractPort(prometheusAddress); err == nil {
ignorePorts[port] = "prometheus"
}
if port, err := extractPort(debugAddress); err == nil {
ignorePorts[port] = "debug"
}
// exchangeToken returns a session token.
@@ -186,10 +252,13 @@ func (r *RootCmd) workspaceAgent() *clibase.Cmd {
return xerrors.Errorf("add executable to $PATH: %w", err)
}
closer := agent.New(agent.Options{
Client: client,
Logger: logger,
LogDir: logDir,
prometheusRegistry := prometheus.NewRegistry()
subsystem := inv.Environ.Get(agent.EnvAgentSubsystem)
agnt := agent.New(agent.Options{
Client: client,
Logger: logger,
LogDir: logDir,
TailnetListenPort: uint16(tailnetListenPort),
ExchangeToken: func(ctx context.Context) (string, error) {
if exchangeToken == nil {
return client.SDK.SessionToken(), nil
@@ -204,11 +273,21 @@ func (r *RootCmd) workspaceAgent() *clibase.Cmd {
EnvironmentVariables: map[string]string{
"GIT_ASKPASS": executablePath,
},
AgentPorts: agentPorts,
IgnorePorts: ignorePorts,
SSHMaxTimeout: sshMaxTimeout,
Subsystem: codersdk.AgentSubsystem(subsystem),
PrometheusRegistry: prometheusRegistry,
})
prometheusSrvClose := ServeHandler(ctx, logger, prometheusMetricsHandler(prometheusRegistry, logger), prometheusAddress, "prometheus")
defer prometheusSrvClose()
debugSrvClose := ServeHandler(ctx, logger, agnt.HTTPDebug(), debugAddress, "debug")
defer debugSrvClose()
<-ctx.Done()
return closer.Close()
return agnt.Close()
},
}
@@ -242,18 +321,64 @@ func (r *RootCmd) workspaceAgent() *clibase.Cmd {
Value: clibase.BoolOf(&noReap),
},
{
Flag: "ssh-max-timeout",
Default: "0",
Flag: "ssh-max-timeout",
// tcpip.KeepaliveIdleOption = 72h + 1min (forwardTCPSockOpts() in tailnet/conn.go)
Default: "72h",
Env: "CODER_AGENT_SSH_MAX_TIMEOUT",
Description: "Specify the max timeout for a SSH connection.",
Description: "Specify the max timeout for a SSH connection, it is advisable to set it to a minimum of 60s, but no more than 72h.",
Value: clibase.DurationOf(&sshMaxTimeout),
},
{
Flag: "tailnet-listen-port",
Default: "0",
Env: "CODER_AGENT_TAILNET_LISTEN_PORT",
Description: "Specify a static port for Tailscale to use for listening.",
Value: clibase.Int64Of(&tailnetListenPort),
},
{
Flag: "prometheus-address",
Default: "127.0.0.1:2112",
Env: "CODER_AGENT_PROMETHEUS_ADDRESS",
Value: clibase.StringOf(&prometheusAddress),
Description: "The bind address to serve Prometheus metrics.",
},
{
Flag: "debug-address",
Default: "127.0.0.1:2113",
Env: "CODER_AGENT_DEBUG_ADDRESS",
Value: clibase.StringOf(&debugAddress),
Description: "The bind address to serve a debug HTTP server.",
},
{
Name: "Human Log Location",
Description: "Output human-readable logs to a given file.",
Flag: "log-human",
Env: "CODER_AGENT_LOGGING_HUMAN",
Default: "/dev/stderr",
Value: clibase.StringOf(&slogHumanPath),
},
{
Name: "JSON Log Location",
Description: "Output JSON logs to a given file.",
Flag: "log-json",
Env: "CODER_AGENT_LOGGING_JSON",
Default: "",
Value: clibase.StringOf(&slogJSONPath),
},
{
Name: "Stackdriver Log Location",
Description: "Output Stackdriver compatible logs to a given file.",
Flag: "log-stackdriver",
Env: "CODER_AGENT_LOGGING_STACKDRIVER",
Default: "",
Value: clibase.StringOf(&slogStackdriverPath),
},
}
return cmd
}
func serveHandler(ctx context.Context, logger slog.Logger, handler http.Handler, addr, name string) (closeFunc func()) {
func ServeHandler(ctx context.Context, logger slog.Logger, handler http.Handler, addr, name string) (closeFunc func()) {
logger.Debug(ctx, "http server listening", slog.F("addr", addr), slog.F("name", name))
// ReadHeaderTimeout is purposefully not enabled. It caused some issues with
@@ -276,16 +401,16 @@ func serveHandler(ctx context.Context, logger slog.Logger, handler http.Handler,
}
}
// closeWriter is a wrapper around an io.WriteCloser that prevents
// writes after Close. This is necessary because lumberjack will
// re-open the file on write.
type closeWriter struct {
// lumberjackWriteCloseFixer is a wrapper around an io.WriteCloser that
// prevents writes after Close. This is necessary because lumberjack
// re-opens the file on Write.
type lumberjackWriteCloseFixer struct {
w io.WriteCloser
mu sync.Mutex // Protects following.
closed bool
}
func (c *closeWriter) Close() error {
func (c *lumberjackWriteCloseFixer) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
@@ -293,7 +418,7 @@ func (c *closeWriter) Close() error {
return c.w.Close()
}
func (c *closeWriter) Write(p []byte) (int, error) {
func (c *lumberjackWriteCloseFixer) Write(p []byte) (int, error) {
c.mu.Lock()
defer c.mu.Unlock()
@@ -327,10 +452,33 @@ func urlPort(u string) (int, error) {
return -1, xerrors.Errorf("invalid url %q: %w", u, err)
}
if parsed.Port() != "" {
port, err := strconv.ParseInt(parsed.Port(), 10, 64)
if err == nil && port > 0 {
port, err := strconv.ParseUint(parsed.Port(), 10, 16)
if err == nil && port > 0 && port < 1<<16 {
return int(port), nil
}
}
return -1, xerrors.Errorf("invalid port: %s", u)
}
func prometheusMetricsHandler(prometheusRegistry *prometheus.Registry, logger slog.Logger) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
// Based on: https://github.com/tailscale/tailscale/blob/280255acae604796a1113861f5a84e6fa2dc6121/ipn/localapi/localapi.go#L489
clientmetric.WritePrometheusExpositionFormat(w)
metricFamilies, err := prometheusRegistry.Gather()
if err != nil {
logger.Error(context.Background(), "Prometheus handler can't gather metric families", slog.Error(err))
return
}
for _, metricFamily := range metricFamilies {
_, err = expfmt.MetricFamilyToText(w, metricFamily)
if err != nil {
logger.Error(context.Background(), "expfmt.MetricFamilyToText failed", slog.Error(err))
return
}
}
})
}
+6
View File
@@ -46,6 +46,12 @@ func Test_extractPort(t *testing.T) {
urlString: "6060",
wantErr: true,
},
{
name: "127.0.0.1",
urlString: "127.0.0.1:2113",
want: 2113,
wantErr: false,
},
}
for _, tt := range tests {
tt := tt
+51 -9
View File
@@ -12,8 +12,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/agent"
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
"github.com/coder/coder/pty/ptytest"
@@ -51,7 +53,8 @@ func TestWorkspaceAgent(t *testing.T) {
pty := ptytest.New(t).Attach(inv)
clitest.Start(t, inv)
pty.ExpectMatch("starting agent")
ctx := inv.Context()
pty.ExpectMatchContext(ctx, "agent is starting now")
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
@@ -97,8 +100,7 @@ func TestWorkspaceAgent(t *testing.T) {
//nolint:revive,staticcheck
context.WithValue(inv.Context(), "azure-client", metadataClient),
)
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
ctx := inv.Context()
clitest.Start(t, inv)
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
workspace, err := client.Workspace(ctx, workspace.ID)
@@ -110,7 +112,7 @@ func TestWorkspaceAgent(t *testing.T) {
dialer, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, nil)
require.NoError(t, err)
defer dialer.Close()
require.True(t, dialer.AwaitReachable(context.Background()))
require.True(t, dialer.AwaitReachable(ctx))
})
t.Run("AWS", func(t *testing.T) {
@@ -151,17 +153,18 @@ func TestWorkspaceAgent(t *testing.T) {
context.WithValue(inv.Context(), "aws-client", metadataClient),
)
clitest.Start(t, inv)
ctx := inv.Context()
coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
workspace, err := client.Workspace(inv.Context(), workspace.ID)
workspace, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err)
resources := workspace.LatestBuild.Resources
if assert.NotEmpty(t, resources) && assert.NotEmpty(t, resources[0].Agents) {
assert.NotEmpty(t, resources[0].Agents[0].Version)
}
dialer, err := client.DialWorkspaceAgent(inv.Context(), resources[0].Agents[0].ID, nil)
dialer, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, nil)
require.NoError(t, err)
defer dialer.Close()
require.True(t, dialer.AwaitReachable(context.Background()))
require.True(t, dialer.AwaitReachable(ctx))
})
t.Run("GoogleCloud", func(t *testing.T) {
@@ -202,7 +205,7 @@ func TestWorkspaceAgent(t *testing.T) {
clitest.Start(t,
inv.WithContext(
//nolint:revive,staticcheck
context.WithValue(context.Background(), "gcp-client", metadataClient),
context.WithValue(inv.Context(), "gcp-client", metadataClient),
),
)
@@ -218,7 +221,7 @@ func TestWorkspaceAgent(t *testing.T) {
dialer, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, nil)
require.NoError(t, err)
defer dialer.Close()
require.True(t, dialer.AwaitReachable(context.Background()))
require.True(t, dialer.AwaitReachable(ctx))
sshClient, err := dialer.SSHClient(ctx)
require.NoError(t, err)
defer sshClient.Close()
@@ -235,4 +238,43 @@ func TestWorkspaceAgent(t *testing.T) {
_, err = uuid.Parse(strings.TrimSpace(string(token)))
require.NoError(t, err)
})
t.Run("PostStartup", func(t *testing.T) {
t.Parallel()
authToken := uuid.NewString()
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
})
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
logDir := t.TempDir()
inv, _ := clitest.New(t,
"agent",
"--auth", "token",
"--agent-token", authToken,
"--agent-url", client.URL.String(),
"--log-dir", logDir,
)
// Set the subsystem for the agent.
inv.Environ.Set(agent.EnvAgentSubsystem, string(codersdk.AgentSubsystemEnvbox))
pty := ptytest.New(t).Attach(inv)
clitest.Start(t, inv)
pty.ExpectMatchContext(inv.Context(), "agent is starting now")
resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.ID)
require.Len(t, resources, 1)
require.Len(t, resources[0].Agents, 1)
require.Equal(t, codersdk.AgentSubsystemEnvbox, resources[0].Agents[0].Subsystem)
})
}
+4 -9
View File
@@ -14,15 +14,10 @@ import (
// Group describes a hierarchy of groups that an option or command belongs to.
type Group struct {
Parent *Group `json:"parent,omitempty"`
Name string `json:"name,omitempty"`
Children []Group `json:"children,omitempty"`
Description string `json:"description,omitempty"`
}
func (g *Group) AddChild(child Group) {
child.Parent = g
g.Children = append(g.Children, child)
Parent *Group `json:"parent,omitempty"`
Name string `json:"name,omitempty"`
YAML string `json:"yaml,omitempty"`
Description string `json:"description,omitempty"`
}
// Ancestry returns the group and all of its parents, in order.
+124 -92
View File
@@ -13,6 +13,7 @@ import (
"github.com/spf13/pflag"
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
"gopkg.in/yaml.v3"
)
// Cmd describes an executable command.
@@ -76,10 +77,8 @@ func (c *Cmd) PrepareAll() error {
}
var merr error
slices.SortFunc(c.Options, func(a, b Option) bool {
return a.Flag < b.Flag
})
for _, opt := range c.Options {
for i := range c.Options {
opt := &c.Options[i]
if opt.Name == "" {
switch {
case opt.Flag != "":
@@ -102,6 +101,10 @@ func (c *Cmd) PrepareAll() error {
}
}
}
slices.SortFunc(c.Options, func(a, b Option) bool {
return a.Name < b.Name
})
slices.SortFunc(c.Children, func(a, b *Cmd) bool {
return a.Name() < b.Name()
})
@@ -142,6 +145,16 @@ func (c *Cmd) FullUsage() string {
return strings.Join(uses, " ")
}
// FullOptions returns the options of the command and its parents.
func (c *Cmd) FullOptions() OptionSet {
var opts OptionSet
if c.Parent != nil {
opts = append(opts, c.Parent.FullOptions()...)
}
opts = append(opts, c.Options...)
return opts
}
// Invoke creates a new invocation of the command, with
// stdio discarded.
//
@@ -172,8 +185,8 @@ type Invocation struct {
// WithOS returns the invocation as a main package, filling in the invocation's unset
// fields with OS defaults.
func (i *Invocation) WithOS() *Invocation {
return i.with(func(i *Invocation) {
func (inv *Invocation) WithOS() *Invocation {
return inv.with(func(i *Invocation) {
i.Stdout = os.Stdout
i.Stderr = os.Stderr
i.Stdin = os.Stdin
@@ -182,18 +195,18 @@ func (i *Invocation) WithOS() *Invocation {
})
}
func (i *Invocation) Context() context.Context {
if i.ctx == nil {
func (inv *Invocation) Context() context.Context {
if inv.ctx == nil {
return context.Background()
}
return i.ctx
return inv.ctx
}
func (i *Invocation) ParsedFlags() *pflag.FlagSet {
if i.parsedFlags == nil {
func (inv *Invocation) ParsedFlags() *pflag.FlagSet {
if inv.parsedFlags == nil {
panic("flags not parsed, has Run() been called?")
}
return i.parsedFlags
return inv.parsedFlags
}
type runState struct {
@@ -218,30 +231,8 @@ func copyFlagSetWithout(fs *pflag.FlagSet, without string) *pflag.FlagSet {
// run recursively executes the command and its children.
// allArgs is wired through the stack so that global flags can be accepted
// anywhere in the command invocation.
func (i *Invocation) run(state *runState) error {
err := i.Command.Options.SetDefaults()
if err != nil {
return xerrors.Errorf("setting defaults: %w", err)
}
// If we set the Default of an array but later see a flag for it, we
// don't want to append, we want to replace. So, we need to keep the state
// of defaulted array options.
defaultedArrays := make(map[string]int)
for _, opt := range i.Command.Options {
sv, ok := opt.Value.(pflag.SliceValue)
if !ok {
continue
}
if opt.Flag == "" {
continue
}
defaultedArrays[opt.Flag] = len(sv.GetSlice())
}
err = i.Command.Options.ParseEnv(i.Environ)
func (inv *Invocation) run(state *runState) error {
err := inv.Command.Options.ParseEnv(inv.Environ)
if err != nil {
return xerrors.Errorf("parsing env: %w", err)
}
@@ -249,8 +240,8 @@ func (i *Invocation) run(state *runState) error {
// Now the fun part, argument parsing!
children := make(map[string]*Cmd)
for _, child := range i.Command.Children {
child.Parent = i.Command
for _, child := range inv.Command.Children {
child.Parent = inv.Command
for _, name := range append(child.Aliases, child.Name()) {
if _, ok := children[name]; ok {
return xerrors.Errorf("duplicate command name: %s", name)
@@ -259,49 +250,65 @@ func (i *Invocation) run(state *runState) error {
}
}
if i.parsedFlags == nil {
i.parsedFlags = pflag.NewFlagSet(i.Command.Name(), pflag.ContinueOnError)
if inv.parsedFlags == nil {
inv.parsedFlags = pflag.NewFlagSet(inv.Command.Name(), pflag.ContinueOnError)
// We handle Usage ourselves.
i.parsedFlags.Usage = func() {}
inv.parsedFlags.Usage = func() {}
}
// If we find a duplicate flag, we want the deeper command's flag to override
// the shallow one. Unfortunately, pflag has no way to remove a flag, so we
// have to create a copy of the flagset without a value.
i.Command.Options.FlagSet().VisitAll(func(f *pflag.Flag) {
if i.parsedFlags.Lookup(f.Name) != nil {
i.parsedFlags = copyFlagSetWithout(i.parsedFlags, f.Name)
inv.Command.Options.FlagSet().VisitAll(func(f *pflag.Flag) {
if inv.parsedFlags.Lookup(f.Name) != nil {
inv.parsedFlags = copyFlagSetWithout(inv.parsedFlags, f.Name)
}
i.parsedFlags.AddFlag(f)
inv.parsedFlags.AddFlag(f)
})
var parsedArgs []string
if !i.Command.RawArgs {
if !inv.Command.RawArgs {
// Flag parsing will fail on intermediate commands in the command tree,
// so we check the error after looking for a child command.
state.flagParseErr = i.parsedFlags.Parse(state.allArgs)
parsedArgs = i.parsedFlags.Args()
state.flagParseErr = inv.parsedFlags.Parse(state.allArgs)
parsedArgs = inv.parsedFlags.Args()
}
i.parsedFlags.VisitAll(func(f *pflag.Flag) {
i, ok := defaultedArrays[f.Name]
if !ok {
return
}
// Set value sources for flags.
for i, opt := range inv.Command.Options {
if fl := inv.parsedFlags.Lookup(opt.Flag); fl != nil && fl.Changed {
inv.Command.Options[i].ValueSource = ValueSourceFlag
}
}
if !f.Changed {
return
}
// Read YAML configs, if any.
for _, opt := range inv.Command.Options {
path, ok := opt.Value.(*YAMLConfigPath)
if !ok || path.String() == "" {
continue
}
sv, ok := f.Value.(pflag.SliceValue)
if !ok {
panic("defaulted array option is not a slice value")
}
err := sv.Replace(sv.GetSlice()[i:])
if err != nil {
panic(err)
}
})
byt, err := os.ReadFile(path.String())
if err != nil {
return xerrors.Errorf("reading yaml: %w", err)
}
var n yaml.Node
err = yaml.Unmarshal(byt, &n)
if err != nil {
return xerrors.Errorf("decoding yaml: %w", err)
}
err = inv.Command.Options.UnmarshalYAML(&n)
if err != nil {
return xerrors.Errorf("applying yaml: %w", err)
}
}
err = inv.Command.Options.SetDefaults()
if err != nil {
return xerrors.Errorf("setting defaults: %w", err)
}
// Run child command if found (next child only)
@@ -310,64 +317,76 @@ func (i *Invocation) run(state *runState) error {
if len(parsedArgs) > state.commandDepth {
nextArg := parsedArgs[state.commandDepth]
if child, ok := children[nextArg]; ok {
child.Parent = i.Command
i.Command = child
child.Parent = inv.Command
inv.Command = child
state.commandDepth++
return i.run(state)
return inv.run(state)
}
}
// Flag parse errors are irrelevant for raw args commands.
if !i.Command.RawArgs && state.flagParseErr != nil && !errors.Is(state.flagParseErr, pflag.ErrHelp) {
if !inv.Command.RawArgs && state.flagParseErr != nil && !errors.Is(state.flagParseErr, pflag.ErrHelp) {
return xerrors.Errorf(
"parsing flags (%v) for %q: %w",
state.allArgs,
i.Command.FullName(), state.flagParseErr,
inv.Command.FullName(), state.flagParseErr,
)
}
if i.Command.RawArgs {
// All options should be set. Check all required options have sources,
// meaning they were set by the user in some way (env, flag, etc).
var missing []string
for _, opt := range inv.Command.Options {
if opt.Required && opt.ValueSource == ValueSourceNone {
missing = append(missing, opt.Flag)
}
}
if len(missing) > 0 {
return xerrors.Errorf("Missing values for the required flags: %s", strings.Join(missing, ", "))
}
if inv.Command.RawArgs {
// If we're at the root command, then the name is omitted
// from the arguments, so we can just use the entire slice.
if state.commandDepth == 0 {
i.Args = state.allArgs
inv.Args = state.allArgs
} else {
argPos, err := findArg(i.Command.Name(), state.allArgs, i.parsedFlags)
argPos, err := findArg(inv.Command.Name(), state.allArgs, inv.parsedFlags)
if err != nil {
panic(err)
}
i.Args = state.allArgs[argPos+1:]
inv.Args = state.allArgs[argPos+1:]
}
} else {
// In non-raw-arg mode, we want to skip over flags.
i.Args = parsedArgs[state.commandDepth:]
inv.Args = parsedArgs[state.commandDepth:]
}
mw := i.Command.Middleware
mw := inv.Command.Middleware
if mw == nil {
mw = Chain()
}
ctx := i.ctx
ctx := inv.ctx
if ctx == nil {
ctx = context.Background()
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
i = i.WithContext(ctx)
inv = inv.WithContext(ctx)
if i.Command.Handler == nil || errors.Is(state.flagParseErr, pflag.ErrHelp) {
if i.Command.HelpHandler == nil {
return xerrors.Errorf("no handler or help for command %s", i.Command.FullName())
if inv.Command.Handler == nil || errors.Is(state.flagParseErr, pflag.ErrHelp) {
if inv.Command.HelpHandler == nil {
return xerrors.Errorf("no handler or help for command %s", inv.Command.FullName())
}
return i.Command.HelpHandler(i)
return inv.Command.HelpHandler(inv)
}
err = mw(i.Command.Handler)(i)
err = mw(inv.Command.Handler)(inv)
if err != nil {
return &RunCommandError{
Cmd: i.Command,
Cmd: inv.Command,
Err: err,
}
}
@@ -430,33 +449,46 @@ func findArg(want string, args []string, fs *pflag.FlagSet) (int, error) {
// If two command share a flag name, the first command wins.
//
//nolint:revive
func (i *Invocation) Run() (err error) {
func (inv *Invocation) Run() (err error) {
defer func() {
// Pflag is panicky, so additional context is helpful in tests.
if flag.Lookup("test.v") == nil {
return
}
if r := recover(); r != nil {
err = xerrors.Errorf("panic recovered for %s: %v", i.Command.FullName(), r)
err = xerrors.Errorf("panic recovered for %s: %v", inv.Command.FullName(), r)
panic(err)
}
}()
err = i.run(&runState{
allArgs: i.Args,
// We close Stdin to prevent deadlocks, e.g. when the command
// has ended but an io.Copy is still reading from Stdin.
defer func() {
if inv.Stdin == nil {
return
}
rc, ok := inv.Stdin.(io.ReadCloser)
if !ok {
return
}
e := rc.Close()
err = errors.Join(err, e)
}()
err = inv.run(&runState{
allArgs: inv.Args,
})
return err
}
// WithContext returns a copy of the Invocation with the given context.
func (i *Invocation) WithContext(ctx context.Context) *Invocation {
return i.with(func(i *Invocation) {
func (inv *Invocation) WithContext(ctx context.Context) *Invocation {
return inv.with(func(i *Invocation) {
i.ctx = ctx
})
}
// with returns a copy of the Invocation with the given function applied.
func (i *Invocation) with(fn func(*Invocation)) *Invocation {
i2 := *i
func (inv *Invocation) with(fn func(*Invocation)) *Invocation {
i2 := *inv
fn(&i2)
return &i2
}
+217 -3
View File
@@ -3,6 +3,8 @@ package clibase_test
import (
"bytes"
"context"
"fmt"
"os"
"strings"
"testing"
@@ -36,6 +38,8 @@ func TestCommand(t *testing.T) {
verbose bool
lower bool
prefix string
reqBool bool
reqStr string
)
return &clibase.Cmd{
Use: "root [subcommand]",
@@ -52,6 +56,34 @@ func TestCommand(t *testing.T) {
},
},
Children: []*clibase.Cmd{
{
Use: "required-flag --req-bool=true --req-string=foo",
Short: "Example with required flags",
Options: clibase.OptionSet{
clibase.Option{
Name: "req-bool",
Flag: "req-bool",
Value: clibase.BoolOf(&reqBool),
Required: true,
},
clibase.Option{
Name: "req-string",
Flag: "req-string",
Value: clibase.Validate(clibase.StringOf(&reqStr), func(value *clibase.String) error {
ok := strings.Contains(value.String(), " ")
if !ok {
return xerrors.Errorf("string must contain a space")
}
return nil
}),
Required: true,
},
},
Handler: func(i *clibase.Invocation) error {
_, _ = i.Stdout.Write([]byte(fmt.Sprintf("%s-%t", reqStr, reqBool)))
return nil
},
},
{
Use: "toupper [word]",
Short: "Converts a word to upper case",
@@ -66,8 +98,8 @@ func TestCommand(t *testing.T) {
Value: clibase.BoolOf(&lower),
},
},
Handler: (func(i *clibase.Invocation) error {
i.Stdout.Write([]byte(prefix))
Handler: func(i *clibase.Invocation) error {
_, _ = i.Stdout.Write([]byte(prefix))
w := i.Args[0]
if lower {
w = strings.ToLower(w)
@@ -83,7 +115,7 @@ func TestCommand(t *testing.T) {
i.Stdout.Write([]byte("!!!"))
}
return nil
}),
},
},
},
}
@@ -211,6 +243,60 @@ func TestCommand(t *testing.T) {
fio := fakeIO(i)
require.Error(t, i.Run(), fio.Stdout.String())
})
t.Run("RequiredFlagsMissing", func(t *testing.T) {
t.Parallel()
i := cmd().Invoke(
"required-flag",
)
fio := fakeIO(i)
err := i.Run()
require.Error(t, err, fio.Stdout.String())
require.ErrorContains(t, err, "Missing values")
})
t.Run("RequiredFlagsMissingBool", func(t *testing.T) {
t.Parallel()
i := cmd().Invoke(
"required-flag", "--req-string", "foo bar",
)
fio := fakeIO(i)
err := i.Run()
require.Error(t, err, fio.Stdout.String())
require.ErrorContains(t, err, "Missing values for the required flags: req-bool")
})
t.Run("RequiredFlagsMissingString", func(t *testing.T) {
t.Parallel()
i := cmd().Invoke(
"required-flag", "--req-bool", "true",
)
fio := fakeIO(i)
err := i.Run()
require.Error(t, err, fio.Stdout.String())
require.ErrorContains(t, err, "Missing values for the required flags: req-string")
})
t.Run("RequiredFlagsInvalid", func(t *testing.T) {
t.Parallel()
i := cmd().Invoke(
"required-flag", "--req-string", "nospace",
)
fio := fakeIO(i)
err := i.Run()
require.Error(t, err, fio.Stdout.String())
require.ErrorContains(t, err, "string must contain a space")
})
t.Run("RequiredFlagsOK", func(t *testing.T) {
t.Parallel()
i := cmd().Invoke(
"required-flag", "--req-bool", "true", "--req-string", "foo bar",
)
fio := fakeIO(i)
err := i.Run()
require.NoError(t, err, fio.Stdout.String())
})
}
func TestCommand_DeepNest(t *testing.T) {
@@ -247,6 +333,7 @@ func TestCommand_FlagOverride(t *testing.T) {
Use: "1",
Options: clibase.OptionSet{
{
Name: "flag",
Flag: "f",
Value: clibase.DiscardValue,
},
@@ -256,6 +343,7 @@ func TestCommand_FlagOverride(t *testing.T) {
Use: "2",
Options: clibase.OptionSet{
{
Name: "flag",
Flag: "f",
Value: clibase.StringOf(&flag),
},
@@ -503,3 +591,129 @@ func TestCommand_SliceFlags(t *testing.T) {
err = cmd("bad", "bad", "bad").Invoke().Run()
require.NoError(t, err)
}
func TestCommand_EmptySlice(t *testing.T) {
t.Parallel()
cmd := func(want ...string) *clibase.Cmd {
var got []string
return &clibase.Cmd{
Use: "root",
Options: clibase.OptionSet{
{
Name: "arr",
Flag: "arr",
Default: "def,def,def",
Env: "ARR",
Value: clibase.StringArrayOf(&got),
},
},
Handler: (func(i *clibase.Invocation) error {
require.Equal(t, want, got)
return nil
}),
}
}
// Base-case, uses default.
err := cmd("def", "def", "def").Invoke().Run()
require.NoError(t, err)
// Empty-env uses default, too.
inv := cmd("def", "def", "def").Invoke()
inv.Environ.Set("ARR", "")
require.NoError(t, err)
// Reset to nothing at all via flag.
inv = cmd().Invoke("--arr", "")
inv.Environ.Set("ARR", "cant see")
err = inv.Run()
require.NoError(t, err)
// Reset to a specific value with flag.
inv = cmd("great").Invoke("--arr", "great")
inv.Environ.Set("ARR", "")
err = inv.Run()
require.NoError(t, err)
}
func TestCommand_DefaultsOverride(t *testing.T) {
t.Parallel()
test := func(name string, want string, fn func(t *testing.T, inv *clibase.Invocation)) {
t.Run(name, func(t *testing.T) {
t.Parallel()
var (
got string
config clibase.YAMLConfigPath
)
cmd := &clibase.Cmd{
Options: clibase.OptionSet{
{
Name: "url",
Flag: "url",
Default: "def.com",
Env: "URL",
Value: clibase.StringOf(&got),
YAML: "url",
},
{
Name: "config",
Flag: "config",
Default: "",
Value: &config,
},
},
Handler: (func(i *clibase.Invocation) error {
_, _ = fmt.Fprintf(i.Stdout, "%s", got)
return nil
}),
}
inv := cmd.Invoke()
stdio := fakeIO(inv)
fn(t, inv)
err := inv.Run()
require.NoError(t, err)
require.Equal(t, want, stdio.Stdout.String())
})
}
test("DefaultOverNothing", "def.com", func(t *testing.T, inv *clibase.Invocation) {})
test("FlagOverDefault", "good.com", func(t *testing.T, inv *clibase.Invocation) {
inv.Args = []string{"--url", "good.com"}
})
test("EnvOverDefault", "good.com", func(t *testing.T, inv *clibase.Invocation) {
inv.Environ.Set("URL", "good.com")
})
test("FlagOverEnv", "good.com", func(t *testing.T, inv *clibase.Invocation) {
inv.Environ.Set("URL", "bad.com")
inv.Args = []string{"--url", "good.com"}
})
test("FlagOverYAML", "good.com", func(t *testing.T, inv *clibase.Invocation) {
fi, err := os.CreateTemp(t.TempDir(), "config.yaml")
require.NoError(t, err)
defer fi.Close()
_, err = fi.WriteString("url: bad.com")
require.NoError(t, err)
inv.Args = []string{"--config", fi.Name(), "--url", "good.com"}
})
test("YAMLOverDefault", "good.com", func(t *testing.T, inv *clibase.Invocation) {
fi, err := os.CreateTemp(t.TempDir(), "config.yaml")
require.NoError(t, err)
defer fi.Close()
_, err = fi.WriteString("url: good.com")
require.NoError(t, err)
inv.Args = []string{"--config", fi.Name()}
})
}
+66 -4
View File
@@ -2,16 +2,31 @@ package clibase
import (
"os"
"strings"
"github.com/hashicorp/go-multierror"
"github.com/spf13/pflag"
"golang.org/x/xerrors"
)
type ValueSource string
const (
ValueSourceNone ValueSource = ""
ValueSourceFlag ValueSource = "flag"
ValueSourceEnv ValueSource = "env"
ValueSourceYAML ValueSource = "yaml"
ValueSourceDefault ValueSource = "default"
)
// Option is a configuration option for a CLI application.
type Option struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
// Required means this value must be set by some means. It requires
// `ValueSource != ValueSourceNone`
// If `Default` is set, then `Required` is ignored.
Required bool `json:"required,omitempty"`
// Flag is the long name of the flag used to configure this option. If unset,
// flag configuring is disabled.
@@ -46,6 +61,19 @@ type Option struct {
UseInstead []Option `json:"use_instead,omitempty"`
Hidden bool `json:"hidden,omitempty"`
ValueSource ValueSource `json:"value_source,omitempty"`
}
func (o Option) YAMLPath() string {
if o.YAML == "" {
return ""
}
var gs []string
for _, g := range o.Group.Ancestry() {
gs = append(gs, g.YAML)
}
return strings.Join(append(gs, o.YAML), ".")
}
// OptionSet is a group of options that can be applied to a command.
@@ -56,6 +84,17 @@ func (s *OptionSet) Add(opts ...Option) {
*s = append(*s, opts...)
}
// Filter will only return options that match the given filter. (return true)
func (s OptionSet) Filter(filter func(opt Option) bool) OptionSet {
cpy := make(OptionSet, 0)
for _, opt := range s {
if filter(opt) {
cpy = append(cpy, opt)
}
}
return cpy
}
// FlagSet returns a pflag.FlagSet for the OptionSet.
func (s *OptionSet) FlagSet() *pflag.FlagSet {
if s == nil {
@@ -115,7 +154,7 @@ func (s *OptionSet) ParseEnv(vs []EnvVar) error {
envs[v.Name] = v.Value
}
for _, opt := range *s {
for i, opt := range *s {
if opt.Env == "" {
continue
}
@@ -126,10 +165,14 @@ func (s *OptionSet) ParseEnv(vs []EnvVar) error {
// way for a user to change a Default value to an empty string from
// the environment. Unfortunately, we have old configuration files
// that rely on the faulty behavior.
//
// TODO: We should remove this hack in May 2023, when deployments
// have had months to migrate to the new behavior.
if !ok || envVal == "" {
continue
}
(*s)[i].ValueSource = ValueSourceEnv
if err := opt.Value.Set(envVal); err != nil {
merr = multierror.Append(
merr, xerrors.Errorf("parse %q: %w", opt.Name, err),
@@ -140,8 +183,8 @@ func (s *OptionSet) ParseEnv(vs []EnvVar) error {
return merr.ErrorOrNil()
}
// SetDefaults sets the default values for each Option.
// It should be called before all parsing (e.g. ParseFlags, ParseEnv).
// SetDefaults sets the default values for each Option, skipping values
// that already have a value source.
func (s *OptionSet) SetDefaults() error {
if s == nil {
return nil
@@ -149,10 +192,16 @@ func (s *OptionSet) SetDefaults() error {
var merr *multierror.Error
for _, opt := range *s {
for i, opt := range *s {
// Skip values that may have already been set by the user.
if opt.ValueSource != ValueSourceNone {
continue
}
if opt.Default == "" {
continue
}
if opt.Value == nil {
merr = multierror.Append(
merr,
@@ -163,6 +212,7 @@ func (s *OptionSet) SetDefaults() error {
)
continue
}
(*s)[i].ValueSource = ValueSourceDefault
if err := opt.Value.Set(opt.Default); err != nil {
merr = multierror.Append(
merr, xerrors.Errorf("parse %q: %w", opt.Name, err),
@@ -171,3 +221,15 @@ func (s *OptionSet) SetDefaults() error {
}
return merr.ErrorOrNil()
}
// ByName returns the Option with the given name, or nil if no such option
// exists.
func (s *OptionSet) ByName(name string) *Option {
for i := range *s {
opt := &(*s)[i]
if opt.Name == name {
return opt
}
}
return nil
}
+48
View File
@@ -118,4 +118,52 @@ func TestOptionSet_ParseEnv(t *testing.T) {
require.NoError(t, err)
require.EqualValues(t, "defname", workspaceName)
})
t.Run("StringSlice", func(t *testing.T) {
t.Parallel()
var actual clibase.StringArray
expected := []string{"foo", "bar", "baz"}
os := clibase.OptionSet{
clibase.Option{
Name: "name",
Value: &actual,
Env: "NAMES",
},
}
err := os.SetDefaults()
require.NoError(t, err)
err = os.ParseEnv([]clibase.EnvVar{
{Name: "NAMES", Value: "foo,bar,baz"},
})
require.NoError(t, err)
require.EqualValues(t, expected, actual)
})
t.Run("StructMapStringString", func(t *testing.T) {
t.Parallel()
var actual clibase.Struct[map[string]string]
expected := map[string]string{"foo": "bar", "baz": "zap"}
os := clibase.OptionSet{
clibase.Option{
Name: "labels",
Value: &actual,
Env: "LABELS",
},
}
err := os.SetDefaults()
require.NoError(t, err)
err = os.ParseEnv([]clibase.EnvVar{
{Name: "LABELS", Value: `{"foo":"bar","baz":"zap"}`},
})
require.NoError(t, err)
require.EqualValues(t, expected, actual.Value)
})
}
+101 -13
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"net"
"net/url"
"reflect"
"strconv"
"strings"
"time"
@@ -23,6 +24,40 @@ type NoOptDefValuer interface {
NoOptDefValue() string
}
// Validator is a wrapper around a pflag.Value that allows for validation
// of the value after or before it has been set.
type Validator[T pflag.Value] struct {
Value T
// validate is called after the value is set.
validate func(T) error
}
func Validate[T pflag.Value](opt T, validate func(value T) error) *Validator[T] {
return &Validator[T]{Value: opt, validate: validate}
}
func (i *Validator[T]) String() string {
return i.Value.String()
}
func (i *Validator[T]) Set(input string) error {
err := i.Value.Set(input)
if err != nil {
return err
}
if i.validate != nil {
err = i.validate(i.Value)
if err != nil {
return err
}
}
return nil
}
func (i *Validator[T]) Type() string {
return i.Value.Type()
}
// values.go contains a standard set of value types that can be used as
// Option Values.
@@ -146,6 +181,10 @@ func writeAsCSV(vals []string) string {
}
func (s *StringArray) Set(v string) error {
if v == "" {
*s = nil
return nil
}
ss, err := readAsCSV(v)
if err != nil {
return err
@@ -186,23 +225,21 @@ func (d *Duration) String() string {
return time.Duration(*d).String()
}
func (d *Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(d.String())
}
func (d *Duration) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return err
}
return d.Set(s)
}
func (Duration) Type() string {
return "duration"
}
func (d *Duration) MarshalYAML() (interface{}, error) {
return yaml.Node{
Kind: yaml.ScalarNode,
Value: d.String(),
}, nil
}
func (d *Duration) UnmarshalYAML(n *yaml.Node) error {
return d.Set(n.Value)
}
type URL url.URL
func URLOf(u *url.URL) *URL {
@@ -223,6 +260,17 @@ func (u *URL) String() string {
return uu.String()
}
func (u *URL) MarshalYAML() (interface{}, error) {
return yaml.Node{
Kind: yaml.ScalarNode,
Value: u.String(),
}, nil
}
func (u *URL) UnmarshalYAML(n *yaml.Node) error {
return u.Set(n.Value)
}
func (u *URL) MarshalJSON() ([]byte, error) {
return json.Marshal(u.String())
}
@@ -286,6 +334,17 @@ func (hp *HostPort) UnmarshalJSON(b []byte) error {
return hp.Set(s)
}
func (hp *HostPort) MarshalYAML() (interface{}, error) {
return yaml.Node{
Kind: yaml.ScalarNode,
Value: hp.String(),
}, nil
}
func (hp *HostPort) UnmarshalYAML(n *yaml.Node) error {
return hp.Set(n.Value)
}
func (*HostPort) Type() string {
return "host:port"
}
@@ -304,10 +363,12 @@ type Struct[T any] struct {
Value T
}
//nolint:revive
func (s *Struct[T]) Set(v string) error {
return yaml.Unmarshal([]byte(v), &s.Value)
}
//nolint:revive
func (s *Struct[T]) String() string {
byt, err := yaml.Marshal(s.Value)
if err != nil {
@@ -326,9 +387,17 @@ func (s *Struct[T]) MarshalYAML() (interface{}, error) {
}
func (s *Struct[T]) UnmarshalYAML(n *yaml.Node) error {
// HACK: for compatibility with flags, we use nil slices instead of empty
// slices. In most cases, nil slices and empty slices are treated
// the same, so this behavior may be removed at some point.
if typ := reflect.TypeOf(s.Value); typ.Kind() == reflect.Slice && len(n.Content) == 0 {
reflect.ValueOf(&s.Value).Elem().Set(reflect.Zero(typ))
return nil
}
return n.Decode(&s.Value)
}
//nolint:revive
func (s *Struct[T]) Type() string {
return fmt.Sprintf("struct[%T]", s.Value)
}
@@ -391,3 +460,22 @@ func (e *Enum) Type() string {
func (e *Enum) String() string {
return *e.Value
}
var _ pflag.Value = (*YAMLConfigPath)(nil)
// YAMLConfigPath is a special value type that encodes a path to a YAML
// configuration file where options are read from.
type YAMLConfigPath string
func (p *YAMLConfigPath) Set(v string) error {
*p = YAMLConfigPath(v)
return nil
}
func (p *YAMLConfigPath) String() string {
return string(*p)
}
func (*YAMLConfigPath) Type() string {
return "yaml-config-path"
}
+202 -12
View File
@@ -1,12 +1,20 @@
package clibase
import (
"github.com/iancoleman/strcase"
"errors"
"fmt"
"strings"
"github.com/mitchellh/go-wordwrap"
"golang.org/x/xerrors"
"gopkg.in/yaml.v3"
)
var (
_ yaml.Marshaler = new(OptionSet)
_ yaml.Unmarshaler = new(OptionSet)
)
// deepMapNode returns the mapping node at the given path,
// creating it if it doesn't exist.
func deepMapNode(n *yaml.Node, path []string, headComment string) *yaml.Node {
@@ -36,27 +44,43 @@ func deepMapNode(n *yaml.Node, path []string, headComment string) *yaml.Node {
return deepMapNode(&valueNode, path[1:], headComment)
}
// ToYAML converts the option set to a YAML node, that can be
// MarshalYAML converts the option set to a YAML node, that can be
// converted into bytes via yaml.Marshal.
//
// The node is returned to enable post-processing higher up in
// the stack.
func (s OptionSet) ToYAML() (*yaml.Node, error) {
//
// It is isomorphic with FromYAML.
func (s *OptionSet) MarshalYAML() (any, error) {
root := yaml.Node{
Kind: yaml.MappingNode,
}
for _, opt := range s {
for _, opt := range *s {
if opt.YAML == "" {
continue
}
defValue := opt.Default
if defValue == "" {
defValue = "<unset>"
}
comment := wordwrap.WrapString(
fmt.Sprintf("%s\n(default: %s, type: %s)", opt.Description, defValue, opt.Value.Type()),
80,
)
nameNode := yaml.Node{
Kind: yaml.ScalarNode,
Value: opt.YAML,
HeadComment: wordwrap.WrapString(opt.Description, 80),
HeadComment: comment,
}
var valueNode yaml.Node
if m, ok := opt.Value.(yaml.Marshaler); ok {
if opt.Value == nil {
valueNode = yaml.Node{
Kind: yaml.ScalarNode,
Value: "null",
}
} else if m, ok := opt.Value.(yaml.Marshaler); ok {
v, err := m.MarshalYAML()
if err != nil {
return nil, xerrors.Errorf(
@@ -71,21 +95,43 @@ func (s OptionSet) ToYAML() (*yaml.Node, error) {
)
}
} else {
valueNode = yaml.Node{
Kind: yaml.ScalarNode,
Value: opt.Value.String(),
// The all-other types case.
//
// A bit of a hack, we marshal and then unmarshal to get
// the underlying node.
byt, err := yaml.Marshal(opt.Value)
if err != nil {
return nil, xerrors.Errorf(
"marshal %q: %w", opt.Name, err,
)
}
var docNode yaml.Node
err = yaml.Unmarshal(byt, &docNode)
if err != nil {
return nil, xerrors.Errorf(
"unmarshal %q: %w", opt.Name, err,
)
}
if len(docNode.Content) != 1 {
return nil, xerrors.Errorf(
"unmarshal %q: expected one node, got %d",
opt.Name, len(docNode.Content),
)
}
valueNode = *docNode.Content[0]
}
var group []string
for _, g := range opt.Group.Ancestry() {
if g.Name == "" {
if g.YAML == "" {
return nil, xerrors.Errorf(
"group name is empty for %q, groups: %+v",
"group yaml name is empty for %q, groups: %+v",
opt.Name,
opt.Group,
)
}
group = append(group, strcase.ToLowerCamel(g.Name))
group = append(group, g.YAML)
}
var groupDesc string
if opt.Group != nil {
@@ -103,3 +149,147 @@ func (s OptionSet) ToYAML() (*yaml.Node, error) {
}
return &root, nil
}
// mapYAMLNodes converts parent into a map with keys of form "group.subgroup.option"
// and values as the corresponding YAML nodes.
func mapYAMLNodes(parent *yaml.Node) (map[string]*yaml.Node, error) {
if parent.Kind != yaml.MappingNode {
return nil, xerrors.Errorf("expected mapping node, got type %v", parent.Kind)
}
if len(parent.Content)%2 != 0 {
return nil, xerrors.Errorf("expected an even number of k/v pairs, got %d", len(parent.Content))
}
var (
key string
m = make(map[string]*yaml.Node, len(parent.Content)/2)
merr error
)
for i, child := range parent.Content {
if i%2 == 0 {
if child.Kind != yaml.ScalarNode {
// We immediately because the rest of the code is bound to fail
// if we don't know to expect a key or a value.
return nil, xerrors.Errorf("expected scalar node for key, got type %v", child.Kind)
}
key = child.Value
continue
}
// We don't know if this is a grouped simple option or complex option,
// so we store both "key" and "group.key". Since we're storing pointers,
// the additional memory is of little concern.
m[key] = child
if child.Kind != yaml.MappingNode {
continue
}
sub, err := mapYAMLNodes(child)
if err != nil {
merr = errors.Join(merr, xerrors.Errorf("mapping node %q: %w", key, err))
continue
}
for k, v := range sub {
m[key+"."+k] = v
}
}
return m, nil
}
func (o *Option) setFromYAMLNode(n *yaml.Node) error {
o.ValueSource = ValueSourceYAML
if um, ok := o.Value.(yaml.Unmarshaler); ok {
return um.UnmarshalYAML(n)
}
switch n.Kind {
case yaml.ScalarNode:
return o.Value.Set(n.Value)
case yaml.SequenceNode:
// We treat empty values as nil for consistency with other option
// mechanisms.
if len(n.Content) == 0 {
o.Value = nil
return nil
}
return n.Decode(o.Value)
case yaml.MappingNode:
return xerrors.Errorf("mapping nodes must implement yaml.Unmarshaler")
default:
return xerrors.Errorf("unexpected node kind %v", n.Kind)
}
}
// UnmarshalYAML converts the given YAML node into the option set.
// It is isomorphic with ToYAML.
func (s *OptionSet) UnmarshalYAML(rootNode *yaml.Node) error {
// The rootNode will be a DocumentNode if it's read from a file. We do
// not support multiple documents in a single file.
if rootNode.Kind == yaml.DocumentNode {
if len(rootNode.Content) != 1 {
return xerrors.Errorf("expected one node in document, got %d", len(rootNode.Content))
}
rootNode = rootNode.Content[0]
}
yamlNodes, err := mapYAMLNodes(rootNode)
if err != nil {
return xerrors.Errorf("mapping nodes: %w", err)
}
matchedNodes := make(map[string]*yaml.Node, len(yamlNodes))
var merr error
for i := range *s {
opt := &(*s)[i]
if opt.YAML == "" {
continue
}
var group []string
for _, g := range opt.Group.Ancestry() {
if g.YAML == "" {
return xerrors.Errorf(
"group yaml name is empty for %q, groups: %+v",
opt.Name,
opt.Group,
)
}
group = append(group, g.YAML)
delete(yamlNodes, strings.Join(group, "."))
}
key := strings.Join(append(group, opt.YAML), ".")
node, ok := yamlNodes[key]
if !ok {
continue
}
matchedNodes[key] = node
if opt.ValueSource != ValueSourceNone {
continue
}
if err := opt.setFromYAMLNode(node); err != nil {
merr = errors.Join(merr, xerrors.Errorf("setting %q: %w", opt.YAML, err))
}
}
// Remove all matched nodes and their descendants from yamlNodes so we
// can accurately report unknown options.
for k := range yamlNodes {
var key string
for _, part := range strings.Split(k, ".") {
if key != "" {
key += "."
}
key += part
if _, ok := matchedNodes[key]; ok {
delete(yamlNodes, k)
}
}
}
for k := range yamlNodes {
merr = errors.Join(merr, xerrors.Errorf("unknown option %q", k))
}
return merr
}
+150 -5
View File
@@ -3,13 +3,15 @@ package clibase_test
import (
"testing"
"github.com/spf13/pflag"
"github.com/stretchr/testify/require"
"golang.org/x/exp/slices"
"gopkg.in/yaml.v3"
"github.com/coder/coder/cli/clibase"
)
func TestOption_ToYAML(t *testing.T) {
func TestOptionSet_YAML(t *testing.T) {
t.Parallel()
t.Run("RequireKey", func(t *testing.T) {
@@ -23,9 +25,9 @@ func TestOption_ToYAML(t *testing.T) {
},
}
node, err := os.ToYAML()
node, err := os.MarshalYAML()
require.NoError(t, err)
require.Len(t, node.Content, 0)
require.Len(t, node.(*yaml.Node).Content, 0)
})
t.Run("SimpleString", func(t *testing.T) {
@@ -39,7 +41,7 @@ func TestOption_ToYAML(t *testing.T) {
Value: &workspaceName,
Default: "billie",
Description: "The workspace's name.",
Group: &clibase.Group{Name: "Names"},
Group: &clibase.Group{YAML: "names"},
YAML: "workspaceName",
},
}
@@ -47,7 +49,7 @@ func TestOption_ToYAML(t *testing.T) {
err := os.SetDefaults()
require.NoError(t, err)
n, err := os.ToYAML()
n, err := os.MarshalYAML()
require.NoError(t, err)
// Visually inspect for now.
byt, err := yaml.Marshal(n)
@@ -55,3 +57,146 @@ func TestOption_ToYAML(t *testing.T) {
t.Logf("Raw YAML:\n%s", string(byt))
})
}
func TestOptionSet_YAMLUnknownOptions(t *testing.T) {
t.Parallel()
os := clibase.OptionSet{
{
Name: "Workspace Name",
Default: "billie",
Description: "The workspace's name.",
YAML: "workspaceName",
Value: new(clibase.String),
},
}
const yamlDoc = `something: else`
err := yaml.Unmarshal([]byte(yamlDoc), &os)
require.Error(t, err)
require.Empty(t, os[0].Value.String())
os[0].YAML = "something"
err = yaml.Unmarshal([]byte(yamlDoc), &os)
require.NoError(t, err)
require.Equal(t, "else", os[0].Value.String())
}
// TestOptionSet_YAMLIsomorphism tests that the YAML representations of an
// OptionSet converts to the same OptionSet when read back in.
func TestOptionSet_YAMLIsomorphism(t *testing.T) {
t.Parallel()
// This is used to form a generic.
//nolint:unused
type kid struct {
Name string `yaml:"name"`
Age int `yaml:"age"`
}
for _, tc := range []struct {
name string
os clibase.OptionSet
zeroValue func() pflag.Value
}{
{
name: "SimpleString",
os: clibase.OptionSet{
{
Name: "Workspace Name",
Default: "billie",
Description: "The workspace's name.",
Group: &clibase.Group{YAML: "names"},
YAML: "workspaceName",
},
},
zeroValue: func() pflag.Value {
return clibase.StringOf(new(string))
},
},
{
name: "Array",
os: clibase.OptionSet{
{
YAML: "names",
Default: "jill,jack,joan",
},
},
zeroValue: func() pflag.Value {
return clibase.StringArrayOf(&[]string{})
},
},
{
name: "ComplexObject",
os: clibase.OptionSet{
{
YAML: "kids",
Default: `- name: jill
age: 12
- name: jack
age: 13`,
},
},
zeroValue: func() pflag.Value {
return &clibase.Struct[[]kid]{}
},
},
{
name: "DeepGroup",
os: clibase.OptionSet{
{
YAML: "names",
Default: "jill,jack,joan",
Group: &clibase.Group{YAML: "kids", Parent: &clibase.Group{YAML: "family"}},
},
},
zeroValue: func() pflag.Value {
return clibase.StringArrayOf(&[]string{})
},
},
} {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Set initial values.
for i := range tc.os {
tc.os[i].Value = tc.zeroValue()
}
err := tc.os.SetDefaults()
require.NoError(t, err)
y, err := tc.os.MarshalYAML()
require.NoError(t, err)
toByt, err := yaml.Marshal(y)
require.NoError(t, err)
t.Logf("Raw YAML:\n%s", string(toByt))
var y2 yaml.Node
err = yaml.Unmarshal(toByt, &y2)
require.NoError(t, err)
os2 := slices.Clone(tc.os)
for i := range os2 {
os2[i].Value = tc.zeroValue()
os2[i].ValueSource = clibase.ValueSourceNone
}
// os2 values should be zeroed whereas tc.os should be
// set to defaults.
// This check makes sure we aren't mixing pointers.
require.NotEqual(t, tc.os, os2)
err = os2.UnmarshalYAML(&y2)
require.NoError(t, err)
want := tc.os
for i := range want {
want[i].ValueSource = clibase.ValueSourceYAML
}
require.Equal(t, tc.os, os2)
})
}
}
+360
View File
@@ -0,0 +1,360 @@
package clistat
import (
"bufio"
"bytes"
"strconv"
"strings"
"github.com/hashicorp/go-multierror"
"github.com/spf13/afero"
"golang.org/x/xerrors"
"tailscale.com/types/ptr"
)
// Paths for CGroupV1.
// Ref: https://www.kernel.org/doc/Documentation/cgroup-v1/cpuacct.txt
const (
// CPU usage of all tasks in cgroup in nanoseconds.
cgroupV1CPUAcctUsage = "/sys/fs/cgroup/cpu,cpuacct/cpuacct.usage"
// CFS quota and period for cgroup in MICROseconds
cgroupV1CFSQuotaUs = "/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_quota_us"
// CFS period for cgroup in MICROseconds
cgroupV1CFSPeriodUs = "/sys/fs/cgroup/cpu,cpuacct/cpu.cfs_period_us"
// Maximum memory usable by cgroup in bytes
cgroupV1MemoryMaxUsageBytes = "/sys/fs/cgroup/memory/memory.limit_in_bytes"
// Current memory usage of cgroup in bytes
cgroupV1MemoryUsageBytes = "/sys/fs/cgroup/memory/memory.usage_in_bytes"
// Other memory stats - we are interested in total_inactive_file
cgroupV1MemoryStat = "/sys/fs/cgroup/memory/memory.stat"
)
// Paths for CGroupV2.
// Ref: https://docs.kernel.org/admin-guide/cgroup-v2.html
const (
// Contains quota and period in microseconds separated by a space.
cgroupV2CPUMax = "/sys/fs/cgroup/cpu.max"
// Contains current CPU usage under usage_usec
cgroupV2CPUStat = "/sys/fs/cgroup/cpu.stat"
// Contains current cgroup memory usage in bytes.
cgroupV2MemoryUsageBytes = "/sys/fs/cgroup/memory.current"
// Contains max cgroup memory usage in bytes.
cgroupV2MemoryMaxBytes = "/sys/fs/cgroup/memory.max"
// Other memory stats - we are interested in total_inactive_file
cgroupV2MemoryStat = "/sys/fs/cgroup/memory.stat"
)
// ContainerCPU returns the CPU usage of the container cgroup.
// This is calculated as difference of two samples of the
// CPU usage of the container cgroup.
// The total is read from the relevant path in /sys/fs/cgroup.
// If there is no limit set, the total is assumed to be the
// number of host cores multiplied by the CFS period.
// If the system is not containerized, this always returns nil.
func (s *Statter) ContainerCPU() (*Result, error) {
// Firstly, check if we are containerized.
if ok, err := IsContainerized(s.fs); err != nil || !ok {
return nil, nil //nolint: nilnil
}
total, err := s.cGroupCPUTotal()
if err != nil {
return nil, xerrors.Errorf("get total cpu: %w", err)
}
used1, err := s.cGroupCPUUsed()
if err != nil {
return nil, xerrors.Errorf("get cgroup CPU usage: %w", err)
}
// The measurements in /sys/fs/cgroup are counters.
// We need to wait for a bit to get a difference.
// Note that someone could reset the counter in the meantime.
// We can't do anything about that.
s.wait(s.sampleInterval)
used2, err := s.cGroupCPUUsed()
if err != nil {
return nil, xerrors.Errorf("get cgroup CPU usage: %w", err)
}
if used2 < used1 {
// Someone reset the counter. Best we can do is count from zero.
used1 = 0
}
r := &Result{
Unit: "cores",
Used: used2 - used1,
Prefix: PrefixDefault,
}
if total > 0 {
r.Total = ptr.To(total)
}
return r, nil
}
func (s *Statter) cGroupCPUTotal() (used float64, err error) {
if s.isCGroupV2() {
return s.cGroupV2CPUTotal()
}
// Fall back to CGroupv1
return s.cGroupV1CPUTotal()
}
func (s *Statter) cGroupCPUUsed() (used float64, err error) {
if s.isCGroupV2() {
return s.cGroupV2CPUUsed()
}
return s.cGroupV1CPUUsed()
}
func (s *Statter) isCGroupV2() bool {
// Check for the presence of /sys/fs/cgroup/cpu.max
_, err := s.fs.Stat(cgroupV2CPUMax)
return err == nil
}
func (s *Statter) cGroupV2CPUUsed() (used float64, err error) {
usageUs, err := readInt64Prefix(s.fs, cgroupV2CPUStat, "usage_usec")
if err != nil {
return 0, xerrors.Errorf("get cgroupv2 cpu used: %w", err)
}
periodUs, err := readInt64SepIdx(s.fs, cgroupV2CPUMax, " ", 1)
if err != nil {
return 0, xerrors.Errorf("get cpu period: %w", err)
}
return float64(usageUs) / float64(periodUs), nil
}
func (s *Statter) cGroupV2CPUTotal() (total float64, err error) {
var quotaUs, periodUs int64
periodUs, err = readInt64SepIdx(s.fs, cgroupV2CPUMax, " ", 1)
if err != nil {
return 0, xerrors.Errorf("get cpu period: %w", err)
}
quotaUs, err = readInt64SepIdx(s.fs, cgroupV2CPUMax, " ", 0)
if err != nil {
if xerrors.Is(err, strconv.ErrSyntax) {
// If the value is not a valid integer, assume it is the string
// 'max' and that there is no limit set.
return -1, nil
}
return 0, xerrors.Errorf("get cpu quota: %w", err)
}
return float64(quotaUs) / float64(periodUs), nil
}
func (s *Statter) cGroupV1CPUTotal() (float64, error) {
periodUs, err := readInt64(s.fs, cgroupV1CFSPeriodUs)
if err != nil {
// Try alternate path under /sys/fs/cpu
var merr error
merr = multierror.Append(merr, xerrors.Errorf("get cpu period: %w", err))
periodUs, err = readInt64(s.fs, strings.Replace(cgroupV1CFSPeriodUs, "cpu,cpuacct", "cpu", 1))
if err != nil {
merr = multierror.Append(merr, xerrors.Errorf("get cpu period: %w", err))
return 0, merr
}
}
quotaUs, err := readInt64(s.fs, cgroupV1CFSQuotaUs)
if err != nil {
// Try alternate path under /sys/fs/cpu
var merr error
merr = multierror.Append(merr, xerrors.Errorf("get cpu quota: %w", err))
quotaUs, err = readInt64(s.fs, strings.Replace(cgroupV1CFSQuotaUs, "cpu,cpuacct", "cpu", 1))
if err != nil {
merr = multierror.Append(merr, xerrors.Errorf("get cpu quota: %w", err))
return 0, merr
}
}
if quotaUs < 0 {
return -1, nil
}
return float64(quotaUs) / float64(periodUs), nil
}
func (s *Statter) cGroupV1CPUUsed() (float64, error) {
usageNs, err := readInt64(s.fs, cgroupV1CPUAcctUsage)
if err != nil {
// Try alternate path under /sys/fs/cgroup/cpuacct
var merr error
merr = multierror.Append(merr, xerrors.Errorf("read cpu used: %w", err))
usageNs, err = readInt64(s.fs, strings.Replace(cgroupV1CPUAcctUsage, "cpu,cpuacct", "cpuacct", 1))
if err != nil {
merr = multierror.Append(merr, xerrors.Errorf("read cpu used: %w", err))
return 0, merr
}
}
// usage is in ns, convert to us
usageNs /= 1000
periodUs, err := readInt64(s.fs, cgroupV1CFSPeriodUs)
if err != nil {
// Try alternate path under /sys/fs/cpu
var merr error
merr = multierror.Append(merr, xerrors.Errorf("get cpu period: %w", err))
periodUs, err = readInt64(s.fs, strings.Replace(cgroupV1CFSPeriodUs, "cpu,cpuacct", "cpu", 1))
if err != nil {
merr = multierror.Append(merr, xerrors.Errorf("get cpu period: %w", err))
return 0, merr
}
}
return float64(usageNs) / float64(periodUs), nil
}
// ContainerMemory returns the memory usage of the container cgroup.
// If the system is not containerized, this always returns nil.
func (s *Statter) ContainerMemory(p Prefix) (*Result, error) {
if ok, err := IsContainerized(s.fs); err != nil || !ok {
return nil, nil //nolint:nilnil
}
if s.isCGroupV2() {
return s.cGroupV2Memory(p)
}
// Fall back to CGroupv1
return s.cGroupV1Memory(p)
}
func (s *Statter) cGroupV2Memory(p Prefix) (*Result, error) {
r := &Result{
Unit: "B",
Prefix: p,
}
maxUsageBytes, err := readInt64(s.fs, cgroupV2MemoryMaxBytes)
if err != nil {
if !xerrors.Is(err, strconv.ErrSyntax) {
return nil, xerrors.Errorf("read memory total: %w", err)
}
// If the value is not a valid integer, assume it is the string
// 'max' and that there is no limit set.
} else {
r.Total = ptr.To(float64(maxUsageBytes))
}
currUsageBytes, err := readInt64(s.fs, cgroupV2MemoryUsageBytes)
if err != nil {
return nil, xerrors.Errorf("read memory usage: %w", err)
}
inactiveFileBytes, err := readInt64Prefix(s.fs, cgroupV2MemoryStat, "inactive_file")
if err != nil {
return nil, xerrors.Errorf("read memory stats: %w", err)
}
r.Used = float64(currUsageBytes - inactiveFileBytes)
return r, nil
}
func (s *Statter) cGroupV1Memory(p Prefix) (*Result, error) {
r := &Result{
Unit: "B",
Prefix: p,
}
maxUsageBytes, err := readInt64(s.fs, cgroupV1MemoryMaxUsageBytes)
if err != nil {
if !xerrors.Is(err, strconv.ErrSyntax) {
return nil, xerrors.Errorf("read memory total: %w", err)
}
// I haven't found an instance where this isn't a valid integer.
// Nonetheless, if it is not, assume there is no limit set.
maxUsageBytes = -1
}
// need a space after total_rss so we don't hit something else
usageBytes, err := readInt64(s.fs, cgroupV1MemoryUsageBytes)
if err != nil {
return nil, xerrors.Errorf("read memory usage: %w", err)
}
totalInactiveFileBytes, err := readInt64Prefix(s.fs, cgroupV1MemoryStat, "total_inactive_file")
if err != nil {
return nil, xerrors.Errorf("read memory stats: %w", err)
}
// If max usage bytes is -1, there is no memory limit set.
if maxUsageBytes > 0 {
r.Total = ptr.To(float64(maxUsageBytes))
}
// Total memory used is usage - total_inactive_file
r.Used = float64(usageBytes - totalInactiveFileBytes)
return r, nil
}
// read an int64 value from path
func readInt64(fs afero.Fs, path string) (int64, error) {
data, err := afero.ReadFile(fs, path)
if err != nil {
return 0, xerrors.Errorf("read %s: %w", path, err)
}
val, err := strconv.ParseInt(string(bytes.TrimSpace(data)), 10, 64)
if err != nil {
return 0, xerrors.Errorf("parse %s: %w", path, err)
}
return val, nil
}
// read an int64 value from path at field idx separated by sep
func readInt64SepIdx(fs afero.Fs, path, sep string, idx int) (int64, error) {
data, err := afero.ReadFile(fs, path)
if err != nil {
return 0, xerrors.Errorf("read %s: %w", path, err)
}
parts := strings.Split(string(data), sep)
if len(parts) < idx {
return 0, xerrors.Errorf("expected line %q to have at least %d parts", string(data), idx+1)
}
val, err := strconv.ParseInt(strings.TrimSpace(parts[idx]), 10, 64)
if err != nil {
return 0, xerrors.Errorf("parse %s: %w", path, err)
}
return val, nil
}
// read the first int64 value from path prefixed with prefix
func readInt64Prefix(fs afero.Fs, path, prefix string) (int64, error) {
data, err := afero.ReadFile(fs, path)
if err != nil {
return 0, xerrors.Errorf("read %s: %w", path, err)
}
scn := bufio.NewScanner(bytes.NewReader(data))
for scn.Scan() {
line := strings.TrimSpace(scn.Text())
if !strings.HasPrefix(line, prefix) {
continue
}
parts := strings.Fields(line)
if len(parts) != 2 {
return 0, xerrors.Errorf("parse %s: expected two fields but got %s", path, line)
}
val, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64)
if err != nil {
return 0, xerrors.Errorf("parse %s: %w", path, err)
}
return val, nil
}
return 0, xerrors.Errorf("parse %s: did not find line with prefix %s", path, prefix)
}
+70
View File
@@ -0,0 +1,70 @@
package clistat
import (
"bufio"
"bytes"
"os"
"github.com/spf13/afero"
"golang.org/x/xerrors"
)
const (
procMounts = "/proc/mounts"
procOneCgroup = "/proc/1/cgroup"
kubernetesDefaultServiceAccountToken = "/var/run/secrets/kubernetes.io/serviceaccount/token" //nolint:gosec
)
// IsContainerized returns whether the host is containerized.
// This is adapted from https://github.com/elastic/go-sysinfo/tree/main/providers/linux/container.go#L31
// with modifications to support Sysbox containers.
// On non-Linux platforms, it always returns false.
func IsContainerized(fs afero.Fs) (ok bool, err error) {
cgData, err := afero.ReadFile(fs, procOneCgroup)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, xerrors.Errorf("read file %s: %w", procOneCgroup, err)
}
scn := bufio.NewScanner(bytes.NewReader(cgData))
for scn.Scan() {
line := scn.Bytes()
if bytes.Contains(line, []byte("docker")) ||
bytes.Contains(line, []byte(".slice")) ||
bytes.Contains(line, []byte("lxc")) ||
bytes.Contains(line, []byte("kubepods")) {
return true, nil
}
}
// Sometimes the above method of sniffing /proc/1/cgroup isn't reliable.
// If a Kubernetes service account token is present, that's
// also a good indication that we are in a container.
_, err = afero.ReadFile(fs, kubernetesDefaultServiceAccountToken)
if err == nil {
return true, nil
}
// Last-ditch effort to detect Sysbox containers.
// Check if we have anything mounted as type sysboxfs in /proc/mounts
mountsData, err := afero.ReadFile(fs, procMounts)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, xerrors.Errorf("read file %s: %w", procMounts, err)
}
scn = bufio.NewScanner(bytes.NewReader(mountsData))
for scn.Scan() {
line := scn.Bytes()
if bytes.Contains(line, []byte("sysboxfs")) {
return true, nil
}
}
// If we get here, we are _probably_ not running in a container.
return false, nil
}
+27
View File
@@ -0,0 +1,27 @@
//go:build !windows
package clistat
import (
"syscall"
"tailscale.com/types/ptr"
)
// Disk returns the disk usage of the given path.
// If path is empty, it returns the usage of the root directory.
func (*Statter) Disk(p Prefix, path string) (*Result, error) {
if path == "" {
path = "/"
}
var stat syscall.Statfs_t
if err := syscall.Statfs(path, &stat); err != nil {
return nil, err
}
var r Result
r.Total = ptr.To(float64(stat.Blocks * uint64(stat.Bsize)))
r.Used = float64(stat.Blocks-stat.Bfree) * float64(stat.Bsize)
r.Unit = "B"
r.Prefix = p
return &r, nil
}
+36
View File
@@ -0,0 +1,36 @@
package clistat
import (
"golang.org/x/sys/windows"
"tailscale.com/types/ptr"
)
// Disk returns the disk usage of the given path.
// If path is empty, it defaults to C:\
func (*Statter) Disk(p Prefix, path string) (*Result, error) {
if path == "" {
path = `C:\`
}
pathPtr, err := windows.UTF16PtrFromString(path)
if err != nil {
return nil, err
}
var freeBytes, totalBytes, availBytes uint64
if err := windows.GetDiskFreeSpaceEx(
pathPtr,
&freeBytes,
&totalBytes,
&availBytes,
); err != nil {
return nil, err
}
var r Result
r.Total = ptr.To(float64(totalBytes))
r.Used = float64(totalBytes - freeBytes)
r.Unit = "B"
r.Prefix = p
return &r, nil
}
+236
View File
@@ -0,0 +1,236 @@
package clistat
import (
"math"
"runtime"
"strconv"
"strings"
"time"
"github.com/elastic/go-sysinfo"
"github.com/spf13/afero"
"golang.org/x/xerrors"
"tailscale.com/types/ptr"
sysinfotypes "github.com/elastic/go-sysinfo/types"
)
// Prefix is a scale multiplier for a result.
// Used when creating a human-readable representation.
type Prefix float64
const (
PrefixDefault = 1.0
PrefixKibi = 1024.0
PrefixMebi = PrefixKibi * 1024.0
PrefixGibi = PrefixMebi * 1024.0
PrefixTebi = PrefixGibi * 1024.0
)
var (
PrefixHumanKibi = "Ki"
PrefixHumanMebi = "Mi"
PrefixHumanGibi = "Gi"
PrefixHumanTebi = "Ti"
)
func (s *Prefix) String() string {
switch *s {
case PrefixKibi:
return "Ki"
case PrefixMebi:
return "Mi"
case PrefixGibi:
return "Gi"
case PrefixTebi:
return "Ti"
default:
return ""
}
}
func ParsePrefix(s string) Prefix {
switch s {
case PrefixHumanKibi:
return PrefixKibi
case PrefixHumanMebi:
return PrefixMebi
case PrefixHumanGibi:
return PrefixGibi
case PrefixHumanTebi:
return PrefixTebi
default:
return PrefixDefault
}
}
// Result is a generic result type for a statistic.
// Total is the total amount of the resource available.
// It is nil if the resource is not a finite quantity.
// Unit is the unit of the resource.
// Used is the amount of the resource used.
type Result struct {
Total *float64 `json:"total"`
Unit string `json:"unit"`
Used float64 `json:"used"`
Prefix Prefix `json:"-"`
}
// String returns a human-readable representation of the result.
func (r *Result) String() string {
if r == nil {
return "-"
}
scale := 1.0
if r.Prefix != 0.0 {
scale = float64(r.Prefix)
}
var sb strings.Builder
var usedScaled, totalScaled float64
usedScaled = r.Used / scale
_, _ = sb.WriteString(humanizeFloat(usedScaled))
if r.Total != (*float64)(nil) {
_, _ = sb.WriteString("/")
totalScaled = *r.Total / scale
_, _ = sb.WriteString(humanizeFloat(totalScaled))
}
_, _ = sb.WriteString(" ")
_, _ = sb.WriteString(r.Prefix.String())
_, _ = sb.WriteString(r.Unit)
if r.Total != (*float64)(nil) && *r.Total > 0 {
_, _ = sb.WriteString(" (")
pct := r.Used / *r.Total * 100.0
_, _ = sb.WriteString(strconv.FormatFloat(pct, 'f', 0, 64))
_, _ = sb.WriteString("%)")
}
return strings.TrimSpace(sb.String())
}
func humanizeFloat(f float64) string {
// humanize.FtoaWithDigits does not round correctly.
prec := precision(f)
rat := math.Pow(10, float64(prec))
rounded := math.Round(f*rat) / rat
return strconv.FormatFloat(rounded, 'f', -1, 64)
}
// limit precision to 3 digits at most to preserve space
func precision(f float64) int {
fabs := math.Abs(f)
if fabs == 0.0 {
return 0
}
if fabs < 1.0 {
return 3
}
if fabs < 10.0 {
return 2
}
if fabs < 100.0 {
return 1
}
return 0
}
// Statter is a system statistics collector.
// It is a thin wrapper around the elastic/go-sysinfo library.
type Statter struct {
hi sysinfotypes.Host
fs afero.Fs
sampleInterval time.Duration
nproc int
wait func(time.Duration)
}
type Option func(*Statter)
// WithSampleInterval sets the sample interval for the statter.
func WithSampleInterval(d time.Duration) Option {
return func(s *Statter) {
s.sampleInterval = d
}
}
// WithFS sets the fs for the statter.
func WithFS(fs afero.Fs) Option {
return func(s *Statter) {
s.fs = fs
}
}
func New(opts ...Option) (*Statter, error) {
hi, err := sysinfo.Host()
if err != nil {
return nil, xerrors.Errorf("get host info: %w", err)
}
s := &Statter{
hi: hi,
fs: afero.NewReadOnlyFs(afero.NewOsFs()),
sampleInterval: 100 * time.Millisecond,
nproc: runtime.NumCPU(),
wait: func(d time.Duration) {
<-time.After(d)
},
}
for _, opt := range opts {
opt(s)
}
return s, nil
}
// HostCPU returns the CPU usage of the host. This is calculated by
// taking two samples of CPU usage and calculating the difference.
// Total will always be equal to the number of cores.
// Used will be an estimate of the number of cores used during the sample interval.
// This is calculated by taking the difference between the total and idle HostCPU time
// and scaling it by the number of cores.
// Units are in "cores".
func (s *Statter) HostCPU() (*Result, error) {
r := &Result{
Unit: "cores",
Total: ptr.To(float64(s.nproc)),
Prefix: PrefixDefault,
}
c1, err := s.hi.CPUTime()
if err != nil {
return nil, xerrors.Errorf("get first cpu sample: %w", err)
}
s.wait(s.sampleInterval)
c2, err := s.hi.CPUTime()
if err != nil {
return nil, xerrors.Errorf("get second cpu sample: %w", err)
}
total := c2.Total() - c1.Total()
if total == 0 {
return r, nil // no change
}
idle := c2.Idle - c1.Idle
used := total - idle
scaleFactor := float64(s.nproc) / total.Seconds()
r.Used = used.Seconds() * scaleFactor
return r, nil
}
// HostMemory returns the memory usage of the host, in gigabytes.
func (s *Statter) HostMemory(p Prefix) (*Result, error) {
r := &Result{
Unit: "B",
Prefix: p,
}
hm, err := s.hi.Memory()
if err != nil {
return nil, xerrors.Errorf("get memory info: %w", err)
}
r.Total = ptr.To(float64(hm.Total))
// On Linux, hm.Used equates to MemTotal - MemFree in /proc/stat.
// This includes buffers and cache.
// So use MemAvailable instead, which only equates to physical memory.
// On Windows, this is also calculated as Total - Available.
r.Used = float64(hm.Total - hm.Available)
return r, nil
}
+398
View File
@@ -0,0 +1,398 @@
package clistat
import (
"testing"
"time"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tailscale.com/types/ptr"
)
func TestResultString(t *testing.T) {
t.Parallel()
for _, tt := range []struct {
Expected string
Result Result
}{
{
Expected: "1.23/5.68 quatloos (22%)",
Result: Result{Used: 1.234, Total: ptr.To(5.678), Unit: "quatloos"},
},
{
Expected: "0/0 HP",
Result: Result{Used: 0.0, Total: ptr.To(0.0), Unit: "HP"},
},
{
Expected: "123 seconds",
Result: Result{Used: 123.01, Total: nil, Unit: "seconds"},
},
{
Expected: "12.3",
Result: Result{Used: 12.34, Total: nil, Unit: ""},
},
{
Expected: "1.5 KiB",
Result: Result{Used: 1536, Total: nil, Unit: "B", Prefix: PrefixKibi},
},
{
Expected: "1.23 things",
Result: Result{Used: 1.234, Total: nil, Unit: "things"},
},
{
Expected: "0/100 TiB (0%)",
Result: Result{Used: 1, Total: ptr.To(100.0 * float64(PrefixTebi)), Unit: "B", Prefix: PrefixTebi},
},
{
Expected: "0.5/8 cores (6%)",
Result: Result{Used: 0.5, Total: ptr.To(8.0), Unit: "cores"},
},
} {
assert.Equal(t, tt.Expected, tt.Result.String())
}
}
func TestStatter(t *testing.T) {
t.Parallel()
// We cannot make many assertions about the data we get back
// for host-specific measurements because these tests could
// and should run successfully on any OS.
// The best we can do is assert that it is non-zero.
t.Run("HostOnly", func(t *testing.T) {
t.Parallel()
fs := initFS(t, fsHostOnly)
s, err := New(WithFS(fs))
require.NoError(t, err)
t.Run("HostCPU", func(t *testing.T) {
t.Parallel()
cpu, err := s.HostCPU()
require.NoError(t, err)
// assert.NotZero(t, cpu.Used) // HostCPU can sometimes be zero.
assert.NotZero(t, cpu.Total)
assert.Equal(t, "cores", cpu.Unit)
})
t.Run("HostMemory", func(t *testing.T) {
t.Parallel()
mem, err := s.HostMemory(PrefixDefault)
require.NoError(t, err)
assert.NotZero(t, mem.Used)
assert.NotZero(t, mem.Total)
assert.Equal(t, "B", mem.Unit)
})
t.Run("HostDisk", func(t *testing.T) {
t.Parallel()
disk, err := s.Disk(PrefixDefault, "") // default to home dir
require.NoError(t, err)
assert.NotZero(t, disk.Used)
assert.NotZero(t, disk.Total)
assert.Equal(t, "B", disk.Unit)
})
})
// Sometimes we do need to "fake" some stuff
// that happens while we wait.
withWait := func(waitF func(time.Duration)) Option {
return func(s *Statter) {
s.wait = waitF
}
}
// Other times we just want things to run fast.
withNoWait := func(s *Statter) {
s.wait = func(time.Duration) {}
}
// We don't want to use the actual host CPU here.
withNproc := func(n int) Option {
return func(s *Statter) {
s.nproc = n
}
}
// For container-specific measurements, everything we need
// can be read from the filesystem. We control the FS, so
// we control the data.
t.Run("CGroupV1", func(t *testing.T) {
t.Parallel()
t.Run("ContainerCPU/Limit", func(t *testing.T) {
t.Parallel()
fs := initFS(t, fsContainerCgroupV1)
fakeWait := func(time.Duration) {
// Fake 1 second in ns of usage
mungeFS(t, fs, cgroupV1CPUAcctUsage, "100000000")
}
s, err := New(WithFS(fs), withWait(fakeWait))
require.NoError(t, err)
cpu, err := s.ContainerCPU()
require.NoError(t, err)
require.NotNil(t, cpu)
assert.Equal(t, 1.0, cpu.Used)
require.NotNil(t, cpu.Total)
assert.Equal(t, 2.5, *cpu.Total)
assert.Equal(t, "cores", cpu.Unit)
})
t.Run("ContainerCPU/NoLimit", func(t *testing.T) {
t.Parallel()
fs := initFS(t, fsContainerCgroupV1NoLimit)
fakeWait := func(time.Duration) {
// Fake 1 second in ns of usage
mungeFS(t, fs, cgroupV1CPUAcctUsage, "100000000")
}
s, err := New(WithFS(fs), withNproc(2), withWait(fakeWait))
require.NoError(t, err)
cpu, err := s.ContainerCPU()
require.NoError(t, err)
require.NotNil(t, cpu)
assert.Equal(t, 1.0, cpu.Used)
require.Nil(t, cpu.Total)
assert.Equal(t, "cores", cpu.Unit)
})
t.Run("ContainerCPU/AltPath", func(t *testing.T) {
t.Parallel()
fs := initFS(t, fsContainerCgroupV1AltPath)
fakeWait := func(time.Duration) {
// Fake 1 second in ns of usage
mungeFS(t, fs, "/sys/fs/cgroup/cpuacct/cpuacct.usage", "100000000")
}
s, err := New(WithFS(fs), withNproc(2), withWait(fakeWait))
require.NoError(t, err)
cpu, err := s.ContainerCPU()
require.NoError(t, err)
require.NotNil(t, cpu)
assert.Equal(t, 1.0, cpu.Used)
require.NotNil(t, cpu.Total)
assert.Equal(t, 2.5, *cpu.Total)
assert.Equal(t, "cores", cpu.Unit)
})
t.Run("ContainerMemory", func(t *testing.T) {
t.Parallel()
fs := initFS(t, fsContainerCgroupV1)
s, err := New(WithFS(fs), withNoWait)
require.NoError(t, err)
mem, err := s.ContainerMemory(PrefixDefault)
require.NoError(t, err)
require.NotNil(t, mem)
assert.Equal(t, 268435456.0, mem.Used)
assert.NotNil(t, mem.Total)
assert.Equal(t, 1073741824.0, *mem.Total)
assert.Equal(t, "B", mem.Unit)
})
t.Run("ContainerMemory/NoLimit", func(t *testing.T) {
t.Parallel()
fs := initFS(t, fsContainerCgroupV1NoLimit)
s, err := New(WithFS(fs), withNoWait)
require.NoError(t, err)
mem, err := s.ContainerMemory(PrefixDefault)
require.NoError(t, err)
require.NotNil(t, mem)
assert.Equal(t, 268435456.0, mem.Used)
assert.Nil(t, mem.Total)
assert.Equal(t, "B", mem.Unit)
})
})
t.Run("CGroupV2", func(t *testing.T) {
t.Parallel()
t.Run("ContainerCPU/Limit", func(t *testing.T) {
t.Parallel()
fs := initFS(t, fsContainerCgroupV2)
fakeWait := func(time.Duration) {
mungeFS(t, fs, cgroupV2CPUStat, "usage_usec 100000")
}
s, err := New(WithFS(fs), withWait(fakeWait))
require.NoError(t, err)
cpu, err := s.ContainerCPU()
require.NoError(t, err)
require.NotNil(t, cpu)
assert.Equal(t, 1.0, cpu.Used)
require.NotNil(t, cpu.Total)
assert.Equal(t, 2.5, *cpu.Total)
assert.Equal(t, "cores", cpu.Unit)
})
t.Run("ContainerCPU/NoLimit", func(t *testing.T) {
t.Parallel()
fs := initFS(t, fsContainerCgroupV2NoLimit)
fakeWait := func(time.Duration) {
mungeFS(t, fs, cgroupV2CPUStat, "usage_usec 100000")
}
s, err := New(WithFS(fs), withNproc(2), withWait(fakeWait))
require.NoError(t, err)
cpu, err := s.ContainerCPU()
require.NoError(t, err)
require.NotNil(t, cpu)
assert.Equal(t, 1.0, cpu.Used)
require.Nil(t, cpu.Total)
assert.Equal(t, "cores", cpu.Unit)
})
t.Run("ContainerMemory/Limit", func(t *testing.T) {
t.Parallel()
fs := initFS(t, fsContainerCgroupV2)
s, err := New(WithFS(fs), withNoWait)
require.NoError(t, err)
mem, err := s.ContainerMemory(PrefixDefault)
require.NoError(t, err)
require.NotNil(t, mem)
assert.Equal(t, 268435456.0, mem.Used)
assert.NotNil(t, mem.Total)
assert.Equal(t, 1073741824.0, *mem.Total)
assert.Equal(t, "B", mem.Unit)
})
t.Run("ContainerMemory/NoLimit", func(t *testing.T) {
t.Parallel()
fs := initFS(t, fsContainerCgroupV2NoLimit)
s, err := New(WithFS(fs), withNoWait)
require.NoError(t, err)
mem, err := s.ContainerMemory(PrefixDefault)
require.NoError(t, err)
require.NotNil(t, mem)
assert.Equal(t, 268435456.0, mem.Used)
assert.Nil(t, mem.Total)
assert.Equal(t, "B", mem.Unit)
})
})
}
func TestIsContainerized(t *testing.T) {
t.Parallel()
for _, tt := range []struct {
Name string
FS map[string]string
Expected bool
Error string
}{
{
Name: "Empty",
FS: map[string]string{},
Expected: false,
Error: "",
},
{
Name: "BareMetal",
FS: fsHostOnly,
Expected: false,
Error: "",
},
{
Name: "Docker",
FS: fsContainerCgroupV1,
Expected: true,
Error: "",
},
{
Name: "Sysbox",
FS: fsContainerSysbox,
Expected: true,
Error: "",
},
} {
tt := tt
t.Run(tt.Name, func(t *testing.T) {
t.Parallel()
fs := initFS(t, tt.FS)
actual, err := IsContainerized(fs)
if tt.Error == "" {
assert.NoError(t, err)
assert.Equal(t, tt.Expected, actual)
} else {
assert.ErrorContains(t, err, tt.Error)
assert.False(t, actual)
}
})
}
}
// helper function for initializing a fs
func initFS(t testing.TB, m map[string]string) afero.Fs {
t.Helper()
fs := afero.NewMemMapFs()
for k, v := range m {
mungeFS(t, fs, k, v)
}
return fs
}
// helper function for writing v to fs under path k
func mungeFS(t testing.TB, fs afero.Fs, k, v string) {
t.Helper()
require.NoError(t, afero.WriteFile(fs, k, []byte(v+"\n"), 0o600))
}
var (
fsHostOnly = map[string]string{
procOneCgroup: "0::/",
procMounts: "/dev/sda1 / ext4 rw,relatime 0 0",
}
fsContainerSysbox = map[string]string{
procOneCgroup: "0::/docker/aa86ac98959eeedeae0ecb6e0c9ddd8ae8b97a9d0fdccccf7ea7a474f4e0bb1f",
procMounts: `overlay / overlay rw,relatime,lowerdir=/some/path:/some/path,upperdir=/some/path:/some/path,workdir=/some/path:/some/path 0 0
sysboxfs /proc/sys proc ro,nosuid,nodev,noexec,relatime 0 0`,
cgroupV2CPUMax: "250000 100000",
cgroupV2CPUStat: "usage_usec 0",
}
fsContainerCgroupV2 = map[string]string{
procOneCgroup: "0::/docker/aa86ac98959eeedeae0ecb6e0c9ddd8ae8b97a9d0fdccccf7ea7a474f4e0bb1f",
procMounts: `overlay / overlay rw,relatime,lowerdir=/some/path:/some/path,upperdir=/some/path:/some/path,workdir=/some/path:/some/path 0 0
proc /proc/sys proc ro,nosuid,nodev,noexec,relatime 0 0`,
cgroupV2CPUMax: "250000 100000",
cgroupV2CPUStat: "usage_usec 0",
cgroupV2MemoryMaxBytes: "1073741824",
cgroupV2MemoryUsageBytes: "536870912",
cgroupV2MemoryStat: "inactive_file 268435456",
}
fsContainerCgroupV2NoLimit = map[string]string{
procOneCgroup: "0::/docker/aa86ac98959eeedeae0ecb6e0c9ddd8ae8b97a9d0fdccccf7ea7a474f4e0bb1f",
procMounts: `overlay / overlay rw,relatime,lowerdir=/some/path:/some/path,upperdir=/some/path:/some/path,workdir=/some/path:/some/path 0 0
proc /proc/sys proc ro,nosuid,nodev,noexec,relatime 0 0`,
cgroupV2CPUMax: "max 100000",
cgroupV2CPUStat: "usage_usec 0",
cgroupV2MemoryMaxBytes: "max",
cgroupV2MemoryUsageBytes: "536870912",
cgroupV2MemoryStat: "inactive_file 268435456",
}
fsContainerCgroupV1 = map[string]string{
procOneCgroup: "0::/docker/aa86ac98959eeedeae0ecb6e0c9ddd8ae8b97a9d0fdccccf7ea7a474f4e0bb1f",
procMounts: `overlay / overlay rw,relatime,lowerdir=/some/path:/some/path,upperdir=/some/path:/some/path,workdir=/some/path:/some/path 0 0
proc /proc/sys proc ro,nosuid,nodev,noexec,relatime 0 0`,
cgroupV1CPUAcctUsage: "0",
cgroupV1CFSQuotaUs: "250000",
cgroupV1CFSPeriodUs: "100000",
cgroupV1MemoryMaxUsageBytes: "1073741824",
cgroupV1MemoryUsageBytes: "536870912",
cgroupV1MemoryStat: "total_inactive_file 268435456",
}
fsContainerCgroupV1NoLimit = map[string]string{
procOneCgroup: "0::/docker/aa86ac98959eeedeae0ecb6e0c9ddd8ae8b97a9d0fdccccf7ea7a474f4e0bb1f",
procMounts: `overlay / overlay rw,relatime,lowerdir=/some/path:/some/path,upperdir=/some/path:/some/path,workdir=/some/path:/some/path 0 0
proc /proc/sys proc ro,nosuid,nodev,noexec,relatime 0 0`,
cgroupV1CPUAcctUsage: "0",
cgroupV1CFSQuotaUs: "-1",
cgroupV1CFSPeriodUs: "100000",
cgroupV1MemoryMaxUsageBytes: "max", // I have never seen this in the wild
cgroupV1MemoryUsageBytes: "536870912",
cgroupV1MemoryStat: "total_inactive_file 268435456",
}
fsContainerCgroupV1AltPath = map[string]string{
procOneCgroup: "0::/docker/aa86ac98959eeedeae0ecb6e0c9ddd8ae8b97a9d0fdccccf7ea7a474f4e0bb1f",
procMounts: `overlay / overlay rw,relatime,lowerdir=/some/path:/some/path,upperdir=/some/path:/some/path,workdir=/some/path:/some/path 0 0
proc /proc/sys proc ro,nosuid,nodev,noexec,relatime 0 0`,
"/sys/fs/cgroup/cpuacct/cpuacct.usage": "0",
"/sys/fs/cgroup/cpu/cpu.cfs_quota_us": "250000",
"/sys/fs/cgroup/cpu/cpu.cfs_period_us": "100000",
cgroupV1MemoryMaxUsageBytes: "1073741824",
cgroupV1MemoryUsageBytes: "536870912",
cgroupV1MemoryStat: "total_inactive_file 268435456",
}
)
+44 -26
View File
@@ -6,7 +6,6 @@ import (
"context"
"errors"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
@@ -18,6 +17,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/cli"
"github.com/coder/coder/cli/clibase"
"github.com/coder/coder/cli/config"
@@ -39,7 +40,7 @@ func New(t *testing.T, args ...string) (*clibase.Invocation, config.Root) {
type logWriter struct {
prefix string
t *testing.T
log slog.Logger
}
func (l *logWriter) Write(p []byte) (n int, err error) {
@@ -47,8 +48,9 @@ func (l *logWriter) Write(p []byte) (n int, err error) {
if trimmed == "" {
return len(p), nil
}
l.t.Log(
l.prefix + ": " + trimmed,
l.log.Info(
context.Background(),
l.prefix+": "+trimmed,
)
return len(p), nil
}
@@ -57,12 +59,13 @@ func NewWithCommand(
t *testing.T, cmd *clibase.Cmd, args ...string,
) (*clibase.Invocation, config.Root) {
configDir := config.Root(t.TempDir())
logger := slogtest.Make(t, nil)
i := &clibase.Invocation{
Command: cmd,
Args: append([]string{"--global-config", string(configDir)}, args...),
Stdin: io.LimitReader(nil, 0),
Stdout: (&logWriter{prefix: "stdout", t: t}),
Stderr: (&logWriter{prefix: "stderr", t: t}),
Stdout: (&logWriter{prefix: "stdout", log: logger}),
Stderr: (&logWriter{prefix: "stderr", log: logger}),
}
t.Logf("invoking command: %s %s", cmd.Name(), strings.Join(i.Args, " "))
@@ -82,7 +85,10 @@ func SetupConfig(t *testing.T, client *codersdk.Client, root config.Root) {
// new temporary testing directory.
func CreateTemplateVersionSource(t *testing.T, responses *echo.Responses) string {
directory := t.TempDir()
f, err := ioutil.TempFile(directory, "*.tf")
f, err := os.CreateTemp(directory, "*.tf")
require.NoError(t, err)
_ = f.Close()
f, err = os.Create(filepath.Join(directory, ".terraform.lock.hcl"))
require.NoError(t, err)
_ = f.Close()
data, err := echo.Tar(responses)
@@ -127,15 +133,23 @@ func extractTar(t *testing.T, data []byte, directory string) {
}
}
// Start runs the command in a goroutine and cleans it up when
// the test completed.
// Start runs the command in a goroutine and cleans it up when the test
// completed.
func Start(t *testing.T, inv *clibase.Invocation) {
t.Helper()
closeCh := make(chan struct{})
// StartWithWaiter adds its own `t.Cleanup`, so we need to be sure it's added
// before ours.
waiter := StartWithWaiter(t, inv)
t.Cleanup(func() {
waiter.Cancel()
<-closeCh
})
go func() {
defer close(closeCh)
err := StartWithWaiter(t, inv).Wait()
err := waiter.Wait()
switch {
case errors.Is(err, context.Canceled):
return
@@ -143,10 +157,6 @@ func Start(t *testing.T, inv *clibase.Invocation) {
assert.NoError(t, err)
}
}()
t.Cleanup(func() {
<-closeCh
})
}
// Run runs the command and asserts that there is no error.
@@ -160,17 +170,22 @@ func Run(t *testing.T, inv *clibase.Invocation) {
type ErrorWaiter struct {
waitOnce sync.Once
cachedError error
cancelFunc context.CancelFunc
c <-chan error
t *testing.T
}
func (w *ErrorWaiter) Cancel() {
w.cancelFunc()
}
func (w *ErrorWaiter) Wait() error {
w.waitOnce.Do(func() {
var ok bool
w.cachedError, ok = <-w.c
if !ok {
panic("unexpoected channel close")
panic("unexpected channel close")
}
})
return w.cachedError
@@ -196,18 +211,18 @@ func (w *ErrorWaiter) RequireAs(want interface{}) {
require.ErrorAs(w.t, w.Wait(), want)
}
// StartWithWaiter runs the command in a goroutine but returns the error
// instead of asserting it. This is useful for testing error cases.
// StartWithWaiter runs the command in a goroutine but returns the error instead
// of asserting it. This is useful for testing error cases.
func StartWithWaiter(t *testing.T, inv *clibase.Invocation) *ErrorWaiter {
t.Helper()
errCh := make(chan error, 1)
var cleaningUp atomic.Bool
var (
ctx = inv.Context()
cancel func()
cleaningUp atomic.Bool
errCh = make(chan error, 1)
doneCh = make(chan struct{})
)
if _, ok := ctx.Deadline(); !ok {
ctx, cancel = context.WithDeadline(ctx, time.Now().Add(testutil.WaitMedium))
@@ -218,14 +233,17 @@ func StartWithWaiter(t *testing.T, inv *clibase.Invocation) *ErrorWaiter {
inv = inv.WithContext(ctx)
go func() {
defer close(doneCh)
defer close(errCh)
err := inv.Run()
if cleaningUp.Load() && errors.Is(err, context.DeadlineExceeded) {
// If we're cleaning up, this error is likely related to the
// CLI teardown process. E.g., the server could be slow to shut
// down Postgres.
// If we're cleaning up, this error is likely related to the CLI
// teardown process. E.g., the server could be slow to shut down
// Postgres.
t.Logf("command %q timed out during test cleanup", inv.Command.FullName())
}
// Whether or not this fails the test is left to the caller.
t.Logf("command %q exited with error: %v", inv.Command.FullName(), err)
errCh <- err
}()
@@ -233,7 +251,7 @@ func StartWithWaiter(t *testing.T, inv *clibase.Invocation) *ErrorWaiter {
t.Cleanup(func() {
cancel()
cleaningUp.Store(true)
<-errCh
<-doneCh
})
return &ErrorWaiter{c: errCh, t: t}
return &ErrorWaiter{c: errCh, t: t, cancelFunc: cancel}
}
+222
View File
@@ -0,0 +1,222 @@
package clitest
import (
"bytes"
"context"
"flag"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/clibase"
"github.com/coder/coder/cli/config"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database/dbtestutil"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/testutil"
)
// UpdateGoldenFiles indicates golden files should be updated.
// To update the golden files:
// make update-golden-files
var UpdateGoldenFiles = flag.Bool("update", false, "update .golden files")
var timestampRegex = regexp.MustCompile(`(?i)\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(.\d+)?Z`)
type CommandHelpCase struct {
Name string
Cmd []string
}
func DefaultCases() []CommandHelpCase {
return []CommandHelpCase{
{
Name: "coder --help",
Cmd: []string{"--help"},
},
{
Name: "coder server --help",
Cmd: []string{"server", "--help"},
},
}
}
// TestCommandHelp will test the help output of the given commands
// using golden files.
//
//nolint:tparallel,paralleltest
func TestCommandHelp(t *testing.T, getRoot func(t *testing.T) *clibase.Cmd, cases []CommandHelpCase) {
ogColorProfile := lipgloss.ColorProfile()
// ANSI256 escape codes are far easier for humans to parse in a diff,
// but TrueColor is probably more popular with modern terminals.
lipgloss.SetColorProfile(termenv.ANSI)
t.Cleanup(func() {
lipgloss.SetColorProfile(ogColorProfile)
})
rootClient, replacements := prepareTestData(t)
root := getRoot(t)
ExtractCommandPathsLoop:
for _, cp := range extractVisibleCommandPaths(nil, root.Children) {
name := fmt.Sprintf("coder %s --help", strings.Join(cp, " "))
cmd := append(cp, "--help")
for _, tt := range cases {
if tt.Name == name {
continue ExtractCommandPathsLoop
}
}
cases = append(cases, CommandHelpCase{Name: name, Cmd: cmd})
}
for _, tt := range cases {
tt := tt
t.Run(tt.Name, func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
var outBuf bytes.Buffer
caseCmd := getRoot(t)
inv, cfg := NewWithCommand(t, caseCmd, tt.Cmd...)
inv.Stderr = &outBuf
inv.Stdout = &outBuf
inv.Environ.Set("CODER_URL", rootClient.URL.String())
inv.Environ.Set("CODER_SESSION_TOKEN", rootClient.SessionToken())
inv.Environ.Set("CODER_CACHE_DIRECTORY", "~/.cache")
SetupConfig(t, rootClient, cfg)
StartWithWaiter(t, inv.WithContext(ctx)).RequireSuccess()
actual := outBuf.Bytes()
if len(actual) == 0 {
t.Fatal("no output")
}
for k, v := range replacements {
actual = bytes.ReplaceAll(actual, []byte(k), []byte(v))
}
actual = NormalizeGoldenFile(t, actual)
goldenPath := filepath.Join("testdata", strings.Replace(tt.Name, " ", "_", -1)+".golden")
if *UpdateGoldenFiles {
t.Logf("update golden file for: %q: %s", tt.Name, goldenPath)
err := os.WriteFile(goldenPath, actual, 0o600)
require.NoError(t, err, "update golden file")
}
expected, err := os.ReadFile(goldenPath)
require.NoError(t, err, "read golden file, run \"make update-golden-files\" and commit the changes")
expected = NormalizeGoldenFile(t, expected)
require.Equal(
t, string(expected), string(actual),
"golden file mismatch: %s, run \"make update-golden-files\", verify and commit the changes",
goldenPath,
)
})
}
}
// NormalizeGoldenFile replaces any strings that are system or timing dependent
// with a placeholder so that the golden files can be compared with a simple
// equality check.
func NormalizeGoldenFile(t *testing.T, byt []byte) []byte {
// Replace any timestamps with a placeholder.
byt = timestampRegex.ReplaceAll(byt, []byte("[timestamp]"))
homeDir, err := os.UserHomeDir()
require.NoError(t, err)
configDir := config.DefaultDir()
byt = bytes.ReplaceAll(byt, []byte(configDir), []byte("~/.config/coderv2"))
byt = bytes.ReplaceAll(byt, []byte(codersdk.DefaultCacheDir()), []byte("[cache dir]"))
// The home directory changes depending on the test environment.
byt = bytes.ReplaceAll(byt, []byte(homeDir), []byte("~"))
for _, r := range []struct {
old string
new string
}{
{"\r\n", "\n"},
{`~\.cache\coder`, "~/.cache/coder"},
{`C:\Users\RUNNER~1\AppData\Local\Temp`, "/tmp"},
{os.TempDir(), "/tmp"},
} {
byt = bytes.ReplaceAll(byt, []byte(r.old), []byte(r.new))
}
return byt
}
func extractVisibleCommandPaths(cmdPath []string, cmds []*clibase.Cmd) [][]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.Children)...)
}
return cmdPaths
}
func prepareTestData(t *testing.T) (*codersdk.Client, map[string]string) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
db, pubsub := dbtestutil.NewDB(t)
rootClient := coderdtest.New(t, &coderdtest.Options{
Database: db,
Pubsub: pubsub,
IncludeProvisionerDaemon: true,
})
firstUser := coderdtest.CreateFirstUser(t, rootClient)
secondUser, err := rootClient.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "testuser2@coder.com",
Username: "testuser2",
Password: coderdtest.FirstUserParams.Password,
OrganizationID: firstUser.OrganizationID,
})
require.NoError(t, err)
version := coderdtest.CreateTemplateVersion(t, rootClient, firstUser.OrganizationID, nil)
version = coderdtest.AwaitTemplateVersionJob(t, rootClient, version.ID)
template := coderdtest.CreateTemplate(t, rootClient, firstUser.OrganizationID, version.ID, func(req *codersdk.CreateTemplateRequest) {
req.Name = "test-template"
})
workspace := coderdtest.CreateWorkspace(t, rootClient, firstUser.OrganizationID, template.ID, func(req *codersdk.CreateWorkspaceRequest) {
req.Name = "test-workspace"
})
workspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, rootClient, workspace.LatestBuild.ID)
replacements := map[string]string{
firstUser.UserID.String(): "[first user ID]",
secondUser.ID.String(): "[second user ID]",
firstUser.OrganizationID.String(): "[first org ID]",
version.ID.String(): "[version ID]",
version.Name: "[version name]",
version.Job.ID.String(): "[version job ID]",
version.Job.FileID.String(): "[version file ID]",
version.Job.WorkerID.String(): "[version worker ID]",
template.ID.String(): "[template ID]",
workspace.ID.String(): "[workspace ID]",
workspaceBuild.ID.String(): "[workspace build ID]",
workspaceBuild.Job.ID.String(): "[workspace build job ID]",
workspaceBuild.Job.FileID.String(): "[workspace build file ID]",
workspaceBuild.Job.WorkerID.String(): "[workspace build worker ID]",
}
return rootClient, replacements
}
+24
View File
@@ -0,0 +1,24 @@
package clitest
import (
"testing"
"github.com/coder/coder/cli/clibase"
)
// HandlersOK asserts that all commands have a handler.
// Without a handler, the command has no default behavior. Even for
// non-root commands (like 'groups' or 'users'), a handler is required.
// These handlers are likely just the 'help' handler, but this must be
// explicitly set.
func HandlersOK(t *testing.T, cmd *clibase.Cmd) {
cmd.Walk(func(cmd *clibase.Cmd) {
if cmd.Handler == nil {
// If you see this error, make the Handler a helper invoker.
// Handler: func(inv *clibase.Invocation) error {
// return inv.Command.HelpHandler(inv)
// },
t.Errorf("command %q has no handler, change to a helper invoker using: 'inv.Command.HelpHandler(inv)'", cmd.Name())
}
})
}
+206 -220
View File
@@ -2,263 +2,249 @@ package cliui
import (
"context"
"fmt"
"io"
"os"
"os/signal"
"sync"
"time"
"github.com/briandowns/spinner"
"github.com/muesli/reflow/indent"
"github.com/muesli/reflow/wordwrap"
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/coder/codersdk"
)
var (
AgentStartError = xerrors.New("agent startup exited with non-zero exit status")
AgentShuttingDown = xerrors.New("agent is shutting down")
)
var errAgentShuttingDown = xerrors.New("agent is shutting down")
type AgentOptions struct {
WorkspaceName string
Fetch func(context.Context) (codersdk.WorkspaceAgent, error)
FetchInterval time.Duration
WarnInterval time.Duration
NoWait bool // If true, don't wait for the agent to be ready.
Fetch func(ctx context.Context, agentID uuid.UUID) (codersdk.WorkspaceAgent, error)
FetchLogs func(ctx context.Context, agentID uuid.UUID, after int64, follow bool) (<-chan []codersdk.WorkspaceAgentLog, io.Closer, error)
Wait bool // If true, wait for the agent to be ready (startup script).
}
// Agent displays a spinning indicator that waits for a workspace agent to connect.
func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error {
func Agent(ctx context.Context, writer io.Writer, agentID uuid.UUID, opts AgentOptions) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
if opts.FetchInterval == 0 {
opts.FetchInterval = 500 * time.Millisecond
}
if opts.WarnInterval == 0 {
opts.WarnInterval = 30 * time.Second
if opts.FetchLogs == nil {
opts.FetchLogs = func(_ context.Context, _ uuid.UUID, _ int64, _ bool) (<-chan []codersdk.WorkspaceAgentLog, io.Closer, error) {
c := make(chan []codersdk.WorkspaceAgentLog)
close(c)
return c, closeFunc(func() error { return nil }), nil
}
}
var resourceMutex sync.Mutex
agent, err := opts.Fetch(ctx)
type fetchAgent struct {
agent codersdk.WorkspaceAgent
err error
}
fetchedAgent := make(chan fetchAgent, 1)
go func() {
t := time.NewTimer(0)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
agent, err := opts.Fetch(ctx, agentID)
select {
case <-fetchedAgent:
default:
}
if err != nil {
fetchedAgent <- fetchAgent{err: xerrors.Errorf("fetch workspace agent: %w", err)}
return
}
fetchedAgent <- fetchAgent{agent: agent}
t.Reset(opts.FetchInterval)
}
}
}()
fetch := func() (codersdk.WorkspaceAgent, error) {
select {
case <-ctx.Done():
return codersdk.WorkspaceAgent{}, ctx.Err()
case f := <-fetchedAgent:
if f.err != nil {
return codersdk.WorkspaceAgent{}, f.err
}
return f.agent, nil
}
}
agent, err := fetch()
if err != nil {
return xerrors.Errorf("fetch: %w", err)
}
// Fast path if the agent is ready (avoid showing connecting prompt).
// We don't take the fast path for opts.NoWait yet because we want to
// show the message.
if agent.Status == codersdk.WorkspaceAgentConnected &&
(agent.LoginBeforeReady || agent.LifecycleState == codersdk.WorkspaceAgentLifecycleReady) {
return nil
}
sw := &stageWriter{w: writer}
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
defer cancel()
spin := spinner.New(spinner.CharSets[78], 100*time.Millisecond, spinner.WithColor("fgHiGreen"))
spin.Writer = writer
spin.ForceOutput = true
spin.Suffix = waitingMessage(agent, opts).Spin
waitMessage := &message{}
showMessage := func() {
resourceMutex.Lock()
defer resourceMutex.Unlock()
m := waitingMessage(agent, opts)
if m.Prompt == waitMessage.Prompt {
return
}
moveUp := ""
if waitMessage.Prompt != "" {
// If this is an update, move a line up
// to keep it tidy and aligned.
moveUp = "\033[1A"
}
waitMessage = m
// Stop the spinner while we write our message.
spin.Stop()
spin.Suffix = waitMessage.Spin
// Clear the line and (if necessary) move up a line to write our message.
_, _ = fmt.Fprintf(writer, "\033[2K%s\n%s\n", moveUp, waitMessage.Prompt)
select {
case <-ctx.Done():
default:
// Safe to resume operation.
if spin.Suffix != "" {
spin.Start()
}
}
}
// Fast path for showing the error message even when using no wait,
// we do this just before starting the spinner to avoid needless
// spinning.
if agent.Status == codersdk.WorkspaceAgentConnected &&
!agent.LoginBeforeReady && opts.NoWait {
showMessage()
return nil
}
// Start spinning after fast paths are handled.
if spin.Suffix != "" {
spin.Start()
}
defer spin.Stop()
warnAfter := time.NewTimer(opts.WarnInterval)
defer warnAfter.Stop()
warningShown := make(chan struct{})
go func() {
select {
case <-ctx.Done():
close(warningShown)
case <-warnAfter.C:
close(warningShown)
showMessage()
}
}()
fetchInterval := time.NewTicker(opts.FetchInterval)
defer fetchInterval.Stop()
showStartupLogs := false
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-fetchInterval.C:
// It doesn't matter if we're connected or not, if the agent is
// shutting down, we don't know if it's coming back.
if agent.LifecycleState.ShuttingDown() {
return errAgentShuttingDown
}
resourceMutex.Lock()
agent, err = opts.Fetch(ctx)
if err != nil {
resourceMutex.Unlock()
return xerrors.Errorf("fetch: %w", err)
}
resourceMutex.Unlock()
switch agent.Status {
case codersdk.WorkspaceAgentConnected:
// NOTE(mafredri): Once we have access to the workspace agent's
// startup script logs, we can show them here.
// https://github.com/coder/coder/issues/2957
if !agent.LoginBeforeReady && !opts.NoWait {
switch agent.LifecycleState {
case codersdk.WorkspaceAgentLifecycleReady:
return nil
case codersdk.WorkspaceAgentLifecycleStartTimeout:
showMessage()
case codersdk.WorkspaceAgentLifecycleStartError:
showMessage()
return AgentStartError
case codersdk.WorkspaceAgentLifecycleShuttingDown, codersdk.WorkspaceAgentLifecycleShutdownTimeout,
codersdk.WorkspaceAgentLifecycleShutdownError, codersdk.WorkspaceAgentLifecycleOff:
showMessage()
return AgentShuttingDown
default:
select {
case <-warningShown:
showMessage()
default:
// This state is normal, we don't want
// to show a message prematurely.
case codersdk.WorkspaceAgentConnecting, codersdk.WorkspaceAgentTimeout:
// Since we were waiting for the agent to connect, also show
// startup logs if applicable.
showStartupLogs = true
stage := "Waiting for the workspace agent to connect"
sw.Start(stage)
for agent.Status == codersdk.WorkspaceAgentConnecting {
if agent, err = fetch(); err != nil {
return xerrors.Errorf("fetch: %w", err)
}
}
if agent.Status == codersdk.WorkspaceAgentTimeout {
now := time.Now()
sw.Log(now, codersdk.LogLevelInfo, "The workspace agent is having trouble connecting, wait for it to connect or restart your workspace.")
sw.Log(now, codersdk.LogLevelInfo, troubleshootingMessage(agent, "https://coder.com/docs/v2/latest/templates#agent-connection-issues"))
for agent.Status == codersdk.WorkspaceAgentTimeout {
if agent, err = fetch(); err != nil {
return xerrors.Errorf("fetch: %w", err)
}
}
continue
}
return nil
case codersdk.WorkspaceAgentTimeout, codersdk.WorkspaceAgentDisconnected:
showMessage()
}
}
}
sw.Complete(stage, agent.FirstConnectedAt.Sub(agent.CreatedAt))
type message struct {
Spin string
Prompt string
Troubleshoot bool
}
func waitingMessage(agent codersdk.WorkspaceAgent, opts AgentOptions) (m *message) {
m = &message{
Spin: fmt.Sprintf("Waiting for connection from %s...", Styles.Field.Render(agent.Name)),
Prompt: "Don't panic, your workspace is booting up!",
}
defer func() {
if agent.Status == codersdk.WorkspaceAgentConnected && opts.NoWait {
m.Spin = ""
}
if m.Spin != "" {
m.Spin = " " + m.Spin
}
// We don't want to wrap the troubleshooting URL, so we'll handle word
// wrapping ourselves (vs using lipgloss).
w := wordwrap.NewWriter(Styles.Paragraph.GetWidth() - Styles.Paragraph.GetMarginLeft()*2)
w.Breakpoints = []rune{' ', '\n'}
_, _ = fmt.Fprint(w, m.Prompt)
if m.Troubleshoot {
if agent.TroubleshootingURL != "" {
_, _ = fmt.Fprintf(w, " See troubleshooting instructions at:\n%s", agent.TroubleshootingURL)
} else {
_, _ = fmt.Fprint(w, " Wait for it to (re)connect or restart your workspace.")
case codersdk.WorkspaceAgentConnected:
if !showStartupLogs && agent.LifecycleState == codersdk.WorkspaceAgentLifecycleReady {
// The workspace is ready, there's nothing to do but connect.
return nil
}
}
_, _ = fmt.Fprint(w, "\n")
// We want to prefix the prompt with a caret, but we want text on the
// following lines to align with the text on the first line (i.e. added
// spacing).
ind := " " + Styles.Prompt.String()
iw := indent.NewWriter(1, func(w io.Writer) {
_, _ = w.Write([]byte(ind))
ind = " " // Set indentation to space after initial prompt.
})
_, _ = fmt.Fprint(iw, w.String())
m.Prompt = iw.String()
}()
stage := "Running workspace agent startup script"
follow := opts.Wait
if !follow {
stage += " (non-blocking)"
}
sw.Start(stage)
switch agent.Status {
case codersdk.WorkspaceAgentTimeout:
m.Prompt = "The workspace agent is having trouble connecting."
case codersdk.WorkspaceAgentDisconnected:
m.Prompt = "The workspace agent lost connection!"
case codersdk.WorkspaceAgentConnected:
m.Spin = fmt.Sprintf("Waiting for %s to become ready...", Styles.Field.Render(agent.Name))
m.Prompt = "Don't panic, your workspace agent has connected and the workspace is getting ready!"
if opts.NoWait {
m.Prompt = "Your workspace is still getting ready, it may be in an incomplete state."
}
err = func() error { // Use func because of defer in for loop.
logStream, logsCloser, err := opts.FetchLogs(ctx, agent.ID, 0, follow)
if err != nil {
return xerrors.Errorf("fetch workspace agent startup logs: %w", err)
}
defer logsCloser.Close()
var lastLog codersdk.WorkspaceAgentLog
fetchedAgentWhileFollowing := fetchedAgent
if !follow {
fetchedAgentWhileFollowing = nil
}
for {
// This select is essentially and inline `fetch()`.
select {
case <-ctx.Done():
return ctx.Err()
case f := <-fetchedAgentWhileFollowing:
if f.err != nil {
return xerrors.Errorf("fetch: %w", f.err)
}
agent = f.agent
// If the agent is no longer starting, stop following
// logs because FetchLogs will keep streaming forever.
// We do one last non-follow request to ensure we have
// fetched all logs.
if !agent.LifecycleState.Starting() {
_ = logsCloser.Close()
fetchedAgentWhileFollowing = nil
logStream, logsCloser, err = opts.FetchLogs(ctx, agent.ID, lastLog.ID, false)
if err != nil {
return xerrors.Errorf("fetch workspace agent startup logs: %w", err)
}
// Logs are already primed, so we can call close.
_ = logsCloser.Close()
}
case logs, ok := <-logStream:
if !ok {
return nil
}
for _, log := range logs {
sw.Log(log.CreatedAt, log.Level, log.Output)
lastLog = log
}
}
}
}()
if err != nil {
return err
}
for follow && agent.LifecycleState.Starting() {
if agent, err = fetch(); err != nil {
return xerrors.Errorf("fetch: %w", err)
}
}
switch agent.LifecycleState {
case codersdk.WorkspaceAgentLifecycleStartTimeout:
m.Prompt = "The workspace is taking longer than expected to get ready, the agent startup script is still executing."
case codersdk.WorkspaceAgentLifecycleStartError:
m.Spin = ""
m.Prompt = "The workspace ran into a problem while getting ready, the agent startup script exited with non-zero status."
default:
switch agent.LifecycleState {
case codersdk.WorkspaceAgentLifecycleShutdownTimeout:
m.Spin = ""
m.Prompt = "The workspace is shutting down, but is taking longer than expected to shut down and the agent shutdown script is still executing."
m.Troubleshoot = true
case codersdk.WorkspaceAgentLifecycleShutdownError:
m.Spin = ""
m.Prompt = "The workspace ran into a problem while shutting down, the agent shutdown script exited with non-zero status."
m.Troubleshoot = true
case codersdk.WorkspaceAgentLifecycleShuttingDown:
m.Spin = ""
m.Prompt = "The workspace is shutting down."
case codersdk.WorkspaceAgentLifecycleOff:
m.Spin = ""
m.Prompt = "The workspace is not running."
case codersdk.WorkspaceAgentLifecycleReady:
sw.Complete(stage, agent.ReadyAt.Sub(*agent.StartedAt))
case codersdk.WorkspaceAgentLifecycleStartError:
sw.Fail(stage, agent.ReadyAt.Sub(*agent.StartedAt))
// Use zero time (omitted) to separate these from the startup logs.
sw.Log(time.Time{}, codersdk.LogLevelWarn, "Warning: The startup script exited with an error and your workspace may be incomplete.")
sw.Log(time.Time{}, codersdk.LogLevelWarn, troubleshootingMessage(agent, "https://coder.com/docs/v2/latest/templates#startup-script-exited-with-an-error"))
default:
switch {
case agent.LifecycleState.Starting():
// Use zero time (omitted) to separate these from the startup logs.
sw.Log(time.Time{}, codersdk.LogLevelWarn, "Notice: The startup script is still running and your workspace may be incomplete.")
sw.Log(time.Time{}, codersdk.LogLevelWarn, troubleshootingMessage(agent, "https://coder.com/docs/v2/latest/templates#your-workspace-may-be-incomplete"))
// Note: We don't complete or fail the stage here, it's
// intentionally left open to indicate this stage didn't
// complete.
case agent.LifecycleState.ShuttingDown():
// We no longer know if the startup script failed or not,
// but we need to tell the user something.
sw.Complete(stage, agent.ReadyAt.Sub(*agent.StartedAt))
return errAgentShuttingDown
}
}
// Not a failure state, no troubleshooting necessary.
return m
return nil
case codersdk.WorkspaceAgentDisconnected:
// If the agent was still starting during disconnect, we'll
// show startup logs.
showStartupLogs = agent.LifecycleState.Starting()
stage := "The workspace agent lost connection"
sw.Start(stage)
sw.Log(time.Now(), codersdk.LogLevelWarn, "Wait for it to reconnect or restart your workspace.")
sw.Log(time.Now(), codersdk.LogLevelWarn, troubleshootingMessage(agent, "https://coder.com/docs/v2/latest/templates#agent-connection-issues"))
for agent.Status == codersdk.WorkspaceAgentDisconnected {
if agent, err = fetch(); err != nil {
return xerrors.Errorf("fetch: %w", err)
}
}
sw.Complete(stage, agent.LastConnectedAt.Sub(*agent.DisconnectedAt))
}
default:
// Not a failure state, no troubleshooting necessary.
return m
}
m.Troubleshoot = true
}
func troubleshootingMessage(agent codersdk.WorkspaceAgent, url string) string {
m := "For more information and troubleshooting, see " + url
if agent.TroubleshootingURL != "" {
m += " and " + agent.TroubleshootingURL
}
return m
}
type closeFunc func() error
func (c closeFunc) Close() error {
return c()
}
+364 -340
View File
@@ -1,363 +1,387 @@
package cliui_test
import (
"bufio"
"bytes"
"context"
"io"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"go.uber.org/atomic"
"golang.org/x/xerrors"
"github.com/coder/coder/cli/clibase"
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/coderd/util/ptr"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/pty/ptytest"
"github.com/coder/coder/testutil"
)
func TestAgent(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
var disconnected atomic.Bool
ptty := ptytest.New(t)
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
err := cliui.Agent(inv.Context(), inv.Stdout, cliui.AgentOptions{
WorkspaceName: "example",
Fetch: func(_ context.Context) (codersdk.WorkspaceAgent, error) {
agent := codersdk.WorkspaceAgent{
Status: codersdk.WorkspaceAgentDisconnected,
LoginBeforeReady: true,
}
if disconnected.Load() {
agent.Status = codersdk.WorkspaceAgentConnected
}
return agent, nil
},
for _, tc := range []struct {
name string
iter []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error
logs chan []codersdk.WorkspaceAgentLog
opts cliui.AgentOptions
want []string
wantErr bool
}{
{
name: "Initial connection",
opts: cliui.AgentOptions{
FetchInterval: time.Millisecond,
WarnInterval: 10 * time.Millisecond,
})
return err
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnecting
return nil
},
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnected
agent.FirstConnectedAt = ptr.Ref(time.Now())
return nil
},
},
want: []string{
"⧗ Waiting for the workspace agent to connect",
"✔ Waiting for the workspace agent to connect",
"⧗ Running workspace agent startup script (non-blocking)",
"Notice: The startup script is still running and your workspace may be incomplete.",
"For more information and troubleshooting, see",
},
},
{
name: "Initial connection timeout",
opts: cliui.AgentOptions{
FetchInterval: 1 * time.Millisecond,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnecting
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleStarting
agent.StartedAt = ptr.Ref(time.Now())
return nil
},
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentTimeout
return nil
},
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnected
agent.FirstConnectedAt = ptr.Ref(time.Now())
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleReady
agent.ReadyAt = ptr.Ref(time.Now())
return nil
},
},
want: []string{
"⧗ Waiting for the workspace agent to connect",
"The workspace agent is having trouble connecting, wait for it to connect or restart your workspace.",
"For more information and troubleshooting, see",
"✔ Waiting for the workspace agent to connect",
"⧗ Running workspace agent startup script (non-blocking)",
"✔ Running workspace agent startup script (non-blocking)",
},
},
{
name: "Disconnected",
opts: cliui.AgentOptions{
FetchInterval: 1 * time.Millisecond,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentDisconnected
agent.FirstConnectedAt = ptr.Ref(time.Now().Add(-1 * time.Minute))
agent.LastConnectedAt = ptr.Ref(time.Now().Add(-1 * time.Minute))
agent.DisconnectedAt = ptr.Ref(time.Now())
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleReady
agent.StartedAt = ptr.Ref(time.Now().Add(-1 * time.Minute))
agent.ReadyAt = ptr.Ref(time.Now())
return nil
},
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnected
agent.LastConnectedAt = ptr.Ref(time.Now())
return nil
},
},
want: []string{
"⧗ The workspace agent lost connection",
"Wait for it to reconnect or restart your workspace.",
"For more information and troubleshooting, see",
"✔ The workspace agent lost connection",
},
},
{
name: "Startup script logs",
opts: cliui.AgentOptions{
FetchInterval: time.Millisecond,
Wait: true,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnected
agent.FirstConnectedAt = ptr.Ref(time.Now())
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleStarting
agent.StartedAt = ptr.Ref(time.Now())
logs <- []codersdk.WorkspaceAgentLog{
{
CreatedAt: time.Now(),
Output: "Hello world",
},
}
return nil
},
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentLog) error {
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleReady
agent.ReadyAt = ptr.Ref(time.Now())
logs <- []codersdk.WorkspaceAgentLog{
{
CreatedAt: time.Now(),
Output: "Bye now",
},
}
return nil
},
},
want: []string{
"⧗ Running workspace agent startup script",
"Hello world",
"Bye now",
"✔ Running workspace agent startup script",
},
},
{
name: "Startup script exited with error",
opts: cliui.AgentOptions{
FetchInterval: time.Millisecond,
Wait: true,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnected
agent.FirstConnectedAt = ptr.Ref(time.Now())
agent.StartedAt = ptr.Ref(time.Now())
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleStartError
agent.ReadyAt = ptr.Ref(time.Now())
logs <- []codersdk.WorkspaceAgentLog{
{
CreatedAt: time.Now(),
Output: "Hello world",
},
}
return nil
},
},
want: []string{
"⧗ Running workspace agent startup script",
"Hello world",
"✘ Running workspace agent startup script",
"Warning: The startup script exited with an error and your workspace may be incomplete.",
"For more information and troubleshooting, see",
},
},
{
name: "Error when shutting down",
opts: cliui.AgentOptions{
FetchInterval: time.Millisecond,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentDisconnected
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleOff
return nil
},
},
wantErr: true,
},
{
name: "Error when shutting down while waiting",
opts: cliui.AgentOptions{
FetchInterval: time.Millisecond,
Wait: true,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnected
agent.FirstConnectedAt = ptr.Ref(time.Now())
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleStarting
agent.StartedAt = ptr.Ref(time.Now())
logs <- []codersdk.WorkspaceAgentLog{
{
CreatedAt: time.Now(),
Output: "Hello world",
},
}
return nil
},
func(_ context.Context, agent *codersdk.WorkspaceAgent, logs chan []codersdk.WorkspaceAgentLog) error {
agent.ReadyAt = ptr.Ref(time.Now())
agent.LifecycleState = codersdk.WorkspaceAgentLifecycleShuttingDown
return nil
},
},
want: []string{
"⧗ Running workspace agent startup script",
"Hello world",
"✔ Running workspace agent startup script",
},
wantErr: true,
},
{
name: "Error during fetch",
opts: cliui.AgentOptions{
FetchInterval: time.Millisecond,
Wait: true,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentConnecting
return nil
},
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
return xerrors.New("bad")
},
},
want: []string{
"⧗ Waiting for the workspace agent to connect",
},
wantErr: true,
},
{
name: "Shows agent troubleshooting URL",
opts: cliui.AgentOptions{
FetchInterval: time.Millisecond,
Wait: true,
},
iter: []func(context.Context, *codersdk.WorkspaceAgent, chan []codersdk.WorkspaceAgentLog) error{
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
agent.Status = codersdk.WorkspaceAgentTimeout
agent.TroubleshootingURL = "https://troubleshoot"
return nil
},
func(_ context.Context, agent *codersdk.WorkspaceAgent, _ chan []codersdk.WorkspaceAgentLog) error {
return xerrors.New("bad")
},
},
want: []string{
"⧗ Waiting for the workspace agent to connect",
"The workspace agent is having trouble connecting, wait for it to connect or restart your workspace.",
"https://troubleshoot",
},
wantErr: true,
},
} {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
var buf bytes.Buffer
agent := codersdk.WorkspaceAgent{
ID: uuid.New(),
Status: codersdk.WorkspaceAgentConnecting,
StartupScriptBehavior: codersdk.WorkspaceAgentStartupScriptBehaviorNonBlocking,
CreatedAt: time.Now(),
LifecycleState: codersdk.WorkspaceAgentLifecycleCreated,
}
logs := make(chan []codersdk.WorkspaceAgentLog, 1)
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
tc.opts.Fetch = func(_ context.Context, _ uuid.UUID) (codersdk.WorkspaceAgent, error) {
var err error
if len(tc.iter) > 0 {
err = tc.iter[0](ctx, &agent, logs)
tc.iter = tc.iter[1:]
}
return agent, err
}
tc.opts.FetchLogs = func(ctx context.Context, _ uuid.UUID, _ int64, follow bool) (<-chan []codersdk.WorkspaceAgentLog, io.Closer, error) {
if follow {
return logs, closeFunc(func() error { return nil }), nil
}
fetchLogs := make(chan []codersdk.WorkspaceAgentLog, 1)
select {
case <-ctx.Done():
return nil, nil, ctx.Err()
case l := <-logs:
fetchLogs <- l
default:
}
close(fetchLogs)
return fetchLogs, closeFunc(func() error { return nil }), nil
}
err := cliui.Agent(inv.Context(), &buf, uuid.Nil, tc.opts)
return err
},
}
inv := cmd.Invoke()
w := clitest.StartWithWaiter(t, inv)
if tc.wantErr {
w.RequireError()
} else {
w.RequireSuccess()
}
s := bufio.NewScanner(&buf)
for s.Scan() {
line := s.Text()
t.Log(line)
if len(tc.want) == 0 {
require.Fail(t, "unexpected line: "+line)
}
require.Contains(t, line, tc.want[0])
tc.want = tc.want[1:]
}
require.NoError(t, s.Err())
if len(tc.want) > 0 {
require.Fail(t, "missing lines: "+strings.Join(tc.want, ", "))
}
})
}
inv := cmd.Invoke()
ptty.Attach(inv)
done := make(chan struct{})
go func() {
defer close(done)
err := inv.Run()
assert.NoError(t, err)
}()
ptty.ExpectMatchContext(ctx, "lost connection")
disconnected.Store(true)
<-done
}
func TestAgent_TimeoutWithTroubleshootingURL(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
wantURL := "https://coder.com/troubleshoot"
var connected, timeout atomic.Bool
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
err := cliui.Agent(inv.Context(), inv.Stdout, cliui.AgentOptions{
WorkspaceName: "example",
Fetch: func(_ context.Context) (codersdk.WorkspaceAgent, error) {
agent := codersdk.WorkspaceAgent{
Status: codersdk.WorkspaceAgentConnecting,
TroubleshootingURL: wantURL,
LoginBeforeReady: true,
}
switch {
case !connected.Load() && timeout.Load():
agent.Status = codersdk.WorkspaceAgentTimeout
case connected.Load():
agent.Status = codersdk.WorkspaceAgentConnected
}
return agent, nil
},
FetchInterval: time.Millisecond,
WarnInterval: 5 * time.Millisecond,
})
return err
},
}
ptty := ptytest.New(t)
inv := cmd.Invoke()
ptty.Attach(inv)
done := make(chan error, 1)
go func() {
done <- inv.WithContext(ctx).Run()
}()
ptty.ExpectMatchContext(ctx, "Don't panic, your workspace is booting")
timeout.Store(true)
ptty.ExpectMatchContext(ctx, wantURL)
connected.Store(true)
require.NoError(t, <-done)
}
func TestAgent_StartupTimeout(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
wantURL := "https://coder.com/this-is-a-really-long-troubleshooting-url-that-should-not-wrap"
var status, state atomic.String
setStatus := func(s codersdk.WorkspaceAgentStatus) { status.Store(string(s)) }
setState := func(s codersdk.WorkspaceAgentLifecycle) { state.Store(string(s)) }
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
err := cliui.Agent(inv.Context(), inv.Stdout, cliui.AgentOptions{
WorkspaceName: "example",
Fetch: func(_ context.Context) (codersdk.WorkspaceAgent, error) {
agent := codersdk.WorkspaceAgent{
Status: codersdk.WorkspaceAgentConnecting,
LoginBeforeReady: false,
LifecycleState: codersdk.WorkspaceAgentLifecycleCreated,
TroubleshootingURL: wantURL,
}
if s := status.Load(); s != "" {
agent.Status = codersdk.WorkspaceAgentStatus(s)
}
if s := state.Load(); s != "" {
agent.LifecycleState = codersdk.WorkspaceAgentLifecycle(s)
}
return agent, nil
},
FetchInterval: time.Millisecond,
WarnInterval: time.Millisecond,
NoWait: false,
})
return err
},
}
ptty := ptytest.New(t)
inv := cmd.Invoke()
ptty.Attach(inv)
done := make(chan error, 1)
go func() {
done <- inv.WithContext(ctx).Run()
}()
setStatus(codersdk.WorkspaceAgentConnecting)
ptty.ExpectMatchContext(ctx, "Don't panic, your workspace is booting")
setStatus(codersdk.WorkspaceAgentConnected)
setState(codersdk.WorkspaceAgentLifecycleStarting)
ptty.ExpectMatchContext(ctx, "workspace is getting ready")
setState(codersdk.WorkspaceAgentLifecycleStartTimeout)
ptty.ExpectMatchContext(ctx, "is taking longer")
ptty.ExpectMatchContext(ctx, wantURL)
setState(codersdk.WorkspaceAgentLifecycleReady)
require.NoError(t, <-done)
}
func TestAgent_StartErrorExit(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
wantURL := "https://coder.com/this-is-a-really-long-troubleshooting-url-that-should-not-wrap"
var status, state atomic.String
setStatus := func(s codersdk.WorkspaceAgentStatus) { status.Store(string(s)) }
setState := func(s codersdk.WorkspaceAgentLifecycle) { state.Store(string(s)) }
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
err := cliui.Agent(inv.Context(), inv.Stdout, cliui.AgentOptions{
WorkspaceName: "example",
Fetch: func(_ context.Context) (codersdk.WorkspaceAgent, error) {
agent := codersdk.WorkspaceAgent{
Status: codersdk.WorkspaceAgentConnecting,
LoginBeforeReady: false,
LifecycleState: codersdk.WorkspaceAgentLifecycleCreated,
TroubleshootingURL: wantURL,
}
if s := status.Load(); s != "" {
agent.Status = codersdk.WorkspaceAgentStatus(s)
}
if s := state.Load(); s != "" {
agent.LifecycleState = codersdk.WorkspaceAgentLifecycle(s)
}
return agent, nil
},
FetchInterval: time.Millisecond,
WarnInterval: 60 * time.Second,
NoWait: false,
})
return err
},
}
ptty := ptytest.New(t)
inv := cmd.Invoke()
ptty.Attach(inv)
done := make(chan error, 1)
go func() {
done <- inv.WithContext(ctx).Run()
}()
setStatus(codersdk.WorkspaceAgentConnected)
setState(codersdk.WorkspaceAgentLifecycleStarting)
ptty.ExpectMatchContext(ctx, "to become ready...")
setState(codersdk.WorkspaceAgentLifecycleStartError)
ptty.ExpectMatchContext(ctx, "ran into a problem")
err := <-done
require.ErrorIs(t, err, cliui.AgentStartError, "lifecycle start_error should exit with error")
}
func TestAgent_NoWait(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
wantURL := "https://coder.com/this-is-a-really-long-troubleshooting-url-that-should-not-wrap"
var status, state atomic.String
setStatus := func(s codersdk.WorkspaceAgentStatus) { status.Store(string(s)) }
setState := func(s codersdk.WorkspaceAgentLifecycle) { state.Store(string(s)) }
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
err := cliui.Agent(inv.Context(), inv.Stdout, cliui.AgentOptions{
WorkspaceName: "example",
Fetch: func(_ context.Context) (codersdk.WorkspaceAgent, error) {
agent := codersdk.WorkspaceAgent{
Status: codersdk.WorkspaceAgentConnecting,
LoginBeforeReady: false,
LifecycleState: codersdk.WorkspaceAgentLifecycleCreated,
TroubleshootingURL: wantURL,
}
if s := status.Load(); s != "" {
agent.Status = codersdk.WorkspaceAgentStatus(s)
}
if s := state.Load(); s != "" {
agent.LifecycleState = codersdk.WorkspaceAgentLifecycle(s)
}
return agent, nil
},
FetchInterval: time.Millisecond,
WarnInterval: time.Second,
NoWait: true,
})
return err
},
}
ptty := ptytest.New(t)
inv := cmd.Invoke()
ptty.Attach(inv)
done := make(chan error, 1)
go func() {
done <- inv.WithContext(ctx).Run()
}()
setStatus(codersdk.WorkspaceAgentConnecting)
ptty.ExpectMatchContext(ctx, "Don't panic, your workspace is booting")
setStatus(codersdk.WorkspaceAgentConnected)
require.NoError(t, <-done, "created - should exit early")
setState(codersdk.WorkspaceAgentLifecycleStarting)
go func() { done <- inv.WithContext(ctx).Run() }()
require.NoError(t, <-done, "starting - should exit early")
setState(codersdk.WorkspaceAgentLifecycleStartTimeout)
go func() { done <- inv.WithContext(ctx).Run() }()
require.NoError(t, <-done, "start timeout - should exit early")
setState(codersdk.WorkspaceAgentLifecycleStartError)
go func() { done <- inv.WithContext(ctx).Run() }()
require.NoError(t, <-done, "start error - should exit early")
setState(codersdk.WorkspaceAgentLifecycleReady)
go func() { done <- inv.WithContext(ctx).Run() }()
require.NoError(t, <-done, "ready - should exit early")
}
func TestAgent_LoginBeforeReadyEnabled(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
wantURL := "https://coder.com/this-is-a-really-long-troubleshooting-url-that-should-not-wrap"
var status, state atomic.String
setStatus := func(s codersdk.WorkspaceAgentStatus) { status.Store(string(s)) }
setState := func(s codersdk.WorkspaceAgentLifecycle) { state.Store(string(s)) }
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
err := cliui.Agent(inv.Context(), inv.Stdout, cliui.AgentOptions{
WorkspaceName: "example",
Fetch: func(_ context.Context) (codersdk.WorkspaceAgent, error) {
agent := codersdk.WorkspaceAgent{
Status: codersdk.WorkspaceAgentConnecting,
LoginBeforeReady: true,
LifecycleState: codersdk.WorkspaceAgentLifecycleCreated,
TroubleshootingURL: wantURL,
}
if s := status.Load(); s != "" {
agent.Status = codersdk.WorkspaceAgentStatus(s)
}
if s := state.Load(); s != "" {
agent.LifecycleState = codersdk.WorkspaceAgentLifecycle(s)
}
return agent, nil
},
FetchInterval: time.Millisecond,
WarnInterval: time.Second,
NoWait: false,
})
return err
},
}
inv := cmd.Invoke()
ptty := ptytest.New(t)
ptty.Attach(inv)
done := make(chan error, 1)
go func() {
done <- inv.WithContext(ctx).Run()
}()
setStatus(codersdk.WorkspaceAgentConnecting)
ptty.ExpectMatchContext(ctx, "Don't panic, your workspace is booting")
setStatus(codersdk.WorkspaceAgentConnected)
require.NoError(t, <-done, "created - should exit early")
setState(codersdk.WorkspaceAgentLifecycleStarting)
go func() { done <- inv.WithContext(ctx).Run() }()
require.NoError(t, <-done, "starting - should exit early")
setState(codersdk.WorkspaceAgentLifecycleStartTimeout)
go func() { done <- inv.WithContext(ctx).Run() }()
require.NoError(t, <-done, "start timeout - should exit early")
setState(codersdk.WorkspaceAgentLifecycleStartError)
go func() { done <- inv.WithContext(ctx).Run() }()
require.NoError(t, <-done, "start error - should exit early")
setState(codersdk.WorkspaceAgentLifecycleReady)
go func() { done <- inv.WithContext(ctx).Run() }()
require.NoError(t, <-done, "ready - should exit early")
t.Run("NotInfinite", func(t *testing.T) {
t.Parallel()
var fetchCalled uint64
cmd := &clibase.Cmd{
Handler: func(inv *clibase.Invocation) error {
buf := bytes.Buffer{}
err := cliui.Agent(inv.Context(), &buf, uuid.Nil, cliui.AgentOptions{
FetchInterval: 10 * time.Millisecond,
Fetch: func(ctx context.Context, agentID uuid.UUID) (codersdk.WorkspaceAgent, error) {
atomic.AddUint64(&fetchCalled, 1)
return codersdk.WorkspaceAgent{
Status: codersdk.WorkspaceAgentConnected,
LifecycleState: codersdk.WorkspaceAgentLifecycleReady,
}, nil
},
})
if err != nil {
return err
}
require.Never(t, func() bool {
called := atomic.LoadUint64(&fetchCalled)
return called > 5 || called == 0
}, time.Second, 100*time.Millisecond)
return nil
},
}
require.NoError(t, cmd.Invoke().Run())
})
}
+48 -33
View File
@@ -1,27 +1,20 @@
package cliui
import (
"os"
"github.com/charmbracelet/charm/ui/common"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
"golang.org/x/xerrors"
)
var (
Canceled = xerrors.New("canceled")
var Canceled = xerrors.New("canceled")
defaultStyles = common.DefaultStyles()
)
// DefaultStyles compose visual elements of the UI.
var DefaultStyles Styles
// ValidateNotEmpty is a helper function to disallow empty inputs!
func ValidateNotEmpty(s string) error {
if s == "" {
return xerrors.New("Must be provided!")
}
return nil
}
// Styles compose visual elements of the UI!
var Styles = struct {
type Styles struct {
Bold,
Checkmark,
Code,
@@ -38,23 +31,45 @@ var Styles = struct {
Logo,
Warn,
Wrap lipgloss.Style
}{
Bold: lipgloss.NewStyle().Bold(true),
Checkmark: defaultStyles.Checkmark,
Code: defaultStyles.Code,
Crossmark: defaultStyles.Error.Copy().SetString("✘"),
DateTimeStamp: defaultStyles.LabelDim,
Error: defaultStyles.Error,
Field: defaultStyles.Code.Copy().Foreground(lipgloss.AdaptiveColor{Light: "#000000", Dark: "#FFFFFF"}),
Keyword: defaultStyles.Keyword,
Paragraph: defaultStyles.Paragraph,
Placeholder: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#585858", Dark: "#4d46b3"}),
Prompt: defaultStyles.Prompt.Copy().Foreground(lipgloss.AdaptiveColor{Light: "#9B9B9B", Dark: "#5C5C5C"}),
FocusedPrompt: defaultStyles.FocusedPrompt.Copy().Foreground(lipgloss.Color("#651fff")),
Fuchsia: defaultStyles.SelectedMenuItem.Copy(),
Logo: defaultStyles.Logo.Copy().SetString("Coder"),
Warn: lipgloss.NewStyle().Foreground(
lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#ECFD65"},
),
Wrap: lipgloss.NewStyle().Width(80),
}
func init() {
lipgloss.SetDefaultRenderer(
lipgloss.NewRenderer(os.Stdout, termenv.WithColorCache(true)),
)
// All Styles are set after we change the DefaultRenderer so that the ColorCache
// is in effect, mitigating the severe performance issue seen here:
// https://github.com/coder/coder/issues/7884.
charmStyles := common.DefaultStyles()
DefaultStyles = Styles{
Bold: lipgloss.NewStyle().Bold(true),
Checkmark: charmStyles.Checkmark,
Code: charmStyles.Code,
Crossmark: charmStyles.Error.Copy().SetString("✘"),
DateTimeStamp: charmStyles.LabelDim,
Error: charmStyles.Error,
Field: charmStyles.Code.Copy().Foreground(lipgloss.AdaptiveColor{Light: "#000000", Dark: "#FFFFFF"}),
Keyword: charmStyles.Keyword,
Paragraph: charmStyles.Paragraph,
Placeholder: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#585858", Dark: "#4d46b3"}),
Prompt: charmStyles.Prompt.Copy().Foreground(lipgloss.AdaptiveColor{Light: "#9B9B9B", Dark: "#5C5C5C"}),
FocusedPrompt: charmStyles.FocusedPrompt.Copy().Foreground(lipgloss.Color("#651fff")),
Fuchsia: charmStyles.SelectedMenuItem.Copy(),
Logo: charmStyles.Logo.Copy().SetString("Coder"),
Warn: lipgloss.NewStyle().Foreground(
lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#ECFD65"},
),
Wrap: lipgloss.NewStyle().Width(80),
}
}
// ValidateNotEmpty is a helper function to disallow empty inputs!
func ValidateNotEmpty(s string) error {
if s == "" {
return xerrors.New("Must be provided!")
}
return nil
}
+1 -2
View File
@@ -2,7 +2,6 @@ package cliui_test
import (
"context"
"net/url"
"sync/atomic"
"testing"
"time"
@@ -33,7 +32,7 @@ func TestGitAuth(t *testing.T) {
ID: "github",
Type: codersdk.GitProviderGitHub,
Authenticated: fetched.Load(),
AuthenticateURL: "https://example.com/gitauth/github?redirect=" + url.QueryEscape("/gitauth?notify"),
AuthenticateURL: "https://example.com/gitauth/github",
}}, nil
},
FetchInterval: time.Millisecond,
+2 -2
View File
@@ -35,7 +35,7 @@ func (m cliMessage) String() string {
// Warn writes a log to the writer provided.
func Warn(wtr io.Writer, header string, lines ...string) {
_, _ = fmt.Fprint(wtr, cliMessage{
Style: Styles.Warn,
Style: DefaultStyles.Warn.Copy(),
Prefix: "WARN: ",
Header: header,
Lines: lines,
@@ -63,7 +63,7 @@ func Infof(wtr io.Writer, fmtStr string, args ...interface{}) {
// Error writes a log to the writer provided.
func Error(wtr io.Writer, header string, lines ...string) {
_, _ = fmt.Fprint(wtr, cliMessage{
Style: Styles.Error,
Style: DefaultStyles.Error.Copy(),
Prefix: "ERROR: ",
Header: header,
Lines: lines,
+53
View File
@@ -3,6 +3,7 @@ package cliui
import (
"context"
"encoding/json"
"fmt"
"reflect"
"strings"
@@ -171,3 +172,55 @@ func (jsonFormat) Format(_ context.Context, data any) (string, error) {
return string(outBytes), nil
}
type textFormat struct{}
var _ OutputFormat = textFormat{}
// TextFormat is a formatter that just outputs unstructured text.
// It uses fmt.Sprintf under the hood.
func TextFormat() OutputFormat {
return textFormat{}
}
func (textFormat) ID() string {
return "text"
}
func (textFormat) AttachOptions(_ *clibase.OptionSet) {}
func (textFormat) Format(_ context.Context, data any) (string, error) {
return fmt.Sprintf("%s", data), nil
}
// DataChangeFormat allows manipulating the data passed to an output format.
// This is because sometimes the data needs to be manipulated before it can be
// passed to the output format.
// For example, you may want to pass something different to the text formatter
// than what you pass to the json formatter.
type DataChangeFormat struct {
format OutputFormat
change func(data any) (any, error)
}
// ChangeFormatterData allows manipulating the data passed to an output
// format.
func ChangeFormatterData(format OutputFormat, change func(data any) (any, error)) *DataChangeFormat {
return &DataChangeFormat{format: format, change: change}
}
func (d *DataChangeFormat) ID() string {
return d.format.ID()
}
func (d *DataChangeFormat) AttachOptions(opts *clibase.OptionSet) {
d.format.AttachOptions(opts)
}
func (d *DataChangeFormat) Format(ctx context.Context, data any) (string, error) {
newData, err := d.change(data)
if err != nil {
return "", err
}
return d.format.Format(ctx, newData)
}
+3
View File
@@ -50,6 +50,9 @@ func Test_OutputFormatter(t *testing.T) {
require.Panics(t, func() {
cliui.NewOutputFormatter(cliui.JSONFormat())
})
require.NotPanics(t, func() {
cliui.NewOutputFormatter(cliui.JSONFormat(), cliui.TextFormat())
})
})
t.Run("NoMissingFormatID", func(t *testing.T) {
+14 -56
View File
@@ -6,63 +6,21 @@ import (
"strings"
"github.com/coder/coder/cli/clibase"
"github.com/coder/coder/coderd/parameter"
"github.com/coder/coder/codersdk"
)
func ParameterSchema(inv *clibase.Invocation, parameterSchema codersdk.ParameterSchema) (string, error) {
_, _ = fmt.Fprintln(inv.Stdout, Styles.Bold.Render("var."+parameterSchema.Name))
if parameterSchema.Description != "" {
_, _ = fmt.Fprintln(inv.Stdout, " "+strings.TrimSpace(strings.Join(strings.Split(parameterSchema.Description, "\n"), "\n "))+"\n")
}
var err error
var options []string
if parameterSchema.ValidationCondition != "" {
options, _, err = parameter.Contains(parameterSchema.ValidationCondition)
if err != nil {
return "", err
}
}
var value string
if len(options) > 0 {
// Move the cursor up a single line for nicer display!
_, _ = fmt.Fprint(inv.Stdout, "\033[1A")
value, err = Select(inv, SelectOptions{
Options: options,
Default: parameterSchema.DefaultSourceValue,
HideSearch: true,
})
if err == nil {
_, _ = fmt.Fprintln(inv.Stdout)
_, _ = fmt.Fprintln(inv.Stdout, " "+Styles.Prompt.String()+Styles.Field.Render(value))
}
} else {
text := "Enter a value"
if parameterSchema.DefaultSourceValue != "" {
text += fmt.Sprintf(" (default: %q)", parameterSchema.DefaultSourceValue)
}
text += ":"
value, err = Prompt(inv, PromptOptions{
Text: Styles.Bold.Render(text),
})
value = strings.TrimSpace(value)
}
if err != nil {
return "", err
}
// If they didn't specify anything, use the default value if set.
if len(options) == 0 && value == "" {
value = parameterSchema.DefaultSourceValue
}
return value, nil
}
func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.TemplateVersionParameter) (string, error) {
_, _ = fmt.Fprintln(inv.Stdout, Styles.Bold.Render(templateVersionParameter.Name))
label := templateVersionParameter.Name
if templateVersionParameter.DisplayName != "" {
label = templateVersionParameter.DisplayName
}
if templateVersionParameter.Ephemeral {
label += DefaultStyles.Warn.Render(" (build option)")
}
_, _ = fmt.Fprintln(inv.Stdout, DefaultStyles.Bold.Render(label))
if templateVersionParameter.DescriptionPlaintext != "" {
_, _ = fmt.Fprintln(inv.Stdout, " "+strings.TrimSpace(strings.Join(strings.Split(templateVersionParameter.DescriptionPlaintext, "\n"), "\n "))+"\n")
}
@@ -87,7 +45,7 @@ func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.Te
}
_, _ = fmt.Fprintln(inv.Stdout)
_, _ = fmt.Fprintln(inv.Stdout, " "+Styles.Prompt.String()+Styles.Field.Render(strings.Join(values, ", ")))
_, _ = fmt.Fprintln(inv.Stdout, " "+DefaultStyles.Prompt.String()+DefaultStyles.Field.Render(strings.Join(values, ", ")))
value = string(v)
}
} else if len(templateVersionParameter.Options) > 0 {
@@ -101,7 +59,7 @@ func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.Te
})
if err == nil {
_, _ = fmt.Fprintln(inv.Stdout)
_, _ = fmt.Fprintln(inv.Stdout, " "+Styles.Prompt.String()+Styles.Field.Render(richParameterOption.Name))
_, _ = fmt.Fprintln(inv.Stdout, " "+DefaultStyles.Prompt.String()+DefaultStyles.Field.Render(richParameterOption.Name))
value = richParameterOption.Value
}
} else {
@@ -112,7 +70,7 @@ func RichParameter(inv *clibase.Invocation, templateVersionParameter codersdk.Te
text += ":"
value, err = Prompt(inv, PromptOptions{
Text: Styles.Bold.Render(text),
Text: DefaultStyles.Bold.Render(text),
Validate: func(value string) error {
return validateRichPrompt(value, templateVersionParameter)
},
+8 -8
View File
@@ -55,21 +55,21 @@ func Prompt(inv *clibase.Invocation, opts PromptOptions) (string, error) {
}
}
_, _ = fmt.Fprint(inv.Stdout, Styles.FocusedPrompt.String()+opts.Text+" ")
_, _ = fmt.Fprint(inv.Stdout, DefaultStyles.FocusedPrompt.String()+opts.Text+" ")
if opts.IsConfirm {
if len(opts.Default) == 0 {
opts.Default = ConfirmYes
}
renderedYes := Styles.Placeholder.Render(ConfirmYes)
renderedNo := Styles.Placeholder.Render(ConfirmNo)
renderedYes := DefaultStyles.Placeholder.Render(ConfirmYes)
renderedNo := DefaultStyles.Placeholder.Render(ConfirmNo)
if opts.Default == ConfirmYes {
renderedYes = Styles.Bold.Render(ConfirmYes)
renderedYes = DefaultStyles.Bold.Render(ConfirmYes)
} else {
renderedNo = Styles.Bold.Render(ConfirmNo)
renderedNo = DefaultStyles.Bold.Render(ConfirmNo)
}
_, _ = fmt.Fprint(inv.Stdout, Styles.Placeholder.Render("("+renderedYes+Styles.Placeholder.Render("/"+renderedNo+Styles.Placeholder.Render(") "))))
_, _ = fmt.Fprint(inv.Stdout, DefaultStyles.Placeholder.Render("("+renderedYes+DefaultStyles.Placeholder.Render("/"+renderedNo+DefaultStyles.Placeholder.Render(") "))))
} else if opts.Default != "" {
_, _ = fmt.Fprint(inv.Stdout, Styles.Placeholder.Render("("+opts.Default+") "))
_, _ = fmt.Fprint(inv.Stdout, DefaultStyles.Placeholder.Render("("+opts.Default+") "))
}
interrupt := make(chan os.Signal, 1)
@@ -126,7 +126,7 @@ func Prompt(inv *clibase.Invocation, opts PromptOptions) (string, error) {
if opts.Validate != nil {
err := opts.Validate(line)
if err != nil {
_, _ = fmt.Fprintln(inv.Stdout, defaultStyles.Error.Render(err.Error()))
_, _ = fmt.Fprintln(inv.Stdout, DefaultStyles.Error.Render(err.Error()))
return Prompt(inv, opts)
}
}
+83 -48
View File
@@ -7,6 +7,7 @@ import (
"io"
"os"
"os/signal"
"strings"
"sync"
"time"
@@ -63,35 +64,32 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
var (
currentStage = "Queued"
currentStageStartedAt = time.Now().UTC()
didLogBetweenStage = false
errChan = make(chan error, 1)
job codersdk.ProvisionerJob
jobMutex sync.Mutex
)
sw := &stageWriter{w: writer, verbose: opts.Verbose, silentLogs: opts.Silent}
printStage := func() {
_, _ = fmt.Fprintf(writer, Styles.Prompt.Render("⧗")+"%s\n", Styles.Field.Render(currentStage))
sw.Start(currentStage)
}
updateStage := func(stage string, startedAt time.Time) {
if currentStage != "" {
prefix := ""
if !didLogBetweenStage {
prefix = "\033[1A\r"
}
mark := Styles.Checkmark
duration := startedAt.Sub(currentStageStartedAt)
if job.CompletedAt != nil && job.Status != codersdk.ProvisionerJobSucceeded {
mark = Styles.Crossmark
sw.Fail(currentStage, duration)
} else {
sw.Complete(currentStage, duration)
}
_, _ = fmt.Fprintf(writer, prefix+mark.String()+Styles.Placeholder.Render(" %s [%dms]")+"\n", currentStage, startedAt.Sub(currentStageStartedAt).Milliseconds())
}
if stage == "" {
return
}
currentStage = stage
currentStageStartedAt = startedAt
didLogBetweenStage = false
printStage()
}
@@ -129,7 +127,7 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
return
}
}
_, _ = fmt.Fprintf(writer, "\033[2K\r\n"+Styles.FocusedPrompt.String()+Styles.Bold.Render("Gracefully canceling...")+"\n\n")
_, _ = fmt.Fprintf(writer, DefaultStyles.FocusedPrompt.String()+DefaultStyles.Bold.Render("Gracefully canceling...")+"\n\n")
err := opts.Cancel()
if err != nil {
errChan <- xerrors.Errorf("cancel: %w", err)
@@ -149,30 +147,15 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
}
defer closer.Close()
var (
// logOutput is where log output is written
logOutput = writer
// logBuffer is where logs are buffered if opts.Silent is true
logBuffer = &bytes.Buffer{}
)
if opts.Silent {
logOutput = logBuffer
}
flushLogBuffer := func() {
if opts.Silent {
_, _ = io.Copy(writer, logBuffer)
}
}
ticker := time.NewTicker(opts.FetchInterval)
defer ticker.Stop()
for {
select {
case err = <-errChan:
flushLogBuffer()
sw.Fail(currentStage, time.Since(currentStageStartedAt))
return err
case <-ctx.Done():
flushLogBuffer()
sw.Fail(currentStage, time.Since(currentStageStartedAt))
return ctx.Err()
case <-ticker.C:
updateJob()
@@ -196,37 +179,89 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
Message: job.Error,
Code: job.ErrorCode,
}
sw.Fail(currentStage, time.Since(currentStageStartedAt))
jobMutex.Unlock()
flushLogBuffer()
return err
}
output := ""
switch log.Level {
case codersdk.LogLevelTrace, codersdk.LogLevelDebug:
if !opts.Verbose {
continue
}
output = Styles.Placeholder.Render(log.Output)
case codersdk.LogLevelError:
output = defaultStyles.Error.Render(log.Output)
case codersdk.LogLevelWarn:
output = Styles.Warn.Render(log.Output)
case codersdk.LogLevelInfo:
output = log.Output
}
jobMutex.Lock()
if log.Stage != currentStage && log.Stage != "" {
updateStage(log.Stage, log.CreatedAt)
jobMutex.Unlock()
continue
}
_, _ = fmt.Fprintf(logOutput, "%s %s\n", Styles.Placeholder.Render(" "), output)
if !opts.Silent {
didLogBetweenStage = true
}
sw.Log(log.CreatedAt, log.Level, log.Output)
jobMutex.Unlock()
}
}
}
type stageWriter struct {
w io.Writer
verbose bool
silentLogs bool
logBuf bytes.Buffer
}
func (s *stageWriter) Start(stage string) {
_, _ = fmt.Fprintf(s.w, "==> ⧗ %s\n", stage)
}
func (s *stageWriter) Complete(stage string, duration time.Duration) {
s.end(stage, duration, true)
}
func (s *stageWriter) Fail(stage string, duration time.Duration) {
s.flushLogs()
s.end(stage, duration, false)
}
//nolint:revive
func (s *stageWriter) end(stage string, duration time.Duration, ok bool) {
s.logBuf.Reset()
mark := "✔"
if !ok {
mark = "✘"
}
if duration < 0 {
duration = 0
}
_, _ = fmt.Fprintf(s.w, "=== %s %s [%dms]\n", mark, stage, duration.Milliseconds())
}
func (s *stageWriter) Log(createdAt time.Time, level codersdk.LogLevel, line string) {
w := s.w
if s.silentLogs {
w = &s.logBuf
}
render := func(s ...string) string { return strings.Join(s, " ") }
var lines []string
if !createdAt.IsZero() {
lines = append(lines, createdAt.Local().Format("2006-01-02 15:04:05.000Z07:00"))
}
lines = append(lines, line)
switch level {
case codersdk.LogLevelTrace, codersdk.LogLevelDebug:
if !s.verbose {
return
}
render = DefaultStyles.Placeholder.Render
case codersdk.LogLevelError:
render = DefaultStyles.Error.Render
case codersdk.LogLevelWarn:
render = DefaultStyles.Warn.Render
case codersdk.LogLevelInfo:
}
_, _ = fmt.Fprintf(w, "%s\n", render(lines...))
}
func (s *stageWriter) flushLogs() {
if s.silentLogs {
_, _ = io.Copy(s.w, &s.logBuf)
}
s.logBuf.Reset()
}
+2
View File
@@ -82,6 +82,8 @@ func TestProvisionerJob(t *testing.T) {
// This cannot be ran in parallel because it uses a signal.
// nolint:paralleltest
t.Run("Cancel", func(t *testing.T) {
t.Skip("This test issues an interrupt signal which will propagate to the test runner.")
if runtime.GOOS == "windows" {
// Sending interrupt signal isn't supported on Windows!
t.SkipNow()
+25 -16
View File
@@ -50,6 +50,7 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource
row := table.Row{"Resource"}
if !options.HideAgentState {
row = append(row, "Status")
row = append(row, "Health")
row = append(row, "Version")
}
if !options.HideAccess {
@@ -78,7 +79,8 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource
// Display a line for the resource.
tableWriter.AppendRow(table.Row{
Styles.Bold.Render(resourceAddress),
DefaultStyles.Bold.Render(resourceAddress),
"",
"",
"",
})
@@ -93,20 +95,20 @@ func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource
fmt.Sprintf("%s─ %s (%s, %s)", pipe, agent.Name, agent.OperatingSystem, agent.Architecture),
}
if !options.HideAgentState {
var agentStatus string
var agentVersion string
var agentStatus, agentHealth, agentVersion string
if !options.HideAgentState {
agentStatus = renderAgentStatus(agent)
agentHealth = renderAgentHealth(agent)
agentVersion = renderAgentVersion(agent.Version, options.ServerVersion)
}
row = append(row, agentStatus, agentVersion)
row = append(row, agentStatus, agentHealth, agentVersion)
}
if !options.HideAccess {
sshCommand := "coder ssh " + options.WorkspaceName
if totalAgents > 1 {
sshCommand += "." + agent.Name
}
sshCommand = Styles.Code.Render(sshCommand)
sshCommand = DefaultStyles.Code.Render(sshCommand)
row = append(row, sshCommand)
}
tableWriter.AppendRow(row)
@@ -121,36 +123,43 @@ func renderAgentStatus(agent codersdk.WorkspaceAgent) string {
switch agent.Status {
case codersdk.WorkspaceAgentConnecting:
since := database.Now().Sub(agent.CreatedAt)
return Styles.Warn.Render("⦾ connecting") + " " +
Styles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
return DefaultStyles.Warn.Render("⦾ connecting") + " " +
DefaultStyles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
case codersdk.WorkspaceAgentDisconnected:
since := database.Now().Sub(*agent.DisconnectedAt)
return Styles.Error.Render("⦾ disconnected") + " " +
Styles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
return DefaultStyles.Error.Render("⦾ disconnected") + " " +
DefaultStyles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
case codersdk.WorkspaceAgentTimeout:
since := database.Now().Sub(agent.CreatedAt)
return fmt.Sprintf(
"%s %s",
Styles.Warn.Render("⦾ timeout"),
Styles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]"),
DefaultStyles.Warn.Render("⦾ timeout"),
DefaultStyles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]"),
)
case codersdk.WorkspaceAgentConnected:
return Styles.Keyword.Render("⦿ connected")
return DefaultStyles.Keyword.Render("⦿ connected")
default:
return Styles.Warn.Render("○ unknown")
return DefaultStyles.Warn.Render("○ unknown")
}
}
func renderAgentHealth(agent codersdk.WorkspaceAgent) string {
if agent.Health.Healthy {
return DefaultStyles.Keyword.Render("✔ healthy")
}
return DefaultStyles.Error.Render("✘ " + agent.Health.Reason)
}
func renderAgentVersion(agentVersion, serverVersion string) string {
if agentVersion == "" {
agentVersion = "(unknown)"
}
if !semver.IsValid(serverVersion) || !semver.IsValid(agentVersion) {
return Styles.Placeholder.Render(agentVersion)
return DefaultStyles.Placeholder.Render(agentVersion)
}
outdated := semver.Compare(agentVersion, serverVersion) < 0
if outdated {
return Styles.Warn.Render(agentVersion + " (outdated)")
return DefaultStyles.Warn.Render(agentVersion + " (outdated)")
}
return Styles.Keyword.Render(agentVersion)
return DefaultStyles.Keyword.Render(agentVersion)
}
+13
View File
@@ -29,6 +29,7 @@ func TestWorkspaceResources(t *testing.T) {
LifecycleState: codersdk.WorkspaceAgentLifecycleCreated,
Architecture: "amd64",
OperatingSystem: "linux",
Health: codersdk.WorkspaceAgentHealth{Healthy: true},
}},
}}, cliui.WorkspaceResourcesOptions{
WorkspaceName: "example",
@@ -65,6 +66,7 @@ func TestWorkspaceResources(t *testing.T) {
Name: "dev",
OperatingSystem: "linux",
Architecture: "amd64",
Health: codersdk.WorkspaceAgentHealth{Healthy: true},
}},
}, {
Transition: codersdk.WorkspaceTransitionStart,
@@ -76,6 +78,7 @@ func TestWorkspaceResources(t *testing.T) {
Name: "go",
Architecture: "amd64",
OperatingSystem: "linux",
Health: codersdk.WorkspaceAgentHealth{Healthy: true},
}, {
DisconnectedAt: &disconnected,
Status: codersdk.WorkspaceAgentDisconnected,
@@ -83,6 +86,10 @@ func TestWorkspaceResources(t *testing.T) {
Name: "postgres",
Architecture: "amd64",
OperatingSystem: "linux",
Health: codersdk.WorkspaceAgentHealth{
Healthy: false,
Reason: "agent has lost connection",
},
}},
}}, cliui.WorkspaceResourcesOptions{
WorkspaceName: "dev",
@@ -94,6 +101,12 @@ func TestWorkspaceResources(t *testing.T) {
}()
ptty.ExpectMatch("google_compute_disk.root")
ptty.ExpectMatch("google_compute_instance.dev")
ptty.ExpectMatch("healthy")
ptty.ExpectMatch("coder ssh dev.dev")
ptty.ExpectMatch("kubernetes_pod.dev")
ptty.ExpectMatch("healthy")
ptty.ExpectMatch("coder ssh dev.go")
ptty.ExpectMatch("agent has lost connection")
ptty.ExpectMatch("coder ssh dev.postgres")
<-done
})
+6 -1
View File
@@ -70,17 +70,22 @@ type RichSelectOptions struct {
// RichSelect displays a list of user options including name and description.
func RichSelect(inv *clibase.Invocation, richOptions RichSelectOptions) (*codersdk.TemplateVersionParameterOption, error) {
opts := make([]string, len(richOptions.Options))
var defaultOpt string
for i, option := range richOptions.Options {
line := option.Name
if len(option.Description) > 0 {
line += ": " + option.Description
}
opts[i] = line
if option.Value == richOptions.Default {
defaultOpt = line
}
}
selected, err := Select(inv, SelectOptions{
Options: opts,
Default: richOptions.Default,
Default: defaultOpt,
Size: richOptions.Size,
HideSearch: richOptions.HideSearch,
})
+24 -9
View File
@@ -188,32 +188,39 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
// returned. If the table tag is malformed, an error is returned.
//
// The returned name is transformed from "snake_case" to "normal text".
func parseTableStructTag(field reflect.StructField) (name string, defaultSort, recursive bool, err error) {
func parseTableStructTag(field reflect.StructField) (name string, defaultSort, recursive bool, skipParentName bool, err error) {
tags, err := structtag.Parse(string(field.Tag))
if err != nil {
return "", false, false, xerrors.Errorf("parse struct field tag %q: %w", string(field.Tag), err)
return "", false, false, false, xerrors.Errorf("parse struct field tag %q: %w", string(field.Tag), err)
}
tag, err := tags.Get("table")
if err != nil || tag.Name == "-" {
// tags.Get only returns an error if the tag is not found.
return "", false, false, nil
return "", false, false, false, nil
}
defaultSortOpt := false
recursiveOpt := false
skipParentNameOpt := false
for _, opt := range tag.Options {
switch opt {
case "default_sort":
defaultSortOpt = true
case "recursive":
recursiveOpt = true
case "recursive_inline":
// recursive_inline is a helper to make recursive tables look nicer.
// It skips prefixing the parent name to the child name. If you do this,
// make sure the child name is unique across all nested structs in the parent.
recursiveOpt = true
skipParentNameOpt = true
default:
return "", false, false, xerrors.Errorf("unknown option %q in struct field tag", opt)
return "", false, false, false, xerrors.Errorf("unknown option %q in struct field tag", opt)
}
}
return strings.ReplaceAll(tag.Name, "_", " "), defaultSortOpt, recursiveOpt, nil
return strings.ReplaceAll(tag.Name, "_", " "), defaultSortOpt, recursiveOpt, skipParentNameOpt, nil
}
func isStructOrStructPointer(t reflect.Type) bool {
@@ -235,7 +242,7 @@ func typeToTableHeaders(t reflect.Type) ([]string, string, error) {
defaultSortName := ""
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
name, defaultSort, recursive, err := parseTableStructTag(field)
name, defaultSort, recursive, skip, err := parseTableStructTag(field)
if err != nil {
return nil, "", xerrors.Errorf("parse struct tags for field %q in type %q: %w", field.Name, t.String(), err)
}
@@ -260,7 +267,11 @@ func typeToTableHeaders(t reflect.Type) ([]string, string, error) {
return nil, "", xerrors.Errorf("get child field header names for field %q in type %q: %w", field.Name, fieldType.String(), err)
}
for _, childName := range childNames {
headers = append(headers, fmt.Sprintf("%s %s", name, childName))
fullName := fmt.Sprintf("%s %s", name, childName)
if skip {
fullName = childName
}
headers = append(headers, fullName)
}
continue
}
@@ -296,7 +307,7 @@ func valueToTableMap(val reflect.Value) (map[string]any, error) {
for i := 0; i < val.NumField(); i++ {
field := val.Type().Field(i)
fieldVal := val.Field(i)
name, _, recursive, err := parseTableStructTag(field)
name, _, recursive, skip, err := parseTableStructTag(field)
if err != nil {
return nil, xerrors.Errorf("parse struct tags for field %q in type %T: %w", field.Name, val, err)
}
@@ -318,7 +329,11 @@ func valueToTableMap(val reflect.Value) (map[string]any, error) {
return nil, xerrors.Errorf("get child field values for field %q in type %q: %w", field.Name, fieldType.String(), err)
}
for childName, childValue := range childMap {
row[fmt.Sprintf("%s %s", name, childName)] = childValue
fullName := fmt.Sprintf("%s %s", name, childName)
if skip {
fullName = childName
}
row[fullName] = childValue
}
continue
}
+30
View File
@@ -49,6 +49,11 @@ type tableTest3 struct {
Sub tableTest2 `table:"inner,recursive,default_sort"`
}
type tableTest4 struct {
Inline tableTest2 `table:"ignored,recursive_inline"`
SortField string `table:"sort_field,default_sort"`
}
func Test_DisplayTable(t *testing.T) {
t.Parallel()
@@ -188,6 +193,31 @@ foo foo1 foo3 2022-08-02T15:49:10Z
compareTables(t, expected, out)
})
t.Run("Inline", func(t *testing.T) {
t.Parallel()
expected := `
NAME AGE
Alice 25
`
inlineIn := []tableTest4{
{
Inline: tableTest2{
Name: stringWrapper{
str: "Alice",
},
Age: 25,
NotIncluded: "IgnoreMe",
},
},
}
out, err := cliui.DisplayTable(inlineIn, "", []string{"name", "age"})
log.Println("rendered table:\n" + out)
require.NoError(t, err)
compareTables(t, expected, out)
})
// This test ensures that safeties against invalid use of `table` tags
// causes errors (even without data).
t.Run("Errors", func(t *testing.T) {
+5 -1
View File
@@ -125,5 +125,9 @@ func read(path string) ([]byte, error) {
}
func DefaultDir() string {
return configdir.LocalConfig("coderv2")
configDir := configdir.LocalConfig("coderv2")
if dir := os.Getenv("CLIDOCGEN_CONFIG_DIRECTORY"); dir != "" {
configDir = dir
}
return configDir
}
+150 -30
View File
@@ -45,7 +45,9 @@ const (
// sshConfigOptions represents options that can be stored and read
// from the coder config in ~/.ssh/coder.
type sshConfigOptions struct {
sshOptions []string
waitEnum string
userHostPrefix string
sshOptions []string
}
// addOptions expects options in the form of "option=value" or "option value".
@@ -62,7 +64,7 @@ func (o *sshConfigOptions) addOptions(options ...string) error {
}
func (o *sshConfigOptions) addOption(option string) error {
key, _, err := codersdk.ParseSSHConfigOption(option)
key, value, err := codersdk.ParseSSHConfigOption(option)
if err != nil {
return err
}
@@ -77,11 +79,20 @@ func (o *sshConfigOptions) addOption(option string) error {
continue
}
if strings.EqualFold(existingKey, key) {
o.sshOptions[i] = option
if value == "" {
// Delete existing option.
o.sshOptions = append(o.sshOptions[:i], o.sshOptions[i+1:]...)
} else {
// Override existing option.
o.sshOptions[i] = option
}
return nil
}
}
o.sshOptions = append(o.sshOptions, option)
// Only append the option if it is not empty.
if value != "" {
o.sshOptions = append(o.sshOptions, option)
}
return nil
}
@@ -91,10 +102,19 @@ func (o sshConfigOptions) equal(other sshConfigOptions) bool {
sort.Strings(opt1)
opt2 := slices.Clone(other.sshOptions)
sort.Strings(opt2)
return slices.Equal(opt1, opt2)
if !slices.Equal(opt1, opt2) {
return false
}
return o.waitEnum == other.waitEnum && o.userHostPrefix == other.userHostPrefix
}
func (o sshConfigOptions) asList() (list []string) {
if o.waitEnum != "auto" {
list = append(list, fmt.Sprintf("wait: %s", o.waitEnum))
}
if o.userHostPrefix != "" {
list = append(list, fmt.Sprintf("ssh-host-prefix: %s", o.userHostPrefix))
}
for _, opt := range o.sshOptions {
list = append(list, fmt.Sprintf("ssh-option: %s", opt))
}
@@ -169,14 +189,16 @@ func sshPrepareWorkspaceConfigs(ctx context.Context, client *codersdk.Client) (r
}
}
//nolint:gocyclo
func (r *RootCmd) configSSH() *clibase.Cmd {
var (
sshConfigFile string
sshConfigOpts sshConfigOptions
usePreviousOpts bool
dryRun bool
skipProxyCommand bool
userHostPrefix string
sshConfigFile string
sshConfigOpts sshConfigOptions
usePreviousOpts bool
dryRun bool
skipProxyCommand bool
forceUnixSeparators bool
coderCliPath string
)
client := new(codersdk.Client)
cmd := &clibase.Cmd{
@@ -198,6 +220,12 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
if sshConfigOpts.waitEnum != "auto" && skipProxyCommand {
// The wait option is applied to the ProxyCommand. If the user
// specifies skip-proxy-command, then wait cannot be applied.
return xerrors.Errorf("cannot specify both --skip-proxy-command and --wait")
}
recvWorkspaceConfigs := sshPrepareWorkspaceConfigs(inv.Context(), client)
out := inv.Stdout
@@ -206,17 +234,23 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
// that it's possible to capture the diff.
out = inv.Stderr
}
coderBinary, err := currentBinPath(out)
if err != nil {
return err
var err error
coderBinary := coderCliPath
if coderBinary == "" {
coderBinary, err = currentBinPath(out)
if err != nil {
return err
}
}
escapedCoderBinary, err := sshConfigExecEscape(coderBinary)
escapedCoderBinary, err := sshConfigExecEscape(coderBinary, forceUnixSeparators)
if err != nil {
return xerrors.Errorf("escape coder binary for ssh failed: %w", err)
}
root := r.createConfig()
escapedGlobalConfig, err := sshConfigExecEscape(string(root))
escapedGlobalConfig, err := sshConfigExecEscape(string(root), forceUnixSeparators)
if err != nil {
return xerrors.Errorf("escape global config for ssh failed: %w", err)
}
@@ -286,7 +320,7 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
// Selecting "no" will use the last config.
sshConfigOpts = *lastConfig
} else {
changes = append(changes, "Use new SSH options")
changes = append(changes, "Use new options")
}
// Only print when prompts are shown.
if yes, _ := inv.ParsedFlags().GetBool("yes"); !yes {
@@ -327,9 +361,9 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
coderdConfig.HostnamePrefix = "coder."
}
if userHostPrefix != "" {
if sshConfigOpts.userHostPrefix != "" {
// Override with user flag.
coderdConfig.HostnamePrefix = userHostPrefix
coderdConfig.HostnamePrefix = sshConfigOpts.userHostPrefix
}
// Ensure stable sorting of output.
@@ -354,13 +388,20 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
}
if !skipProxyCommand {
flags := ""
if sshConfigOpts.waitEnum != "auto" {
flags += " --wait=" + sshConfigOpts.waitEnum
}
defaultOptions = append(defaultOptions, fmt.Sprintf(
"ProxyCommand %s --global-config %s ssh --stdio %s",
escapedCoderBinary, escapedGlobalConfig, workspaceHostname,
"ProxyCommand %s --global-config %s ssh --stdio%s %s",
escapedCoderBinary, escapedGlobalConfig, flags, workspaceHostname,
))
}
var configOptions sshConfigOptions
// Create a copy of the options so we can modify them.
configOptions := sshConfigOpts
configOptions.sshOptions = nil
// Add standard options.
err := configOptions.addOptions(defaultOptions...)
if err != nil {
@@ -467,6 +508,24 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
Description: "Specifies the path to an SSH config.",
Value: clibase.StringOf(&sshConfigFile),
},
{
Flag: "coder-binary-path",
Env: "CODER_SSH_CONFIG_BINARY_PATH",
Default: "",
Description: "Optionally specify the absolute path to the coder binary used in ProxyCommand. " +
"By default, the binary invoking this command ('config ssh') is used.",
Value: clibase.Validate(clibase.StringOf(&coderCliPath), func(value *clibase.String) error {
if runtime.GOOS == goosWindows {
// For some reason filepath.IsAbs() does not work on windows.
return nil
}
absolute := filepath.IsAbs(value.String())
if !absolute {
return xerrors.Errorf("coder cli path must be an absolute path")
}
return nil
}),
},
{
Flag: "ssh-option",
FlagShorthand: "o",
@@ -496,9 +555,29 @@ func (r *RootCmd) configSSH() *clibase.Cmd {
},
{
Flag: "ssh-host-prefix",
Env: "",
Env: "CODER_CONFIGSSH_SSH_HOST_PREFIX",
Description: "Override the default host prefix.",
Value: clibase.StringOf(&userHostPrefix),
Value: clibase.StringOf(&sshConfigOpts.userHostPrefix),
},
{
Flag: "wait",
Env: "CODER_CONFIGSSH_WAIT", // Not to be mixed with CODER_SSH_WAIT.
Description: "Specifies whether or not to wait for the startup script to finish executing. Auto means that the agent startup script behavior configured in the workspace template is used.",
Default: "auto",
Value: clibase.EnumOf(&sshConfigOpts.waitEnum, "yes", "no", "auto"),
},
{
Flag: "force-unix-filepaths",
Env: "CODER_CONFIGSSH_UNIX_FILEPATHS",
Description: "By default, 'config-ssh' uses the os path separator when writing the ssh config. " +
"This might be an issue in Windows machine that use a unix-like shell. " +
"This flag forces the use of unix file paths (the forward slash '/').",
Value: clibase.BoolOf(&forceUnixSeparators),
// On non-windows showing this command is useless because it is a noop.
// Hide vs disable it though so if a command is copied from a Windows
// machine to a unix machine it will still work and not throw an
// "unknown flag" error.
Hidden: hideForceUnixSlashes,
},
cliui.SkipPromptOption(),
}
@@ -515,12 +594,22 @@ func sshConfigWriteSectionHeader(w io.Writer, addNewline bool, o sshConfigOption
_, _ = fmt.Fprint(w, nl+sshStartToken+"\n")
_, _ = fmt.Fprint(w, sshConfigSectionHeader)
_, _ = fmt.Fprint(w, sshConfigDocsHeader)
if len(o.sshOptions) > 0 {
_, _ = fmt.Fprint(w, sshConfigOptionsHeader)
for _, opt := range o.sshOptions {
_, _ = fmt.Fprintf(w, "# :%s=%s\n", "ssh-option", opt)
}
var ow strings.Builder
if o.waitEnum != "auto" {
_, _ = fmt.Fprintf(&ow, "# :%s=%s\n", "wait", o.waitEnum)
}
if o.userHostPrefix != "" {
_, _ = fmt.Fprintf(&ow, "# :%s=%s\n", "ssh-host-prefix", o.userHostPrefix)
}
for _, opt := range o.sshOptions {
_, _ = fmt.Fprintf(&ow, "# :%s=%s\n", "ssh-option", opt)
}
if ow.Len() > 0 {
_, _ = fmt.Fprint(w, sshConfigOptionsHeader)
_, _ = fmt.Fprint(w, ow.String())
}
_, _ = fmt.Fprint(w, "#\n")
}
@@ -529,6 +618,9 @@ func sshConfigWriteSectionEnd(w io.Writer) {
}
func sshConfigParseLastOptions(r io.Reader) (o sshConfigOptions) {
// Default values.
o.waitEnum = "auto"
s := bufio.NewScanner(r)
for s.Scan() {
line := s.Text()
@@ -536,6 +628,10 @@ func sshConfigParseLastOptions(r io.Reader) (o sshConfigOptions) {
line = strings.TrimPrefix(line, "# :")
parts := strings.SplitN(line, "=", 2)
switch parts[0] {
case "wait":
o.waitEnum = parts[1]
case "ssh-host-prefix":
o.userHostPrefix = parts[1]
case "ssh-option":
o.sshOptions = append(o.sshOptions, parts[1])
default:
@@ -670,7 +766,31 @@ func writeWithTempFileAndMove(path string, r io.Reader) (err error) {
// - https://github.com/openssh/openssh-portable/blob/V_9_0_P1/sshconnect.c#L158-L167
// - https://github.com/PowerShell/openssh-portable/blob/v8.1.0.0/sshconnect.c#L231-L293
// - https://github.com/PowerShell/openssh-portable/blob/v8.1.0.0/contrib/win32/win32compat/w32fd.c#L1075-L1100
func sshConfigExecEscape(path string) (string, error) {
//
// Additional Windows-specific notes:
//
// In some situations a Windows user could be using a unix-like shell such as
// git bash. In these situations the coder.exe is using the windows filepath
// separator (\), but the shell wants the unix filepath separator (/).
// Trying to determine if the shell is unix-like is difficult, so this function
// takes the argument 'forceUnixPath' to force the filepath to be unix-like.
//
// On actual unix machines, this is **always** a noop. Even if a windows
// path is provided.
//
// Passing a "false" for forceUnixPath will result in the filepath separator
// untouched from the original input.
// ---
// This is a control flag, and that is ok. It is a control flag
// based on the OS of the user. Making this a different file is excessive.
// nolint:revive
func sshConfigExecEscape(path string, forceUnixPath bool) (string, error) {
if forceUnixPath {
// This is a workaround for #7639, where the filepath separator is
// incorrectly the Windows separator (\) instead of the unix separator (/).
path = filepath.ToSlash(path)
}
// This is unlikely to ever happen, but newlines are allowed on
// certain filesystems, but cannot be used inside ssh config.
if strings.ContainsAny(path, "\n") {
+79 -8
View File
@@ -12,6 +12,11 @@ import (
"github.com/stretchr/testify/require"
)
func init() {
// For golden files, always show the flag.
hideForceUnixSlashes = false
}
func Test_sshConfigSplitOnCoderSection(t *testing.T) {
t.Parallel()
@@ -140,14 +145,14 @@ func Test_sshConfigExecEscape(t *testing.T) {
name string
path string
wantErr bool
windows bool
}{
{"no spaces", "simple", false, true},
{"spaces", "path with spaces", false, true},
{"quotes", "path with \"quotes\"", false, false},
{"backslashes", "path with \\backslashes", false, false},
{"tabs", "path with \ttabs", false, false},
{"newline fails", "path with \nnewline", true, false},
{"windows path", `C:\Program Files\Coder\bin\coder.exe`, false},
{"no spaces", "simple", false},
{"spaces", "path with spaces", false},
{"quotes", "path with \"quotes\"", false},
{"backslashes", "path with \\backslashes", false},
{"tabs", "path with \ttabs", false},
{"newline fails", "path with \nnewline", true},
}
for _, tt := range tests {
tt := tt
@@ -166,7 +171,7 @@ func Test_sshConfigExecEscape(t *testing.T) {
err = os.WriteFile(bin, contents, 0o755) //nolint:gosec
require.NoError(t, err)
escaped, err := sshConfigExecEscape(bin)
escaped, err := sshConfigExecEscape(bin, false)
if tt.wantErr {
require.Error(t, err)
return
@@ -181,6 +186,72 @@ func Test_sshConfigExecEscape(t *testing.T) {
}
}
func Test_sshConfigExecEscapeSeparatorForce(t *testing.T) {
t.Parallel()
tests := []struct {
name string
path string
// Behavior is different on Windows
expWindowsPath string
expOtherPath string
forceUnix bool
wantErr bool
}{
{
name: "windows_keep_forward_slashes_with_spaces",
// Has a space, expect quotes
path: `C:\Program Files\Coder\bin\coder.exe`,
expWindowsPath: `"C:\Program Files\Coder\bin\coder.exe"`,
expOtherPath: `"C:\Program Files\Coder\bin\coder.exe"`,
forceUnix: false,
wantErr: false,
},
{
name: "windows_keep_forward_slashes",
path: `C:\ProgramFiles\Coder\bin\coder.exe`,
expWindowsPath: `C:\ProgramFiles\Coder\bin\coder.exe`,
expOtherPath: `C:\ProgramFiles\Coder\bin\coder.exe`,
forceUnix: false,
wantErr: false,
},
{
name: "windows_force_unix_with_spaces",
path: `C:\Program Files\Coder\bin\coder.exe`,
expWindowsPath: `"C:/Program Files/Coder/bin/coder.exe"`,
expOtherPath: `"C:\Program Files\Coder\bin\coder.exe"`,
forceUnix: true,
wantErr: false,
},
{
name: "windows_force_unix",
path: `C:\ProgramFiles\Coder\bin\coder.exe`,
expWindowsPath: `C:/ProgramFiles/Coder/bin/coder.exe`,
expOtherPath: `C:\ProgramFiles\Coder\bin\coder.exe`,
forceUnix: true,
wantErr: false,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
found, err := sshConfigExecEscape(tt.path, tt.forceUnix)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
if runtime.GOOS == "windows" {
require.Equal(t, tt.expWindowsPath, found, "(Windows) expected path")
} else {
// this is a noop on non-windows!
require.Equal(t, tt.expOtherPath, found, "(Non-Windows) expected path")
}
})
}
}
func Test_sshConfigOptions_addOption(t *testing.T) {
t.Parallel()
testCases := []struct {
+5
View File
@@ -0,0 +1,5 @@
//go:build !windows
package cli
var hideForceUnixSlashes = true
+56 -12
View File
@@ -66,6 +66,7 @@ func TestConfigSSH(t *testing.T) {
const hostname = "test-coder."
const expectedKey = "ConnectionAttempts"
const removeKey = "ConnectionTimeout"
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
ConfigSSH: codersdk.SSHConfigResponse{
@@ -73,6 +74,7 @@ func TestConfigSSH(t *testing.T) {
SSHConfigOptions: map[string]string{
// Something we can test for
expectedKey: "3",
removeKey: "",
},
},
})
@@ -176,6 +178,7 @@ func TestConfigSSH(t *testing.T) {
fileContents, err := os.ReadFile(sshConfigFile)
require.NoError(t, err, "read ssh config file")
require.Contains(t, string(fileContents), expectedKey, "ssh config file contains expected key")
require.NotContains(t, string(fileContents), removeKey, "ssh config file should not have removed key")
home := filepath.Dir(filepath.Dir(sshConfigFile))
// #nosec
@@ -213,18 +216,20 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
ssh string
}
type wantConfig struct {
ssh string
ssh string
regexMatch string
}
type match struct {
match, write string
}
tests := []struct {
name string
args []string
matches []match
writeConfig writeConfig
wantConfig wantConfig
wantErr bool
name string
args []string
matches []match
writeConfig writeConfig
wantConfig wantConfig
wantErr bool
echoResponse *echo.Responses
}{
{
name: "Config file is created",
@@ -478,12 +483,32 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
},
args: []string{"--yes"},
},
{
name: "Serialize supported flags",
wantConfig: wantConfig{
ssh: strings.Join([]string{
headerStart,
"# Last config-ssh options:",
"# :wait=yes",
"# :ssh-host-prefix=coder-test.",
"#",
headerEnd,
"",
}, "\n"),
},
args: []string{
"--yes",
"--wait=yes",
"--ssh-host-prefix", "coder-test.",
},
},
{
name: "Do not prompt for new options when prev opts flag is set",
writeConfig: writeConfig{
ssh: strings.Join([]string{
headerStart,
"# Last config-ssh options:",
"# :wait=no",
"# :ssh-option=ForwardAgent=yes",
"#",
headerEnd,
@@ -494,6 +519,7 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
ssh: strings.Join([]string{
headerStart,
"# Last config-ssh options:",
"# :wait=no",
"# :ssh-option=ForwardAgent=yes",
"#",
headerEnd,
@@ -555,6 +581,20 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
},
wantErr: true,
},
{
name: "Custom CLI Path",
args: []string{
"-y", "--coder-binary-path", "/foo/bar/coder",
},
wantErr: false,
echoResponse: &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: echo.ProvisionApplyWithAgent(""),
},
wantConfig: wantConfig{
regexMatch: "ProxyCommand /foo/bar/coder",
},
},
}
for _, tt := range tests {
tt := tt
@@ -564,7 +604,7 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
var (
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user = coderdtest.CreateFirstUser(t, client)
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, tt.echoResponse)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID)
@@ -586,8 +626,7 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
inv.Stdin = pty.Input()
inv.Stdout = pty.Output()
pty.Attach(inv)
done := tGo(t, func() {
err := inv.Run()
if !tt.wantErr {
@@ -604,9 +643,14 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
<-done
if tt.wantConfig.ssh != "" {
if tt.wantConfig.ssh != "" || tt.wantConfig.regexMatch != "" {
got := sshConfigFileRead(t, sshConfigName)
assert.Equal(t, tt.wantConfig.ssh, got)
if tt.wantConfig.ssh != "" {
assert.Equal(t, tt.wantConfig.ssh, got)
}
if tt.wantConfig.regexMatch != "" {
assert.Regexp(t, tt.wantConfig.regexMatch, got, "regex match")
}
}
})
}
+6
View File
@@ -0,0 +1,6 @@
//go:build windows
package cli
// Must be a var for unit tests to conform behavior
var hideForceUnixSlashes = false
+55 -105
View File
@@ -6,6 +6,7 @@ import (
"io"
"time"
"github.com/google/uuid"
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
@@ -17,7 +18,6 @@ import (
func (r *RootCmd) create() *clibase.Cmd {
var (
parameterFile string
richParameterFile string
templateName string
startAt string
@@ -29,15 +29,25 @@ func (r *RootCmd) create() *clibase.Cmd {
Annotations: workspaceCommand,
Use: "create [name]",
Short: "Create a workspace",
Middleware: clibase.Chain(r.InitClient(client)),
Long: formatExamples(
example{
Description: "Create a workspace for another user (if you have permission)",
Command: "coder create <username>/<workspace_name>",
},
),
Middleware: clibase.Chain(r.InitClient(client)),
Handler: func(inv *clibase.Invocation) error {
organization, err := CurrentOrganization(inv, client)
if err != nil {
return err
}
workspaceOwner := codersdk.Me
if len(inv.Args) >= 1 {
workspaceName = inv.Args[0]
workspaceOwner, workspaceName, err = splitNamedWorkspace(inv.Args[0])
if err != nil {
return err
}
}
if workspaceName == "" {
@@ -56,14 +66,14 @@ func (r *RootCmd) create() *clibase.Cmd {
}
}
_, err = client.WorkspaceByOwnerAndName(inv.Context(), codersdk.Me, workspaceName, codersdk.WorkspaceOptions{})
_, err = client.WorkspaceByOwnerAndName(inv.Context(), workspaceOwner, workspaceName, codersdk.WorkspaceOptions{})
if err == nil {
return xerrors.Errorf("A workspace already exists named %q!", workspaceName)
}
var template codersdk.Template
if templateName == "" {
_, _ = fmt.Fprintln(inv.Stdout, cliui.Styles.Wrap.Render("Select a template below to preview the provisioned infrastructure:"))
_, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Wrap.Render("Select a template below to preview the provisioned infrastructure:"))
templates, err := client.TemplatesByOrganization(inv.Context(), organization.ID)
if err != nil {
@@ -81,7 +91,7 @@ func (r *RootCmd) create() *clibase.Cmd {
templateName := template.Name
if template.ActiveUserCount > 0 {
templateName += cliui.Styles.Placeholder.Render(
templateName += cliui.DefaultStyles.Placeholder.Render(
fmt.Sprintf(
" (used by %s)",
formatActiveDevelopers(template.ActiveUserCount),
@@ -121,8 +131,6 @@ func (r *RootCmd) create() *clibase.Cmd {
buildParams, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
Template: template,
ExistingParams: []codersdk.Parameter{},
ParameterFile: parameterFile,
RichParameterFile: richParameterFile,
NewWorkspaceName: workspaceName,
})
@@ -141,16 +149,13 @@ func (r *RootCmd) create() *clibase.Cmd {
var ttlMillis *int64
if stopAfter > 0 {
ttlMillis = ptr.Ref(stopAfter.Milliseconds())
} else if template.MaxTTLMillis > 0 {
ttlMillis = &template.MaxTTLMillis
}
workspace, err := client.CreateWorkspace(inv.Context(), organization.ID, codersdk.Me, codersdk.CreateWorkspaceRequest{
workspace, err := client.CreateWorkspace(inv.Context(), organization.ID, workspaceOwner, codersdk.CreateWorkspaceRequest{
TemplateID: template.ID,
Name: workspaceName,
AutostartSchedule: schedSpec,
TTLMillis: ttlMillis,
ParameterValues: buildParams.parameters,
RichParameterValues: buildParams.richParameters,
})
if err != nil {
@@ -162,7 +167,7 @@ func (r *RootCmd) create() *clibase.Cmd {
return xerrors.Errorf("watch build: %w", err)
}
_, _ = fmt.Fprintf(inv.Stdout, "\nThe %s workspace has been created at %s!\n", cliui.Styles.Keyword.Render(workspace.Name), cliui.Styles.DateTimeStamp.Render(time.Now().Format(time.Stamp)))
_, _ = fmt.Fprintf(inv.Stdout, "\nThe %s workspace has been created at %s!\n", cliui.DefaultStyles.Keyword.Render(workspace.Name), cliui.DefaultStyles.DateTimeStamp.Render(time.Now().Format(time.Stamp)))
return nil
},
}
@@ -174,12 +179,6 @@ func (r *RootCmd) create() *clibase.Cmd {
Description: "Specify a template name.",
Value: clibase.StringOf(&templateName),
},
clibase.Option{
Flag: "parameter-file",
Env: "CODER_PARAMETER_FILE",
Description: "Specify a file path with parameter values.",
Value: clibase.StringOf(&parameterFile),
},
clibase.Option{
Flag: "rich-parameter-file",
Env: "CODER_RICH_PARAMETER_FILE",
@@ -200,24 +199,21 @@ func (r *RootCmd) create() *clibase.Cmd {
},
cliui.SkipPromptOption(),
)
return cmd
}
type prepWorkspaceBuildArgs struct {
Template codersdk.Template
ExistingParams []codersdk.Parameter
ParameterFile string
ExistingRichParams []codersdk.WorkspaceBuildParameter
RichParameterFile string
NewWorkspaceName string
UpdateWorkspace bool
BuildOptions bool
WorkspaceID uuid.UUID
}
type buildParameters struct {
// Parameters contains legacy parameters stored in /parameters.
parameters []codersdk.CreateParameterRequest
// Rich parameters stores values for build parameters annotated with description, icon, type, etc.
richParameters []codersdk.WorkspaceBuildParameter
}
@@ -227,109 +223,42 @@ type buildParameters struct {
func prepWorkspaceBuild(inv *clibase.Invocation, client *codersdk.Client, args prepWorkspaceBuildArgs) (*buildParameters, error) {
ctx := inv.Context()
var useRichParameters bool
if len(args.ExistingRichParams) > 0 && len(args.RichParameterFile) > 0 {
useRichParameters = true
}
var useLegacyParameters bool
if len(args.ExistingParams) > 0 || len(args.ParameterFile) > 0 {
useLegacyParameters = true
}
if useRichParameters && useLegacyParameters {
return nil, xerrors.Errorf("Rich parameters can't be used together with legacy parameters.")
}
templateVersion, err := client.TemplateVersion(ctx, args.Template.ActiveVersionID)
if err != nil {
return nil, err
}
// Legacy parameters
parameterSchemas, err := client.TemplateVersionSchema(ctx, templateVersion.ID)
if err != nil {
return nil, err
}
// parameterMapFromFile can be nil if parameter file is not specified
var parameterMapFromFile map[string]string
useParamFile := false
if args.ParameterFile != "" {
useParamFile = true
_, _ = fmt.Fprintln(inv.Stdout, cliui.Styles.Paragraph.Render("Attempting to read the variables from the parameter file.")+"\r\n")
parameterMapFromFile, err = createParameterMapFromFile(args.ParameterFile)
if err != nil {
return nil, err
}
}
disclaimerPrinted := false
legacyParameters := make([]codersdk.CreateParameterRequest, 0)
PromptParamLoop:
for _, parameterSchema := range parameterSchemas {
if !parameterSchema.AllowOverrideSource {
continue
}
if !disclaimerPrinted {
_, _ = fmt.Fprintln(inv.Stdout, cliui.Styles.Paragraph.Render("This template has customizable parameters. Values can be changed after create, but may have unintended side effects (like data loss).")+"\r\n")
disclaimerPrinted = true
}
// Param file is all or nothing
if !useParamFile {
for _, e := range args.ExistingParams {
if e.Name == parameterSchema.Name {
// If the param already exists, we do not need to prompt it again.
// The workspace scope will reuse params for each build.
continue PromptParamLoop
}
}
}
parameterValue, err := getParameterValueFromMapOrInput(inv, parameterMapFromFile, parameterSchema)
if err != nil {
return nil, err
}
legacyParameters = append(legacyParameters, codersdk.CreateParameterRequest{
Name: parameterSchema.Name,
SourceValue: parameterValue,
SourceScheme: codersdk.ParameterSourceSchemeData,
DestinationScheme: parameterSchema.DefaultDestinationScheme,
})
}
if disclaimerPrinted {
_, _ = fmt.Fprintln(inv.Stdout)
}
// Rich parameters
templateVersionParameters, err := client.TemplateVersionRichParameters(inv.Context(), templateVersion.ID)
if err != nil {
return nil, xerrors.Errorf("get template version rich parameters: %w", err)
}
parameterMapFromFile = map[string]string{}
useParamFile = false
parameterMapFromFile := map[string]string{}
useParamFile := false
if args.RichParameterFile != "" {
useParamFile = true
_, _ = fmt.Fprintln(inv.Stdout, cliui.Styles.Paragraph.Render("Attempting to read the variables from the rich parameter file.")+"\r\n")
_, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Paragraph.Render("Attempting to read the variables from the rich parameter file.")+"\r\n")
parameterMapFromFile, err = createParameterMapFromFile(args.RichParameterFile)
if err != nil {
return nil, err
}
}
disclaimerPrinted = false
disclaimerPrinted := false
richParameters := make([]codersdk.WorkspaceBuildParameter, 0)
PromptRichParamLoop:
for _, templateVersionParameter := range templateVersionParameters {
if !args.BuildOptions && templateVersionParameter.Ephemeral {
continue
}
if !disclaimerPrinted {
_, _ = fmt.Fprintln(inv.Stdout, cliui.Styles.Paragraph.Render("This template has customizable parameters. Values can be changed after create, but may have unintended side effects (like data loss).")+"\r\n")
_, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Paragraph.Render("This template has customizable parameters. Values can be changed after create, but may have unintended side effects (like data loss).")+"\r\n")
disclaimerPrinted = true
}
// Param file is all or nothing
if !useParamFile {
if !useParamFile && !templateVersionParameter.Ephemeral {
for _, e := range args.ExistingRichParams {
if e.Name == templateVersionParameter.Name {
// If the param already exists, we do not need to prompt it again.
@@ -340,8 +269,17 @@ PromptRichParamLoop:
}
if args.UpdateWorkspace && !templateVersionParameter.Mutable {
_, _ = fmt.Fprintln(inv.Stdout, cliui.Styles.Warn.Render(fmt.Sprintf(`Parameter %q is not mutable, so can't be customized after workspace creation.`, templateVersionParameter.Name)))
continue
// Check if the immutable parameter was used in the previous build. If so, then it isn't a fresh one
// and the user should be warned.
exists, err := workspaceBuildParameterExists(ctx, client, args.WorkspaceID, templateVersionParameter)
if err != nil {
return nil, err
}
if exists {
_, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Warn.Render(fmt.Sprintf(`Parameter %q is not mutable, so can't be customized after workspace creation.`, templateVersionParameter.Name)))
continue
}
}
parameterValue, err := getWorkspaceBuildParameterValueFromMapOrInput(inv, parameterMapFromFile, templateVersionParameter)
@@ -368,7 +306,6 @@ PromptRichParamLoop:
// Run a dry-run with the given parameters to check correctness
dryRun, err := client.CreateTemplateVersionDryRun(inv.Context(), templateVersion.ID, codersdk.CreateTemplateVersionDryRunRequest{
WorkspaceName: args.NewWorkspaceName,
ParameterValues: legacyParameters,
RichParameterValues: richParameters,
})
if err != nil {
@@ -410,7 +347,20 @@ PromptRichParamLoop:
}
return &buildParameters{
parameters: legacyParameters,
richParameters: richParameters,
}, nil
}
func workspaceBuildParameterExists(ctx context.Context, client *codersdk.Client, workspaceID uuid.UUID, templateVersionParameter codersdk.TemplateVersionParameter) (bool, error) {
lastBuildParameters, err := client.WorkspaceBuildParameters(ctx, workspaceID)
if err != nil {
return false, xerrors.Errorf("can't fetch last workspace build parameters: %w", err)
}
for _, p := range lastBuildParameters {
if p.Name == templateVersionParameter.Name {
return true, nil
}
}
return false, nil
}
+67 -216
View File
@@ -2,7 +2,6 @@ package cli_test
import (
"context"
"fmt"
"net/http"
"os"
"regexp"
@@ -15,6 +14,7 @@ import (
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/gitauth"
"github.com/coder/coder/coderd/util/ptr"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
@@ -79,6 +79,63 @@ func TestCreate(t *testing.T) {
}
})
t.Run("CreateForOtherUser", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: provisionCompleteWithAgent,
ProvisionPlan: provisionCompleteWithAgent,
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
_, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
args := []string{
"create",
user.Username + "/their-workspace",
"--template", template.Name,
"--start-at", "9:30AM Mon-Fri US/Central",
"--stop-after", "8h",
}
inv, root := clitest.New(t, args...)
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
go func() {
defer close(doneChan)
err := inv.Run()
assert.NoError(t, err)
}()
matches := []struct {
match string
write string
}{
{match: "compute.main"},
{match: "smith (linux, i386)"},
{match: "Confirm create", write: "yes"},
}
for _, m := range matches {
pty.ExpectMatch(m.match)
if len(m.write) > 0 {
pty.WriteLine(m.write)
}
}
<-doneChan
ws, err := client.WorkspaceByOwnerAndName(context.Background(), user.Username, "their-workspace", codersdk.WorkspaceOptions{})
if assert.NoError(t, err, "expected workspace to be created") {
assert.Equal(t, ws.TemplateName, template.Name)
if assert.NotNil(t, ws.AutostartSchedule) {
assert.Equal(t, *ws.AutostartSchedule, "CRON_TZ=US/Central 30 9 * * Mon-Fri")
}
if assert.NotNil(t, ws.TTLMillis) {
assert.Equal(t, *ws.TTLMillis, 8*time.Hour.Milliseconds())
}
}
})
t.Run("InheritStopAfterFromTemplate", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
@@ -180,181 +237,6 @@ func TestCreate(t *testing.T) {
assert.Nil(t, ws.AutostartSchedule, "expected workspace autostart schedule to be nil")
}
})
t.Run("WithParameter", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
defaultValue := "something"
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: createTestParseResponseWithDefault(defaultValue),
ProvisionApply: echo.ProvisionComplete,
ProvisionPlan: echo.ProvisionComplete,
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
_ = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
inv, root := clitest.New(t, "create", "")
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
go func() {
defer close(doneChan)
err := inv.Run()
assert.NoError(t, err)
}()
matches := []string{
"Specify a name", "my-workspace",
fmt.Sprintf("Enter a value (default: %q):", defaultValue), "bingo",
"Enter a value:", "boingo",
"Confirm create?", "yes",
}
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
pty.WriteLine(value)
}
<-doneChan
})
t.Run("WithParameterFileContainingTheValue", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
defaultValue := "something"
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: createTestParseResponseWithDefault(defaultValue),
ProvisionApply: echo.ProvisionComplete,
ProvisionPlan: echo.ProvisionComplete,
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
_ = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
tempDir := t.TempDir()
removeTmpDirUntilSuccessAfterTest(t, tempDir)
parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml")
_, _ = parameterFile.WriteString("region: \"bingo\"\nusername: \"boingo\"")
inv, root := clitest.New(t, "create", "", "--parameter-file", parameterFile.Name())
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
go func() {
defer close(doneChan)
err := inv.Run()
assert.NoError(t, err)
}()
matches := []string{
"Specify a name", "my-workspace",
"Confirm create?", "yes",
}
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
pty.WriteLine(value)
}
<-doneChan
})
t.Run("WithParameterFileNotContainingTheValue", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
defaultValue := "something"
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: createTestParseResponseWithDefault(defaultValue),
ProvisionApply: echo.ProvisionComplete,
ProvisionPlan: echo.ProvisionComplete,
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
_ = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
tempDir := t.TempDir()
removeTmpDirUntilSuccessAfterTest(t, tempDir)
parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml")
_, _ = parameterFile.WriteString("username: \"boingo\"")
inv, root := clitest.New(t, "create", "", "--parameter-file", parameterFile.Name())
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
go func() {
defer close(doneChan)
err := inv.Run()
assert.NoError(t, err)
}()
matches := []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})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: []*proto.Parse_Response{{
Type: &proto.Parse_Response_Complete{
Complete: &proto.Parse_Complete{
ParameterSchemas: echo.ParameterSuccess,
},
},
}},
ProvisionPlan: []*proto.Provision_Response{
{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{},
},
},
},
})
tempDir := t.TempDir()
parameterFile, err := os.CreateTemp(tempDir, "testParameterFile*.yaml")
require.NoError(t, err)
defer parameterFile.Close()
_, _ = parameterFile.WriteString(fmt.Sprintf("%s: %q", echo.ParameterExecKey, echo.ParameterError("fail")))
// The template import job should end up failed, but we need it to be
// succeeded so the dry-run can begin.
version = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
require.Equal(t, codersdk.ProvisionerJobSucceeded, version.Job.Status, "job is not failed")
_ = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
inv, root := clitest.New(t, "create", "test", "--parameter-file", parameterFile.Name(), "-y")
clitest.SetupConfig(t, client, root)
ptytest.New(t).Attach(inv)
err = inv.Run()
require.Error(t, err)
require.ErrorContains(t, err, "dry-run workspace")
})
}
func TestCreateWithRichParameters(t *testing.T) {
@@ -366,12 +248,13 @@ func TestCreateWithRichParameters(t *testing.T) {
firstParameterValue = "1"
secondParameterName = "second_parameter"
secondParameterDisplayName = "Second Parameter"
secondParameterDescription = "This is second parameter"
secondParameterValue = "2"
immutableParameterName = "third_parameter"
immutableParameterDescription = "This is not mutable parameter"
immutableParameterValue = "3"
immutableParameterValue = "4"
)
echoResponses := &echo.Responses{
@@ -382,7 +265,7 @@ func TestCreateWithRichParameters(t *testing.T) {
Complete: &proto.Provision_Complete{
Parameters: []*proto.RichParameter{
{Name: firstParameterName, Description: firstParameterDescription, Mutable: true},
{Name: secondParameterName, Description: secondParameterDescription, Mutable: true},
{Name: secondParameterName, DisplayName: secondParameterDisplayName, Description: secondParameterDescription, Mutable: true},
{Name: immutableParameterName, Description: immutableParameterDescription, Mutable: false},
},
},
@@ -418,6 +301,7 @@ func TestCreateWithRichParameters(t *testing.T) {
matches := []string{
firstParameterDescription, firstParameterValue,
secondParameterDisplayName, "",
secondParameterDescription, secondParameterValue,
immutableParameterDescription, immutableParameterValue,
"Confirm create?", "yes",
@@ -426,7 +310,10 @@ func TestCreateWithRichParameters(t *testing.T) {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
pty.WriteLine(value)
if value != "" {
pty.WriteLine(value)
}
}
<-doneChan
})
@@ -489,7 +376,7 @@ func TestCreateValidateRichParameters(t *testing.T) {
)
numberRichParameters := []*proto.RichParameter{
{Name: numberParameterName, Type: "number", Mutable: true, ValidationMin: 3, ValidationMax: 10},
{Name: numberParameterName, Type: "number", Mutable: true, ValidationMin: ptr.Ref(int32(3)), ValidationMax: ptr.Ref(int32(10))},
}
stringRichParameters := []*proto.RichParameter{
@@ -745,39 +632,3 @@ func TestCreateWithGitAuth(t *testing.T) {
pty.ExpectMatch("Confirm create?")
pty.WriteLine("yes")
}
func createTestParseResponseWithDefault(defaultValue string) []*proto.Parse_Response {
return []*proto.Parse_Response{{
Type: &proto.Parse_Response_Complete{
Complete: &proto.Parse_Complete{
ParameterSchemas: []*proto.ParameterSchema{
{
AllowOverrideSource: true,
Name: "region",
Description: "description 1",
DefaultSource: &proto.ParameterSource{
Scheme: proto.ParameterSource_DATA,
Value: defaultValue,
},
DefaultDestination: &proto.ParameterDestination{
Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
},
},
{
AllowOverrideSource: true,
Name: "username",
Description: "description 2",
DefaultSource: &proto.ParameterSource{
Scheme: proto.ParameterSource_DATA,
// No default value
Value: "",
},
DefaultDestination: &proto.ParameterDestination{
Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
},
},
},
},
},
}}
}
+1 -9
View File
@@ -37,14 +37,6 @@ func (r *RootCmd) deleteWorkspace() *clibase.Cmd {
}
var state []byte
if orphan {
cliui.Warn(
inv.Stderr,
"Orphaning workspace requires template edit permission",
)
}
build, err := client.CreateWorkspaceBuild(inv.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionDelete,
ProvisionerState: state,
@@ -59,7 +51,7 @@ func (r *RootCmd) deleteWorkspace() *clibase.Cmd {
return err
}
_, _ = fmt.Fprintf(inv.Stdout, "\nThe %s workspace has been deleted at %s!\n", cliui.Styles.Keyword.Render(workspace.Name), cliui.Styles.DateTimeStamp.Render(time.Now().Format(time.Stamp)))
_, _ = fmt.Fprintf(inv.Stdout, "\nThe %s workspace has been deleted at %s!\n", cliui.DefaultStyles.Keyword.Render(workspace.Name), cliui.DefaultStyles.DateTimeStamp.Render(time.Now().Format(time.Stamp)))
return nil
},
}
+49
View File
@@ -2,6 +2,7 @@ package cli_test
import (
"context"
"fmt"
"io"
"testing"
@@ -10,8 +11,11 @@ import (
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/dbauthz"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/pty/ptytest"
"github.com/coder/coder/testutil"
)
func TestDelete(t *testing.T) {
@@ -68,6 +72,51 @@ func TestDelete(t *testing.T) {
<-doneChan
})
// Super orphaned, as the workspace doesn't even have a user.
// This is not a scenario we should ever get into, as we do not allow users
// to be deleted if they have workspaces. However issue #7872 shows that
// it is possible to get into this state. An admin should be able to still
// force a delete action on the workspace.
t.Run("OrphanDeletedUser", func(t *testing.T) {
t.Parallel()
client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
deleteMeClient, deleteMeUser := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, deleteMeClient, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, deleteMeClient, workspace.LatestBuild.ID)
// The API checks if the user has any workspaces, so we cannot delete a user
// this way.
ctx := testutil.Context(t, testutil.WaitShort)
// nolint:gocritic // Unit test
err := api.Database.UpdateUserDeletedByID(dbauthz.AsSystemRestricted(ctx), database.UpdateUserDeletedByIDParams{
ID: deleteMeUser.ID,
Deleted: true,
})
require.NoError(t, err)
inv, root := clitest.New(t, "delete", fmt.Sprintf("%s/%s", deleteMeUser.ID, workspace.Name), "-y", "--orphan")
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
inv.Stderr = pty.Output()
go func() {
defer close(doneChan)
err := inv.Run()
// When running with the race detector on, we sometimes get an EOF.
if err != nil {
assert.ErrorIs(t, err, io.EOF)
}
}()
pty.ExpectMatch("workspace has been deleted")
<-doneChan
})
t.Run("DifferentUser", func(t *testing.T) {
t.Parallel()
adminClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
+88 -1
View File
@@ -1,6 +1,7 @@
package cli
import (
"bytes"
"errors"
"fmt"
"io/fs"
@@ -18,6 +19,8 @@ import (
func (r *RootCmd) dotfiles() *clibase.Cmd {
var symlinkDir string
var gitbranch string
cmd := &clibase.Cmd{
Use: "dotfiles <git_repo_url>",
Middleware: clibase.RequireNArgs(1),
@@ -102,6 +105,9 @@ func (r *RootCmd) dotfiles() *clibase.Cmd {
}
gitCmdDir = cfgDir
subcommands = []string{"clone", inv.Args[0], dotfilesRepoDir}
if gitbranch != "" {
subcommands = append(subcommands, "--branch", gitbranch)
}
promptText = fmt.Sprintf("Cloning %s into directory %s.\n\n Continue?", gitRepo, dotfilesDir)
}
@@ -137,7 +143,24 @@ func (r *RootCmd) dotfiles() *clibase.Cmd {
return err
}
// if the repo exists we soft fail the update operation and try to continue
_, _ = fmt.Fprintln(inv.Stdout, cliui.Styles.Error.Render("Failed to update repo, continuing..."))
_, _ = fmt.Fprintln(inv.Stdout, cliui.DefaultStyles.Error.Render("Failed to update repo, continuing..."))
}
if dotfilesExists && gitbranch != "" {
// If the repo exists and the git-branch is specified, we need to check out the branch. We do this after
// git pull to make sure the branch was pulled down locally. If we do this before the pull, we could be
// trying to checkout a branch that does not yet exist locally and get a git error.
_, _ = fmt.Fprintf(inv.Stdout, "Dotfiles git branch %q specified\n", gitbranch)
err := ensureCorrectGitBranch(inv, ensureCorrectGitBranchParams{
repoDir: dotfilesDir,
gitSSHCommand: gitsshCmd,
gitBranch: gitbranch,
})
if err != nil {
// Do not block on this error, just log it and continue
_, _ = fmt.Fprintln(inv.Stdout,
cliui.DefaultStyles.Error.Render(fmt.Sprintf("Failed to use branch %q (%s), continuing...", err.Error(), gitbranch)))
}
}
// save git repo url so we can detect changes next time
@@ -170,6 +193,18 @@ func (r *RootCmd) dotfiles() *clibase.Cmd {
}
_, _ = fmt.Fprintf(inv.Stdout, "Running %s...\n", script)
// Check if the script is executable and notify on error
scriptPath := filepath.Join(dotfilesDir, script)
fi, err := os.Stat(scriptPath)
if err != nil {
return xerrors.Errorf("stat %s: %w", scriptPath, err)
}
if fi.Mode()&0o111 == 0 {
return xerrors.Errorf("script %q is not executable. See https://coder.com/docs/v2/latest/dotfiles for information on how to resolve the issue.", script)
}
// it is safe to use a variable command here because it's from
// a filtered list of pre-approved install scripts
// nolint:gosec
@@ -225,6 +260,10 @@ func (r *RootCmd) dotfiles() *clibase.Cmd {
}
}
// attempt to delete the file before creating a new symlink. This overwrites any existing symlinks
// which are typically leftover from a previous call to coder dotfiles. We do this best effort and
// ignore errors because the symlink may or may not exist. Any regular files are backed up above.
_ = os.Remove(to)
err = os.Symlink(from, to)
if err != nil {
return xerrors.Errorf("symlinking %s to %s: %w", from, to, err)
@@ -242,11 +281,59 @@ func (r *RootCmd) dotfiles() *clibase.Cmd {
Description: "Specifies the directory for the dotfiles symlink destinations. If empty, will use $HOME.",
Value: clibase.StringOf(&symlinkDir),
},
{
Flag: "branch",
FlagShorthand: "b",
Description: "Specifies which branch to clone. " +
"If empty, will default to cloning the default branch or using the existing branch in the cloned repo on disk.",
Value: clibase.StringOf(&gitbranch),
},
cliui.SkipPromptOption(),
}
return cmd
}
type ensureCorrectGitBranchParams struct {
repoDir string
gitSSHCommand string
gitBranch string
}
func ensureCorrectGitBranch(baseInv *clibase.Invocation, params ensureCorrectGitBranchParams) error {
dotfileCmd := func(cmd string, args ...string) *exec.Cmd {
c := exec.CommandContext(baseInv.Context(), cmd, args...)
c.Dir = params.repoDir
c.Env = append(baseInv.Environ.ToOS(), fmt.Sprintf(`GIT_SSH_COMMAND=%s -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no`, params.gitSSHCommand))
c.Stdout = baseInv.Stdout
c.Stderr = baseInv.Stderr
return c
}
c := dotfileCmd("git", "branch", "--show-current")
// Save the output
var out bytes.Buffer
c.Stdout = &out
err := c.Run()
if err != nil {
return xerrors.Errorf("getting current git branch: %w", err)
}
if strings.TrimSpace(out.String()) != params.gitBranch {
// Checkout and pull the branch
c := dotfileCmd("git", "checkout", params.gitBranch)
err := c.Run()
if err != nil {
return xerrors.Errorf("checkout git branch %q: %w", params.gitBranch, err)
}
c = dotfileCmd("git", "pull", "--ff-only")
err = c.Run()
if err != nil {
return xerrors.Errorf("pull git branch %q: %w", params.gitBranch, err)
}
}
return nil
}
// dirExists checks if the path exists and is a directory.
func dirExists(name string) (bool, error) {
fi, err := os.Stat(name)
+62
View File
@@ -80,6 +80,52 @@ func TestDotfiles(t *testing.T) {
require.NoError(t, err)
require.Equal(t, string(b), "wow\n")
})
t.Run("InstallScriptChangeBranch", func(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("install scripts on windows require sh and aren't very practical")
}
_, root := clitest.New(t)
testRepo := testGitRepo(t, root)
// We need an initial commit to start the `main` branch
c := exec.Command("git", "commit", "--allow-empty", "-m", `"initial commit"`)
c.Dir = testRepo
err := c.Run()
require.NoError(t, err)
// nolint:gosec
err = os.WriteFile(filepath.Join(testRepo, "install.sh"), []byte("#!/bin/bash\necho wow > "+filepath.Join(string(root), ".bashrc")), 0o750)
require.NoError(t, err)
c = exec.Command("git", "checkout", "-b", "other_branch")
c.Dir = testRepo
err = c.Run()
require.NoError(t, err)
c = exec.Command("git", "add", "install.sh")
c.Dir = testRepo
err = c.Run()
require.NoError(t, err)
c = exec.Command("git", "commit", "-m", `"add install.sh"`)
c.Dir = testRepo
err = c.Run()
require.NoError(t, err)
c = exec.Command("git", "checkout", "main")
c.Dir = testRepo
err = c.Run()
require.NoError(t, err)
inv, _ := clitest.New(t, "dotfiles", "--global-config", string(root), "--symlink-dir", string(root), "-y", testRepo, "-b", "other_branch")
err = inv.Run()
require.NoError(t, err)
b, err := os.ReadFile(filepath.Join(string(root), ".bashrc"))
require.NoError(t, err)
require.Equal(t, string(b), "wow\n")
})
t.Run("SymlinkBackup", func(t *testing.T) {
t.Parallel()
_, root := clitest.New(t)
@@ -116,6 +162,17 @@ func TestDotfiles(t *testing.T) {
b, err = os.ReadFile(filepath.Join(string(root), ".bashrc.bak"))
require.NoError(t, err)
require.Equal(t, string(b), "backup")
// check for idempotency
inv, _ = clitest.New(t, "dotfiles", "--global-config", string(root), "--symlink-dir", string(root), "-y", testRepo)
err = inv.Run()
require.NoError(t, err)
b, err = os.ReadFile(filepath.Join(string(root), ".bashrc"))
require.NoError(t, err)
require.Equal(t, string(b), "wow")
b, err = os.ReadFile(filepath.Join(string(root), ".bashrc.bak"))
require.NoError(t, err)
require.Equal(t, string(b), "backup")
})
}
@@ -141,5 +198,10 @@ func testGitRepo(t *testing.T, root config.Root) string {
err = c.Run()
require.NoError(t, err)
c = exec.Command("git", "checkout", "-b", "main")
c.Dir = dir
err = c.Run()
require.NoError(t, err)
return dir
}
+18
View File
@@ -0,0 +1,18 @@
package cli
import "github.com/coder/coder/cli/clibase"
func (r *RootCmd) expCmd() *clibase.Cmd {
cmd := &clibase.Cmd{
Use: "exp",
Short: "Internal commands for testing and experimentation. These are prone to breaking changes with no notice.",
Handler: func(i *clibase.Invocation) error {
return i.Command.HelpHandler(i)
},
Hidden: true,
Children: []*clibase.Cmd{
r.scaletestCmd(),
},
}
return cmd
}
+451 -146
View File
@@ -14,9 +14,14 @@ import (
"time"
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.opentelemetry.io/otel/trace"
"golang.org/x/xerrors"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
"github.com/coder/coder/cli/clibase"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/coderd/httpapi"
@@ -25,14 +30,16 @@ import (
"github.com/coder/coder/cryptorand"
"github.com/coder/coder/scaletest/agentconn"
"github.com/coder/coder/scaletest/createworkspaces"
"github.com/coder/coder/scaletest/dashboard"
"github.com/coder/coder/scaletest/harness"
"github.com/coder/coder/scaletest/reconnectingpty"
"github.com/coder/coder/scaletest/workspacebuild"
"github.com/coder/coder/scaletest/workspacetraffic"
)
const scaletestTracerName = "coder_scaletest"
func (r *RootCmd) scaletest() *clibase.Cmd {
func (r *RootCmd) scaletestCmd() *clibase.Cmd {
cmd := &clibase.Cmd{
Use: "scaletest",
Short: "Run a scale test against the Coder API",
@@ -41,7 +48,9 @@ func (r *RootCmd) scaletest() *clibase.Cmd {
},
Children: []*clibase.Cmd{
r.scaletestCleanup(),
r.scaletestDashboard(),
r.scaletestCreateWorkspaces(),
r.scaletestWorkspaceTraffic(),
},
}
@@ -107,7 +116,10 @@ func (s *scaletestTracingFlags) provider(ctx context.Context) (trace.TracerProvi
return tracerProvider, func(ctx context.Context) error {
var err error
closeTracingOnce.Do(func() {
err = closeTracing(ctx)
// Allow time to upload traces even if ctx is canceled
traceCtx, traceCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer traceCancel()
err = closeTracing(traceCtx)
})
return err
@@ -307,6 +319,30 @@ func (s *scaletestOutputFlags) parse() ([]scaleTestOutput, error) {
return out, nil
}
type scaletestPrometheusFlags struct {
Address string
Wait time.Duration
}
func (s *scaletestPrometheusFlags) attach(opts *clibase.OptionSet) {
*opts = append(*opts,
clibase.Option{
Flag: "scaletest-prometheus-address",
Env: "CODER_SCALETEST_PROMETHEUS_ADDRESS",
Default: "0.0.0.0:21112",
Description: "Address on which to expose scaletest Prometheus metrics.",
Value: clibase.StringOf(&s.Address),
},
clibase.Option{
Flag: "scaletest-prometheus-wait",
Env: "CODER_SCALETEST_PROMETHEUS_WAIT",
Default: "15s",
Description: "How long to wait before exiting in order to allow Prometheus metrics to be scraped.",
Value: clibase.DurationOf(&s.Wait),
},
)
}
func requireAdmin(ctx context.Context, client *codersdk.Client) (codersdk.User, error) {
me, err := client.User(ctx, codersdk.Me)
if err != nil {
@@ -384,33 +420,9 @@ func (r *RootCmd) scaletestCleanup() *clibase.Cmd {
}
cliui.Infof(inv.Stdout, "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...)
workspaces, err := getScaletestWorkspaces(ctx, client)
if err != nil {
return err
}
cliui.Errorf(inv.Stderr, "Found %d scaletest workspaces\n", len(workspaces))
@@ -441,37 +453,13 @@ func (r *RootCmd) scaletestCleanup() *clibase.Cmd {
}
cliui.Infof(inv.Stdout, "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...)
users, err := getScaletestUsers(ctx, client)
if err != nil {
return err
}
cliui.Errorf(inv.Stderr, "Found %d scaletest users\n", len(users))
if len(workspaces) != 0 {
if len(users) != 0 {
cliui.Infof(inv.Stdout, "Deleting scaletest users..."+"\n")
harness := harness.NewTestHarness(cleanupStrategy.toStrategy(), harness.ConcurrentExecutionStrategy{})
@@ -510,10 +498,8 @@ func (r *RootCmd) scaletestCleanup() *clibase.Cmd {
func (r *RootCmd) scaletestCreateWorkspaces() *clibase.Cmd {
var (
count int64
template string
parametersFile string
parameters []string // key=value
count int64
template string
noPlan bool
noCleanup bool
@@ -535,6 +521,8 @@ func (r *RootCmd) scaletestCreateWorkspaces() *clibase.Cmd {
connectInterval time.Duration
connectTimeout time.Duration
useHostUser bool
tracingFlags = &scaletestTracingFlags{}
strategy = &scaletestStrategyFlags{}
cleanupStrategy = &scaletestStrategyFlags{cleanup: true}
@@ -607,51 +595,11 @@ func (r *RootCmd) scaletestCreateWorkspaces() *clibase.Cmd {
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,
WorkspaceName: "scaletest",
})
if err != nil {
return xerrors.Errorf("start dry run workspace creation: %w", err)
@@ -681,10 +629,11 @@ func (r *RootCmd) scaletestCreateWorkspaces() *clibase.Cmd {
}
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)
// canceled. This is a no-op if tracing is not enabled.
_, _ = fmt.Fprintln(inv.Stderr, "\nUploading traces...")
if err := closeTracing(ctx); err != nil {
_, _ = fmt.Fprintf(inv.Stderr, "\nError uploading traces: %+v\n", err)
}
}()
tracer := tracerProvider.Tracer(scaletestTracerName)
@@ -693,35 +642,36 @@ func (r *RootCmd) scaletestCreateWorkspaces() *clibase.Cmd {
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,
TemplateID: tpl.ID,
},
NoWaitForAgents: noWaitForAgents,
},
NoCleanup: noCleanup,
}
if useHostUser {
config.User.SessionToken = client.SessionToken()
} else {
config.User.Username, config.User.Email, err = newScaleTestUser(id)
if err != nil {
return xerrors.Errorf("create scaletest username and email: %w", err)
}
}
config.Workspace.Request.Name, err = newScaleTestWorkspace(id)
if err != nil {
return xerrors.Errorf("create scaletest workspace name: %w", err)
}
if runCommand != "" {
config.ReconnectingPTY = &reconnectingpty.Config{
// AgentID is set by the test automatically.
@@ -796,17 +746,6 @@ func (r *RootCmd) scaletestCreateWorkspaces() *clibase.Cmd {
return xerrors.Errorf("cleanup tests: %w", err)
}
// Upload traces.
if tracingEnabled {
_, _ = fmt.Fprintln(inv.Stderr, "\nUploading traces...")
ctx, cancel := context.WithTimeout(ctx, 1*time.Minute)
defer cancel()
err := closeTracing(ctx)
if err != nil {
_, _ = fmt.Fprintf(inv.Stderr, "\nError uploading traces: %+v\n", err)
}
}
if res.TotalFail > 0 {
return xerrors.New("load test failed, see above for more details")
}
@@ -831,18 +770,6 @@ func (r *RootCmd) scaletestCreateWorkspaces() *clibase.Cmd {
Description: "Required: Name or ID of the template to use for workspaces.",
Value: clibase.StringOf(&template),
},
{
Flag: "parameters-file",
Env: "CODER_SCALETEST_PARAMETERS_FILE",
Description: "Path to a YAML file containing the parameters to use for each workspace.",
Value: clibase.StringOf(&parametersFile),
},
{
Flag: "parameter",
Env: "CODER_SCALETEST_PARAMETERS",
Description: "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.",
Value: clibase.StringArrayOf(&parameters),
},
{
Flag: "no-plan",
Env: "CODER_SCALETEST_NO_PLAN",
@@ -927,6 +854,13 @@ func (r *RootCmd) scaletestCreateWorkspaces() *clibase.Cmd {
Description: "Timeout for each request to the --connect-url.",
Value: clibase.DurationOf(&connectTimeout),
},
{
Flag: "use-host-login",
Env: "CODER_SCALETEST_USE_HOST_LOGIN",
Default: "false",
Description: "Use the use logged in on the host machine, instead of creating users.",
Value: clibase.BoolOf(&useHostUser),
},
}
tracingFlags.attach(&cmd.Options)
@@ -936,6 +870,311 @@ func (r *RootCmd) scaletestCreateWorkspaces() *clibase.Cmd {
return cmd
}
func (r *RootCmd) scaletestWorkspaceTraffic() *clibase.Cmd {
var (
tickInterval time.Duration
bytesPerTick int64
ssh bool
client = &codersdk.Client{}
tracingFlags = &scaletestTracingFlags{}
strategy = &scaletestStrategyFlags{}
cleanupStrategy = &scaletestStrategyFlags{cleanup: true}
output = &scaletestOutputFlags{}
prometheusFlags = &scaletestPrometheusFlags{}
)
cmd := &clibase.Cmd{
Use: "workspace-traffic",
Short: "Generate traffic to scaletest workspaces through coderd",
Middleware: clibase.Chain(
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
ctx := inv.Context()
reg := prometheus.NewRegistry()
metrics := workspacetraffic.NewMetrics(reg, "username", "workspace_name", "agent_name")
logger := slog.Make(sloghuman.Sink(io.Discard))
prometheusSrvClose := ServeHandler(ctx, logger, promhttp.HandlerFor(reg, promhttp.HandlerOpts{}), prometheusFlags.Address, "prometheus")
defer prometheusSrvClose()
// Bypass rate limiting
client.HTTPClient = &http.Client{
Transport: &headerTransport{
transport: http.DefaultTransport,
header: map[string][]string{
codersdk.BypassRatelimitHeader: {"true"},
},
},
}
workspaces, err := getScaletestWorkspaces(inv.Context(), client)
if err != nil {
return err
}
if len(workspaces) == 0 {
return xerrors.Errorf("no scaletest workspaces exist")
}
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. This is a no-op if tracing is not enabled.
_, _ = fmt.Fprintln(inv.Stderr, "\nUploading traces...")
if err := closeTracing(ctx); err != nil {
_, _ = fmt.Fprintf(inv.Stderr, "\nError uploading traces: %+v\n", err)
}
// Wait for prometheus metrics to be scraped
_, _ = fmt.Fprintf(inv.Stderr, "Waiting %s for prometheus metrics to be scraped\n", prometheusFlags.Wait)
<-time.After(prometheusFlags.Wait)
}()
tracer := tracerProvider.Tracer(scaletestTracerName)
outputs, err := output.parse()
if err != nil {
return xerrors.Errorf("could not parse --output flags")
}
th := harness.NewTestHarness(strategy.toStrategy(), cleanupStrategy.toStrategy())
for idx, ws := range workspaces {
var (
agentID uuid.UUID
agentName string
name = "workspace-traffic"
id = strconv.Itoa(idx)
)
for _, res := range ws.LatestBuild.Resources {
if len(res.Agents) == 0 {
continue
}
agentID = res.Agents[0].ID
agentName = res.Agents[0].Name
}
if agentID == uuid.Nil {
_, _ = fmt.Fprintf(inv.Stderr, "WARN: skipping workspace %s: no agent\n", ws.Name)
continue
}
// Setup our workspace agent connection.
config := workspacetraffic.Config{
AgentID: agentID,
BytesPerTick: bytesPerTick,
Duration: strategy.timeout,
TickInterval: tickInterval,
ReadMetrics: metrics.ReadMetrics(ws.OwnerName, ws.Name, agentName),
WriteMetrics: metrics.WriteMetrics(ws.OwnerName, ws.Name, agentName),
SSH: ssh,
}
if err := config.Validate(); err != nil {
return xerrors.Errorf("validate config: %w", err)
}
var runner harness.Runnable = workspacetraffic.NewRunner(client, config)
if tracingEnabled {
runner = &runnableTraceWrapper{
tracer: tracer,
spanName: fmt.Sprintf("%s/%s", name, id),
runner: runner,
}
}
th.AddRun(name, id, runner)
}
_, _ = fmt.Fprintln(inv.Stderr, "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, inv.Stdout)
if err != nil {
return xerrors.Errorf("write output %q to %q: %w", o.format, o.path, err)
}
}
if res.TotalFail > 0 {
return xerrors.New("load test failed, see above for more details")
}
return nil
},
}
cmd.Options = []clibase.Option{
{
Flag: "bytes-per-tick",
Env: "CODER_SCALETEST_WORKSPACE_TRAFFIC_BYTES_PER_TICK",
Default: "1024",
Description: "How much traffic to generate per tick.",
Value: clibase.Int64Of(&bytesPerTick),
},
{
Flag: "tick-interval",
Env: "CODER_SCALETEST_WORKSPACE_TRAFFIC_TICK_INTERVAL",
Default: "100ms",
Description: "How often to send traffic.",
Value: clibase.DurationOf(&tickInterval),
},
{
Flag: "ssh",
Env: "CODER_SCALETEST_WORKSPACE_TRAFFIC_SSH",
Default: "",
Description: "Send traffic over SSH.",
Value: clibase.BoolOf(&ssh),
},
}
tracingFlags.attach(&cmd.Options)
strategy.attach(&cmd.Options)
cleanupStrategy.attach(&cmd.Options)
output.attach(&cmd.Options)
prometheusFlags.attach(&cmd.Options)
return cmd
}
func (r *RootCmd) scaletestDashboard() *clibase.Cmd {
var (
count int64
minWait time.Duration
maxWait time.Duration
client = &codersdk.Client{}
tracingFlags = &scaletestTracingFlags{}
strategy = &scaletestStrategyFlags{}
cleanupStrategy = &scaletestStrategyFlags{cleanup: true}
output = &scaletestOutputFlags{}
prometheusFlags = &scaletestPrometheusFlags{}
)
cmd := &clibase.Cmd{
Use: "dashboard",
Short: "Generate traffic to the HTTP API to simulate use of the dashboard.",
Middleware: clibase.Chain(
r.InitClient(client),
),
Handler: func(inv *clibase.Invocation) error {
ctx := inv.Context()
logger := slog.Make(sloghuman.Sink(inv.Stdout)).Leveled(slog.LevelInfo)
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. This is a no-op if tracing is not enabled.
_, _ = fmt.Fprintln(inv.Stderr, "\nUploading traces...")
if err := closeTracing(ctx); err != nil {
_, _ = fmt.Fprintf(inv.Stderr, "\nError uploading traces: %+v\n", err)
}
// Wait for prometheus metrics to be scraped
_, _ = fmt.Fprintf(inv.Stderr, "Waiting %s for prometheus metrics to be scraped\n", prometheusFlags.Wait)
<-time.After(prometheusFlags.Wait)
}()
tracer := tracerProvider.Tracer(scaletestTracerName)
outputs, err := output.parse()
if err != nil {
return xerrors.Errorf("could not parse --output flags")
}
reg := prometheus.NewRegistry()
prometheusSrvClose := ServeHandler(ctx, logger, promhttp.HandlerFor(reg, promhttp.HandlerOpts{}), prometheusFlags.Address, "prometheus")
defer prometheusSrvClose()
metrics := dashboard.NewMetrics(reg)
th := harness.NewTestHarness(strategy.toStrategy(), cleanupStrategy.toStrategy())
for i := int64(0); i < count; i++ {
name := fmt.Sprintf("dashboard-%d", i)
config := dashboard.Config{
MinWait: minWait,
MaxWait: maxWait,
Trace: tracingEnabled,
Logger: logger.Named(name),
RollTable: dashboard.DefaultActions,
}
if err := config.Validate(); err != nil {
return err
}
var runner harness.Runnable = dashboard.NewRunner(client, metrics, config)
if tracingEnabled {
runner = &runnableTraceWrapper{
tracer: tracer,
spanName: name,
runner: runner,
}
}
th.AddRun("dashboard", name, runner)
}
_, _ = fmt.Fprintln(inv.Stderr, "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, inv.Stdout)
if err != nil {
return xerrors.Errorf("write output %q to %q: %w", o.format, o.path, err)
}
}
if res.TotalFail > 0 {
return xerrors.New("load test failed, see above for more details")
}
return nil
},
}
cmd.Options = []clibase.Option{
{
Flag: "count",
Env: "CODER_SCALETEST_DASHBOARD_COUNT",
Default: "1",
Description: "Number of concurrent workers.",
Value: clibase.Int64Of(&count),
},
{
Flag: "min-wait",
Env: "CODER_SCALETEST_DASHBOARD_MIN_WAIT",
Default: "100ms",
Description: "Minimum wait between fetches.",
Value: clibase.DurationOf(&minWait),
},
{
Flag: "max-wait",
Env: "CODER_SCALETEST_DASHBOARD_MAX_WAIT",
Default: "1s",
Description: "Maximum wait between fetches.",
Value: clibase.DurationOf(&maxWait),
},
}
tracingFlags.attach(&cmd.Options)
strategy.attach(&cmd.Options)
cleanupStrategy.attach(&cmd.Options)
output.attach(&cmd.Options)
prometheusFlags.attach(&cmd.Options)
return cmd
}
type runnableTraceWrapper struct {
tracer trace.Tracer
spanName string
@@ -1009,9 +1248,75 @@ func isScaleTestUser(user codersdk.User) bool {
}
func isScaleTestWorkspace(workspace codersdk.Workspace) bool {
if !strings.HasPrefix(workspace.OwnerName, "scaletest-") {
return false
return strings.HasPrefix(workspace.OwnerName, "scaletest-") ||
strings.HasPrefix(workspace.Name, "scaletest-")
}
func getScaletestWorkspaces(ctx context.Context, client *codersdk.Client) ([]codersdk.Workspace, error) {
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 nil, 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...)
}
return workspaces, nil
}
func getScaletestUsers(ctx context.Context, client *codersdk.Client) ([]codersdk.User, error) {
var (
pageNumber = 0
limit = 100
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 nil, 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...)
}
return strings.HasPrefix(workspace.Name, "scaletest-")
return users, nil
}
+106
View File
@@ -0,0 +1,106 @@
package cli_test
import (
"bytes"
"context"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/pty/ptytest"
"github.com/coder/coder/testutil"
)
func TestScaleTestCreateWorkspaces(t *testing.T) {
t.Parallel()
// This test only validates that the CLI command accepts known arguments.
// More thorough testing is done in scaletest/createworkspaces/run_test.go.
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancelFunc()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
_ = coderdtest.CreateFirstUser(t, client)
// Write a parameters file.
tDir := t.TempDir()
outputFile := filepath.Join(tDir, "output.json")
inv, root := clitest.New(t, "exp", "scaletest", "create-workspaces",
"--count", "2",
"--template", "doesnotexist",
"--no-cleanup",
"--no-wait-for-agents",
"--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)
inv.Stdout = pty.Output()
inv.Stderr = pty.Output()
err := inv.WithContext(ctx).Run()
require.ErrorContains(t, err, "could not find template \"doesnotexist\" in any organization")
}
// This test just validates that the CLI command accepts its known arguments.
// A more comprehensive test is performed in workspacetraffic/run_test.go
func TestScaleTestWorkspaceTraffic(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitMedium)
defer cancelFunc()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
inv, root := clitest.New(t, "exp", "scaletest", "workspace-traffic",
"--timeout", "1s",
"--bytes-per-tick", "1024",
"--tick-interval", "100ms",
"--scaletest-prometheus-address", "127.0.0.1:0",
"--scaletest-prometheus-wait", "0s",
"--ssh",
)
clitest.SetupConfig(t, client, root)
var stdout, stderr bytes.Buffer
inv.Stdout = &stdout
inv.Stderr = &stderr
err := inv.WithContext(ctx).Run()
require.ErrorContains(t, err, "no scaletest workspaces exist")
}
// This test just validates that the CLI command accepts its known arguments.
func TestScaleTestDashboard(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitMedium)
defer cancelFunc()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
inv, root := clitest.New(t, "exp", "scaletest", "dashboard",
"--count", "1",
"--min-wait", "100ms",
"--max-wait", "1s",
"--timeout", "1s",
"--scaletest-prometheus-address", "127.0.0.1:0",
"--scaletest-prometheus-wait", "0s",
)
clitest.SetupConfig(t, client, root)
var stdout, stderr bytes.Buffer
inv.Stdout = &stdout
inv.Stderr = &stderr
err := inv.WithContext(ctx).Run()
require.NoError(t, err, "")
}

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