Compare commits

...

631 Commits

Author SHA1 Message Date
Kyle Carberry 43f622a52d fix: Remove unused workspace routes in favor of list with filter (#2038)
* fix: Remove unused workspace routes in favor of list with filter

This consolidates the workspace routes into a single place.
It allows users to fetch a workspace by their username and
workspace name, which will be used by the frontend for routing.

* Fix RBAC

* Fix CLI usages
2022-06-03 14:36:08 -05:00
Spike Curtis d8c440188e feat: Remove organization and user scoped parameters (#2007)
* feat: Remove organization and user scoped parameters

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

* Fixup dump.sql

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

* Fix dump.sql again

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

* Fix down migration

Signed-off-by: Spike Curtis <spike@coder.com>
2022-06-03 11:49:58 -07:00
David Wahler 582d636e54 feat: support fully-qualified workspace names in CLI (#2036) 2022-06-03 12:47:56 -05:00
Abhineet Jain fc38b61819 feat: use ConfirmDialog for ResetPasswordDialog (#2035)
* feat: use ConfirmDialog for ResetPasswordDialog

* fix lint

* make description typography a div

* use paragraph for string description, div otherwise

* fix lint
2022-06-03 17:35:09 +00:00
Colin Adler 60102cb22f chore: fix codecov ignore for queries.sql.go (#2037) 2022-06-03 12:26:59 -05:00
Garrett Delfosse 8b03e2b0e1 feat: Workspaces filtering (#1972)
Co-authored-by: G r e y <grey@coder.com>
Co-authored-by: Kira Pilot <kira@coder.com>
2022-06-03 17:20:28 +00:00
G r e y ac6cb269db feat: ws schedule timezone select (#2032)
Resolves: #1959

Summary:

The package tzdata is used to create a meaningful select-list for
timezone in the workspace schedule form.

Impact:

Improved UX. Furthermore, we guess your timezone if the form is being
initialized from scratch.
2022-06-03 12:52:02 -04:00
Abhineet Jain 2b12beef98 feat: Update success confirmation dialog and snackbar (#2005)
* feat: update success confirmation dialog and snackbar

* add success variants to confirm dialog and snackbar

* update story name

* use success variant for snackbar
2022-06-03 11:42:46 -04:00
G r e y 37aff0c8a9 fix: FE parsing of schedule with day strings (#2006)
Resolves: #1901

Summary:

We had a homegrown parser that only understood numbers, not strings like
MON or TUES. We replace the homegrown parser with cron-parser.

Details:

This was nearly a straight drop-in.

Impact:

Much less code/maintenance burden :D

What I learned:

Don't trust the README, sometimes you just gotta read the code or import
it and try it out. The `fields` representation of the parsed expression
was missing from their docs. I might open an issue or PR to update them!
2022-06-03 11:08:57 -04:00
Abhineet Jain 7e89d91ce3 feat: link to timezone database spells out timezone (#2026) 2022-06-03 14:57:09 +00:00
G r e y c2720577cb fix: ws schedule top-down restriction (#2008)
Resolves: #1958

Summary:

The workspace schedule form no longer disables certain fields based on
whether or not a start time is filled out. Instead, we validate that a
start time is provided if any of the days are checked.
2022-06-03 10:50:36 -04:00
Bruno Quaresma 88e8c96ddd feature: Load workspace build logs from streaming (#1997) 2022-06-03 09:23:45 -05:00
Abhineet Jain d6e9eab258 feat: Use consistent colors for links and highlighting (#1989)
* feat: consistent highlight colors

* update user dropdown menu border

* update borderedmenurow active color
2022-06-03 10:16:50 -04:00
Bruno Quaresma 6bb76782a6 feat: Open terminal in a new window (#2017) 2022-06-03 09:06:44 -05:00
Mathias Fredriksson b4f5920df5 fix: Avoid use of r.Context() after r.Hijack() (#1978) 2022-06-03 12:50:10 +03:00
Kyle Carberry 61aacff444 chore: Refactor site to improve testing (#2014)
It was difficult to develop this package due to the
embed build tag being mandatory on the tests. The logic
to test doesn't require any embedded files.
2022-06-03 04:27:21 +00:00
Colin Adler 89dde21837 fix: ensure listen websocket isn't opened for non-latest agents (#2002)
Exponential backoff is only enabled if the websocket fails to open. If
the websocket is opened but immediately killed, the agent will try to
immediately reconnect. This is desireable in cases where coderd is being
replaced or network conditions cause the connection to die, but not for
permanent errors.
2022-06-02 15:03:01 -05:00
Ketan Gangatirkar 0e1f868f5f tweak README.md headings around one liner 2022-06-02 14:48:22 -05:00
Ketan Gangatirkar 597994548d added one liner to run Coder at very top of README.md 2022-06-02 14:47:34 -05:00
G r e y 0b59ed30d0 feat: ui autostop extension (#1987)
Resolves: #1460

Summary:

An 'Extend' CTA on workspace schedule banner is added so that a user can
extend their workspace lease from the UI.

Details:

* feat: putWorkspaceExtension handler

* refactor: TypesGen dflt import in workspace.ts

* feat: defaultWorkspaceExtension util

Impact:

This completes the UI<-->CLI parity epic in an MVP way. Of course, a
future improvement to make is extending by times other than the default
90 minutes.
2022-06-02 15:44:11 -04:00
Oxylibrium 1a07d021fe ux: change colors for inflight workspace actions (#1986) 2022-06-02 12:52:20 -04:00
Abhineet Jain e09cd3e9cf feat: Update UI for error dialog and snackbar (#1971)
* feat: update ui for error dialog and snackbar

* update padding for buttons
2022-06-02 11:23:52 -04:00
Abhineet Jain 47c7eda670 feat: add a divider after Account menu item (#1927)
* add a divider after Account menu item

* test: improve Storybook tests

* add closed and open userdropdown tests

* add default isOpen

* extract UserDropdownContent into a single component

* remove the isOpen prop

* address nit comments

* update test name
2022-06-02 11:09:19 -04:00
Steven Masley e6ee7dd652 chore: Add linting rule to help catch InTx misuse (#1980)
* chore: Add linting rule to help catch InTx misuse

This isn't perfect, as if you nest your misuse in another code block
like an if statement, it won't catch it :/. It is better
than nothing
2022-06-02 14:50:15 +00:00
Abhineet Jain c463e7801c feat: Update TTL language to Time until shutdown (#1948)
* feat: update ttl language in frontend

* Update TTL Helper text

Co-authored-by: Presley Pizzo <1290996+presleyp@users.noreply.github.com>

* update TTL helper string

Co-authored-by: Presley Pizzo <1290996+presleyp@users.noreply.github.com>
2022-06-02 10:15:36 -04:00
G r e y ab69c22ddc fix: missing FE ttl constraint validation (#1952)
Resolves: #1908
2022-06-02 10:14:42 -04:00
Steven Masley b9983e417f feat: Handle pagination cases where after_id does not exist (#1947)
* feat: Handle pagination cases where after_id does not exist

Throw an error to the user in these cases
- Templateversions
- Workspacebuilds

User pagination does not need it as suspended users still
have rows in the database
2022-06-02 09:01:45 -05:00
Kira Pilot 419dc6b036 feat: flexbox updates on workspace page (#1963)
* feat: flexbox work on workspace page

resolves 1910

* fixing cancel text

* chromatic fixes

* resolves #1953

no overflox text on smaller screens
2022-06-02 09:57:36 -04:00
Bruno Quaresma 3fd4dcd9d5 fix: Display member role when user has no role (#1965) 2022-06-02 08:46:06 -05:00
Cian Johnston dcf03d8ba3 chore: refactor time.Duration -> int64 milliseconds for FE consumption (#1944)
* Changes all public-facing codersdk types to use a plain int64 (milliseconds) instead of time.Duration.
* Makes autostart_schedule a *string as it may not be present.
* Adds a utils/ptr package with some useful methods.
2022-06-02 11:23:34 +01:00
Mathias Fredriksson 51c420c90a feat: Add support for --identity-agent in coder ssh (#1954) 2022-06-02 11:13:38 +03:00
Spike Curtis 9e3a625898 Show workspace name in WorkspaceBuildStats component (#1933)
* Show workspace name in WorkspaceBuildStats component

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

* Fix WorkspaceBuildPage tests

Signed-off-by: Spike Curtis <spike@coder.com>
2022-06-01 16:49:43 -07:00
Cian Johnston b203d40123 fix: fix duplicate migrations (#1968) 2022-06-01 20:58:22 +00:00
Steven Masley 913c0f5e7f feat: Longer lived api keys for cli (#1935)
* feat: Longer lived api keys for cli
* feat: Refresh tokens based on their lifetime set in the db
* test: Add unit test for refreshing
2022-06-01 14:58:55 -05:00
Presley Pizzo bb400a4e82 fix: Show error message from backend on create existing user (#1964)
* Show error message from backend on create existing user

* Format
2022-06-01 15:52:54 -04:00
Ben Potter 46ffb67d60 feat: one-line install script (#1924)
* feat: one-line install script

* remove homebrew support

* remove arch linux

* use proper filename for packages

* fix variable format

* fix systemd instructions

* fixes to standalone script

* fix missing var bugs

* fix standalone install

* fix for MacOS

* format

* fix armv7 assets and zips

* remove windows

* update install docs

* support external sources with shellcheck

* shfmt

* add external sources to GitHub action & unfold

* change wording

* first template docs

* default to /usr/local instead

* add option for binary name
2022-06-01 14:15:09 -05:00
Bruno Quaresma f5a8d17aa8 feat: Add copy button to the SSH Page (#1962) 2022-06-01 19:01:36 +00:00
Bruno Quaresma b85de3ee79 feat: Improve empty states for workspaces and templates (#1950) 2022-06-01 17:32:55 +00:00
Dean Sheather 6be8a373e0 feat: run a terraform plan before creating workspaces with the given template parameters (#1732) 2022-06-02 00:44:53 +10:00
Steven Masley cc87a0cf6b feat: Implied 'member' roles for site and organization (#1917)
* feat: Member roles are implied and never exlpicitly added
* Rename "GetAllUserRoles" to "GetAuthorizationRoles"
* feat: Add migration to remove implied roles
* rename user auth role middleware
2022-06-01 09:07:50 -05:00
Presley Pizzo 2878346f19 Use backend error if possible (#1938) 2022-06-01 09:09:58 -04:00
Kyle Carberry 1fa50a9da1 fix: Race when writing to a closed pipe (#1916) 2022-06-01 07:59:03 -05:00
Cian Johnston 1c5d94ed5b fix: add all regions to aws examples (#1934) 2022-06-01 11:20:14 +01:00
Cian Johnston 7b40c692eb fix: coderd: dev mode should show verbose output by default (#1898)
* check buildinfo for devel prerelease tag and show verbose output if so
2022-06-01 11:00:42 +01:00
Steven Masley 7acb742218 feat: Prevent role changing on yourself. (#1931)
* feat: Prevent role changing on yourself.

Only allow changing roles on other users. Not much value in self changing
at the moment
2022-05-31 15:50:38 -05:00
Spike Curtis 4b0ed06a26 Remove set -u on yarn_install.sh to allow it to run on zsh (#1930)
Signed-off-by: Spike Curtis <spike@coder.com>
2022-05-31 13:50:18 -07:00
G r e y 56ec53d04b fix: derive running ws stop time from deadline (#1920)
* refactor: isWorkspaceOn utility

Summary:

A utility is function is added that answers the question if a workspace
is on.

Impact:

This is a shared piece of logic in workspace scheduling presentations.
In particular it unblocks work in 1779, or at least allows an
implementation that shares details with the WorkspaceScheduleBanner.

Notes:

We could possibly instead return whether the workspace is "ON",
"UNKNOWN", or "OFF". Maybe a future improvement for that could be made
as the neds arrises.

* fix: derive running ws stop time from deadline

Summary:

When a workspace is on, the remaining time until shutdown needs to be
derived from the deadline timestamp, not implied from the TTL
2022-05-31 15:50:03 -04:00
G r e y c6167a94ef refactor: remove dangling comment (#1929) 2022-05-31 19:39:29 +00:00
Bruno Quaresma 65c17a04df feat: Add selected template link at the template select field (#1918) 2022-05-31 18:28:22 +00:00
Bruno Quaresma 75bcb739f9 refactor: Make login headline one line and add auth method section (#1922) 2022-05-31 16:40:56 +00:00
Kira Pilot 555bf2461a fix: change color of time icon for dark mode (#1923)
resolves #1791
2022-05-31 12:33:15 -04:00
G r e y bdacbd4989 refactor: mock provisioner job typings (#1919)
An unnecessary type assertion was being made on the status property;
instead we just type the object as a ProvisionerJob
2022-05-31 15:16:15 +00:00
Presley Pizzo 6f7b7f0248 feat: Delete workspace (#1822)
* Add delete button

* Add confirmation dialog

* Extract dialog, storybook it, and test it

* Fix cancel and redirect

* Remove fragment
2022-05-31 10:43:31 -04:00
Abhineet Jain 9b19dc9154 refactor: rename SettingsPages directory to UserSettingsPage (#1877) 2022-05-31 14:16:17 +00:00
Bruno Quaresma 83edbee2e1 fix: Replace yes by true and add set -x (#1914) 2022-05-31 14:14:14 +00:00
Kira Pilot dd55d4577d chore: remove react imports (#1867)
reolves #1856
2022-05-31 10:01:37 -04:00
Steven Masley 26a2a169df fix: Suspended users cannot authenticate (#1849)
* fix: Suspended users cannot authenticate

- Merge roles and apikey extract httpmw
- Add member account to make dev
- feat: UI Shows suspended error logging into suspended account
- change 'active' route to 'activate'
2022-05-31 08:06:42 -05:00
Cian Johnston e02ef6f228 chore: executor_test: reduce test execution time (#1876)
Removes 5-second wait in autobuild.executor unit tests:

- Adds a write-only channel to Executor and plumbs through to unit tests
- Modifies runOnce to return an executor.RunStats struct and write to statsCh if not nil
2022-05-30 20:23:36 +01:00
Ketan Gangatirkar ae4b2d88cd added links to our issues to reduce necessary thinking to report issues 2022-05-30 14:19:48 -05:00
Cian Johnston a8ae9b39b3 feat: enforce upper bounds on workspace TTL and Deadline (#1902)
* Enforces upper bound for workspace TTL
* Enforces upper bound for workspace deadline
2022-05-30 20:19:17 +01:00
Ketan Gangatirkar 17a57a44eb added community links 2022-05-30 14:16:02 -05:00
Ketan Gangatirkar 02692402d8 added #coder in the most prominent least awkward place 2022-05-30 14:12:33 -05:00
Ben Potter 6850db2a47 chore: fix additional typo in templates doc 2022-05-28 08:14:46 -05:00
Ben Potter 80ec67f3fd chore: fix typo in templates docs 2022-05-28 08:13:50 -05:00
Ben Potter 7ad68ca36b example: docker: support Windows hosts (#1880) 2022-05-28 01:09:29 +00:00
Kyle Carberry da7ed8b292 chore: Ignore scripts from code coverage (#1878)
Our CI scripts don't need to have thorough tests, and aren't
in the hot path of the product.
2022-05-27 22:25:24 +00:00
Garrett Delfosse 5598ac05dc fix: prevent email from being altered (#1863) 2022-05-27 22:25:04 +00:00
Asher cfa316be89 fix: incomplete message when intercepting console logger (#1875)
I was getting a message like "Warning: Failed type %s: %s%s".
2022-05-27 17:16:19 -05:00
Asher dd1484e24f fix: add missing key to resource row (#1874) 2022-05-27 17:16:04 -05:00
Garrett Delfosse 8222bdc3bc feat: add user password change page (#1866) 2022-05-27 18:08:28 -04:00
Ben 8cd7d4fa9c chore: update hero 2022-05-27 20:48:52 +00:00
Abhineet Jain d623eeb8d1 feat: delete API token in /logout API (#1770)
* delete API token in logout api

* add deleteapikeybyid to databasefake

* set blank cookie on logout always

* refactor logout flow, add unit tests

* update logout messsage

* use read-only file mode for windows

* fix file mode on windows for cleanup

* change file permissions on windows

* assert error is not nil

* refactor cli

* try different file mode on windows

* try different file mode on windows

* try keeping the files open on Windows

* fix the error message on Windows
2022-05-27 16:47:03 -04:00
Kyle Carberry d0ed107b08 fix: Add command to reconnecting PTY (#1860)
This fixes #1708 and opens the door for PTYs to execute
non-shell commands!
2022-05-27 14:51:20 -05:00
Kira Pilot 6052607936 feat: add user roles to menu (#1862)
* view user roles in menu

resolves #1524

* fix stories

* PR feedback
2022-05-27 15:27:51 -04:00
G r e y 8d7499feb7 feat: ui alert <= 30mins from deadline (#1825)
Summary:

When a workspace build is <= 30 minutes from auto-scheduled shutdown,
then an alert banner is displayed on the workspace page.
2022-05-27 15:23:56 -04:00
Cian Johnston ff542afe87 feat: allow bumping workspace deadline (#1828)
* Adds a `bump` command to extend workspace build deadline
 * Reduces WARN-level logging spam from autobuild executor
 * Modifies `cli/ssh` notifications to read from workspace build deadline and to notify relative time instead (sidestepping the problem of figuring out a user's timezone across multiple OSes)
 * Shows workspace extension time in `coder list` output e.g.
    ```
    WORKSPACE        TEMPLATE  STATUS   LAST BUILT  OUTDATED  AUTOSTART        TTL        
    developer/test1  docker    Running  4m          false     0 9 * * MON-FRI  15m (+5m)  
    ```
2022-05-27 20:04:33 +01:00
Ben Potter bde3779fec chore: clarify install options in README (#1844)
* chore: clarify install options in README

* clarify the path is an example, not a requirement

* Update README.md

Co-authored-by: Katie Horne <katie@coder.com>

* Update README.md

Co-authored-by: Katie Horne <katie@coder.com>

* Update README.md

Co-authored-by: Katie Horne <katie@coder.com>

* Update README.md

Co-authored-by: Katie Horne <katie@coder.com>

* Update README.md

Co-authored-by: Katie Horne <katie@coder.com>

Co-authored-by: Katie Horne <katie@coder.com>
2022-05-27 18:10:54 +00:00
Ben Potter 5000edbfe0 example: docker warning on Coder host (#1842) 2022-05-27 13:02:59 -05:00
Kyle Carberry 984dc2bffd fix: Close peer negotiate mutex if we haven't negotiated (#1774)
Closes #1706 and #1644.
2022-05-27 17:34:13 +00:00
Garrett Delfosse 24d1a6744a fix: Add route for user to change own password (#1812) 2022-05-27 17:29:55 +00:00
Mathias Fredriksson 608eb322a8 chore: Add .editorconfig, shfmt, shellcheck and subshell dir changes (#1649) 2022-05-27 20:15:19 +03:00
Mathias Fredriksson 1a70298b5c feat: Add examples/templates/do-linux for Digital Ocean Droplets (#1749)
Co-authored-by: Cian Johnston <cian@coder.com>
2022-05-27 20:04:43 +03:00
Oxylibrium 14cdd85b66 fix(site): username validation in forms (#1851)
* refactor(site): move name validation to utils
* fix(site): username validation in forms
2022-05-27 17:02:56 +00:00
Garrett Delfosse 8a5277e291 fix: restore previous session on coder server --dev (#1821) 2022-05-27 17:02:02 +00:00
Bruno Quaresma 7eacab82a2 refactor: Update users page to looks like others (#1850) 2022-05-27 16:47:11 +00:00
Ammar Bandukwala e2030bba38 Move competitive comparison to README
And rewrite a bit.

Resolves #1365.
2022-05-27 11:38:25 -05:00
Steven Masley ec1fe46138 feat: Move create organizations route (#1831)
* feat: last rbac routes
- move create organization to /organizations.
2022-05-27 11:19:13 -05:00
ketang d73a0f4f23 fixed grammar 2022-05-27 11:09:53 -05:00
Ben Potter 655f348812 chore: change README to fancy alpha note 2022-05-27 10:56:35 -05:00
Bruno Quaresma 2b2d0291c2 fix: Suspend user in the UI (#1841) 2022-05-27 15:23:56 +00:00
Bruno Quaresma 4125863226 fix: Fix template README when has front-matter notation (#1840) 2022-05-27 15:19:32 +00:00
Steven Masley a409a34819 fix: Open csp-images to allow external (#1835)
External images are required for the README parts of templates.
Only allowing https right now
2022-05-27 14:59:13 +00:00
Abhineet Jain 7a5c8734ee test: Fix unit test in 'TestWorkspaceExtend' (#1836) 2022-05-27 14:45:22 +00:00
Abhineet Jain 9929189c45 feat: add tag and value in validation error details (#1760)
* add tag and value in validation error details

* fix unit tests and linter

* add quotes around value

* fix unit tests
2022-05-27 10:13:13 -04:00
Ammar Bandukwala c5f06acb01 Add alpha disclaimer to README 2022-05-27 09:08:35 -05:00
Steven Masley ebaae75993 test: Unit test to assert role capabilities (#1781)
* test: Unit test to assert role permissions

This unit test allows for asserting which roles can perform
actions on various objects. This is much easier than making
unit tests to hit the api.
2022-05-27 08:48:19 -05:00
Mathias Fredriksson 12227874a8 fix: Detect changes to examples/templates in Makefile (#1829) 2022-05-27 16:34:32 +03:00
Garrett Delfosse 1361c1357a feat: inject USER into shells (#1818) 2022-05-26 18:01:47 -05:00
ketang 951dc2d8b0 update tagline 2022-05-26 17:00:09 -05:00
Joe Previte d01a687caa fix: typo in docker terraform template (#1811) 2022-05-26 21:28:17 +00:00
Joe Previte 4d79b806c0 docs: clarify installing Coder instructions (#1809) 2022-05-26 14:11:58 -07:00
G r e y b6d6276149 ci: disable chromatic on forks (#1806) 2022-05-26 20:27:32 +00:00
Colin Adler 5833e37354 fix: macos flake (#1804)
https://github.com/coder/coder/runs/6614638495?check_suite_focus=true#step:9:104
2022-05-26 15:21:48 -05:00
Colin Adler d135f85f69 fix: use correct devnull device on windows for proxy logs (#1803) 2022-05-26 15:21:36 -05:00
G r e y 7467bfe4ed chore: organize ws stats, schedule stories (#1790)
Resolves: #1681

Summary:

- Moves WorkspaceSchedule out of WorkspaceStats
- Adds WorkspaceScheduleForm directory

Impact:

Improves breadth of our chromatic visual regression tests since the
examples for WorkspaceStats were non-representative of the component
2022-05-26 16:14:08 -04:00
Kira Pilot d4c26d534c chore: remove admin dropdown (#1802)
resolves #1748
2022-05-26 16:04:51 -04:00
Presley Pizzo 07ebd59e94 fix: Remove workspace Settings button and page (#1807) 2022-05-26 20:02:37 +00:00
Garrett Delfosse 4d6e8526a8 chore: tolerate codecov failures in CI (#1798) 2022-05-26 14:48:34 -05:00
Kira Pilot b4c41d3904 chore: add users link to nav bar (#1797)
* chore: add users link to nav bar

resolves #1746

* fix test names
2022-05-26 15:25:13 -04:00
Garrett Delfosse 781f3d0641 fix: use dir over full path for coder bin (#1795) 2022-05-26 19:05:46 +00:00
Bruno Quaresma 7b393526c5 fix: Fix sensitive parameters being displayed in the new workspace form (#1796) 2022-05-26 13:42:25 -05:00
Presley Pizzo d2ff5904c0 fix: hide New user button if no permission (#1794) 2022-05-26 14:25:23 -04:00
Bruno Quaresma e1b0cb0bca Remove create template button from the UI (#1793) 2022-05-26 18:22:47 +00:00
Garrett Delfosse 3052a6d88e Add coder executable to PATH (#1771) 2022-05-26 12:59:41 -05:00
Presley Pizzo fc67c6efb1 fix: remove unused pages from Admin dropdown (org and settings) (#1788)
* Delete Orgs Page

* Delete Admin Settings page
2022-05-26 13:10:54 -04:00
Cian Johnston 8f0a5a81f1 feat: add API/SDK support for autostop extension (#1778)
* Adds deadline column to workspace_builds, associated DB/API plumbing
* database: Upon inserting a row into workspace_builds, deadline will 
  initially be zero.
* autobuild: Executor now checks the Deadline field of the workspace_build
  for the purpose of autostop logic.
* coderd: Adds a new route /api/v2/workspaces/:workspace/extend which allows
  updating the deadline of the currently active workspace build. The new
  deadline must be after the existing deadline, and not the zero time.
* provisionerd: updates workspace_build.deadline upon successful workspace 
  build completion (equal to now plus workspace TTL, if it exists).
2022-05-26 18:08:11 +01:00
Steven Masley c04d045279 feat: RBAC provisionerdaemons and parameters (#1755)
* chore: Remove org_id from provisionerdaemons
2022-05-26 11:20:54 -05:00
Bruno Quaresma 104d07f659 feat: Add the template page (#1754) 2022-05-26 16:19:11 +00:00
G r e y 7c59ec4a2b feat: edit workspace schedule page (#1701)
Resolves: #1455 
Resolves: #1456

Summary:

Adds a page (accessible from Workspace Schedule section on a workspace) to edit a schedule.

Impact:

General parity with CLI for autostart/autostop: that is you can update your schedule from the UI
2022-05-26 12:11:30 -04:00
Kira Pilot 9a70c345c7 fix: update workspace form fields when switching templates (#1761)
resolves #1716
2022-05-26 08:43:07 -04:00
Kyle Carberry 31b819e83f chore: Remove interface from coderd and lift API surface (#1772)
Abstracting coderd into an interface added misdirection because
the interface was never intended to be fulfilled outside of a single
implementation.

This lifts the abstraction, and attaches all handlers to a root struct
named `*coderd.API`.
2022-05-26 03:14:08 +00:00
Abhineet Jain c78f947e09 feat: Upgrade terraform version to 1.1.9 (#1745)
* upgrade terraform version to 1.1.9

* Fix docs typo

Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2022-05-25 19:35:41 -04:00
Presley Pizzo 841d9f277c feat: UI for canceling workspace builds (#1735)
* Start hooking up cancel

* Update xservice

* Render cancel

Changes behavior of other buttons too

* Make outdated workspace story show max buttons

* Remove retry code

* Remove loading button state

* Fix type, extend tests

* Update story
2022-05-25 17:58:00 -04:00
Garrett Delfosse 35ccb88f60 feat: add dotfiles command (#1723) 2022-05-25 16:43:20 -05:00
Ben Potter 47ef03fea4 example: fix: properly tag aws-windows workspaces (#1744) 2022-05-25 22:11:29 +01:00
Colin Adler b5d615367e chore: update cdr.dev/slog (#1759)
Fixes #1626
2022-05-25 20:22:38 +00:00
Mathias Fredriksson 527f1f3bc3 feat: Add SSH agent forwarding support to coder agent (#1548)
* feat: Add SSH agent forwarding support to coder agent

* feat: Add forward agent flag to `coder ssh`

* refactor: Share setup between SSH tests, sync goroutines

* feat: Add test for `coder ssh --forward-agent`

* fix: Fix test flakes and implement Deans suggestion for helpers

* fix: Add example to config-ssh

* fix: Allow forwarding agent via -A

Co-authored-by: Cian Johnston <cian@coder.com>
2022-05-25 21:28:10 +03:00
dependabot[bot] 22ef456164 chore: bump github.com/gohugoio/hugo from 0.98.0 to 0.99.1 (#1699)
* chore: bump github.com/gohugoio/hugo from 0.98.0 to 0.99.1

Bumps [github.com/gohugoio/hugo](https://github.com/gohugoio/hugo) from 0.98.0 to 0.99.1.
- [Release notes](https://github.com/gohugoio/hugo/releases)
- [Changelog](https://github.com/gohugoio/hugo/blob/master/goreleaser.yml)
- [Commits](https://github.com/gohugoio/hugo/compare/v0.98.0...v0.99.1)

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

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

* fixup! chore: bump github.com/gohugoio/hugo from 0.98.0 to 0.99.1

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Colin Adler <colin1adler@gmail.com>
2022-05-25 12:44:23 -05:00
dependabot[bot] 088f842e17 chore: bump github.com/hashicorp/terraform-json from 0.13.0 to 0.14.0 (#1736)
Bumps [github.com/hashicorp/terraform-json](https://github.com/hashicorp/terraform-json) from 0.13.0 to 0.14.0.
- [Release notes](https://github.com/hashicorp/terraform-json/releases)
- [Commits](https://github.com/hashicorp/terraform-json/compare/v0.13.0...v0.14.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-25 12:20:28 -05:00
dependabot[bot] 29175d3158 chore: bump github.com/ory/dockertest/v3 from 3.8.1 to 3.9.0 (#1738)
Bumps [github.com/ory/dockertest/v3](https://github.com/ory/dockertest) from 3.8.1 to 3.9.0.
- [Release notes](https://github.com/ory/dockertest/releases)
- [Commits](https://github.com/ory/dockertest/compare/v3.8.1...v3.9.0)

---
updated-dependencies:
- dependency-name: github.com/ory/dockertest/v3
  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>
2022-05-25 12:20:11 -05:00
dependabot[bot] cd6fdc7832 chore: bump google.golang.org/api from 0.79.0 to 0.81.0 (#1737)
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.79.0 to 0.81.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.79.0...v0.81.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>
2022-05-25 11:51:21 -05:00
dependabot[bot] 3c21b070d7 chore: bump github.com/pion/webrtc/v3 from 3.1.39 to 3.1.41 (#1697)
Bumps [github.com/pion/webrtc/v3](https://github.com/pion/webrtc) from 3.1.39 to 3.1.41.
- [Release notes](https://github.com/pion/webrtc/releases)
- [Commits](https://github.com/pion/webrtc/compare/v3.1.39...v3.1.41)

---
updated-dependencies:
- dependency-name: github.com/pion/webrtc/v3
  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>
2022-05-25 16:35:02 +00:00
Steven Masley eea8dc6c16 feat: Add rbac to templateversion+orgmember endpoints (#1713) 2022-05-25 11:00:59 -05:00
David Wahler f8410dee3a fix: include subdirectories in example templates (#1715) 2022-05-25 10:34:28 -05:00
Bruno Quaresma 5492ab75c2 feat: Add resource type and restyle terminal link (#1722) 2022-05-25 13:32:01 +00:00
Ammar Bandukwala c5f4d80eda Shorten README before Install docs 2022-05-24 20:20:08 -05:00
Kyle Carberry 74329f479f fix: Use Terraform address to index resource + agent association (#1727)
Closes #1705.

There was an issue in the implementation brought by #1577 by not trimming
the array value when resources use counts. This should fix it, and adds
a test to be sure!
2022-05-24 20:00:34 -05:00
Ammar Bandukwala 95d7e39c80 Rewrite README for launch (#1731) 2022-05-25 00:40:57 +00:00
Kyle Carberry 4d9168c076 fix: Increase release timeout (#1729)
This is unfortunate, but with the containers it can take a while.
We should spend some time making these parallel in the future,
but for now this is fine!
2022-05-24 16:54:27 -05:00
Colin Adler 4543a3b277 fix: log after test exit in TestAgent/StartupScript (#1726)
```
$ go test ./agent/ -v -run TestAgent/StartupScript -count 1
=== RUN   TestAgent
=== PAUSE TestAgent
=== CONT  TestAgent
=== RUN   TestAgent/StartupScript
=== PAUSE TestAgent/StartupScript
=== CONT  TestAgent/StartupScript
    t.go:56: 2022-05-24 20:22:39.648 [INFO]	<agent.go:112>	connected
--- PASS: TestAgent (0.00s)
    --- PASS: TestAgent/StartupScript (0.17s)
PASS
panic: Log in goroutine after TestAgent/StartupScript has completed: 2022-05-24 20:22:39.651 [WARN]	<agent.go:130>	agent script failed ...
"error": run:
             github.com/coder/coder/agent.(*agent).runStartupScript
                 /home/colin/Projects/coder/coder/agent/agent.go:183
           - signal: killed
```
2022-05-24 16:03:42 -05:00
Oxylibrium 99c79c79db docs(README): fix links to subpages (#1724) 2022-05-24 19:26:25 +00:00
G r e y 104c76b8bc ci: limit chromatic to site (#1700) 2022-05-24 14:30:15 -04:00
Joe Previte 0ade49b758 docs: rephrase value statement in README (#1711) 2022-05-24 10:59:20 -07:00
Abhineet Jain 7ba6449054 Improve CLI logout flow (#1692)
* Improve CLI logout flow

* Fix lint error

* Make notLoggedInMessage a const

* successful logout with a msg when cfg files are absent

* use require, os.remove, show only one message, add prompt
2022-05-24 13:11:01 -04:00
Ammar Bandukwala 33e2e40942 Expand stalebot to issues (#1672)
Removing old, stale issues is essential to keeping a workable tracker.
2022-05-24 10:03:43 -07:00
Steven Masley d3a0578fe1 feat: Allow regen-ssh and fetching a single user from the cli (#1619)
* feat: Allow regen-ssh and fetching a single user from the cli
2022-05-24 16:53:04 +00:00
Steven Masley 363b16af38 fix: Add template read permission node to members (#1712) 2022-05-24 16:35:34 +00:00
Joe Previte 61ffd03aaf docs: update contribution guidelines (#1691)
* docs(contributing): add subheading backend under styling

* docs: add styling for frontend
2022-05-24 15:30:15 +00:00
G r e y b0d52039f9 refactor: resource strings in WorkspaceSchedule (#1702) 2022-05-24 09:55:30 -04:00
Steven Masley c7ca86d374 feat: Implement RBAC checks on /templates endpoints (#1678)
* feat: Generic Filter method for rbac objects
2022-05-24 08:43:34 -05:00
Bruno Quaresma fcd610ee7b refactor: Update create workspace flow to allow creation from the workspaces page (#1684) 2022-05-24 08:37:44 -05:00
Steven Masley 5f8d0e5dad feat: Add RBAC to /files endpoints (#1664)
* feat: Add RBAC to /files endpoints
2022-05-24 08:25:02 -05:00
Bruno Quaresma f763472609 fix: Fix template label (#1685) 2022-05-24 12:38:31 +00:00
Mathias Fredriksson 34b1e19338 fix: Try to fix cli portforward test flakes (#1650)
* fix: Try to fix cli portforward test flakes

* fix: Guard against agent exit outside test func

* fix: Improve test teardown in setupTestListener, cleanup
2022-05-24 11:15:06 +03:00
Cian Johnston c2f74f3cc2 chore: avoid concurrent usage of t.FailNow (#1683)
* chore: golangci: add linter rule to report usage of t.FailNow inside goroutines
* chore: avoid t.FailNow in goroutines to appease the race detector
2022-05-24 08:58:39 +01:00
Presley Pizzo 9b70a9b2eb Fix: fix Workspace storybook and remove unnecessary fetching from xService (#1682)
* Make workspace machine ephemeral to limit polling

* Fix Workspace storybook

* Lint

* Remove breadcrumb from workspaceXService
2022-05-23 20:04:38 -04:00
dependabot[bot] 4ba3eedb70 chore: bump github.com/lib/pq from 1.10.5 to 1.10.6 (#1653)
Bumps [github.com/lib/pq](https://github.com/lib/pq) from 1.10.5 to 1.10.6.
- [Release notes](https://github.com/lib/pq/releases)
- [Commits](https://github.com/lib/pq/compare/v1.10.5...v1.10.6)

---
updated-dependencies:
- dependency-name: github.com/lib/pq
  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>
2022-05-23 18:02:12 -05:00
dependabot[bot] 62acfc9a07 chore: bump goreleaser/goreleaser-action from 2 to 3 (#1652)
Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 2 to 3.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  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>
2022-05-23 22:55:40 +00:00
dependabot[bot] 98345e3d24 chore: bump github.com/hashicorp/go-version from 1.4.0 to 1.5.0 (#1654)
Bumps [github.com/hashicorp/go-version](https://github.com/hashicorp/go-version) from 1.4.0 to 1.5.0.
- [Release notes](https://github.com/hashicorp/go-version/releases)
- [Changelog](https://github.com/hashicorp/go-version/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/go-version/compare/v1.4.0...v1.5.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-23 17:49:21 -05:00
dependabot[bot] e9818d79da chore: bump jaxxstorm/action-install-gh-release from 1.6.0 to 1.7.1 (#1651)
Bumps [jaxxstorm/action-install-gh-release](https://github.com/jaxxstorm/action-install-gh-release) from 1.6.0 to 1.7.1.
- [Release notes](https://github.com/jaxxstorm/action-install-gh-release/releases)
- [Commits](https://github.com/jaxxstorm/action-install-gh-release/compare/v1.6.0...v1.7.1)

---
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>
2022-05-23 17:47:38 -05:00
dependabot[bot] 2de47ef9f0 chore: bump github.com/quasilyte/go-ruleguard/dsl from 0.3.19 to 0.3.21 (#1655)
Bumps [github.com/quasilyte/go-ruleguard/dsl](https://github.com/quasilyte/go-ruleguard) from 0.3.19 to 0.3.21.
- [Release notes](https://github.com/quasilyte/go-ruleguard/releases)
- [Commits](https://github.com/quasilyte/go-ruleguard/compare/dsl/v0.3.19...dsl/v0.3.21)

---
updated-dependencies:
- dependency-name: github.com/quasilyte/go-ruleguard/dsl
  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>
2022-05-23 22:45:40 +00:00
Cian Johnston b2020761d9 feat: add default autostart and ttl for new workspaces (#1632)
* database: add autostart_schedule and ttl to InsertWorkspace; make gen
* coderd: workspaces: consume additional fields of CreateWorkspaceRequest
* cli: update: add support for TTL and autostart_schedule
* cli: create: add unit tests
* coder: import  `time/tzdata` for embedded timezone database
* autobuild: fix unit test that only runs with a real db
2022-05-23 23:31:41 +01:00
G r e y c465f8a8a3 feat: add retry to ErrorSummary (#1690)
Summary:

The ErrorSummary accepts a retry callback and received improvements to
style and product copy

Impact:

This allows xstate-controlled pages to send re-fetch events
2022-05-23 21:07:52 +00:00
Asher dd4bb07193 feat: add terminal links (#1636) 2022-05-23 15:49:02 -05:00
Oxylibrium 80f8f605fd chore: Add self to CONTRIBUTORS.md (#1680) 2022-05-23 16:46:03 -04:00
Bruno Quaresma 57c6d887a1 chore: Ignore last built value on Chromatic (#1687) 2022-05-23 20:42:05 +00:00
Katie Horne 98c89f80b0 chore: add instructions for installation w/ Docker Compose (#1599)
Co-authored-by: Ben Potter <ben@coder.com>
2022-05-23 19:42:45 +00:00
ketang ba66052181 fix incorrect retention field on artifacts in coder.yaml 2022-05-23 14:13:05 -05:00
Abhineet Jain fc46818e31 chore: move contributor list to contributors.md (#1496) 2022-05-23 19:09:45 +00:00
ketang 7de4cd6231 replace .deb artifact with Windows .zip 2022-05-23 13:54:13 -05:00
Abhineet Jain 4a78bade6d bug: Cleaner error message for non logged-in users (#1670)
* add helper text to unauthorized error messages

* fix lint error, add unit tests

* fix test name

* fix test name

* fix lint errors in test

* add unauthorized test for templates create

* remove unnecessary variable

* remove Error struct, change error message

* change [url] to <url>
2022-05-23 14:51:49 -04:00
ketang c543fca92f add tar.gz to artifacts and a 7 day retention period to .deb 2022-05-23 13:35:25 -05:00
Katie Horne b0298a3157 chore: fix in-product copy casing (#1671) 2022-05-23 13:30:38 -05:00
Presley Pizzo 7ac3cbe772 Make workspace machine ephemeral to limit polling (#1674) 2022-05-23 13:25:46 -04:00
Steven Masley 873ae90f39 feat: cli configs should not be space sensitive (#1668) 2022-05-23 12:19:33 -05:00
Mathias Fredriksson c8ed213347 fix: Guard against CLI cmd running after test exit (#1658)
* fix: Guard against CLI cmd running after test exit

* fix: cli: avoid calling t.FailNow in non-test-main goroutine

* fix: cli: server_test: avoid calling t.FailNow outside main goroutine

* fix: cli: clitest_test: avoid calling t.FailNow outside main goroutine

* fix: cli: list_test: avoid calling t.FailNow outside main goroutine

* fix: TestGitSSH use-of-t-after-exit

* fix: TestGitSSH "too many authentication failures"

Due to local SSH keys being given

* chore: clitest: fix TestCli

* chore: Simplify TestTemplateInit

Co-authored-by: Cian Johnston <cian@coder.com>
2022-05-23 20:09:58 +03:00
Kira Pilot fa957d6d65 fix: omit url params on login (#1666)
resolves #1282
2022-05-23 11:01:32 -04:00
Bruno Quaresma 9f3a6d631c refactor: Move schedule info to the sidebar (#1665) 2022-05-23 09:41:04 -05:00
Bruno Quaresma 1f03277f1c refactor: Increase navbar height (#1662) 2022-05-23 10:22:48 -04:00
Cian Johnston a8a8f9dbf3 chore: skip some flaky tests (#1643)
* chore: skip some flaky tests

* Update peer/conn_test.go

* add makefile targets, reduce parallelism in go test
2022-05-21 00:39:51 +01:00
G r e y 4f75291446 feat: form for editing ws schedule (#1634)
* feat: ui for editing ws schedule

Summary:

This presents a form component and storybook. The UI will be a routed
page and added into the dashboard in a separate PR. It is likely a
XService will be used at the page level to supply errors and actions to
this form.

Impact of Change:

Further progress on #1455

Squashed Commits:

* refactor: add className prop to Stack

combine classes with internal classes and an optional external className
to better control the Stack.

* fix: getFormHelpers helperText

the helperText logic was incorrect, the helperText would only show if not touched.
2022-05-20 20:26:43 +00:00
Bruno Quaresma b29a2dfdde refactor: Minor design adjustments (#1637) 2022-05-20 19:37:03 +00:00
Joe Previte 3653fcf256 fix: remove outdated doc paths in goreleaser (#1633)
It appears we were manually moving the `README.md`. This should have been updated in https://github.com/coder/coder/pull/1630 but slipped through CI
2022-05-20 19:02:38 +00:00
Presley Pizzo e40c68399d feat: resources card (#1627)
* Set up table

* Format

* Hook up api and test - bug assigning resources

* Remove debugging code

* Format

* Remove unnecessary cards

* Fix test

* Fix assignment

* Fix tests

* Lint
2022-05-20 18:29:42 +00:00
Steven Masley c189fc52c1 fix: using a trailing slash on login url (#1622) 2022-05-20 12:42:01 -05:00
Bruno Quaresma ce7bf0b847 feat: Redesign the workspace page (#1620) 2022-05-20 17:05:00 +00:00
Joe Previte 0622603220 docs: move README to root (#1630)
We noticed that when you download the repo as a ZIP from GitHub, it
places the `README.md` in the root, which causes the relative links to
break.

By moving it to the root, this will fix that issue.
2022-05-20 09:56:50 -07:00
Steven Masley ad946c3902 feat: Add confirm prompts to some cli actions (#1591)
* feat: Add confirm prompts to some cli actions
- Add optional -y skip. Standardize -y flag across commands
2022-05-20 15:59:04 +00:00
G r e y 4f70f84635 feat: WorkspaceSection action, styles (#1623)
This PR is a squash of refactors and improvements in our Workspace and
WorkspaceSection components. An action prop is added to WorkspaceSection
and along the way, I refactored things that were not meeting conventions
or were hard to read. With this addition, I am further unblocked in
making auto-start/off editable in the UI, as I intend to use the Action
prop to trigger a modal (or routed page view) with the form.

Squashed commits:

* refactor: spaces for readability
It's hard to read HTMl markup without spaces on adjacent nodes

* refactor: props
Our components had unused props and arbitrary ordering.
2022-05-20 11:55:39 -04:00
Garrett Delfosse 0effb71f43 feat: add tracing for sql (#1610) 2022-05-20 10:51:06 -05:00
Abhineet Jain 7c3e1a5d97 feat: Read params from file for template/workspace creation (#1541)
* Read params from file for template/workspace creation

* Use os.ReadFile

* Refactor reading params into a separate module

* Add comments and unit tests

* Rename variable

* Uncomment and fix unit test

* Fix comment

* Refactor tests

* Fix unit tests for windows

* Fix unit tests for Windows

* Add comments for the hotfix
2022-05-20 11:29:10 -04:00
Kira Pilot d0fd0d7040 feat: added error boundary (#1602)
* added error boundary and error ui components

* add body txt and standardize btn size

* added story

* feat: added error boundary

closes #1013

* committing lockfile

* added email body to help link
2022-05-20 10:48:39 -04:00
Cian Johnston 52230fab56 feat: make default autobuild poll intervals configurable (#1618)
* feat: make default poll intervals for autobuild and ssh ttl polling configurable
2022-05-20 10:57:02 +00:00
Mathias Fredriksson 992b58389b fix: Use the cobra CommandPath for usage to avoid duplication (#1617) 2022-05-20 12:42:56 +03:00
Dean Sheather adb7d20c16 feat: skip terraform destroy if there is no state when deleting (#1594) 2022-05-20 14:07:23 +10:00
Spike Curtis a03615a01f feature: disable provisionerd listen endpoint (#1614)
* feature: disable provisionerd listen endpoint

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

* Regenerate ts types

Signed-off-by: Spike Curtis <spike@coder.com>
2022-05-19 23:52:17 +00:00
Spike Curtis d1817310a1 fix build and lint (#1613)
Signed-off-by: Spike Curtis <spike@coder.com>
2022-05-19 23:28:29 +00:00
Spike Curtis 1871b09697 feat: in-process provisionerd connection (#1568)
* in-process provisionerd connection

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

* disable lint for server.go/newProvisionerDaemon

Signed-off-by: Spike Curtis <spike@coder.com>
2022-05-19 17:47:45 -05:00
Garrett Delfosse 376c6819e0 feat: Move from datadog to generic otel (#1567) 2022-05-19 17:43:07 -05:00
Colin Adler 2a85d3d083 chore: unconditionally run all make cmds in CI (#1608) 2022-05-19 17:42:49 -05:00
Garrett Delfosse 077f16ce2c feat: add coder logout command (#1609) 2022-05-19 22:42:32 +00:00
David Wahler 0c4a65b113 fix: manually fix coderd/database/dump.sql and make style/gen check run more reliably (#1607) 2022-05-19 22:37:22 +00:00
Joe Previte 6dae48a1a8 fix: show --help message for CLI errors, add tests for delete (#1403)
* feat(cli): add test for delete

This adds a new test for the `delete` command to ensure it works as
expected when provided the correct args.

* fix(cli): use ExecuteC() to match Cobra

This modifies the `cli.Root().Execute()` to `cli.Root).ExecuteC()` to
match the default behavior of Cobra. We do this so errors will always
print the "run --help" line.

* feat(cli): add WithoutParameters test for delete

This adds a new test to the `delete_test.go` suite to ensure the correct
behavior occurs when `delete` is called without an argument.

* fixup! feat(cli): add WithoutParameters test for delete

* refactor(cli): show --help error message on main

This adds an error message which shows when there is an error with any
commands called to improve the UX.

* fixup! refactor(cli): show --help error message on main

* refactor(cli): handle err with FormatCobraError

This adds a new helper function called `FormatCobraError` to `root.go`
so that we can colorize and add "--help" message to cobra command errors
like calling `delete`.

* refactor(cli): add root_test.go, move delete test
2022-05-19 22:35:59 +00:00
G r e y a64ab6538e chore: update CODEOWNERS (#1600)
Resolves: #1559
2022-05-19 16:26:39 -05:00
Bruno Quaresma 0ffcc47f32 fix: Fix log order in the workspace build page (#1604) 2022-05-19 21:19:28 +00:00
Kyle Carberry 3be356095f feat: Add create workspace page (#1589) 2022-05-19 20:51:10 +00:00
Ben Potter 4afc66faf5 chore: remove docker host from docker-compose (#1596) 2022-05-19 20:38:07 +00:00
Bruno Quaresma 0b1a35f7b8 feat: Add workspace build logs page (#1598) 2022-05-19 15:34:42 -05:00
Cian Johnston d72c45e483 refactor: workspace autostop_schedule -> ttl (#1578)
Co-authored-by: G r e y <grey@coder.com>
2022-05-19 15:09:27 -04:00
Steven Masley 6c1117094d chore: Force codersdk to not import anything from database (#1576)
* chore: Force codersdk to not import anything from database (linter rule)
* chore: Move all database types in codersdk out
2022-05-19 13:04:44 -05:00
G r e y a0834404f7 chore: rm dead code; add check:all (#1595) 2022-05-19 12:40:40 -05:00
Ben Potter c47b6f0381 chore: use docker host in docker-compose (#1592) 2022-05-19 11:49:22 -05:00
G r e y 67333b6186 feat: getWorkspaces filter site api (#1564) 2022-05-19 12:08:55 -04:00
LG 0438430c7c fix: missing spacing added; typo fix (#1586)
Co-authored-by: Ben <ben@coder.com>
2022-05-19 15:51:49 +00:00
G r e y e0165c5d89 fix: static data in mocks (#1574) 2022-05-19 11:36:14 -04:00
Bruno Quaresma 3f770e1111 fix: User permissions on UI (#1570) 2022-05-19 15:10:18 +00:00
Dean Sheather 4eb0bb6afd feat: don't return 200 for deleted workspaces (#1556) 2022-05-20 00:29:10 +10:00
Ben Potter eb8f371f34 chore: add container image to footer of releases (#1579)
* chore: add docker pull to footer
2022-05-19 13:38:05 +00:00
Kyle Carberry 38ee519f42 feat: Expose the values contained in an HCL validation string to the API (#1587)
* feat: Expose the values contained in an HCL validation string to the API

This allows the frontend to render inputs displaying these values!

* Update codersdk/parameters.go

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

* Call a spade a space

* Fix linting errors with type conversion

Co-authored-by: Cian Johnston <cian@coder.com>
2022-05-19 13:29:36 +00:00
Mathias Fredriksson ad9bdb7bd1 fix: More robust provisionersdk agent init scripts (#1551)
Related #1544

Co-authored-by: Dean Sheather <dean@deansheather.com>
2022-05-19 13:02:42 +00:00
Katie Horne 6f969214d3 chore: validate docs (#1485) 2022-05-19 08:01:19 -05:00
Dean Sheather cabc164f74 feat: use and display default template values when creating wkspc. (#1584) 2022-05-19 22:49:40 +10:00
Cian Johnston 8814cb0722 Revert "fix: Use Terraform address to index resource + agent association (#1577)" (#1585)
This reverts commit f3fe2a08ce.
2022-05-19 12:18:40 +01:00
Steven Masley c034e8389e feat: Add RBAC to /workspace endpoints (#1566)
* feat: Add RBAC to /workspace endpoints
2022-05-18 18:15:19 -05:00
Kyle Carberry f3fe2a08ce fix: Use Terraform address to index resource + agent association (#1577)
This fixes resources created from Terraform modules not
properly being associated with an agent.

By not using the address, and resource identifiers prefixed
with `module.<name>` would be missed!
2022-05-18 16:26:08 -05:00
Garrett Delfosse 0706c60445 chore: Add watch workspace endpoint (#1493) 2022-05-18 16:16:26 -05:00
Ben Potter b8ee939e52 chore: change Slack to Discord link (#1573) 2022-05-18 21:14:31 +00:00
Ben Potter 37cf3bb491 example: add docker-image-builds + docker docs (#1526)
Co-authored-by: Katie Horne <katie@23spoons.com>
2022-05-18 16:03:20 -05:00
Kyle Carberry 97699e9704 fix: Rename NewMemoryCoderd to NewWithServer (#1571)
This name felt invalid, because `New` was also in memory.
2022-05-18 15:49:46 -05:00
Steven Masley 2638c274cb fix: User's should be able to read what roles available (#1575) 2022-05-18 20:47:43 +00:00
Steven Masley 8bd1abee33 fix: Use sdk type in coderd api response (#1569)
Was using the database type
2022-05-18 15:34:00 -05:00
Garrett Delfosse e2ed581708 Add stages to all proto.Logs (#1563) 2022-05-18 17:33:29 +00:00
David Wahler a50a6e8638 fix: Make TestAgent and TestWorkspaceAgentPTY less flaky (#1562) 2022-05-18 17:06:17 +00:00
Spike Curtis 9f402fa27f Spike/222 workspace build order (#1534)
* chore: refactor before_id/after_id to build_number

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

* pagination of workspace_builds

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

* Disable parallel on postgres tests

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

* Fix lint

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

* Fix workspace build postgres query

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

* Fix JS tests

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

* Fix workspace builds postgres query

Signed-off-by: Spike Curtis <spike@coder.com>
2022-05-18 16:33:33 +00:00
Cian Johnston 13571b0393 examples/docker-local: add explanatory comment (#1545) 2022-05-18 17:10:23 +01:00
Garrett Delfosse 89fb59aa9a chore: remove make build dep from make dev (#1557) 2022-05-18 16:00:20 +00:00
Asher e4e7e10690 feat: add terminal link component (#1538)
* Fix not being able to specify agent when connecting to terminal

The `workspace.agent` syntax was only used when fetching the agent and
not the workspace so it would try to fetch a workspace called
`workspace.agent` instead of just `workspace`.

* Add terminal link component

Currently it does not show anywhere but we can drop it into the
resources card later.
2022-05-18 10:53:59 -05:00
David Wahler 5f21a145d1 bug: Don't try to handle SIGINT when prompting for passwords (#1498) 2022-05-18 15:26:38 +00:00
Steven Masley a3556b12da feat: Single query for all workspaces with optional filter (#1537)
* feat: Add single query for all workspaces using a filter
2022-05-18 10:09:07 -05:00
dependabot[bot] 894646cb7c chore: bump @testing-library/user-event from 14.1.1 to 14.2.0 in /site (#1521)
Bumps [@testing-library/user-event](https://github.com/testing-library/user-event) from 14.1.1 to 14.2.0.
- [Release notes](https://github.com/testing-library/user-event/releases)
- [Changelog](https://github.com/testing-library/user-event/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/user-event/compare/v14.1.1...v14.2)

---
updated-dependencies:
- dependency-name: "@testing-library/user-event"
  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>
2022-05-18 11:01:28 -04:00
Kira Pilot 85a932bfaf bug: fixed menu height diff (#1546)
resolves #1229
2022-05-18 10:34:56 -04:00
Dean Sheather 9141be3656 feat: add port-forward subcommand (#1350) 2022-05-19 00:10:40 +10:00
Kyle Carberry 76fc59aa79 feat: Add templates page (#1510)
* feat: Add template page

* Create xService

* Update column names

* Show create template conditionally

* Add template description

* Route to templates

* Add empty states

* Add tests

* Add loading indicator

* Requested changes
2022-05-18 09:05:18 -05:00
Bruno Quaresma b7481489b1 feat: Add timeline in the workspace page (#1533) 2022-05-18 13:54:06 +00:00
Ben Potter 6bed620d6c example: ec2: document "minimal" policy (#1536)
* example: ec2: document "minimal" policy

* move DescribeInstances

* move ModifyInstanceCreditSpecification
2022-05-18 08:17:05 -05:00
Steven Masley 4e28b2d9c5 test: Using local time in unit test fails in certain time zones (#1540)
* test: Using local time in unit test fails in certain time zones

This test was failing when running in CST (GMT-5) timezone.
My local timezone pushed the next to the upcoming monday

* fix: schedule: assert expected result of String() separately from input spec

Co-authored-by: Cian Johnston <cian@coder.com>
2022-05-18 13:09:36 +00:00
Kyle Carberry ba818b3a10 fix: Append Terraform module resources to list (#1539)
This was causing module resources to be skipped!
2022-05-17 19:07:20 -05:00
David Wahler 72c2bf80aa feat: "coder ssh --shuffle" easter egg (#1084) 2022-05-17 17:55:58 -05:00
Kyle Carberry 33701862de fix: Publish Linux releases in tar.gz archives (#1535)
This reduces download size by ~70%.
2022-05-17 21:11:13 +00:00
Colin Adler 98ccd0eb89 feat: add README parsing to template versions (#1500) 2022-05-17 15:00:48 -05:00
dependabot[bot] 0f9559a784 chore: bump cronstrue from 2.4.0 to 2.5.0 in /site (#1518)
Bumps [cronstrue](https://github.com/bradymholt/cronstrue) from 2.4.0 to 2.5.0.
- [Release notes](https://github.com/bradymholt/cronstrue/releases)
- [Commits](https://github.com/bradymholt/cronstrue/compare/v2.4.0...v2.5.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>
2022-05-17 15:03:45 -04:00
Kira Pilot 65acfc9bef bug: using NavLink for menus (#1530)
* bug: using NavLink for menus

resolves #955

* lets fix our story
2022-05-17 15:01:53 -04:00
Steven Masley 4ad5ac2d4a feat: Rbac more coderd endpoints, unit test to confirm (#1437)
* feat: Enforce authorize call on all endpoints
- Make 'request()' exported for running custom requests
* Rbac users endpoints
* 401 -> 403
2022-05-17 13:43:19 -05:00
Garrett Delfosse 495c87b6c3 chore: add make dev (#1527) 2022-05-17 13:12:14 -05:00
dependabot[bot] 841b792e8e chore: bump @typescript-eslint/parser from 5.23.0 to 5.25.0 in /site (#1517)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.23.0 to 5.25.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/v5.25.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  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>
2022-05-17 11:36:03 -04:00
dependabot[bot] c0b80ef899 chore: bump sql-formatter from 4.0.2 to 6.1.1 in /site (#1514)
Bumps [sql-formatter](https://github.com/zeroturnaround/sql-formatter) from 4.0.2 to 6.1.1.
- [Release notes](https://github.com/zeroturnaround/sql-formatter/releases)
- [Changelog](https://github.com/zeroturnaround/sql-formatter/blob/master/.release-it.json)
- [Commits](https://github.com/zeroturnaround/sql-formatter/compare/v4.0.2...v6.1.1)

---
updated-dependencies:
- dependency-name: sql-formatter
  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>
2022-05-17 11:35:29 -04:00
dependabot[bot] 5227a74ae3 chore: bump webpack-dev-server from 4.8.1 to 4.9.0 in /site (#1511)
Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 4.8.1 to 4.9.0.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v4.8.1...v4.9.0)

---
updated-dependencies:
- dependency-name: webpack-dev-server
  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>
2022-05-17 11:35:04 -04:00
dependabot[bot] 3eaca0d436 chore: bump @pmmmwh/react-refresh-webpack-plugin in /site (#1484)
Bumps [@pmmmwh/react-refresh-webpack-plugin](https://github.com/pmmmwh/react-refresh-webpack-plugin) from 0.5.5 to 0.5.6.
- [Release notes](https://github.com/pmmmwh/react-refresh-webpack-plugin/releases)
- [Changelog](https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pmmmwh/react-refresh-webpack-plugin/compare/v0.5.5...v0.5.6)

---
updated-dependencies:
- dependency-name: "@pmmmwh/react-refresh-webpack-plugin"
  dependency-type: direct:development
  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>
2022-05-17 11:33:52 -04:00
Cian Johnston 190210b18d fix: set autostop notify to t minus 30 minutes (#1513) 2022-05-17 16:14:10 +01:00
dependabot[bot] bb2740e7c3 chore: bump @playwright/test from 1.21.1 to 1.22.1 in /site (#1512)
Bumps [@playwright/test](https://github.com/Microsoft/playwright) from 1.21.1 to 1.22.1.
- [Release notes](https://github.com/Microsoft/playwright/releases)
- [Commits](https://github.com/Microsoft/playwright/compare/v1.21.1...v1.22.1)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  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>
2022-05-17 11:12:50 -04:00
dependabot[bot] 8dd32e2a0a chore: bump eslint from 8.14.0 to 8.15.0 in /site (#1344)
Bumps [eslint](https://github.com/eslint/eslint) from 8.14.0 to 8.15.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.14.0...v8.15.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>
2022-05-17 11:08:26 -04:00
dependabot[bot] d5a500a73f chore: bump @fontsource/inter from 4.5.7 to 4.5.10 in /site (#1251)
Bumps [@fontsource/inter](https://github.com/fontsource/fontsource/tree/HEAD/fonts/google/inter) from 4.5.7 to 4.5.10.
- [Release notes](https://github.com/fontsource/fontsource/releases)
- [Changelog](https://github.com/fontsource/fontsource/blob/main/CHANGELOG.md)
- [Commits](https://github.com/fontsource/fontsource/commits/HEAD/fonts/google/inter)

---
updated-dependencies:
- dependency-name: "@fontsource/inter"
  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>
2022-05-17 11:05:28 -04:00
G r e y d177937e1c chore: clean site dependencies (#1509)
* delete package-lock.json
* pin dayjs
* pin uuid, @types/uuid
* pin xterm dependencies
* pin jest deps
2022-05-17 10:45:51 -04:00
Cian Johnston 75dc8f59f6 fix: example: update docker-local to use host-gateway (#1507)
* fix: example: update docker-local to use host-gateway
* docker-compose.yaml: Add POSTGRES_ environment variables to CODER_PG_CONNECTION_URL

Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
2022-05-17 14:40:13 +00:00
Kyle Carberry fc9efc2b79 fix: Allow setting STUN to an empty string (#1502)
This allows users to entirely disable STUN.
2022-05-18 00:12:48 +10:00
Kyle Carberry 668a6712e6 fix: Use relative timestamp for workspaces page Storybook (#1505) 2022-05-17 07:58:18 -05:00
Kyle Carberry 55bd7aa747 fix: Run "make gen" to regenerate queries (#1504)
This is broken in our CI, and should be fixed!
I'll likely tackle in a future PR.
2022-05-16 23:47:31 -05:00
Kyle Carberry f75d29e38e fix: Remove grouping for workspace owner counts (#1503)
This caused templates to show max ownership of one developer!
2022-05-17 04:10:52 +00:00
Kyle Carberry a2ba69dd28 fix: Parse resources from Terraform Modules (#1501)
Fixes when Terraform modules are used to primariy provision
infrastructure!
2022-05-16 20:56:50 -05:00
Kyle Carberry 9b1ef29694 fix: Allow fetching of non-personal workspaces (#1495)
RBAC should cover this anyways!
2022-05-16 22:47:31 +00:00
Ben Potter 1ed69b95fc example(k8s): clarify kubeconfig location (#1494) 2022-05-16 22:29:15 +00:00
Kyle Carberry 22ec366535 feat: Redesign workspaces page (#1450)
* feat: Improve navbar to be more compact

The navbar was unnecessarily large before, which made
the UI feel a bit bloaty from my perspective.

* Attempt to remove overrides

* Update theme

* Add text field

* Update theme to dark!

* Fix import ordering

* Fix page location

* Fix requested changes

* Add storybook for workspaces page view

* Add empty view

* Add tests for empty view

* Remove templates page

* Fix local port

* Remove templates from nav

* Fix e2e test

* Remove time.ts

* Remove dep

* Add background color to margins

* Merge status checking from workspace page

* Fix requested changes

* Fix workspace status tests
2022-05-16 16:52:54 -05:00
Colin Adler e925818526 feat: add template description (#1489) 2022-05-16 20:56:11 +00:00
Steven Masley b55d83ca82 feat: Add suspend/active user to cli (#1422)
* feat: Add suspend/active user to cli
* UserID is now a string and allows for username too
2022-05-16 15:29:27 -05:00
Kyle Carberry a77da8445e fix: Resolve symlinks being written with size 0 in tar (#1488)
Solution found here:
https://stackoverflow.com/questions/38454850/getting-write-too-long-error-when-trying-to-create-tar-gz-file-from-file-and-d

Symlink's were being written with a size of 0, which surfaced an error
for write too long.
2022-05-16 20:26:23 +00:00
Colin Adler 680de709a5 chore: organize http handlers (#1486)
They're currently randomly in a bunch of different files. This cleans up
the handler functions to be in the file of the type they return.
2022-05-16 14:36:27 -05:00
G r e y 9d4182b189 fix: CreateWorkspaceForm name validation (#1453)
* refactor: allow helperText in getFormHelpers

By passing in helperText, we will not accidentally overwrite it.

* fix: CreateWorkspaceForm name validation

Resolves: #1421
2022-05-16 15:29:59 -04:00
Bruno Quaresma 4103ba0b71 chore: Rename Preferences to Settings (#1487) 2022-05-16 18:53:07 +00:00
Garrett Delfosse eeaa5c3b7b feat: Support reading from token flag on coder login (#1483) 2022-05-16 18:07:35 +00:00
dependabot[bot] abc2257624 chore: bump xstate from 4.31.0 to 4.32.1 in /site (#1481)
Bumps [xstate](https://github.com/statelyai/xstate) from 4.31.0 to 4.32.1.
- [Release notes](https://github.com/statelyai/xstate/releases)
- [Commits](https://github.com/statelyai/xstate/compare/xstate@4.31.0...xstate@4.32.1)

---
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>
2022-05-16 13:03:50 -05:00
Colin Adler f007aeee1f chore: standardize migration names in create_migration.sh (#1480) 2022-05-16 17:35:00 +00:00
dependabot[bot] b73be75aeb chore: bump @typescript-eslint/eslint-plugin in /site (#1475)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.21.0 to 5.23.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/v5.23.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  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>
2022-05-16 12:19:08 -05:00
dependabot[bot] 0655742147 chore: bump eslint-plugin-jest from 26.1.5 to 26.2.2 in /site (#1474)
Bumps [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) from 26.1.5 to 26.2.2.
- [Release notes](https://github.com/jest-community/eslint-plugin-jest/releases)
- [Changelog](https://github.com/jest-community/eslint-plugin-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jest-community/eslint-plugin-jest/compare/v26.1.5...v26.2.2)

---
updated-dependencies:
- dependency-name: eslint-plugin-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>
2022-05-16 12:18:48 -05:00
Bruno Quaresma abbe548d5c feat: Add SSH Keys page on /preferences/ssh-keys (#1478) 2022-05-16 12:15:45 -05:00
dependabot[bot] 5447c4a3cf chore: bump docker-practice/actions-setup-docker from 1.0.8 to 1.0.10 (#1469)
Bumps [docker-practice/actions-setup-docker](https://github.com/docker-practice/actions-setup-docker) from 1.0.8 to 1.0.10.
- [Release notes](https://github.com/docker-practice/actions-setup-docker/releases)
- [Changelog](https://github.com/docker-practice/actions-setup-docker/blob/master/CHANGELOG.md)
- [Commits](https://github.com/docker-practice/actions-setup-docker/compare/v1.0.8...1.0.10)

---
updated-dependencies:
- dependency-name: docker-practice/actions-setup-docker
  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>
2022-05-16 12:10:28 -05:00
dependabot[bot] 1e25bf2455 chore: bump docker/login-action from 1 to 2 (#1470)
Bumps [docker/login-action](https://github.com/docker/login-action) from 1 to 2.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v1...v2)

---
updated-dependencies:
- dependency-name: docker/login-action
  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>
2022-05-16 12:09:11 -05:00
dependabot[bot] 8ba18dd222 chore: bump jaxxstorm/action-install-gh-release from 1.5.0 to 1.6.0 (#1472)
Bumps [jaxxstorm/action-install-gh-release](https://github.com/jaxxstorm/action-install-gh-release) from 1.5.0 to 1.6.0.
- [Release notes](https://github.com/jaxxstorm/action-install-gh-release/releases)
- [Commits](https://github.com/jaxxstorm/action-install-gh-release/compare/v1.5.0...v1.6.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>
2022-05-16 12:08:34 -05:00
Mathias Fredriksson 6c1ef851a2 fix: Update cli usage template for cobra feature parity (#1463)
Fixes #1423

Related #1233, #1403
2022-05-16 20:01:42 +03:00
Presley Pizzo b06ef0ae6e feat: Workspace StatusBar (#1362)
* Move component and prep

* Make WorkspaceSection more reusable

* Lay out elements

* Layout tweaks

* Add outdated to Workspace type

* Fill out status bar component

* Format

* Add transition to types

* Add api handlers for build toggle

* Format

* Parallelize machine

* Lay out basics of build submachine

* Pipe start and stop events through - needs status

* Attempt at a machine

It's so big, but collapsing start and stop made it hard to distinguish retry from toggle

* Update mock

* Render status and buttons

* Fix type error on template page

* Move Settings

* Format

* Keep refreshed workspace

* Make it switch workspaces

* Lint

* Fix relative api path

* Test

* Fix polling

* Add loading workspace state

* Format

* Add stub settings page

* Format

* Lint

* Get rid of let

* Add update

* Make start use version id

Important for update

* Fix imports

* Add polling for outdated

* Rely on context instead of finite state for status

* Handle canceling

* Fix tests

* Format

* Display errors so users know when button presses didn't work

* Fix api typo, remove logging

* Lint

* Simplify type

Co-authored-by: G r e y <grey@coder.com>

* Add type, extract helper

Co-authored-by: G r e y <grey@coder.com>
2022-05-16 16:34:22 +00:00
Colin Adler e990a9ac28 feat: add audit diffing for all user editable types (#1413) 2022-05-16 11:20:11 -05:00
Cian Johnston b7049032a0 fix: cli: prettify schedule when printing output (#1440)
* Adds methods to schedule.Schedule to show the raw cron string and timezone
* Uses these methods to clean up output of auto(start|stop) show or ls
* Defaults CRON_TZ=UTC if not provided
2022-05-16 17:02:44 +01:00
dependabot[bot] 2a278b8698 chore: bump github.com/prometheus/client_golang from 1.12.1 to 1.12.2 (#1467)
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.12.1 to 1.12.2.
- [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.12.1...v1.12.2)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_golang
  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>
2022-05-16 09:35:11 -05:00
dependabot[bot] f3b922bbd5 chore: bump github.com/moby/moby (#1464)
Bumps [github.com/moby/moby](https://github.com/moby/moby) from 20.10.15+incompatible to 20.10.16+incompatible.
- [Release notes](https://github.com/moby/moby/releases)
- [Changelog](https://github.com/moby/moby/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moby/moby/compare/v20.10.15...v20.10.16)

---
updated-dependencies:
- dependency-name: github.com/moby/moby
  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>
2022-05-16 09:13:20 -05:00
Bruno Quaresma 8857c0d076 refactor: Update start coder command (#1476) 2022-05-16 13:37:42 +00:00
dependabot[bot] 02087db65a chore: bump docker/setup-qemu-action from 1 to 2 (#1471)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 1 to 2.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v1...v2)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  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>
2022-05-16 08:36:12 -05:00
dependabot[bot] 6ca7f0b89c chore: bump google.golang.org/api from 0.78.0 to 0.79.0 (#1466)
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.78.0 to 0.79.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.78.0...v0.79.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>
2022-05-16 08:32:25 -05:00
dependabot[bot] 19a18164ec chore: bump golangci/golangci-lint-action from 3.1.0 to 3.2.0 (#1473)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 3.1.0 to 3.2.0.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v3.1.0...v3.2.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>
2022-05-16 08:31:54 -05:00
dependabot[bot] d7163b2f9f chore: bump github.com/gliderlabs/ssh from 0.3.3 to 0.3.4 (#1468)
Bumps [github.com/gliderlabs/ssh](https://github.com/gliderlabs/ssh) from 0.3.3 to 0.3.4.
- [Release notes](https://github.com/gliderlabs/ssh/releases)
- [Commits](https://github.com/gliderlabs/ssh/compare/v0.3.3...v0.3.4)

---
updated-dependencies:
- dependency-name: github.com/gliderlabs/ssh
  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>
2022-05-16 08:31:21 -05:00
dependabot[bot] 889ec88de2 chore: bump github.com/pion/webrtc/v3 from 3.1.34 to 3.1.39 (#1465)
Bumps [github.com/pion/webrtc/v3](https://github.com/pion/webrtc) from 3.1.34 to 3.1.39.
- [Release notes](https://github.com/pion/webrtc/releases)
- [Commits](https://github.com/pion/webrtc/compare/v3.1.34...v3.1.39)

---
updated-dependencies:
- dependency-name: github.com/pion/webrtc/v3
  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>
2022-05-16 08:30:53 -05:00
G r e y 7bb7c6c295 example: use codercom/enterprise-intellij:ubuntu in docker-local (#1454)
Resolves: #1113
2022-05-14 13:58:12 -04:00
Kyle Carberry dbd5b4a47b feat: Add workspace owner name to response (#1448)
This will be rendered in the workspace page!
2022-05-13 20:41:21 -05:00
Ammar Bandukwala 4cfc9af442 Fix codecov (#1447)
The notify block was syntactically invalid, preventing
any of this file from working.
2022-05-14 00:33:58 +00:00
G r e y e061715315 fix: error when clicking on login screen (#1445)
Resolves: #1435
2022-05-13 20:07:54 -04:00
Colin Adler fe7645b8a9 feat: add templates delete command (#1443) 2022-05-13 22:54:32 +00:00
Ammar Bandukwala 19335df0eb Disable codecov comment (#1442)
You can find the information by clicking on the CI check, and it's only rarely of interest to the reviewer.
2022-05-13 16:48:48 -05:00
Eric Paulsen 695b709173 fix: examples link in README (#1441) 2022-05-13 16:43:10 -05:00
Bruno Quaresma 50ad2f8e31 refactor: Improve the load state for the list pages (#1428) 2022-05-13 14:12:35 -05:00
Cian Johnston f970829b9e feat: add autostart/autostop show, show autostart/autostop schedule in ls output (#1436)
* feat: add autostart/autostop show, show autostart/autostop schedule in ls output
2022-05-13 19:03:27 +00:00
Cian Johnston 9410237ed5 chore: un-hide autostart and autostop commands (#1418) 2022-05-13 18:09:16 +01:00
Cian Johnston b2760b1faf feat: send native system notification on scheduled workspace shutdown (#1414)
* feat: send native system notification on scheduled workspace shutdown

This commit adds a fairly generic notification package and uses it
to notify users connected over SSH of pending workspace shutdowns.
Only one notification will be sent at most 5 minutes prior to the scheduled
shutdown, and only one CLI instance will send notifications if multiple
instances are running.
2022-05-13 18:09:04 +01:00
Cian Johnston 4ab7a41f08 chore: executor: add unit test, rename LifecycleTicker (#1420)
* chore: add a unit test to ensure correct behaviour with multiple coderd replicas
* nit: rename LifecycleTicker to AutobuildTicker
2022-05-13 17:14:24 +01:00
G r e y 89e44da899 fix: remove account description (#1427)
Resolves: #1419
2022-05-13 11:53:14 -04:00
Bruno Quaresma e6168ba238 feat: Add permissions for links (#1407) 2022-05-13 14:25:57 +00:00
Ben Potter 64a8b4ac47 chore: add bpmct to contributors list (#1332) 2022-05-13 02:01:31 +00:00
David Wahler 86cba4d3f8 chore: Deploy internally accessible godoc container (#1415) 2022-05-12 19:39:18 -05:00
Ben Potter 333d6a4374 fix: typo in CODER_VERSION 2022-05-12 16:13:43 -05:00
Steven Masley 64e408c954 feat: Check permissions endpoint (#1389)
* feat: Check permissions endpoint

Allows FE to query backend for permission capabilities.
Batch requests supported
2022-05-12 20:56:23 +00:00
Ben Potter 75a5877c1d fix: remove docker release flake (#1412) 2022-05-12 15:44:46 -05:00
G r e y 9ef64fd192 chore: add myself as a contributor (#1408) 2022-05-12 14:44:54 -04:00
Asher 6f7d9bb1e4 fix: remove unimplemented account pages (#1411)
It appears unclear as of now when/if these will be implemented so I
opted to remove them (as opposed to commenting them out) to avoid having
them rot (they are easily added back anyway).

Closes #1232.
2022-05-12 13:34:49 -05:00
Ben Potter bbb8f836bf feat: build & release cross-platform Docker images (#1178)
* feat: add dockerfile and docker-compose

* build docker images on release

* add Docker dependencies to release.yaml

* remove docker compose for now

* fix license mismatch

* add docker-compose

* rename volume

* add WF dispatch for debugging
2022-05-12 17:59:34 +00:00
Kyle Carberry 7b5300d0cc fix: Improve develop script to start tunnel by default (#1409)
This allows for running the development script to actually
build workspaces!
2022-05-12 12:37:51 -05:00
David Wahler 20916281d8 feat: Add reset-password command (#1380)
* allow non-destructively checking if database needs to be migrated

* feat: Add reset-password command

* fix linter errors

* clean up reset-password usage prompt

* Add confirmation to reset-password command

* Ping database before checking migration, to improve error message
2022-05-12 12:32:56 -05:00
Bruno Quaresma a629a705d0 refactor: Add vertical space to the footer (#1410) 2022-05-12 12:28:20 -05:00
Asher 26b04cc96f chore: switch to generated types (#1394)
* Make column renderer use the same type as its key

That way the renderer only takes `string` for example when rendering the
name field instead of `string | number` when the interface has some
fields that are strings and some fields are numbers.

This will be necessary when switching to generated types since some of
the fields are numbers (like the owner count on a template).

* Switch fully to generated types

In some places the organization ID is part of the URL but not part of
the request so I separated out the ID into a separate argument in the
relevant API functions.

Otherwise this was a straightforward replacement where I mostly only
needed to change some of the interface names (User instead of
UserResponse for example) and add a few missing but required properties.

I kind of winged the template form; I am not sure what the difference
between a template and template version is or why the latter comes
before the former so the form just returns all the data required to
create both.

* Delete handwritten types

Except for UserAgent which seems to be purely frontend and
ReconnectingPTYRequest which is not in codersdk so I am just leaving it
for now.

* Remove implemented omitempty as a future idea

This was implemented in 2d3dc436a8.

* Add missing optionalities to generated request interfaces
2022-05-12 10:01:28 -05:00
Mathias Fredriksson 56076a0aa2 feat: Unify cli behavior for templates create and update (#1385)
Fixes #565
2022-05-12 14:54:58 +03:00
Kira Pilot 2569787324 chore: add eslint extension recommendation (#1400)
resolves #1399
2022-05-11 19:01:21 -04:00
Asher ce660f8bbc fix: add missing return when template version is not found (#1402) 2022-05-11 17:58:22 -05:00
Cian Johnston f4da5d4f3a feat: add lifecycle.Executor to manage autostart and autostop (#1183)
This PR adds a package lifecycle and an Executor implementation that attempts to schedule a build of workspaces with autostart configured.

- lifecycle.Executor takes a chan time.Time in its constructor (e.g. time.Tick(time.Minute))
- Whenever a value is received from this channel, it executes one iteration of looping through the workspaces and triggering lifecycle operations.
- When the context passed to the executor is Done, it exits.
- Only workspaces that meet the following criteria will have a lifecycle operation applied to them:
  - Workspace has a valid and non-empty autostart or autostop schedule (either)
  - Workspace's last build was successful
- The following transitions will be applied depending on the current workspace state:
  - If the workspace is currently running, it will be stopped.
  - If the workspace is currently stopped, it will be started.
  - Otherwise, nothing will be done.
- Workspace builds will be created with the same parameters and template version as the last successful build (for example, template version)
2022-05-11 23:03:02 +01:00
Kira Pilot e8e6d3c2f1 chore: updated documentation link (#1387)
* chore: updated documentation link

* PR feedback
2022-05-11 17:10:03 -04:00
Kira Pilot f93804a2a0 chore: renaming index files (#1397) 2022-05-11 17:02:28 -04:00
Garrett Delfosse be3bc5cc55 Remove coder templates edit command (#1396) 2022-05-11 20:05:45 +00:00
Ben Potter 537897c0bb chore: add "needs grooming" label to new issues (#1384) 2022-05-11 14:35:58 -05:00
Ben Potter 982769f0dc fix: uses "projects" instead of "templates" in examples docs 2022-05-11 13:42:47 -05:00
Kyle Carberry 3024e25c09 example: Add Kubernetes multi-service (#1092)
* example: Add Kubernetes multi-service

* fix: change to CODER_AGENT_TOKEN

* example: use ServiceAccount for cluster authentication (#1096)

Co-authored-by: Ben <ben@coder.com>
2022-05-11 18:27:31 +00:00
Ben Potter f5817248de feat: arm(v7/64) builds for releases and agent scripts (#1337)
* feat: build armv7 linux releases

* upload ARM binaries to bin

* Only build arm 7 for Linux

* add ARM agent scripts

* fix: specify armv7 to match tf provider

* append arm version to slim builds

* use descript armv7 binary

* Add script mappings for each architecture

Co-authored-by: kylecarbs <kyle@carberry.com>
2022-05-11 09:44:43 -05:00
Ben Potter a169542bda chore: bump hc-install from v0.3.1 to v0.3.2 (#1382)
* chore: bump hc-install from v0.3.1 to v0.3.2

* fix
2022-05-11 09:05:25 -05:00
Spike Curtis 9d94f4f714 fix: macOS backspace processing (#1379)
Revert "fix: Remove line length limit on MacOS for input prompts (#839)"

This reverts commit ccba2ba99d.
2022-05-10 15:20:36 -07:00
Colin Adler f816bbe801 feat: add codegen for audit.AuditableResources entries (#1370) 2022-05-10 21:27:45 +00:00
Colin Adler 97a95f1377 chore: upgrade golangci-lint to v1.46.0 (#1373) 2022-05-10 16:04:23 -05:00
Kyle Carberry e0a7aec228 fix: Match kubectl table style for simpler scripting (#1363)
Fixes #1322.
2022-05-10 15:57:07 -05:00
Bruno Quaresma 2df92e6fd3 feat: Add update user roles action (#1361) 2022-05-10 19:13:07 +00:00
Spike Curtis c96d439f3d chore: add Spike Curtis to contributors (#1333)
Signed-off-by: Spike Curtis <spike@coder.com>
2022-05-10 16:46:28 +00:00
Ben Potter e8e4cf9a37 feat: add comparison to docs (#1315)
* fix typo

* feat: add comparison table

* Update docs/about.md

Co-authored-by: Katie Horne <katie@23spoons.com>

* Update docs/about.md

Co-authored-by: Katie Horne <katie@23spoons.com>

* Update docs/about.md

Co-authored-by: Katie Horne <katie@23spoons.com>

* chore: remove gitpod

Co-authored-by: Katie Horne <katie@23spoons.com>
2022-05-10 16:31:59 +00:00
dependabot[bot] 47f1fd57e4 chore: bump ts-loader from 9.2.9 to 9.3.0 in /site (#1253)
Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 9.2.9 to 9.3.0.
- [Release notes](https://github.com/TypeStrong/ts-loader/releases)
- [Changelog](https://github.com/TypeStrong/ts-loader/blob/main/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/ts-loader/compare/v9.2.9...v9.3.0)

---
updated-dependencies:
- dependency-name: ts-loader
  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>
2022-05-10 09:45:31 -04:00
Mathias Fredriksson 2d3dc436a8 feat: Implement unified pagination and add template versions support (#1308)
* feat: Implement pagination for template versions

* feat: Use unified pagination between users and template versions

* Sync codepaths between users and template versions

* Create requestOption type in codersdk and add test

* Fix created_at edge case for pagination cursor in queries

* feat: Add support for json omitempty and embedded structs in apitypings (#1318)

* Add scripts/apitypings/main.go to Makefile
2022-05-10 07:44:09 +00:00
Kyle Carberry dc115b8ca0 fix: Use proper endpoint for user workspaces (#1356)
This was a silly mistake in a prior PR, so the code wasn't
actually being called!
2022-05-10 03:10:47 +00:00
Kyle Carberry b675aec4dd feat: Add endpoint to get all workspaces a user can access (#1354)
This iterates through user organizations to get permitted
workspaces. This will allow admins to manage user workspaces!
2022-05-10 02:38:20 +00:00
Kyle Carberry e6f1ce1fb2 fix: Allow coderd to exit on error channel (#1355)
coderd would fail silently if this was called, because connections
would never drain. HashiCorp's hc-install package broke today,
and we couldn't notice because this was hanging!
2022-05-10 02:19:20 +00:00
dependabot[bot] 3660483b97 chore: bump github.com/moby/moby (#1339)
Bumps [github.com/moby/moby](https://github.com/moby/moby) from 20.10.14+incompatible to 20.10.15+incompatible.
- [Release notes](https://github.com/moby/moby/releases)
- [Changelog](https://github.com/moby/moby/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moby/moby/compare/v20.10.14...v20.10.15)

---
updated-dependencies:
- dependency-name: github.com/moby/moby
  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>
2022-05-09 19:01:33 -05:00
dependabot[bot] 48f004bb3d chore: bump github.com/charmbracelet/charm from 0.12.0 to 0.12.1 (#1341)
Bumps [github.com/charmbracelet/charm](https://github.com/charmbracelet/charm) from 0.12.0 to 0.12.1.
- [Release notes](https://github.com/charmbracelet/charm/releases)
- [Changelog](https://github.com/charmbracelet/charm/blob/main/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/charm/compare/v0.12.0...v0.12.1)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/charm
  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>
2022-05-09 19:00:11 -05:00
dependabot[bot] 5653c4455a chore: bump google.golang.org/api from 0.77.0 to 0.78.0 (#1340)
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.77.0 to 0.78.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.77.0...v0.78.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>
2022-05-09 18:50:32 -05:00
dependabot[bot] 9b30ff8e59 chore: bump @typescript-eslint/parser from 5.22.0 to 5.23.0 in /site (#1353)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.22.0 to 5.23.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/v5.23.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  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>
2022-05-09 18:29:40 -05:00
Kyle Carberry ddb9631d7a fix: Group subcommands for cognitive ease (#1351) 2022-05-09 17:42:02 -05:00
Colin Adler 20caee1502 feat: add audit exporting and filtering (#1314) 2022-05-09 22:05:01 +00:00
dependabot[bot] ac27f645eb chore: bump @typescript-eslint/parser from 5.21.0 to 5.22.0 in /site (#1343)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.21.0 to 5.22.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/v5.22.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  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>
2022-05-09 16:41:54 -05:00
David Wahler d847d2b1c5 chore: add dwahler as contributor (#1352) 2022-05-09 20:57:03 +00:00
Kira Pilot f5693dff3d chore: added contributer (#1349) 2022-05-09 14:49:23 -04:00
Bruno Quaresma e54324d880 refactor: Add roles into the user response (#1347) 2022-05-09 16:38:14 +00:00
Spike Curtis ad8d9dd71a feat: make it harder to skip graceful shutdown accidentally (#1327)
* feat: make it harder to skip graceful shutdown accidentally

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

* fixup: don't use unbuffered signal channel

Signed-off-by: Spike Curtis <spike@coder.com>
2022-05-06 13:45:18 -07:00
Bruno Quaresma 00806580f5 refactor: Return the display_name and name in the roles endpoint (#1328) 2022-05-06 19:18:00 +00:00
Kyle Carberry 97ee5600c7 fix: Remove "CODER_URL" and "CODER_TOKEN" (#1330)
These aren't being used, so it's best to remove for now.
It caused issues with dogfooding from v1 too!
2022-05-06 18:46:21 +00:00
Bruno Quaresma cf5aca799d Add reset user password action (#1320) 2022-05-06 13:23:03 -05:00
Bruno Quaresma 57bb108465 feat: Add update user password endpoint (#1310) 2022-05-06 09:20:08 -05:00
Presley Pizzo a2be7c0294 fix: create and read workspace page (#1294)
* Change name of existing workspace call

* Add new api call (has handler already)

* WorkspacesPage -> WorkspacePage

* starting to replace swr

* Add other api calls

* Fix api call

* Replace swr with xstate

* Format

* Test - wip

* Fix route in template page

* Fix endpoint in create workspace

* Fix tests

* Lint
2022-05-06 10:02:17 -04:00
Mathias Fredriksson 3dbcddc310 fix: Confirm password in cli create first user step (#1220)
Fixes #1182
2022-05-06 15:47:38 +03:00
Kyle Carberry 914a2f477c fix: Restore terminal on interrupt when connecting (#1312)
Fixes #1292.
2022-05-05 20:11:44 -05:00
Ben Potter f965066517 feat: add screenshot to readme (#1313)
* feat: add screenshot to readme

* change
2022-05-06 01:09:27 +00:00
Ben Potter 568574c118 fix: use CODER_AGENT_TOKEN for docker example (#1295) 2022-05-04 22:42:58 +00:00
Colin Adler 0ccf0102d7 fix: ensure correct version of sqlc is executed (#1287)
If `/usr/local/bin` is searched before `$GOPATH/bin` in your `$PATH` the
wrong version of `sqlc` can be executed.
2022-05-04 20:09:13 +00:00
Kyle Carberry d7f63217f1 feat: Add code splitting to reduce bundle size (#1285)
This splits our pages to use separate JavaScript bundles. It
initially splits the terminal, which reduces our primary
bundle size by ~400KB.

We should do this for all pages, but that can come in a future
change. This leaves the loading page empty for now, which I
think is fine. None of our pages are large enough that the blank
screen temporarily would be concerning.
2022-05-04 14:24:31 -05:00
Bruno Quaresma f911c8a781 feat: Add suspend user action (#1275) 2022-05-04 16:10:38 +00:00
Presley Pizzo 34b91fd577 feat: add margins to pages (#1217)
* Add Margin, use constants

* Change throughout

* Add to a page, lint

* Format
2022-05-04 11:36:54 -04:00
Kyle Carberry 4c35b8174a fix: Prefix paths in find on macOS (#1284)
This fixes paths not resolving in macOS, causing
the build target to fail. This also renames the
site target to specify the index.html, which is
the output artifact of building the site.
2022-05-04 09:47:48 -05:00
Kyle Carberry e860cc4814 fix: Build site in release (#1283)
This was using Mac Make, which is missing some options:
https://github.com/coder/coder/runs/6265845123?check_suite_focus=true#step:10:6
2022-05-04 14:23:48 +00:00
Colin Adler 9d2e788fea feat: allow verbose logging in coder server (#1280) 2022-05-03 16:13:30 -05:00
Steven Masley d0293e4d33 feat: Implement list roles & enforce authorize examples (#1273) 2022-05-03 16:10:19 -05:00
Colin Adler 0f9e30e54f fix: use correct enable bool for pprof (#1279) 2022-05-03 19:54:10 +00:00
Colin Adler e530ab2838 chore: add api specific 404 (#1272)
Prevents weird errors when routes are moved, like
https://github.com/coder/coder/issues/1205
2022-05-03 09:22:08 -05:00
dependabot[bot] ba80c799d7 chore: bump typescript from 4.6.3 to 4.6.4 in /site (#1252)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.6.3 to 4.6.4.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.6.3...v4.6.4)

---
updated-dependencies:
- dependency-name: typescript
  dependency-type: direct:development
  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>
2022-05-03 09:11:39 -05:00
Kyle Carberry 60aa40a56f fix: Remove microsecond wait in peer.(*Channel) (#1270)
This was implemented when our WebRTC code was much less hardened.
It's likely this was a cause of some other problem.

Closes #1076.
2022-05-03 14:00:59 +00:00
Colin Adler eda85a0141 fix: force logs to flush on close in peer.(*Conn) (#1268) 2022-05-03 08:36:48 -05:00
Colin Adler 9319c39257 fix: additional provisionerd test double closes (#1267) 2022-05-03 08:02:54 -05:00
Colin Adler 55ad97bbd7 feat: add pprof and prometheus metrics to coder server (#1266) 2022-05-03 12:48:02 +00:00
Kyle Carberry 5dcaf940b6 fix: Build site in deploy (#1265) 2022-05-02 23:12:13 +00:00
Kyle Carberry fd49a18b47 feat: Add "state" command to pull and push workspace state (#1264)
It's possible for a workspace to become in an invalid state.
This is something we'll detect for jobs, and allow monitoring of.

These commands will allow admins to manually reconcile state.
2022-05-02 17:51:58 -05:00
Kyle Carberry 43c6bff5ae fix: Use "terraform state pull" instead of "terraform show" (#1262)
Although the terraform-exec docs don't indicate this, the result of
"terraform show" isn't actually the state... it's a trimmed version
of the state that excludes resource identifiers, essentially removing
all state that did exist.

Tests will be written to ensure Terraform state reconciliation can occur.
This will happen in another PR, as dogfood is currently broken because of this.
2022-05-02 20:02:38 +00:00
Kyle Carberry fc642edf51 fix: Use GoReleaser Action in deploy script (#1263)
This reduces conflictions with our Makefile, which is presently
primarily a user script.
2022-05-02 20:00:08 +00:00
Colin Adler 81bef1c83e feat: add audit logging database schema (#1225) 2022-05-02 19:30:46 +00:00
Kyle Carberry e4e60256ac fix: Use "make build" on deploy (#1261)
This was a missed item in https://github.com/coder/coder/pull/1259.
2022-05-02 14:04:45 -05:00
Kyle Carberry dacc025cf3 fix: Adjust Makefile tagets to use dependencies (#1259)
It was getting slow to run `make gen` and other operations,
but this resolves it by targeting files properly.
2022-05-02 13:23:13 -05:00
dependabot[bot] 2293d7efd1 chore: bump github.com/gohugoio/hugo from 0.97.3 to 0.98.0 (#1247)
Bumps [github.com/gohugoio/hugo](https://github.com/gohugoio/hugo) from 0.97.3 to 0.98.0.
- [Release notes](https://github.com/gohugoio/hugo/releases)
- [Changelog](https://github.com/gohugoio/hugo/blob/master/goreleaser.yml)
- [Commits](https://github.com/gohugoio/hugo/compare/v0.97.3...v0.98.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>
2022-05-02 17:49:02 +00:00
dependabot[bot] 9032b7e33f chore: bump github.com/open-policy-agent/opa from 0.39.0 to 0.40.0 (#1245)
Bumps [github.com/open-policy-agent/opa](https://github.com/open-policy-agent/opa) from 0.39.0 to 0.40.0.
- [Release notes](https://github.com/open-policy-agent/opa/releases)
- [Changelog](https://github.com/open-policy-agent/opa/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-policy-agent/opa/compare/v0.39.0...v0.40.0)

---
updated-dependencies:
- dependency-name: github.com/open-policy-agent/opa
  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>
2022-05-02 17:36:02 +00:00
dependabot[bot] 6dd378c194 chore: bump google.golang.org/api from 0.75.0 to 0.77.0 (#1249)
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.75.0 to 0.77.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.75.0...v0.77.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>
2022-05-02 17:32:27 +00:00
Kyle Carberry 3d96785bf5 fix: Add lock around read/write of circular buffer (#1258)
This fixes a race seen in:
https://github.com/coder/coder/runs/6260926628?check_suite_focus=true#step:10:666
2022-05-02 17:31:04 +00:00
dependabot[bot] 7fb3c5728b chore: bump gopkg.in/DataDog/dd-trace-go.v1 from 1.38.0 to 1.38.1 (#1246)
Bumps [gopkg.in/DataDog/dd-trace-go.v1](https://github.com/DataDog/dd-trace-go) from 1.38.0 to 1.38.1.
- [Release notes](https://github.com/DataDog/dd-trace-go/releases)
- [Commits](https://github.com/DataDog/dd-trace-go/compare/v1.38.0...v1.38.1)

---
updated-dependencies:
- dependency-name: gopkg.in/DataDog/dd-trace-go.v1
  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>
2022-05-02 17:16:45 +00:00
Kyle Carberry 3176e10562 fix: Use atomic value for logger in peer (#1257)
This caused many races where logs would escape the tests
by milliseconds. By using an atomic on the logger,
we can fix all of it!
2022-05-02 11:49:59 -05:00
Kyle Carberry e531c0930c fix: Write agent logs to "/tmp/coder-agent.log" for debugging (#1239)
It was difficult to obtain logs for the agent if it failed to
start for some reason. Now they'll go to a consistent spot!
2022-05-02 16:36:51 +00:00
Kyle Carberry c2b5009208 fix: Unnest workspaces command to the top-level (#1241)
This changes all "coder workspace *" commands to root.
A few of these were already at the root, like SSH. The
inconsistency made for a confusing experience.
2022-05-02 11:08:52 -05:00
Kyle Carberry 252d868298 fix: Prefix buildinfo tag with "v" (#1256)
This fixes the format of production and development build
tags.
2022-05-02 15:58:36 +00:00
Kyle Carberry d139a16446 fix: Allow remote state to be used with Terraform (#1242)
The Terraform Provisioner depended on the statefile content
being at a specific path, which disallowed the use of external
state providers. This fixes it!
2022-05-02 15:41:27 +00:00
Ben Potter aaf6aee979 fix: clarify AWS access key ID vs secret (#1231) 2022-05-02 10:28:58 -05:00
Kyle Carberry 8701e0084c feat: Update Terraform provider to support "dir" in "coder_agent" (#1219)
This allows users to specify a starting directory for shell sessions.
2022-05-02 10:27:34 -05:00
Kyle Carberry a79aa6418a fix: Use cliui.WorkspaceBuild to prevent cancel of builds jobs (#1255)
Build jobs cannot gracefully terminate because Terraform generally
cannot gracefully terminate.
2022-05-02 10:20:47 -05:00
Bruno Quaresma 75343288ff feat: Add user menu on users table (#1222) 2022-05-02 14:58:49 +00:00
dependabot[bot] a71f3934c5 chore: bump github.com/go-playground/validator/v10 (#1254)
Bumps [github.com/go-playground/validator/v10](https://github.com/go-playground/validator) from 10.10.1 to 10.11.0.
- [Release notes](https://github.com/go-playground/validator/releases)
- [Commits](https://github.com/go-playground/validator/compare/v10.10.1...v10.11.0)

---
updated-dependencies:
- dependency-name: github.com/go-playground/validator/v10
  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>
2022-05-02 08:21:35 -05:00
Bruno Quaresma 2043d1a4cc chore: Port ConfirmDialog from v1 (#1228) 2022-05-02 08:17:15 -05:00
dependabot[bot] 4ff5734720 chore: bump github.com/charmbracelet/charm from 0.11.0 to 0.12.0 (#1248)
Bumps [github.com/charmbracelet/charm](https://github.com/charmbracelet/charm) from 0.11.0 to 0.12.0.
- [Release notes](https://github.com/charmbracelet/charm/releases)
- [Changelog](https://github.com/charmbracelet/charm/blob/main/.goreleaser.yml)
- [Commits](https://github.com/charmbracelet/charm/compare/v0.11.0...v0.12.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/charm
  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>
2022-05-02 07:59:46 -05:00
Kyle Carberry 34dbca7166 fix: Remove unnecessary warnings on SSH (#1243)
This disables the "Warning: Permanently added 'hostname' (RSA) to the list of known hosts."
message from appearing on every SSH. This happens because we ignore the known hosts.
2022-05-01 20:31:22 -05:00
Kyle Carberry 9b37a0de31 fix: Disable ErrorLog in http.Server (#1244)
Vault does similarly: https://github.com/hashicorp/vault/blob/e2490059d0711635e529a4efcbaa1b26998d6e1c/command/server.go#L2714

These messages have primarily been noise.
2022-05-01 20:31:12 -05:00
Kyle Carberry b948f2dab5 fix: Use environment variables for agent authentication (#1238)
* fix: Update GIT_COMMITTER_NAME to use username

This was a mistake when adding the committer fields 🤦.

* fix: Use environment variables for agent authentication

Using files led to situations where running "coder server --dev" would
break `gitssh`. This is applicable in a production environment too. Users
should be able to log into another Coder deployment from their workspace.

Users can still set "CODER_URL" if they'd like with agent env vars!
2022-04-30 16:40:30 +00:00
Kyle Carberry eb606924ab fix: Update GIT_COMMITTER_NAME to use username (#1237)
This was a mistake when adding the committer fields 🤦.
2022-04-29 20:51:11 -05:00
Kyle Carberry 2acdd3b44f fix: Add "GIT_COMMITTER_*" to remove Git prompt (#1236) 2022-04-30 01:22:17 +00:00
Kyle Carberry e15566c7fa test: Regenerate GitSSHKey flake when comparing times (#1235)
This is a crazy one! The in-memory DB is fast, which allows the same
exact timestamp to occur for regenerating the key!

See: https://github.com/coder/coder/runs/6234173911?check_suite_focus=true#step:9:82
2022-04-29 20:19:16 -05:00
Kyle Carberry 81577f120a feat: Add web terminal with reconnecting TTYs (#1186)
* feat: Add web terminal with reconnecting TTYs

This adds a web terminal that can reconnect to resume sessions!
No more disconnects, and no more bad bufferring!

* Add xstate service

* Add the webpage for accessing a web terminal

* Add terminal page tests

* Use Ticker instead of Timer

* Active Windows mode on Windows
2022-04-29 17:30:10 -05:00
Kyle Carberry 23e5636dd0 fix: Use verified and primary email for GitHub signup (#1230)
This was causing a panic due to nil pointer dereference.
It required all users signing up had a public email,
which is an unreasonable requirement!
2022-04-29 15:13:35 -05:00
Bruno Quaresma 021e4cd957 refactor: Load users from the API (#1218) 2022-04-29 13:59:14 -05:00
Steven Masley 69e26c4036 feat: Allow using username in user queries (#1221)
* feat: Allow using username in user queries
* Test needs a username/email to not match empty string
2022-04-29 11:44:22 -05:00
dependabot[bot] 365c96ccaa chore: bump @fontsource/fira-code from 4.5.8 to 4.5.9 in /site (#1188)
Bumps [@fontsource/fira-code](https://github.com/fontsource/fontsource/tree/HEAD/fonts/google/fira-code) from 4.5.8 to 4.5.9.
- [Release notes](https://github.com/fontsource/fontsource/releases)
- [Changelog](https://github.com/fontsource/fontsource/blob/main/CHANGELOG.md)
- [Commits](https://github.com/fontsource/fontsource/commits/HEAD/fonts/google/fira-code)

---
updated-dependencies:
- dependency-name: "@fontsource/fira-code"
  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>
2022-04-29 15:58:33 +00:00
dependabot[bot] a3decc4fba chore: bump eslint-plugin-react-hooks from 4.4.0 to 4.5.0 in /site (#1190)
Bumps [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) from 4.4.0 to 4.5.0.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/HEAD/packages/eslint-plugin-react-hooks)

---
updated-dependencies:
- dependency-name: eslint-plugin-react-hooks
  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>
2022-04-29 10:55:15 -05:00
dependabot[bot] 27811976ad chore: bump ts-loader from 9.2.8 to 9.2.9 in /site (#1192)
Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 9.2.8 to 9.2.9.
- [Release notes](https://github.com/TypeStrong/ts-loader/releases)
- [Changelog](https://github.com/TypeStrong/ts-loader/blob/main/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/ts-loader/compare/v9.2.8...v9.2.9)

---
updated-dependencies:
- dependency-name: ts-loader
  dependency-type: direct:development
  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>
2022-04-29 10:54:54 -05:00
dependabot[bot] 3ebe1d27b1 chore: bump @playwright/test from 1.21.0 to 1.21.1 in /site (#1191)
Bumps [@playwright/test](https://github.com/Microsoft/playwright) from 1.21.0 to 1.21.1.
- [Release notes](https://github.com/Microsoft/playwright/releases)
- [Commits](https://github.com/Microsoft/playwright/compare/v1.21.0...v1.21.1)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-type: direct:development
  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>
2022-04-29 10:54:39 -05:00
Steven Masley 35211e2190 feat: Add user roles, but do not yet enforce them (#1200)
* chore: Rework roles to be expandable by name alone
2022-04-29 09:04:19 -05:00
Bruno Quaresma ba4c3ce3b9 feat: add filter by status on GET /users (#1206) 2022-04-29 08:29:53 -05:00
Colin Adler 82364d174f fix: ensure rtc state changes can't log after close (#1213) 2022-04-28 16:38:59 -05:00
Bruno Quaresma 00cac37a07 chore: force desktop view on mobile (#1214) 2022-04-28 17:05:03 -04:00
Presley Pizzo c16f105727 feat: Create user page (#1197)
* Add button and route

* Hook up api

* Lint

* Add basic form

* Get users on page mount

* Make cancel work

* Creating -> idle bc users page refetches

* Import as TypesGen

* Handle api errors

* Lint

* Add handler

* Add FormFooter

* Add FullPageForm

* Lint

* Better form, error, stories

bug in formErrors story

* Make detail optional

* Use Language

* Remove detail prop

* Add back autoFocus

* Remove displayError, use displaySuccess

* Lint, export Language

* Tests - wip

* Fix cancel tests

* Switch back to mock

* Add navigate to xservice

Doesn't work in test

* Move error type predicate to xservice

* Lint

* Switch to using creation mode in XState

still problems in tests

* Lint

* Lint

* Lint

* Revert "Switch to using creation mode in XState"

This reverts commit cf8442fa4b.

* Give XService a navigate action

* Add missing validation messages

* Fix XState warning

* Fix tests

IRL is broken bc I need to send org id

* Pretend user has org id and make it work

* Format

* Lint

* Switch to org ids array

* Skip lines between tests

Co-authored-by: G r e y <grey@coder.com>

* Punctuate notification messages

Co-authored-by: G r e y <grey@coder.com>
2022-04-28 16:32:23 -04:00
Mathias Fredriksson 4efde58726 fix: Restore static credentials for develop.sh (#1212) 2022-04-28 21:47:35 +03:00
Colin Adler 1661588bd1 fix: user passwords cleanup (#1202)
1. Adds benchmarks comparing bcrypt and our pbkdf2 settings
1. Changes the pbkdf2 hash iterations back to 65k. 1024 is insecure
1. Gets rid of the short circuit when the user isn't found, preventing
   timing attacks which can reveal which emails exist on a deployment

```
$ go test -bench .
goos: linux
goarch: amd64
pkg: github.com/coder/coder/coderd/userpassword
cpu: Intel(R) Core(TM) i9-9900K CPU @ 3.60GHz
BenchmarkBcryptMinCost-16        	    1651	    702727 ns/op	    5165 B/op      10 allocs/op
BenchmarkPbkdf2MinCost-16        	    1669	    714843 ns/op	     804 B/op      10 allocs/op
BenchmarkBcryptDefaultCost-16    	      27	  42676316 ns/op	    5246 B/op      10 allocs/op
BenchmarkPbkdf2-16               	      26	  45902236 ns/op	     804 B/op      10 allocs/op
PASS
ok  	github.com/coder/coder/coderd/userpassword	5.036s
```
2022-04-28 18:22:38 +00:00
Steven Masley e330dc1321 feat: Switch packages for typescript generation code (#1196)
* feat: Switch packages for typescript generation code

Supports a larger set of types
  - [x] Basics (string/int/etc) 
  - [x] Maps
  - [x] Slices
  - [x] Enums
  - [x] Pointers
2022-04-28 16:59:14 +00:00
Mathias Fredriksson afc43fe95f feat: Generate random admin user password in dev mode (#1207)
* feat: Generate random admin user password in dev mode

* Add dev mode test with email/pass from env

* Set email/pass for playwright e2e test via cli flags
2022-04-28 19:13:44 +03:00
Kyle Carberry eea9729704 fix: Update buildinfo package location in ldflags (#1208)
This was causing the version to not be injected!
2022-04-28 10:46:18 -05:00
Bruno Quaresma 816441eff7 feat: add organization_ids in the user(s) response (#1184) 2022-04-28 09:10:17 -05:00
Colin Adler a7fb018414 chore: update github.com/golang-migrate/migrate/v4 to v4.5.2 (#1201)
Should fix the security warning for https://github.com/advisories/GHSA-crp2-qrr5-8pq7
2022-04-27 22:04:51 +00:00
Mathias Fredriksson 8661f92a10 feat: Output username and password for code server --dev (#1193)
Fixes #825
2022-04-27 17:59:37 +03:00
Mathias Fredriksson 0b1ee3303d chore: Add Contributors section to readme (#1144)
Co-authored-by: Cian Johnston <public@cianjohnston.ie>
2022-04-27 11:48:45 +00:00
dependabot[bot] a769e8623d chore: bump @types/node from 14.18.15 to 14.18.16 in /site (#1189)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 14.18.15 to 14.18.16.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-27 06:04:44 +00:00
dependabot[bot] 26f3ceda93 chore: bump @typescript-eslint/parser from 5.19.0 to 5.21.0 in /site (#1160)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.19.0 to 5.21.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/v5.21.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  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>
Co-authored-by: G r e y <grey@coder.com>
2022-04-27 05:52:53 +00:00
dependabot[bot] d85e36ce9e chore: bump @xstate/cli from 0.1.6 to 0.1.7 in /site (#1139)
Bumps @xstate/cli from 0.1.6 to 0.1.7.

---
updated-dependencies:
- dependency-name: "@xstate/cli"
  dependency-type: direct:development
  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>
2022-04-27 01:44:19 -04:00
dependabot[bot] 7f423951bf chore: bump jest-junit from 13.1.0 to 13.2.0 in /site (#1137)
Bumps [jest-junit](https://github.com/jest-community/jest-junit) from 13.1.0 to 13.2.0.
- [Release notes](https://github.com/jest-community/jest-junit/releases)
- [Commits](https://github.com/jest-community/jest-junit/compare/v13.1.0...v13.2.0)

---
updated-dependencies:
- dependency-name: jest-junit
  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>
2022-04-27 01:43:34 -04:00
dependabot[bot] 65d5975592 chore: bump cronstrue from 2.2.0 to 2.4.0 in /site (#1165)
Bumps [cronstrue](https://github.com/bradymholt/cronstrue) from 2.2.0 to 2.4.0.
- [Release notes](https://github.com/bradymholt/cronstrue/releases)
- [Commits](https://github.com/bradymholt/cronstrue/compare/v2.2.0...v2.4.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>
2022-04-27 05:38:17 +00:00
Presley Pizzo 23a1a4dc40 refactor: Add components for styling forms (#1169)
* Add FormFooter

* Add FullPageForm

* Lint

* Make detail optional

* Use Language
2022-04-26 19:45:48 -04:00
Bruno Quaresma 454ccf7547 refactor: remove the user name (#1185) 2022-04-26 19:04:57 +00:00
Ben Potter 22668c388c feat: initial docs pages (#1107)
* docs structure and edits to getting started

* draft for about page

* skeleton for concepts page

* attempt at explaining templates

* left-align tables

* add best practices and variables

* update structrure

* update structure

* templates are shared

* workspaces docs

* remove coming soon

* fix typos

* docs structure and edits to getting started

* draft for about page

* skeleton for concepts page

* attempt at explaining templates

* left-align tables

* add best practices and variables

* update structrure

* update structure

* templates are shared

* workspaces docs

* remove coming soon

* fix typos

* fix typos

* Update docs/about.md

Co-authored-by: Joe Previte <jjprevite@gmail.com>

* remove line breaks between bullets

* rename variables to parameters

* reduce limits

* chore: edit text

* revert some changes, fix footnotes

Co-authored-by: Katie Horne <katie@coder.com>
Co-authored-by: Joe Previte <jjprevite@gmail.com>
2022-04-26 12:10:50 -05:00
Kyle Carberry 603b7da413 test: Wrap provisionerd channel closes in sync.Once (#1181)
This caused a few flakes, so figured I'd tackle all of them:
https://github.com/coder/coder/runs/6167856950?check_suite_focus=true#step:9:246
2022-04-26 09:44:16 -05:00
Bruno Quaresma 441ffd6a0b feat: add PUT /api/v2/users/:user-id/suspend endpoint (#1154) 2022-04-26 09:00:07 -05:00
Kyle Carberry f9ce54a51e fix: Increase default provisioner daemons to 3 (#1180)
It's odd to only build one workspace at a time as a default.
2022-04-26 07:19:17 -05:00
Kyle Carberry 8d85d80a55 fix: Use proper shutdown signal for systemd (#1179)
Coder was being killed instantly, which caused builds
to randomly fail!
2022-04-25 21:09:11 -05:00
Kyle Carberry 744a00a55d feat: Add GIT_COMMITTER information to agent env vars (#1171)
This makes setting up git a bit simpler, and users
can always override these values!

We'll probably add a way to disable our Git integration
anyways, so these could be part of that.
2022-04-25 20:03:54 -05:00
Kyle Carberry 877854a2f3 fix: Display proper access URL on server start (#1172)
Fixes #1170.
2022-04-25 23:30:22 +00:00
Kyle Carberry 29d55887f9 fix: Update initial window size on SSH TTY (#1174)
It required a window resize before to trigger
a size update. This fixes it!
2022-04-25 23:02:54 +00:00
Kyle Carberry 947e8f9d2e fix: Manually format external URL (#1168)
path.Join escaped the double slash!
2022-04-25 22:22:31 +00:00
Kyle Carberry 159024a196 fix: Add redirect for OAuth from /login (#1163)
This allows for a complete sign-up flow:
1. coder login https://dev.coder.com
2. Navigates to /cli-auth
3. Redirects to /login?redirect=%2Fcli-auth
4. User signs in with GitHub
5. User is redirected back to /cli-auth
2022-04-25 17:07:10 -05:00
Kyle Carberry 4f7ceebe65 fix: Use target="_blank" to redirect for footer version (#1166) 2022-04-25 16:27:14 -05:00
Kyle Carberry 88669fd578 feat: Move workspaces under organizations (#1109)
This removes split ownership for workspaces. They are now
a resource of organizations and have a designated owner,
which is a user.

This enables simple administration for commands like:
- `coder stop ben/dev`
- `coder build logs colin/arch`

or if we decide to allow administrators to access workspaces,
they could even SSH using this syntax: `coder ssh colin/dev`.
2022-04-25 16:11:03 -05:00
Kyle Carberry 759fa5f626 fix: Use Lax mode for OAuth redirect cookies (#1162)
OAuthing was resulting in an error, because Strict
cookies are not sent on redirects.
2022-04-25 20:42:18 +00:00
dependabot[bot] a201610761 chore: bump eslint-plugin-jest from 26.1.4 to 26.1.5 in /site (#1136)
Bumps [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) from 26.1.4 to 26.1.5.
- [Release notes](https://github.com/jest-community/eslint-plugin-jest/releases)
- [Changelog](https://github.com/jest-community/eslint-plugin-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jest-community/eslint-plugin-jest/compare/v26.1.4...v26.1.5)

---
updated-dependencies:
- dependency-name: eslint-plugin-jest
  dependency-type: direct:development
  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>
2022-04-25 20:38:52 +00:00
Kyle Carberry 587cbac498 fix: Swap height and width for TTY size (#1161)
This was causing the TTY to be real wonky on Windows.
It didn't seem to have an effect on Linux, but I suspect
that's because of escape codes.
2022-04-25 15:30:02 -05:00
Colin Adler 0c9f27c63b fix: ensure frontend is built in make install (#1157) 2022-04-25 15:24:23 -05:00
dependabot[bot] 8f464ce8c7 chore: bump @types/react-dom from 17.0.15 to 17.0.16 in /site (#1159)
Bumps [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) from 17.0.15 to 17.0.16.
- [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-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-25 20:13:04 +00:00
dependabot[bot] 9fb660a6a3 chore: bump @typescript-eslint/eslint-plugin in /site (#1153)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.19.0 to 5.21.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/v5.21.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  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>
2022-04-25 16:03:50 -04:00
dependabot[bot] bb3420d006 chore: bump @types/node from 14.18.13 to 14.18.15 in /site (#1155)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 14.18.13 to 14.18.15.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-25 16:01:15 -04:00
Presley Pizzo bdc17f49e4 refactor: curry GetFormHelpers (#1156) 2022-04-25 15:45:33 -04:00
Kyle Carberry 33b58a0363 fix: Use forward slashes on Windows for gitssh (#1146) 2022-04-25 19:41:52 +00:00
Kyle Carberry fccd4fab96 fix: Windows resize syscall using incorrect pointer (#1152)
Resizing of PTYs weren't working on Windows before,
but they are now!
2022-04-25 14:30:57 -05:00
dependabot[bot] 82552a9315 chore: bump eslint from 8.13.0 to 8.14.0 in /site (#1135)
Bumps [eslint](https://github.com/eslint/eslint) from 8.13.0 to 8.14.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.13.0...v8.14.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>
2022-04-25 15:04:48 -04:00
Kyle Carberry 64348882a0 test: Loop if content length is zero for Windows (#1151)
On Windows it seems the file can be created before the
content has been written, which results in comparing
an empty string.
2022-04-25 19:01:49 +00:00
Kyle Carberry 9056c53054 fix: Disable known hosts checking for config-ssh (#1150)
This was causing the remote identity changed message.
We don't need to verify remote hosts because we
already auth via our API.
2022-04-25 18:59:18 +00:00
Colin Adler 2a57ea757a feat: add audit package (#1046) 2022-04-25 18:57:59 +00:00
Kyle Carberry a2dd618849 feat: Use environment variables and startup script in agent (#1147)
These values were ignored. Environment variables are applied to
new sessions, and are refreshed on reconnect. This is cool because
a workspace could be updated with new environment variables without
requiring a complete start/stop.

The startup script is only ran once regardless of changes, which
feels like the expected behavior.
2022-04-25 18:30:39 +00:00
Kyle Carberry 09405ddc40 test: Wait for WorkspaceResources to complete before exiting (#1149)
This caused a flake seen in:
https://github.com/coder/coder/runs/6162655678?check_suite_focus=true#step:9:87
2022-04-25 18:11:35 +00:00
dependabot[bot] 1e6f2cf750 chore: bump hashicorp/setup-terraform from 1 to 2 (#1130)
Bumps [hashicorp/setup-terraform](https://github.com/hashicorp/setup-terraform) from 1 to 2.
- [Release notes](https://github.com/hashicorp/setup-terraform/releases)
- [Changelog](https://github.com/hashicorp/setup-terraform/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/setup-terraform/compare/v1...v2)

---
updated-dependencies:
- dependency-name: hashicorp/setup-terraform
  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>
2022-04-25 12:59:47 -05:00
Kyle Carberry 5e6e626ace test: Disable TestAgent/SessionTTY on Windows (#1148)
Either our ConPTY implementation is unstable, or something is
flakey with how it sends output. I'm not sure how our
implementation would sometimes work, so it's best to disable
this for CI stability for now.
2022-04-25 17:53:45 +00:00
dependabot[bot] fdb27eaaf8 chore: bump github.com/hashicorp/hcl/v2 from 2.11.1 to 2.12.0 (#1142)
Bumps [github.com/hashicorp/hcl/v2](https://github.com/hashicorp/hcl) from 2.11.1 to 2.12.0.
- [Release notes](https://github.com/hashicorp/hcl/releases)
- [Changelog](https://github.com/hashicorp/hcl/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/hcl/compare/v2.11.1...v2.12.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-25 17:38:59 +00:00
Kyle Carberry 185d97a65b fix: Increase ptytest buffer to resolve flake (#1141)
From investigating the following run:
https://github.com/coder/coder/runs/6156348454?check_suite_focus=true

It's believed that the cause is data being discarded due to the buffer
filling up. This _might_ fix it, but not entirely sure.
2022-04-25 12:14:16 -05:00
Kyle Carberry 66d45f391e test: Check if provisionerd is closed before setting run chan (#1145)
This race can be seen here:
https://github.com/coder/coder/runs/6159662393?check_suite_focus=true
2022-04-25 12:00:54 -05:00
dependabot[bot] 8c27b4e23d chore: bump jaxxstorm/action-install-gh-release from 1.4.0 to 1.5.0 (#1131)
Bumps [jaxxstorm/action-install-gh-release](https://github.com/jaxxstorm/action-install-gh-release) from 1.4.0 to 1.5.0.
- [Release notes](https://github.com/jaxxstorm/action-install-gh-release/releases)
- [Commits](https://github.com/jaxxstorm/action-install-gh-release/compare/v1.4.0...v1.5.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>
2022-04-25 11:17:15 -05:00
dependabot[bot] 1acc454035 chore: bump github.com/mitchellh/mapstructure from 1.4.3 to 1.5.0 (#1133)
Bumps [github.com/mitchellh/mapstructure](https://github.com/mitchellh/mapstructure) from 1.4.3 to 1.5.0.
- [Release notes](https://github.com/mitchellh/mapstructure/releases)
- [Changelog](https://github.com/mitchellh/mapstructure/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mitchellh/mapstructure/compare/v1.4.3...v1.5.0)

---
updated-dependencies:
- dependency-name: github.com/mitchellh/mapstructure
  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>
2022-04-25 11:16:32 -05:00
Steven Masley 8b54ea8562 test: Fix user pagination sort order (#1143)
Sort by uuid in expected output to cover when times are equal
for 2 users. The database (fake & pg) use id as as second ordering
to cover this edge case. Should realistically never happen in
production.
2022-04-25 10:27:08 -05:00
dependabot[bot] c08fdc0c8c chore: bump github.com/gohugoio/hugo from 0.97.2 to 0.97.3 (#1132)
Bumps [github.com/gohugoio/hugo](https://github.com/gohugoio/hugo) from 0.97.2 to 0.97.3.
- [Release notes](https://github.com/gohugoio/hugo/releases)
- [Changelog](https://github.com/gohugoio/hugo/blob/master/goreleaser.yml)
- [Commits](https://github.com/gohugoio/hugo/compare/v0.97.2...v0.97.3)

---
updated-dependencies:
- dependency-name: github.com/gohugoio/hugo
  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>
2022-04-25 09:43:57 -05:00
dependabot[bot] 2959137f0d chore: bump github.com/pion/webrtc/v3 from 3.1.29 to 3.1.34 (#1134)
Bumps [github.com/pion/webrtc/v3](https://github.com/pion/webrtc) from 3.1.29 to 3.1.34.
- [Release notes](https://github.com/pion/webrtc/releases)
- [Commits](https://github.com/pion/webrtc/compare/v3.1.29...v3.1.34)

---
updated-dependencies:
- dependency-name: github.com/pion/webrtc/v3
  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>
2022-04-25 09:43:40 -05:00
dependabot[bot] dbb4a979cc chore: bump cloud.google.com/go/compute from 1.6.0 to 1.6.1 (#1140)
Bumps [cloud.google.com/go/compute](https://github.com/googleapis/google-cloud-go) from 1.6.0 to 1.6.1.
- [Release notes](https://github.com/googleapis/google-cloud-go/releases)
- [Changelog](https://github.com/googleapis/google-cloud-go/blob/main/CHANGES.md)
- [Commits](https://github.com/googleapis/google-cloud-go/compare/pubsub/v1.6.0...pubsub/v1.6.1)

---
updated-dependencies:
- dependency-name: cloud.google.com/go/compute
  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>
2022-04-25 09:43:20 -05:00
dependabot[bot] 68d79e0f5f chore: bump google.golang.org/api from 0.74.0 to 0.75.0 (#1129)
Bumps [google.golang.org/api](https://github.com/googleapis/google-api-go-client) from 0.74.0 to 0.75.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.74.0...v0.75.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>
2022-04-25 09:28:09 -05:00
dependabot[bot] 5575e3c485 chore: bump gopkg.in/DataDog/dd-trace-go.v1 from 1.37.1 to 1.38.0 (#1128) 2022-04-25 06:29:18 -05:00
Cian Johnston c32a006e8f feat: add competitive advanced git functionality (#1077) 2022-04-25 09:01:32 +01:00
Kyle Carberry 7e33d80fa9 feat: Add helper output on failed gitssh (#1127) 2022-04-25 00:29:15 -05:00
Kyle Carberry 4417dd5951 feat: Add STUN servers to enable P2P (#1126)
This exposes a `--stun-server` option for listing
STUN servers. By default it uses the Google STUN
server, but this is not required.
2022-04-25 04:52:07 +00:00
Kyle Carberry c8e566fe42 feat: Add links for registering git key (#1125) 2022-04-25 04:27:45 +00:00
Kyle Carberry 8ff0c8b02a fix: Write agent URL to file for gitssh (#1123)
This was broken because gitssh couldn't find the URL.
Now it can, and SSH is confirmed to work on dogfood! 🥳
2022-04-25 04:06:52 +00:00
Kyle Carberry 68f67c54b6 fix: Add sync.Once to prevent double close in test (#1124)
https://github.com/coder/coder/runs/6151451291?check_suite_focus=true
2022-04-25 04:06:18 +00:00
Colin Adler abc13c5a92 fix: use fmt.Fprintln to print workspaces table (#1122) 2022-04-25 04:04:34 +00:00
Kyle Carberry a6ea99541e fix: Return from update loop when job completes (#1121)
Update was running forever, which stopped jobs from timing
out unless a restart occurred. This also fixes complete properly
reporting an error.
2022-04-25 03:41:03 +00:00
Kyle Carberry d44876382d fix: Remove unique index on auth tokens (#1120)
If a workspace is started multiple times, resources may
not be invalidated. This means an auth token can be
reused for a workspace.

coderd closes old agent connections, so this is expected
behavior, and the agent will reconnect properly.
2022-04-24 22:24:02 -05:00
Kyle Carberry 885d5f2098 fix: Monitor TTY size when using SSH (#1119)
The TTY wasn't resizing properly, and reasonably so considering
we weren't updating it 🤦.
2022-04-24 22:23:54 -05:00
Kyle Carberry 0c042dc249 fix: Remove duplicate index that blocked same name (#1118)
Multiple workspaces couldn't be created with the same names!
2022-04-25 02:40:14 +00:00
Kyle Carberry 23295f7f07 fix: Check for job status on another incoming (#1117)
If a job silently failed, it wasn't possible for another one
to execute. This fixes it by using the API status to return
active state.
2022-04-25 02:22:36 +00:00
Kyle Carberry db7ed4d019 fix: Add resiliency to daemon connections (#1116)
Connections could fail when massive payloads were transmitted.
This fixes an upstream bug in dRPC where the connection would
end with a context canceled if a message was too large.

This adds retransmission of completion and failures too. If
Coder somehow loses connection with a provisioner daemon,
upon the next connection the state will be properly reported.
2022-04-24 20:33:19 -05:00
Kyle Carberry be974cf280 feat: Add users create and list commands (#1111)
This allows for *extremely basic* user management.
2022-04-24 20:08:26 -05:00
Kyle Carberry 7496c3da81 feat: Add GitHub OAuth (#1050)
* Initial oauth

* Add Github authentication

* Add AuthMethods endpoint

* Add frontend

* Rename basic authentication to password

* Add flags for configuring GitHub auth

* Remove name from API keys

* Fix authmethods in test

* Add stories and display auth methods error
2022-04-23 22:58:57 +00:00
Kyle Carberry 3976994781 chore: Rename "start" to "server" (#1110)
Workspace commands will be aliased at the top-level, so
"start" would easily be confused with starting a workspace.

Server seems like a more appropriate name too.
2022-04-23 12:19:20 -05:00
Steven Masley da3681246e chore: Bump protoc to 3.20.0 (#1104)
* chore: Bump protoc to 3.20.0
* Make gen with 3.20.0 protoc
2022-04-23 01:53:22 +00:00
Garrett Delfosse e181007de1 fix: Add more golang types -> number ts type (#1108) 2022-04-22 21:01:43 +00:00
Colin Adler 95a24cb43a chore: update github.com/fatedier/frp (#1106)
Fixes https://github.com/advisories/GHSA-xcf7-q56x-78gh
2022-04-22 15:49:43 -05:00
Colin Adler d6c1c49868 fix: make install references incorrect folders (#1105) 2022-04-22 15:41:45 -05:00
Steven Masley 548de7d6f3 feat: User pagination using offsets (#1062)
Offset pagination and cursor pagination supported
2022-04-22 15:27:55 -05:00
Kyle Carberry 2a95917557 fix: Add site to release build (#1094)
This was accidentally removed when adding MacOS signing.
2022-04-20 14:06:07 -04:00
Kyle Carberry 65d77383d0 fix: Allow nested Terraform resources (#1093)
This fixes the dependency tree by adding recursion. It
now finds indirect connections and associates it with
an agent.

An example is attached which surfaced this issue.
2022-04-20 12:28:48 -05:00
Kyle Carberry e35a4fdcf0 fix: Disable Windows Defender on agent binary (#1095)
Windows (reasonably) detected our CLI as a virus due to the name
being "sshd" for VS Code support. See:
https://github.com/microsoft/vscode-remote-release/issues/5699

This disables monitoring for our binary prior to run, which
fixes our Windows example.
2022-04-20 15:01:35 +00:00
Joe Previte 44f68b5942 chore(develop): fix processes not killing (#1083) 2022-04-20 07:34:05 -07:00
Asher 3151befb38 chore: un-nest components (#1090)
Closes #936.
2022-04-19 14:16:11 -05:00
Joe Previte 98e46cdd2a chore: use PascalCase for pages files (#1089)
* chore: use PascalCase for pages files

* fixup

* fixup

* fixup

* fixup

* fixup

* fixup
2022-04-19 18:45:03 +00:00
Joe Previte db1127def1 chore: assign site/ to frontend (#1091) 2022-04-19 11:30:55 -07:00
Bruno Quaresma 301451be40 refactor: remove index files from components (#1086) 2022-04-19 13:20:28 -05:00
Presley Pizzo 6c9c1298e4 Reorganize Storybook (#1087) 2022-04-19 13:52:52 -04:00
G r e y 5141d6f970 feat: coder logo html source (#1088) 2022-04-19 13:14:32 -04:00
G r e y c35be02a7e feat: web console logo (#1085) 2022-04-19 12:48:50 -04:00
Garrett Delfosse 8165a6ef75 feat: Add spooky hidden flag (#1065) 2022-04-19 16:40:01 +00:00
Presley Pizzo a68b076b96 refactor: Rename Icons and add stories (#1080)
* Rename CloseIcon

* Rename FileCopyIcon, take out of index

* Rename LogoutIcon and remove from index

* Delete icons index files

* Add icon stories

* Lint
2022-04-19 12:38:43 -04:00
Bruno Quaresma b9933d493a refactor: camel case files (#1081) 2022-04-19 11:18:12 -05:00
G r e y 5ce06769cd chore: replace todos with issues (#1066) 2022-04-19 12:16:57 -04:00
Kyle Carberry 04985a1754 fix: Close TURN connections to resolve flake (#1079) 2022-04-19 11:14:55 -05:00
Garrett Delfosse 89aa39b5c8 fix: sort enum decls (#1075) 2022-04-19 16:02:32 +00:00
Joe Previte 97e07a49e9 chore(webpack): allow process.env.PORT (#1071) 2022-04-19 08:58:03 -07:00
Joe Previte 73b8a5a929 chore: use /usr/bin/env bash (#1070) 2022-04-19 08:45:13 -07:00
Kyle Carberry c8246e3e8a feat: Add Azure instance identitity authentication (#1064)
This enables zero-trust authentication for Azure instances. Now
we support the three major clouds: AWS, Azure, and GCP 😎.
2022-04-19 13:48:13 +00:00
G r e y 118a47e4e1 fix: types-generated.ts (#1063) 2022-04-18 22:03:37 -04:00
Garrett Delfosse f46b4cf3da feat: generate typescript types from codersdk structs (#1047) 2022-04-19 00:45:22 +00:00
Kyle Carberry 1df943e010 fix: Disable TURN logs (#1061)
This was accidentally merged as part of the TURN PR. In the future
we can wrap this to provide useful output, but right now it's too
verbose.
2022-04-18 18:01:49 -05:00
Kyle Carberry d202f20fdb feat: Add TURN proxying to enable offline deployments (#1000)
* Add turnconn

* Add option for passing ICE servers

* Log TURN remote address

* Add TURN server to coder start
2022-04-18 22:40:25 +00:00
Kyle Carberry e5a1c305d3 fix: Remove quotes on GitSSH (#1043) 2022-04-18 15:12:39 -05:00
Kyle Carberry 6d948ffba2 fix: Panic if Terraform fails installation (#1056) 2022-04-18 15:11:59 -05:00
Kyle Carberry 866205c145 feat: Sign MacOS binaries (#1060)
This fixes virus warnings when launching Coder on darwin.
2022-04-18 14:57:41 -05:00
dependabot[bot] a5f36ad4e2 chore: bump @storybook/addon-links from 6.4.21 to 6.4.22 in /site (#1022)
Bumps [@storybook/addon-links](https://github.com/storybookjs/storybook/tree/HEAD/addons/links) from 6.4.21 to 6.4.22.
- [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/v6.4.22/addons/links)

---
updated-dependencies:
- dependency-name: "@storybook/addon-links"
  dependency-type: direct:development
  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>
2022-04-18 19:55:50 +00:00
dependabot[bot] 50751a2d6f chore: bump @storybook/addon-essentials from 6.4.21 to 6.4.22 in /site (#1020)
Bumps [@storybook/addon-essentials](https://github.com/storybookjs/storybook/tree/HEAD/addons/essentials) from 6.4.21 to 6.4.22.
- [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/v6.4.22/addons/essentials)

---
updated-dependencies:
- dependency-name: "@storybook/addon-essentials"
  dependency-type: direct:development
  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>
2022-04-18 15:34:39 -04:00
dependabot[bot] 18819e37ee chore: bump @storybook/react from 6.4.21 to 6.4.22 in /site (#1054)
Bumps [@storybook/react](https://github.com/storybookjs/storybook/tree/HEAD/app/react) from 6.4.21 to 6.4.22.
- [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/v6.4.22/app/react)

---
updated-dependencies:
- dependency-name: "@storybook/react"
  dependency-type: direct:development
  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>
2022-04-18 14:16:14 -04:00
Kyle Carberry d98b7ec469 fix: Test flake for DataDog agent logs (#1026)
Sometimes the DataDog agent would fail to connect and
angrily log using the standard lib logger. This would
fail tests. See:

https://github.com/coder/coder/runs/6038192436?check_suite_focus=true
2022-04-18 12:37:01 -05:00
Bruno Quaresma 1df750bf1a feat: add GET /api/v2/users (#1028) 2022-04-18 17:19:47 +00:00
Cian Johnston af672803a2 autostart/autostop: move to traditional 5-valued cron string for compatibility (#1049)
This PR modfies the original 3-valued cron strings used in package schedule to be traditional 5-valued cron strings.

- schedule.Weekly will validate that the month and dom fields are equal to *
- cli autostart/autostop will attempt to detect local timezone using TZ env var, defaulting to UTC
- cli autostart/autostop no longer accepts a raw schedule -- instead use the --minute, --hour, --dow, and --tz arguments.
- Default schedules are provided that should suffice for most users.

Fixes #993
2022-04-18 11:04:48 -05:00
Bruno Quaresma 3311c2f65d refactor: replace Code by Detail in the http API error (#1011) 2022-04-18 11:02:54 -05:00
Timo 9faa39aa23 example: added docker local workspace (#1025) 2022-04-18 10:20:45 -05:00
dependabot[bot] 48a6cd9cee chore: bump @testing-library/user-event from 14.1.0 to 14.1.1 in /site (#1055)
Bumps [@testing-library/user-event](https://github.com/testing-library/user-event) from 14.1.0 to 14.1.1.
- [Release notes](https://github.com/testing-library/user-event/releases)
- [Changelog](https://github.com/testing-library/user-event/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/user-event/compare/v14.1...v14.1.1)

---
updated-dependencies:
- dependency-name: "@testing-library/user-event"
  dependency-type: direct:development
  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>
2022-04-18 10:11:32 -05:00
dependabot[bot] 37c1c6840c chore: bump @playwright/test from 1.20.2 to 1.21.0 in /site (#1053)
Bumps [@playwright/test](https://github.com/Microsoft/playwright) from 1.20.2 to 1.21.0.
- [Release notes](https://github.com/Microsoft/playwright/releases)
- [Commits](https://github.com/Microsoft/playwright/compare/v1.20.2...v1.21.0)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  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>
2022-04-18 14:04:47 +00:00
dependabot[bot] f5eb8a98ff chore: bump @storybook/addon-actions from 6.4.21 to 6.4.22 in /site (#1021)
Bumps [@storybook/addon-actions](https://github.com/storybookjs/storybook/tree/HEAD/addons/actions) from 6.4.21 to 6.4.22.
- [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/v6.4.22/addons/actions)

---
updated-dependencies:
- dependency-name: "@storybook/addon-actions"
  dependency-type: direct:development
  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>
2022-04-18 08:55:25 -05:00
dependabot[bot] 5e2b519b36 chore: bump @testing-library/react from 12.1.4 to 12.1.5 in /site (#1019)
Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 12.1.4 to 12.1.5.
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/react-testing-library/compare/v12.1.4...v12.1.5)

---
updated-dependencies:
- dependency-name: "@testing-library/react"
  dependency-type: direct:development
  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>
2022-04-18 08:54:55 -05:00
dependabot[bot] 65e0533d11 chore: bump @types/node from 14.18.12 to 14.18.13 in /site (#1023)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 14.18.12 to 14.18.13.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-18 08:53:22 -05:00
dependabot[bot] f56eb87fed chore: bump async from 2.6.3 to 2.6.4 in /site (#1027)
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-18 08:52:52 -05:00
dependabot[bot] 71acf4b8a1 chore: bump github.com/gohugoio/hugo from 0.96.0 to 0.97.2 (#1052)
Bumps [github.com/gohugoio/hugo](https://github.com/gohugoio/hugo) from 0.96.0 to 0.97.2.
- [Release notes](https://github.com/gohugoio/hugo/releases)
- [Changelog](https://github.com/gohugoio/hugo/blob/master/goreleaser.yml)
- [Commits](https://github.com/gohugoio/hugo/compare/v0.96.0...v0.97.2)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-18 08:50:32 -05:00
dependabot[bot] e5a401fce8 chore: bump github.com/pion/webrtc/v3 from 3.1.28 to 3.1.29 (#1051)
Bumps [github.com/pion/webrtc/v3](https://github.com/pion/webrtc) from 3.1.28 to 3.1.29.
- [Release notes](https://github.com/pion/webrtc/releases)
- [Commits](https://github.com/pion/webrtc/compare/v3.1.28...v3.1.29)

---
updated-dependencies:
- dependency-name: github.com/pion/webrtc/v3
  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>
2022-04-18 08:50:11 -05:00
Kyle Carberry 6dedd0caac ci: Don't run internal steps if forked (#1048)
This was causing CI to fail for contributions.
2022-04-16 13:51:01 -04:00
G r e y cf8a20d6f6 refactor: strong type for getFormHelpers name (#1029) 2022-04-15 16:31:23 -04:00
Kyle Carberry 104a3c6b9c fix: Prefix tmp directory for agent download with "coder" (#1038)
This makes it easier to find the temporary dir for the coder binary.
2022-04-15 15:30:37 -05:00
G r e y 148e7cddd3 chore: update semantic types (#1030)
Summary:

PRs like #1025 feel like they deserve a doc: type, but we didn't have one.
Furthermore our definitions for correct and fix were stale.
2022-04-15 16:26:20 -04:00
Colin Adler a13cceea3b chore: run github actions on pull_request instead of push (#1035) 2022-04-15 15:55:13 -04:00
Bruno Quaresma 88e30bec55 feat: add the preferences/account page (#999) 2022-04-15 13:17:50 -04:00
dependabot[bot] c853eb3350 chore: bump cloud.google.com/go/compute from 1.5.0 to 1.6.0 (#1018)
Bumps [cloud.google.com/go/compute](https://github.com/googleapis/google-cloud-go) from 1.5.0 to 1.6.0.
- [Release notes](https://github.com/googleapis/google-cloud-go/releases)
- [Changelog](https://github.com/googleapis/google-cloud-go/blob/main/CHANGES.md)
- [Commits](https://github.com/googleapis/google-cloud-go/compare/redis/v1.5.0...pubsub/v1.6.0)

---
updated-dependencies:
- dependency-name: cloud.google.com/go/compute
  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>
2022-04-15 10:36:47 -05:00
dependabot[bot] 721cb88225 chore: bump github.com/jedib0t/go-pretty/v6 from 6.3.0 to 6.3.1 (#1017)
Bumps [github.com/jedib0t/go-pretty/v6](https://github.com/jedib0t/go-pretty) from 6.3.0 to 6.3.1.
- [Release notes](https://github.com/jedib0t/go-pretty/releases)
- [Commits](https://github.com/jedib0t/go-pretty/compare/v6.3.0...v6.3.1)

---
updated-dependencies:
- dependency-name: github.com/jedib0t/go-pretty/v6
  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>
2022-04-15 10:03:23 -05:00
dependabot[bot] b36ed2dec7 chore: bump github.com/pion/webrtc/v3 from 3.1.27 to 3.1.28 (#1016)
Bumps [github.com/pion/webrtc/v3](https://github.com/pion/webrtc) from 3.1.27 to 3.1.28.
- [Release notes](https://github.com/pion/webrtc/releases)
- [Commits](https://github.com/pion/webrtc/compare/v3.1.27...v3.1.28)

---
updated-dependencies:
- dependency-name: github.com/pion/webrtc/v3
  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>
2022-04-15 10:01:39 -05:00
Bruno Quaresma 1c5557279a feat: add footer to the login page (#1012) 2022-04-15 09:38:18 -03:00
Colin Adler 732e0f063a chore: add dependabot config for terraform examples (#1014) 2022-04-14 16:14:49 -04:00
Presley Pizzo 76f8ff9f21 feat: Add Footer to every page with nav (#1009)
* Add Footer to AuthAndNav, rename

* Fix in preferences layout

* Remove double footer
2022-04-14 13:58:53 -04:00
Presley Pizzo 82275a81c7 feat(site): Read users into basic UsersTable (#981)
* Start users

* Set up fake response

* Update handler

* Update types

* Set up page

* Start adding table

* Add header

* Add Header

* Remove roles

* Add UsersPageView

* Add test

* Lint

* Storybook error summary

* Strip Pager to just what's currently needed

* Clean up ErrorSummary while I'm here

* Storybook tweaks

* Extract language

* Lint

* Add missing $

Co-authored-by: G r e y <grey@coder.com>

* Lint

* Lint

* Fix syntax error

* Lint

Co-authored-by: G r e y <grey@coder.com>
2022-04-14 13:57:55 -04:00
Colin Adler f803e37505 chore: use workspace name as arg in coder workspaces create (#1007) 2022-04-14 17:23:20 +00:00
Kyle Carberry 7090227d38 fix: GitSSH test flake on slow CI runs (#1001)
There was a 5s timeout on the context, which was occasionally
hit during slow runs. See:

https://github.com/coder/coder/runs/6025622326?check_suite_focus=true

I also removed the AWS authentication, because it added to the test
time for key-generation and such.
2022-04-14 11:38:54 -05:00
G r e y f3f39f3770 ci: remove building from test/js (#1005)
Summary:

There's no reason to build in `test/js`, since we have e2e tests that build.

Details:

- Remove superfluous `yarn build` from `test/js` step in CI

Relates to #1004 but does not fix it.
2022-04-14 16:32:34 +00:00
G r e y 30877bb71f ci: reduce maxWorkers in jest tests (#1006)
Summary:

When `maxWorkers` is high, there's a bug in `jest` that causes OOM kills.
Unfortunately, CI is experiencing this as well as local. For now, the best solution
is just reducing `maxWorkers`.

Resolves: #1004
2022-04-14 12:23:16 -04:00
Kyle Carberry 3304db08dd fix: Agent/SessionTTY flake waiting for terminal prompt (#1002)
For an unknown reason, the prompt wouldn't appear on Windows
randomly in CI. This shouldn't be a necessary check anyways,
because terminal input will be buffered.
2022-04-14 15:55:43 +00:00
Colin Adler fed02cdcdc chore: replace cloudflare dev tunnel with frp (#867) 2022-04-14 11:29:40 -04:00
G r e y 42e9956779 feat: workspace view for schedules (#991)
Summary:

This adds the client-side implementation to match the types introduced
in #879 and #844 as well as a card in the Workspaces page to present
workspace the data.

Details:

* Added a convenient line break in the example schedule.Weekly
* Added missing `json:""` annotations in codersdk/workspaces.go
* Installed cronstrue for displaying human-friendly cron strings
* Adjusted/Added client-side types to match codersdk/workspaces.go
* Added new component WorkspaceSchedule.tsx

Next Steps:

The WorkspaceSchedule.tsx card only presents data (on purpose). In order
to make it PUT/modify data, a few changes will be made:

- a form for updating workspace schedule will be created
- the form will wrapped in a dialog or modal
- the WorkspaceSchedule card will have a way of opening the modal which
will likely be generalized up to WorkspaceSection.tsx

Impact:

This is user-facing

This does not fully resolve either #274 or #275 (I may further decompose
that work to reflect reality and keep things in small deliverable
increments), but adds significant progress towards both.
2022-04-13 20:35:47 -04:00
Garrett Delfosse 027d89dd9b chore: Add alias coder agent (#986) 2022-04-13 20:55:55 +00:00
Bruno Quaresma 300c6d0824 feat: add global notification component (#996)
* feat: add global notification component

* fix: update yarn.lock

* fix: pin @testing-library/react-hooks

* fix: update yarn.lock

* refactor: remove displayError
2022-04-13 13:34:06 -05:00
G r e y 5ecc8236b8 chore: fix storybook warnings (#989)
* chore: fix storybook deprecation warning

Removes a deprecated CLI option and uses main.js staticDirs instead.

See: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#64-deprecations

* chore: fix storybook babel warning

Resolves: #533
2022-04-13 12:03:46 -04:00
G r e y 0536a140ed chore: fix storybook fonts (#988)
Summary:

Configures storybook with MUI themes as according to their
documentation. We were previously not aligned with their example.

See: https://storybook.js.org/addons/@react-theming/storybook-addon

Details:

- configure a providerFn for MUI with CssBaseline. We were previously
missing the CssBaseline implementation, causing the inconsistency.

Impact:

Resolves inconsistency between Storybook and production. I had tested
the Tabpanel in production vs Storybook. In storybook, the font had
fallen back to Times New Roman, whereas in production it had fallen back
to Inter. This was because of CssBaseline being configured as a child of
ThemeProvider.

Resolves: #914
2022-04-13 12:03:30 -04:00
G r e y 6edd7cb036 fix: typo in create workspaces command hint (#995)
Resolves: #994
2022-04-13 15:30:58 +00:00
Steven Masley 770c567123 feat: Add RBAC package for managing user permissions (#929)
This PR adds an RBAC package for managing using permissions:
- The top-level `authz.Authorize` function is the main user-facing entrypoint to the package.
- Actual permission evaluation is handled in `policy.rego`.
- Unit tests for `authz.Authorize` are in `authz_test.go`
- Documentation for the package is in `README.md`.

Co-authored-by: Cian Johnston <cian@coder.com>
2022-04-13 08:35:35 -05:00
dependabot[bot] 103d7eab14 chore: bump @types/react from 17.0.43 to 17.0.44 in /site (#969)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 17.0.43 to 17.0.44.
- [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-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-12 23:45:18 -04:00
dependabot[bot] fb70b3abf3 chore: bump webpack-dev-server from 4.7.4 to 4.8.1 in /site (#970)
Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 4.7.4 to 4.8.1.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v4.7.4...v4.8.1)

---
updated-dependencies:
- dependency-name: webpack-dev-server
  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>
2022-04-12 23:45:04 -04:00
dependabot[bot] ce4996668a chore: bump @types/react-dom from 17.0.14 to 17.0.15 in /site (#968)
Bumps [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) from 17.0.14 to 17.0.15.
- [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-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-12 16:37:12 -04:00
Kyle Carberry e3458277df fix: Multiple builds using the incorrect agent token (#983)
This was an issue with our in-memory database that caused
newer builds to return an outdated agent, which would then
be rejected.

A test case has been added to ensure this can't happen again!
2022-04-12 20:11:57 +00:00
Kyle Carberry e8b310166f fix: Remove resource addresses (#982)
These were added under the impression that there was significant
user-experience impact if multiple resources share the same name.

This hasn't proven to be true yet, so figured we'd take this out
until it becomes necessary.
2022-04-12 14:38:02 -05:00
Bruno Quaresma 52271ff9f8 fix: use httapi.Write instead of render (#980) 2022-04-12 16:29:07 +00:00
Garrett Delfosse d9d4599ba9 chore: idea: unify http responses further (#941) 2022-04-12 10:17:33 -05:00
Kyle Carberry 4f0f216015 ci: Add timeouts to limit hanging execution (#976)
For some reason, CI hung for ~6hours last night on `main`.

https://github.com/coder/coder/runs/5982978236?check_suite_focus=true

The fact that it went this long is bad, but it should have cancelled
much earlier.
2022-04-12 09:35:17 -05:00
Bruno Quaresma 63d1465019 feat: Add update profile endpoint (#916) 2022-04-12 14:05:21 +00:00
Kyle Carberry db9d5b7e8c fix: Rename coder to sshd on Windows for VS Code Remote support (#974)
On Windows, VS Code Remote requires a parent process of the
executing shell to be named sshd, otherwise it fails. See:
https://github.com/microsoft/vscode-remote-release/issues/5699
2022-04-11 21:14:30 -05:00
Kyle Carberry e8b1a57929 feat: Add support for VS Code and JetBrains Gateway via SSH (#956)
* Improve CLI documentation

* feat: Allow workspace resources to attach multiple agents

This enables a "kubernetes_pod" to attach multiple agents that
could be for multiple services. Each agent is required to have
a unique name, so SSH syntax is:

`coder ssh <workspace>.<agent>`

A resource can have zero agents too, they aren't required.

* Add tree view

* Improve table UI

* feat: Allow workspace resources to attach multiple agents

This enables a "kubernetes_pod" to attach multiple agents that
could be for multiple services. Each agent is required to have
a unique name, so SSH syntax is:

`coder ssh <workspace>.<agent>`

A resource can have zero agents too, they aren't required.

* Rename `tunnel` to `skip-tunnel`

This command was `true` by default, which causes
a confusing user experience.

* Add disclaimer about editing templates

* Add help to template create

* Improve workspace create flow

* Add end-to-end test for config-ssh

* Improve testing of config-ssh

* Fix workspace list

* feat: Add support for VS Code and JetBrains Gateway via SSH

This fixes various bugs that made this not work:
- Incorrect max message size in `peer`
- Incorrect reader buffer size in `peer`
- Lack of SFTP support in `agent`
- Lack of direct-tcpip support in `agent`
- Misuse of command from session. It should always use the shell
- Blocking on SSH session, only allowing one at a time

Fixes #833 too.

* Fix config-ssh command with socat
2022-04-12 00:17:18 +00:00
Kyle Carberry fb9dc4f346 feat: Improve resource preview and first-time experience (#946)
* Improve CLI documentation

* feat: Allow workspace resources to attach multiple agents

This enables a "kubernetes_pod" to attach multiple agents that
could be for multiple services. Each agent is required to have
a unique name, so SSH syntax is:

`coder ssh <workspace>.<agent>`

A resource can have zero agents too, they aren't required.

* Add tree view

* Improve table UI

* feat: Allow workspace resources to attach multiple agents

This enables a "kubernetes_pod" to attach multiple agents that
could be for multiple services. Each agent is required to have
a unique name, so SSH syntax is:

`coder ssh <workspace>.<agent>`

A resource can have zero agents too, they aren't required.

* Rename `tunnel` to `skip-tunnel`

This command was `true` by default, which causes
a confusing user experience.

* Add disclaimer about editing templates

* Add help to template create

* Improve workspace create flow

* Add end-to-end test for config-ssh

* Improve testing of config-ssh

* Fix workspace list

* Fix config ssh tests

* Update cli/configssh.go

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

* Fix requested changes

* Remove socat requirement

* Fix resources not reading in TTY

Co-authored-by: Cian Johnston <public@cianjohnston.ie>
2022-04-11 18:54:30 -05:00
Kyle Carberry 19b4323512 feat: Allow workspace resources to attach multiple agents (#942)
This enables a "kubernetes_pod" to attach multiple agents that
could be for multiple services. Each agent is required to have
a unique name, so SSH syntax is:

`coder ssh <workspace>.<agent>`

A resource can have zero agents too, they aren't required.
2022-04-11 16:06:15 -05:00
G r e y 2835bb45e5 chore: bump @testing-library/user-event from ^13.5.0 to 14.1.0 (#972) 2022-04-11 20:33:52 +00:00
dependabot[bot] c97180c18d chore: bump @xstate/react from 2.0.1 to 3.0.0 in /site (#966)
Bumps [@xstate/react](https://github.com/statelyai/xstate) from 2.0.1 to 3.0.0.
- [Release notes](https://github.com/statelyai/xstate/releases)
- [Commits](https://github.com/statelyai/xstate/compare/@xstate/react@2.0.1...@xstate/react@3.0.0)

---
updated-dependencies:
- dependency-name: "@xstate/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>
2022-04-11 16:31:50 -04:00
dependabot[bot] 3cb11fc906 chore: bump webpack from 5.71.0 to 5.72.0 in /site (#965)
Bumps [webpack](https://github.com/webpack/webpack) from 5.71.0 to 5.72.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.71.0...v5.72.0)

---
updated-dependencies:
- dependency-name: webpack
  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>
2022-04-11 16:07:57 -04:00
dependabot[bot] ffa450ddd4 chore: bump eslint from 8.12.0 to 8.13.0 in /site (#952)
* chore: bump eslint from 8.12.0 to 8.13.0 in /site

Bumps [eslint](https://github.com/eslint/eslint) from 8.12.0 to 8.13.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.12.0...v8.13.0)

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

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

* more updates

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: G r e y <grey@coder.com>
2022-04-11 16:06:41 -04:00
dependabot[bot] f7b72ddc7a chore: bump xstate from 4.30.6 to 4.31.0 in /site (#962)
Bumps [xstate](https://github.com/statelyai/xstate) from 4.30.6 to 4.31.0.
- [Release notes](https://github.com/statelyai/xstate/releases)
- [Commits](https://github.com/statelyai/xstate/compare/xstate@4.30.6...xstate@4.31.0)

---
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>
2022-04-11 15:08:30 -04:00
dependabot[bot] 0dfba86744 chore: bump @typescript-eslint/parser from 5.18.0 to 5.19.0 in /site (#960)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.18.0 to 5.19.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/v5.19.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  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>
2022-04-11 15:07:53 -04:00
dependabot[bot] 6186594332 chore: bump @storybook/addon-actions from 6.4.20 to 6.4.21 in /site (#953)
Bumps [@storybook/addon-actions](https://github.com/storybookjs/storybook/tree/HEAD/addons/actions) from 6.4.20 to 6.4.21.
- [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/v6.4.21/addons/actions)

---
updated-dependencies:
- dependency-name: "@storybook/addon-actions"
  dependency-type: direct:development
  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>
2022-04-11 14:57:16 -04:00
dependabot[bot] a7c4c059e9 chore: bump @typescript-eslint/eslint-plugin in /site (#958)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.18.0 to 5.19.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/v5.19.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  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>
2022-04-11 14:52:40 -04:00
dependabot[bot] e13c38f0e4 chore: bump @storybook/addon-essentials from 6.4.19 to 6.4.21 in /site (#950)
Bumps [@storybook/addon-essentials](https://github.com/storybookjs/storybook/tree/HEAD/addons/essentials) from 6.4.19 to 6.4.21.
- [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/v6.4.21/addons/essentials)

---
updated-dependencies:
- dependency-name: "@storybook/addon-essentials"
  dependency-type: direct:development
  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>
2022-04-11 14:40:00 -04:00
G r e y 78e727a1c4 chore: consolidate js, ts/js labels (#957)
Dependabot automagically applies the `javascript` label, but we use `typescript/js` elsewhere.
2022-04-11 18:29:35 +00:00
dependabot[bot] 8c26d934b7 chore: bump eslint-plugin-jest from 26.1.3 to 26.1.4 in /site (#951)
Bumps [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) from 26.1.3 to 26.1.4.
- [Release notes](https://github.com/jest-community/eslint-plugin-jest/releases)
- [Changelog](https://github.com/jest-community/eslint-plugin-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jest-community/eslint-plugin-jest/compare/v26.1.3...v26.1.4)

---
updated-dependencies:
- dependency-name: eslint-plugin-jest
  dependency-type: direct:development
  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>
2022-04-11 14:22:22 -04:00
dependabot[bot] b3f69a8d1d chore: bump chromatic from 6.5.3 to 6.5.4 in /site (#949)
Bumps [chromatic](https://github.com/chromaui/chromatic-cli) from 6.5.3 to 6.5.4.
- [Release notes](https://github.com/chromaui/chromatic-cli/releases)
- [Changelog](https://github.com/chromaui/chromatic-cli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chromaui/chromatic-cli/compare/v6.5.3...v6.5.4)

---
updated-dependencies:
- dependency-name: chromatic
  dependency-type: direct:development
  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>
2022-04-11 14:16:38 -04:00
dependabot[bot] 99a685b7df chore: bump github.com/lib/pq from 1.10.4 to 1.10.5 (#947)
Bumps [github.com/lib/pq](https://github.com/lib/pq) from 1.10.4 to 1.10.5.
- [Release notes](https://github.com/lib/pq/releases)
- [Commits](https://github.com/lib/pq/compare/v1.10.4...v1.10.5)

---
updated-dependencies:
- dependency-name: github.com/lib/pq
  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>
2022-04-11 11:00:37 -05:00
dependabot[bot] 9474f66d27 chore: bump actions/setup-go from 2 to 3 (#948)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2 to 3.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v2...v3)

---
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>
2022-04-11 08:54:24 -05:00
Presley Pizzo 3f21ea472f feat(site): Add Admin Dropdown menu (#885)
* Start porting components for Admin menu

* More porting, wip

* Add icons

* Extract arrow components, navHeight

* Add Admin Dropdown

* Format

* Delete types

* Fix styles

* Lint

* Add stub pages

* Use navHeight constant

* Move files

* Add and organize stories

* Storybook and organize text stories

* Add test

* Lint

* Lint

* Fix double navigation

* Lint

* Wrap new routes in AuthAndNav

* Undo unrelated storybook changes

* Refactor according to conventions
2022-04-08 18:25:53 -04:00
Presley Pizzo 4c1ef38280 chore: remove CodeCov annotations (#928)
* chore: remove CodeCov annotations

* Format
2022-04-08 15:57:28 -04:00
G r e y 0a2903c5c5 chore: assign soft ownership for xServices (#932) 2022-04-08 15:17:24 -04:00
Bruno Quaresma b317f9a83a fix: preferences routing and dropdown positioning (#930)
* fix: preferences routing

* fix: Fix popover miss correct position
2022-04-08 14:58:51 -04:00
Bruno Quaresma cbd1c3e0be fix: Remove excessive margin on the right (#931) 2022-04-08 17:45:20 +00:00
Kyle Carberry 35a0acc9c6 fix: Disable Terraform plugin cache on Darwin (#927)
It was occasionally failing without any clear indication of
what to fix on our side. The plugins weren't being found
by Terraform.

We already disable this on Windows, so figured it's fine on
Darwin too considering most production deployments will be Linux.
2022-04-08 12:30:31 -05:00
Cian Johnston 53db17803a feat: cli: add autostart and autostop commands (#922)
* feat: cli: add autostart and autostop commands

* fix: autostart/autostop: add help and usage, hide for now
2022-04-08 16:29:07 +00:00
Kyle Carberry cb5b228a21 fix: Disable raw using confirm prompt on Darwin (#926)
This caused cancel errors on prompt, but would have caused incorrect
content in parameter values if it surfaced.

Fixes #915.
2022-04-08 11:25:19 -05:00
Presley Pizzo 0bf9dee7e3 chore(site): rename userXService to authXService (#924)
* chore(site): rename userXService to authXService

* Rename contents of xService
2022-04-08 11:42:57 -04:00
Cian Johnston 94ab6f3d8e feat: add debug-level request logging (#923)
This commit adds a small middleware to coderd that logs all requests at DEBUG level.
2022-04-08 14:35:29 +00:00
Garrett Delfosse 38f074254b feat: wrap ssh with coder key (#894) 2022-04-07 22:40:27 +00:00
Ben Potter b7d7e19606 fix: rename projects to templates in README (#920) 2022-04-07 17:58:17 -04:00
Ben Potter c6a0078c35 fix: assign labels for issue templates (#919) 2022-04-07 21:04:56 +00:00
Bruno Quaresma 75ef1f4b26 feat: Add preferences pages (#893) 2022-04-07 16:44:08 -03:00
Joe Previte 17848b3b86 chore: add feature request issue template (#913)
* chore: add feature request issue template

* Update .github/ISSUE_TEMPLATE/feature.md

Co-authored-by: G r e y <grey@coder.com>

* Update .github/ISSUE_TEMPLATE/feature.md

Co-authored-by: G r e y <grey@coder.com>

Co-authored-by: G r e y <grey@coder.com>
2022-04-07 10:35:32 -07:00
Asher 18595791c0 feat: add version to footer (#882)
* Add endpoint for getting build info

* Add build info XService

* Add version with link to page footer

Partially addresses #376.

* Lift buildinfo package
2022-04-07 12:18:58 -05:00
dependabot[bot] 2e5859f226 chore: bump codecov/codecov-action from 2 to 3 (#898)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 2 to 3.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  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>
2022-04-07 11:41:35 -05:00
dependabot[bot] 313b51d3fb chore: bump @xstate/cli from 0.1.5 to 0.1.6 in /site (#900)
Bumps @xstate/cli from 0.1.5 to 0.1.6.

---
updated-dependencies:
- dependency-name: "@xstate/cli"
  dependency-type: direct:development
  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>
2022-04-07 12:08:17 -04:00
Bruno Quaresma 90388a38f3 feat: Add user menu (#887) 2022-04-07 13:00:40 -03:00
dependabot[bot] 2ca725386f chore: bump @typescript-eslint/parser from 5.17.0 to 5.18.0 in /site (#899)
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 5.17.0 to 5.18.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/v5.18.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  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>
2022-04-07 11:34:59 -04:00
Joe Previte ce7af872ff docs: add PR template (#873) 2022-04-07 15:27:25 +00:00
Joe Previte 14dec177a4 docs: add SECURITY policy (#891) 2022-04-07 15:26:10 +00:00
Kyle Carberry 770d212094 ci: Enable forks to run CI (#910)
* ci: Enable forks to run CI

All steps that require tokens are optional for forks,
and will be skipped if the owner is not "coder".

* Empty commit to force CI
2022-04-07 08:33:10 -05:00
Cian Johnston 23f989127d coderd: autostart: codersdk, http api, database plumbing (#879)
* feat: add columns autostart_schedule, autostop_schedule to database schema
* feat: database: add UpdateWorkspaceAutostart and UpdateWorkspaceAutostop methods
* feat: add AutostartSchedule/AutostopSchedule to api workspace struct
* feat: codersdk: implement update workspace autostart and autostop methods
* chore: add unit tests for workspace autostarat and autostop methods
2022-04-07 10:03:35 +01:00
dependabot[bot] c1ff537beb chore: bump @typescript-eslint/eslint-plugin in /site (#902)
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 5.17.0 to 5.18.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/v5.18.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  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>
2022-04-06 19:08:34 -04:00
G r e y 579fd4bc89 chore: speed up chromatic ci (#904)
This is an oversight from #896 . It turns out that because we use the GitHub integration with Chromatic, we don't need to wait for the results to be reported in the action - they get reported in the other checks created by Chromatic.

This option was spit-out in a check:

https://github.com/coder/coder/runs/5859427236?check_suite_focus=true#step:4:38

Relates to #444
2022-04-06 22:40:30 +00:00
dependabot[bot] 6d40f34057 chore: bump eslint-plugin-import from 2.25.4 to 2.26.0 in /site (#901)
Bumps [eslint-plugin-import](https://github.com/import-js/eslint-plugin-import) from 2.25.4 to 2.26.0.
- [Release notes](https://github.com/import-js/eslint-plugin-import/releases)
- [Changelog](https://github.com/import-js/eslint-plugin-import/blob/main/CHANGELOG.md)
- [Commits](https://github.com/import-js/eslint-plugin-import/compare/v2.25.4...v2.26.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-import
  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>
2022-04-06 18:22:15 -04:00
dependabot[bot] 1224a34abd chore: bump jest-junit from 13.0.0 to 13.1.0 in /site (#903)
Bumps [jest-junit](https://github.com/jest-community/jest-junit) from 13.0.0 to 13.1.0.
- [Release notes](https://github.com/jest-community/jest-junit/releases)
- [Commits](https://github.com/jest-community/jest-junit/compare/v13.0.0...v13.1.0)

---
updated-dependencies:
- dependency-name: jest-junit
  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>
2022-04-06 18:20:51 -04:00
G r e y 5782879f2f chore: configure chromatic snapshot tests (#896)
Resolves: #444

Summary:

This commit installs and configures a GH action for chromatic. Chromatic
is used for snapshot testing build-over-build.

Details:

* chore: install chromatic

* chore: add chromatic package.json script

Suggested by the docs for convenience so that we can run chromatic like:

```console
yarn run chromatic ...
```

* chore: gitignore storybook builds

* ci: configure chromatic

This action configures chromatic to run in CI on pushes to all branches.
By running this in CI, we get the following:

- snapshot (build-over-build)
- checks in our CI

The snapshots and build-over-build behavior are per branch; this way we
can work on a feature branch without worrying about changes being made
to mainline independently.

* chore: remove manual storybook build from CI

This is now the responsibility of Chromatic
2022-04-06 17:51:49 -04:00
Joe Previte eefca43064 chore: rename .yaml to .yml (#895)
* chore: rename .yml to .yaml

* chore: remove .yml from prettierrc
2022-04-06 21:32:59 +00:00
759 changed files with 53405 additions and 13954 deletions
+16
View File
@@ -0,0 +1,16 @@
root = true
[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = tab
[*.{md,json,yaml,tf,tfvars}]
indent_style = space
indent_size = 2
[coderd/database/dump.sql]
indent_style = space
indent_size = 4
+1 -1
View File
@@ -1 +1 @@
site @coder/frontend
site/ @coder/frontend
+1 -1
View File
@@ -2,7 +2,7 @@
name: Bug report
about: Report a bug
title: "Bug: "
labels: "bug 🐛"
labels: ["bug :bug:", "needs grooming :razor:"]
---
## OS Information
+1 -1
View File
@@ -2,7 +2,7 @@
name: Documentation improvement
about: Suggest a documentation improvement
title: "Docs: "
labels: "documentation 📝"
labels: ["documentation :memo:", "needs grooming :razor:"]
---
## What is your suggestion?
+14
View File
@@ -0,0 +1,14 @@
---
name: Feature request
about: Suggest an idea to improve coder
title: "Feat: "
labels: ["new feature :sparkles:", "needs grooming :razor:"]
---
## What is your suggestion?
## Why do you want this feature?
## Are there any workarounds to get this functionality today?
## Are you interested in submitting a PR for this?
+6 -12
View File
@@ -1,18 +1,12 @@
codecov:
require_ci_to_pass: false
comment:
show_carryforward_flags: yes
comment: false
github_checks:
annotations: false
coverage:
notify:
slack:
default:
url: secret:v1::ALa1/e2X+k36fPseab5D7+kBFc9bJyIoIQioD0IMA5jr+0HXVpBRNDCHZhHjCdGc67yff6PPixPEOLwEZpxC37rM23RBZOYlqAq9A5e0MeZVlEoVq19aOYN4Xel17hMJ6GGm7n17wrYpCpcvlVSqNrN0+cr3guVDyG10kQyfh2Y=
threshold: 1%
only_pulls: false
branches:
- "main"
status:
patch:
default:
@@ -25,7 +19,7 @@ coverage:
ignore:
# This is generated code.
- coderd/database/models.go
- coderd/database/query.sql.go
- coderd/database/queries.sql.go
- coderd/database/databasefake
# These are generated or don't require tests.
- cmd
@@ -35,6 +29,6 @@ ignore:
- peerbroker/proto
- provisionerd/proto
- provisionersdk/proto
- scripts/datadog-cireport
- scripts
- site/.storybook
- rules.go
@@ -32,6 +32,9 @@ updates:
timezone: "America/Chicago"
commit-message:
prefix: "chore"
labels:
- "dependencies"
- "go"
- package-ecosystem: "npm"
directory: "/site/"
@@ -41,9 +44,27 @@ updates:
timezone: "America/Chicago"
commit-message:
prefix: "chore"
labels:
- "dependencies"
- "typescript/js"
ignore:
# 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
- package-ecosystem: "terraform"
directory: "/examples/templates"
schedule:
interval: "weekly"
time: "06:00"
timezone: "America/Chicago"
commit-message:
prefix: "chore"
labels:
- "dependencies"
- "terraform"
ignore:
# We likely want to update this ourselves.
- dependency-name: "coder/coder"
+13
View File
@@ -0,0 +1,13 @@
<!-- Help reviewers by listing the subtasks in this PR
Here's an example:
This PR adds a new feature to the CLI.
## Subtasks
- [x] added a test for feature
Fixes #345
-->
+8 -12
View File
@@ -25,26 +25,22 @@ types:
# A build of any kind.
- build
# A RELEASED fix that will NOT be back-ported. The originating issue may have
# been discovered internally or externally to Coder.
- fix
# Any code task that is ignored for changelog purposes. Examples include
# devbin scripts and internal-only configurations.
# Any code task that operates outside of CI, docs, or the product. Examples
# include configurations, linters etc.
- chore
# Any work performed on CI.
- ci
# An UNRELEASED correction. For example, features are often built
# incrementally and sometimes introduce minor flaws during a release cycle.
# Corrections address those increments and flaws.
- correct
- example
# Work that directly implements or supports the implementation of a feature.
- feat
# A fix for a RELEASED bug (regression fix) that is intended for patch-release
# A fix for either a released or unrelesed bug.
- fix
# A fix for a released bug (regression fix) that is intended for patch-release
# purposes.
- hotfix
+2 -4
View File
@@ -1,9 +1,7 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 14
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 5
# Only apply the stale logic to pulls, since we are using issues to manage work
only: pulls
daysUntilClose: 7
# Label to apply when stale.
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
+68
View File
@@ -0,0 +1,68 @@
# Note: Chromatic is a separate workflow for coder.yaml as suggested by the
# chromatic docs. Explicitly, Chromatic works best on 'push' instead of other
# event types (like pull request), keep in mind that it works build-over-build
# by storing snapshots.
#
# SEE: https://www.chromatic.com/docs/ci
name: chromatic
# REMARK: We want Chromatic to run whenever anything in the FE or its deps
# change, including node_modules and generated code. Currently, all
# node_modules and generated code live in site. If any of these are
# hoisted, we'll want to adjust the paths filter to account for them.
on:
push:
paths:
- site/**
branches:
- main
tags:
- "*"
pull_request:
paths:
- site/**
jobs:
deploy:
# REMARK: this is only used to build storybook and deploy it to Chromatic.
runs-on: ubuntu-latest
steps:
- 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
- name: Install dependencies
run: cd site && yarn
# This step is not meant for mainline because any detected changes to
# storybook snapshots will require manual approval/review in order for
# the check to pass. This is desired in PRs, but not in mainline.
- name: Publish to Chromatic (non-mainline)
if: github.ref != 'refs/heads/main' && github.repository_owner == 'coder'
uses: chromaui/action@v1
with:
buildScriptName: "storybook:build"
exitOnceUploaded: true
# Chromatic states its fine to make this token public. See:
# https://www.chromatic.com/docs/github-actions#forked-repositories
projectToken: 695c25b6cb65
workingDir: "./site"
# 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
# avoid the same changeset from requiring review once squashed into
# main. Chromatic is supposed to be able to detect that we use squash
# commits, but it's good to be defensive in case, otherwise CI remains
# infinitely "in progress" in mainline unless we re-review each build.
- name: Publish to Chromatic (mainline)
if: github.ref == 'refs/heads/main' && github.repository_owner == 'coder'
uses: chromaui/action@v1
with:
autoAcceptChanges: true
buildScriptName: "storybook:build"
projectToken: 695c25b6cb65
workingDir: "./site"
@@ -1,98 +0,0 @@
# This workflow (aka The Gauntlet) is a high-iteration run of our tests,
# used to evaluate stability and shake out intermittent failures.
name: coder-test-stability
on:
schedule:
# Run everyday around midnight Central.
- cron: "0 6 * * *"
pull_request:
branches:
- main
paths:
- .github/workflows/coder-test-stability.yaml
workflow_dispatch:
inputs:
iterationCount:
description: "Iteration Count"
required: false
default: "10"
# Cancel in-progress runs for pull requests when developers push
# additional changes, and serialize builds in branches.
# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-concurrency-to-cancel-any-in-progress-job-or-run
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
coder-test-stability:
name: "test/go/stability/${{ matrix.os }}/${{ matrix.instance }}"
runs-on: ${{ matrix.os }}
strategy:
matrix:
os:
- ubuntu-latest
- macos-latest
- windows-2022
instance:
- 1
- 2
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v2
with:
go-version: "~1.18"
- uses: actions/cache@v3
with:
# Go mod cache, Linux build cache, Mac build cache, Windows build cache
path: |
~/go/pkg/mod
~/.cache/go-build
~/Library/Caches/go-build
%LocalAppData%\go-build
key: ${{ matrix.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ matrix.os }}-go-
- run: go install gotest.tools/gotestsum@latest
- uses: hashicorp/setup-terraform@v1
with:
terraform_version: 1.1.2
terraform_wrapper: false
- name: Test with Mock Database
shell: bash
env:
GOCOUNT: ${{ github.event.inputs.iterationCount || 10 }}
GOMAXPROCS: ${{ runner.os == 'Windows' && 1 || 2 }}
run: gotestsum --junitfile="gotests.xml" --packages="./..." --
-covermode=atomic -coverprofile="gotests.coverage"
-timeout=15m -count=$GOCOUNT -race -short -failfast
- name: Upload DataDog Trace
if: (success() || failure()) && github.actor != 'dependabot[bot]'
env:
DATADOG_API_KEY: ${{ secrets.DATADOG_API_KEY }}
DD_DATABASE: fake
GIT_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: go run scripts/datadog-cireport/main.go gotests.xml
- name: Test with PostgreSQL Database
if: runner.os == 'Linux'
env:
GOCOUNT: ${{ github.event.inputs.iterationCount || 10 }}
run: DB=true gotestsum --junitfile="gotests.xml" --packages="./..." --
-covermode=atomic -coverprofile="gotests.coverage" -timeout=30m
-count=$GOCOUNT -race -parallel=2 -failfast
- name: Upload DataDog Trace
if: (success() || failure()) && github.actor != 'dependabot[bot]' && runner.os == 'Linux'
env:
DATADOG_API_KEY: ${{ secrets.DATADOG_API_KEY }}
DD_DATABASE: postgresql
GIT_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: go run scripts/datadog-cireport/main.go gotests.xml
+98 -59
View File
@@ -4,13 +4,10 @@ on:
push:
branches:
- main
- "release/*"
tags:
- "*"
pull_request:
branches:
- "*"
workflow_dispatch:
@@ -35,19 +32,34 @@ concurrency:
jobs:
style-lint-golangci:
name: style/lint/golangci
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v2
- uses: actions/setup-go@v3
with:
go-version: "~1.18"
- name: golangci-lint
uses: golangci/golangci-lint-action@v3.1.0
uses: golangci/golangci-lint-action@v3.2.0
with:
version: v1.45.2
version: v1.46.0
style-lint-shellcheck:
name: style/lint/shellcheck
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@1.1.0
env:
SHELLCHECK_OPTS: --external-sources
with:
ignore: node_modules
style-lint-typescript:
name: "style/lint/typescript"
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -73,29 +85,46 @@ jobs:
gen:
name: "style/gen"
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- 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: Install node_modules
run: ./scripts/yarn_install.sh
- name: Install Protoc
uses: arduino/setup-protoc@v1
with:
version: "3.19.4"
- uses: actions/setup-go@v2
version: "3.20.0"
- uses: actions/setup-go@v3
with:
go-version: "~1.18"
- run: curl -sSL
https://github.com/kyleconroy/sqlc/releases/download/v1.11.0/sqlc_1.11.0_linux_amd64.tar.gz
https://github.com/kyleconroy/sqlc/releases/download/v1.13.0/sqlc_1.13.0_linux_amd64.tar.gz
| sudo tar -C /usr/bin -xz sqlc
- run: go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
- run: go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.26
- run: go install golang.org/x/tools/cmd/goimports@latest
- run: "make --output-sync -j gen"
- run: "make --output-sync -j -B gen"
- run: ./scripts/check_unstaged.sh
style-fmt:
name: "style/fmt"
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -117,12 +146,17 @@ jobs:
- name: Install node_modules
run: ./scripts/yarn_install.sh
- name: "make fmt"
run: "make --output-sync -j fmt"
- name: Install shfmt
run: go install mvdan.cc/sh/v3/cmd/shfmt@v3.5.0
- run: |
export PATH=${PATH}:$(go env GOPATH)/bin
make --output-sync -j -B fmt
test-go:
name: "test/go"
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
matrix:
os:
@@ -132,7 +166,7 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v2
- uses: actions/setup-go@v3
with:
go-version: "~1.18"
@@ -155,14 +189,14 @@ jobs:
key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}
- name: Install goreleaser
uses: jaxxstorm/action-install-gh-release@v1.4.0
uses: jaxxstorm/action-install-gh-release@v1.7.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
repo: gotestyourself/gotestsum
tag: v1.7.0
- uses: hashicorp/setup-terraform@v1
- uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.1.2
terraform_wrapper: false
@@ -178,7 +212,7 @@ jobs:
-timeout=3m -count=$GOCOUNT -short -failfast
- name: Upload DataDog Trace
if: (success() || failure()) && github.actor != 'dependabot[bot]'
if: always() && github.actor != 'dependabot[bot]' && !github.event.pull_request.head.repo.fork
env:
DATADOG_API_KEY: ${{ secrets.DATADOG_API_KEY }}
DD_DATABASE: fake
@@ -186,21 +220,23 @@ jobs:
GIT_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: go run scripts/datadog-cireport/main.go gotests.xml
- uses: codecov/codecov-action@v2
if: github.actor != 'dependabot[bot]'
- uses: codecov/codecov-action@v3
if: github.actor != 'dependabot[bot]' && !github.event.pull_request.head.repo.fork
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./gotests.coverage
flags: unittest-go-${{ matrix.os }}
fail_ci_if_error: true
# this flakes and sometimes fails the build
fail_ci_if_error: false
test-go-postgres:
name: "test/go/postgres"
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v2
- uses: actions/setup-go@v3
with:
go-version: "~1.18"
@@ -223,14 +259,14 @@ jobs:
key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}
- name: Install goreleaser
uses: jaxxstorm/action-install-gh-release@v1.4.0
uses: jaxxstorm/action-install-gh-release@v1.7.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
repo: gotestyourself/gotestsum
tag: v1.7.0
- uses: hashicorp/setup-terraform@v1
- uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.1.2
terraform_wrapper: false
@@ -258,31 +294,30 @@ jobs:
done
- name: Test with PostgreSQL Database
run: DB=ci gotestsum --junitfile="gotests.xml" --packages="./..." --
-covermode=atomic -coverprofile="gotests.coverage" -timeout=3m
-coverpkg=./...,github.com/coder/coder/codersdk
-count=1 -parallel=2 -race -failfast
run: "make test-postgres"
- name: Upload DataDog Trace
if: (success() || failure()) && github.actor != 'dependabot[bot]'
if: always() && github.actor != 'dependabot[bot]' && !github.event.pull_request.head.repo.fork
env:
DATADOG_API_KEY: ${{ secrets.DATADOG_API_KEY }}
DD_DATABASE: postgresql
GIT_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
run: go run scripts/datadog-cireport/main.go gotests.xml
- uses: codecov/codecov-action@v2
if: github.actor != 'dependabot[bot]'
- uses: codecov/codecov-action@v3
if: github.actor != 'dependabot[bot]' && !github.event.pull_request.head.repo.fork
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./gotests.coverage
flags: unittest-go-${{ matrix.os }}
fail_ci_if_error: true
flags: unittest-go-postgres-${{ matrix.os }}
# this flakes and sometimes fails the build
fail_ci_if_error: false
deploy:
name: "deploy"
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
timeout-minutes: 20
if: github.ref == 'refs/heads/main' && !github.event.pull_request.head.repo.fork
permissions:
contents: read
id-token: write
@@ -298,7 +333,7 @@ jobs:
- name: Set up Google Cloud SDK
uses: google-github-actions/setup-gcloud@v0
- uses: actions/setup-go@v2
- uses: actions/setup-go@v3
with:
go-version: "~1.18"
@@ -320,7 +355,7 @@ jobs:
path: ${{ steps.go-cache-paths.outputs.go-mod }}
key: ${{ runner.os }}-release-go-mod-${{ hashFiles('**/go.sum') }}
- uses: goreleaser/goreleaser-action@v2
- uses: goreleaser/goreleaser-action@v3
with:
install-only: true
@@ -335,13 +370,26 @@ jobs:
restore-keys: |
js-${{ runner.os }}-
- name: Build site
run: make -B site/out/index.html
- name: Build Release
run: make release
uses: goreleaser/goreleaser-action@v3
with:
version: latest
args: release --snapshot --rm-dist --skip-sign
- uses: actions/upload-artifact@v3
with:
name: coder_linux_amd64.deb
path: ./dist/coder_*_linux_amd64.deb
name: coder_windows_amd64.zip
path: ./dist/coder_*_windows_amd64.zip
retention-days: 7
- uses: actions/upload-artifact@v3
with:
name: coder_linux_amd64.tar.gz
path: ./dist/coder_*_linux_amd64.tar.gz
retention-days: 7
- name: Install Release
run: |
@@ -357,6 +405,7 @@ jobs:
test-js:
name: "test/js"
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v3
@@ -372,7 +421,7 @@ jobs:
js-${{ runner.os }}-
# Go is required for uploading the test results to datadog
- uses: actions/setup-go@v2
- uses: actions/setup-go@v3
with:
go-version: "~1.18"
@@ -383,27 +432,20 @@ jobs:
- name: Install node_modules
run: ./scripts/yarn_install.sh
- name: Build frontend
run: yarn build
working-directory: site
- name: Build Storybook
run: yarn storybook:build
working-directory: site
- run: yarn test:coverage
working-directory: site
- uses: codecov/codecov-action@v2
if: github.actor != 'dependabot[bot]'
- uses: codecov/codecov-action@v3
if: github.actor != 'dependabot[bot]' && !github.event.pull_request.head.repo.fork
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./site/coverage/lcov.info
flags: unittest-js
fail_ci_if_error: true
# this flakes and sometimes fails the build
fail_ci_if_error: false
- name: Upload DataDog Trace
if: (success() || failure()) && github.actor != 'dependabot[bot]'
if: always() && github.actor != 'dependabot[bot]' && !github.event.pull_request.head.repo.fork
env:
DATADOG_API_KEY: ${{ secrets.DATADOG_API_KEY }}
DD_CATEGORY: unit
@@ -413,14 +455,11 @@ jobs:
test-e2e:
name: "test/e2e/${{ matrix.os }}"
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
matrix:
os:
- ubuntu-latest
- macos-latest
# TODO: Get `make build` running on Windows 2022
# https://github.com/coder/coder/issues/384
# - windows-2022
steps:
- uses: actions/checkout@v3
@@ -436,11 +475,11 @@ jobs:
js-${{ runner.os }}-
# Go is required for uploading the test results to datadog
- uses: actions/setup-go@v2
- uses: actions/setup-go@v3
with:
go-version: "~1.18"
- uses: hashicorp/setup-terraform@v1
- uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.1.2
terraform_wrapper: false
@@ -449,7 +488,7 @@ jobs:
with:
node-version: "14"
- uses: goreleaser/goreleaser-action@v2
- uses: goreleaser/goreleaser-action@v3
with:
install-only: true
@@ -473,7 +512,7 @@ jobs:
- name: Build
run: |
make site/out
make -B site/out/index.html
- run: yarn playwright:install
working-directory: site
@@ -487,7 +526,7 @@ jobs:
working-directory: site
- name: Upload DataDog Trace
if: (success() || failure()) && github.actor != 'dependabot[bot]' && runner.os == 'Linux'
if: always() && github.actor != 'dependabot[bot]' && runner.os == 'Linux' && !github.event.pull_request.head.repo.fork
env:
DATADOG_API_KEY: ${{ secrets.DATADOG_API_KEY }}
DD_CATEGORY: e2e
+40 -5
View File
@@ -3,17 +3,47 @@ on:
push:
tags:
- "v*"
workflow_dispatch:
jobs:
goreleaser:
runs-on: ubuntu-latest
runs-on: macos-latest
env:
# Necessary for Docker manifest
DOCKER_CLI_EXPERIMENTAL: "enabled"
steps:
# Docker is not included on macos-latest
- uses: docker-practice/actions-setup-docker@1.0.10
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-go@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Docker Login
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-go@v3
with:
go-version: "~1.18"
- name: Install Gon
run: |
brew tap mitchellh/gon
brew install mitchellh/gon/gon
- name: Import Signing Certificates
uses: Apple-Actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ secrets.AC_CERTIFICATE_P12_BASE64 }}
p12-password: ${{ secrets.AC_CERTIFICATE_PASSWORD }}
- name: Echo Go Cache Paths
id: go-cache-paths
run: |
@@ -43,13 +73,18 @@ jobs:
restore-keys: |
js-${{ runner.os }}-
- name: Install make
run: brew install make
- name: Build Site
run: make site/out
run: make site/out/index.html
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2.9.1
uses: goreleaser/goreleaser-action@v3
with:
version: latest
args: release --rm-dist
args: release --rm-dist --timeout 60m
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AC_USERNAME: ${{ secrets.AC_USERNAME }}
AC_PASSWORD: ${{ secrets.AC_PASSWORD }}
+5
View File
@@ -14,6 +14,7 @@ vendor
.eslintcache
yarn-error.log
.idea
.DS_Store
# Front-end ignore
.next/
@@ -25,6 +26,7 @@ site/test-results/
site/yarn-error.log
coverage/
site/**/*.typegen.ts
site/build-storybook.log
# Build
dist/
@@ -34,3 +36,6 @@ site/out/
*.tfplan
*.lock.hcl
.terraform/
.vscode/*.log
**/*.swp
+12 -3
View File
@@ -77,7 +77,7 @@ linters-settings:
# - sloppyReassign
- sloppyTypeAssert
- sortSlice
# - sprintfQuotedString
- sprintfQuotedString
- sqlQuery
# - stringConcatSimplify
# - stringXbytes
@@ -103,7 +103,14 @@ linters-settings:
settings:
ruleguard:
failOn: all
rules: rules.go
rules: '${configDir}/scripts/rules.go'
staticcheck:
# https://staticcheck.io/docs/options#checks
# We disable SA1019 because it gets angry about our usage of xerrors. We
# intentionally xerrors because stack frame support didn't make it into the
# stdlib port.
checks: ["all", "-SA1019"]
goimports:
local-prefixes: coder.com,cdr.dev,go.coder.com,github.com/cdr,github.com/coder
@@ -194,6 +201,8 @@ run:
concurrency: 4
skip-dirs:
- node_modules
skip-files:
- scripts/rules.go
timeout: 5m
# Over time, add more and more linters from
@@ -235,7 +244,7 @@ linters:
# without testing any exported functions. This is enabled to promote
# decomposing a package before testing it's internals. A function caller
# should be able to test most of the functionality from exported functions.
#
#
# There are edge-cases to this rule, but they should be carefully considered
# to avoid structural inconsistency.
- testpackage
+159
View File
@@ -0,0 +1,159 @@
archives:
- id: coder-linux
builds: [coder-linux]
format: tar.gz
- id: coder-darwin
builds: [coder-darwin]
format: zip
- id: coder-windows
builds: [coder-windows]
format: zip
before:
hooks:
- go mod tidy
- rm -f site/out/bin/coder*
builds:
- id: coder-slim
dir: cmd/coder
ldflags: ["-s -w -X github.com/coder/coder/buildinfo.tag={{ .Version }}"]
env: [CGO_ENABLED=0]
goos: [darwin, linux, windows]
goarch: [amd64, arm, arm64]
goarm: ["7"]
# Only build arm 7 for Linux
ignore:
- goos: windows
goarm: "7"
- goos: darwin
goarm: "7"
hooks:
# The "trimprefix" appends ".exe" on Windows.
post: |
cp {{.Path}} site/out/bin/coder-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ trimprefix .Name "coder" }}
- id: coder-linux
dir: cmd/coder
flags: [-tags=embed]
ldflags: ["-s -w -X github.com/coder/coder/buildinfo.tag={{ .Version }}"]
env: [CGO_ENABLED=0]
goos: [linux]
goarch: [amd64, arm, arm64]
goarm: ["7"]
- id: coder-windows
dir: cmd/coder
flags: [-tags=embed]
ldflags: ["-s -w -X github.com/coder/coder/buildinfo.tag={{ .Version }}"]
env: [CGO_ENABLED=0]
goos: [windows]
goarch: [amd64, arm64]
- id: coder-darwin
dir: cmd/coder
flags: [-tags=embed]
ldflags: ["-s -w -X github.com/coder/coder/buildinfo.tag={{ .Version }}"]
env: [CGO_ENABLED=0]
goos: [darwin]
goarch: [amd64, arm64]
hooks:
# This signs the binary that will be located inside the zip.
# MacOS requires the binary to be signed for notarization.
#
# If it doesn't successfully sign, the zip sign step will error.
post: |
sh -c 'codesign -s {{.Env.AC_APPLICATION_IDENTITY}} -f -v --timestamp --options runtime {{.Path}} || true'
env:
# Apple identity for signing!
- AC_APPLICATION_IDENTITY=BDB050EB749EDD6A80C6F119BF1382ECA119CCCC
nfpms:
- id: packages
vendor: Coder
homepage: https://coder.com
maintainer: Coder <support@coder.com>
description: |
Provision development environments with infrastructure with code
formats:
- apk
- deb
- rpm
suggests:
- postgresql
builds:
- coder-linux
bindir: /usr/bin
contents:
- src: coder.env
dst: /etc/coder.d/coder.env
type: "config|noreplace"
- src: coder.service
dst: /usr/lib/systemd/system/coder.service
dockers:
- image_templates: ["ghcr.io/coder/coder:{{ .Tag }}-amd64"]
id: coder-linux
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- --platform=linux/amd64
- --label=org.opencontainers.image.title=Coder
- --label=org.opencontainers.image.description=A tool for provisioning self-hosted development environments with Terraform.
- --label=org.opencontainers.image.url=https://github.com/coder/coder
- --label=org.opencontainers.image.source=https://github.com/coder/coder
- --label=org.opencontainers.image.version={{ .Version }}
- --label=org.opencontainers.image.revision={{ .FullCommit }}
- --label=org.opencontainers.image.licenses=AGPL-3.0
- image_templates: ["ghcr.io/coder/coder:{{ .Tag }}-arm64"]
goarch: arm64
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- --platform=linux/arm64/v8
- --label=org.opencontainers.image.title=coder
- --label=org.opencontainers.image.description=A tool for provisioning self-hosted development environments with Terraform.
- --label=org.opencontainers.image.url=https://github.com/coder/coder
- --label=org.opencontainers.image.source=https://github.com/coder/coder
- --label=org.opencontainers.image.version={{ .Tag }}
- --label=org.opencontainers.image.revision={{ .FullCommit }}
- --label=org.opencontainers.image.licenses=AGPL-3.0
- image_templates: ["ghcr.io/coder/coder:{{ .Tag }}-armv7"]
goarch: arm
goarm: "7"
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- --platform=linux/arm/v7
- --label=org.opencontainers.image.title=Coder
- --label=org.opencontainers.image.description=A tool for provisioning self-hosted development environments with Terraform.
- --label=org.opencontainers.image.url=https://github.com/coder/coder
- --label=org.opencontainers.image.source=https://github.com/coder/coder
- --label=org.opencontainers.image.version={{ .Tag }}
- --label=org.opencontainers.image.revision={{ .FullCommit }}
- --label=org.opencontainers.image.licenses=AGPL-3.0
docker_manifests:
- name_template: ghcr.io/coder/coder:{{ .Tag }}
image_templates:
- ghcr.io/coder/coder:{{ .Tag }}-amd64
- ghcr.io/coder/coder:{{ .Tag }}-arm64
- ghcr.io/coder/coder:{{ .Tag }}-armv7
release:
ids: [coder-linux, coder-darwin, coder-windows, packages]
footer: |
## Container Image
- `docker pull ghcr.io/coder/coder:{{ .Tag }}`
signs:
- ids: [coder-darwin]
artifacts: archive
cmd: ./scripts/sign_macos.sh
args: ["${artifact}"]
output: true
snapshot:
name_template: "{{ .Version }}-devel+{{ .ShortCommit }}"
-64
View File
@@ -1,64 +0,0 @@
archives:
- id: coder
builds:
- coder
files:
- src: docs/README.md
dst: README.md
format_overrides:
- goos: windows
format: zip
before:
hooks:
- go mod tidy
- rm -f site/out/bin/coder*
builds:
- id: coder-slim
dir: cmd/coder
ldflags: ["-s -w -X github.com/coder/coder/cli/buildinfo.tag={{ .Version }}"]
env: [CGO_ENABLED=0]
goos: [darwin, linux, windows]
goarch: [amd64]
hooks:
# The "trimprefix" appends ".exe" on Windows.
post: |
cp {{.Path}} site/out/bin/coder-{{ .Os }}-{{ .Arch }}{{ trimprefix .Name "coder" }}
- id: coder
dir: cmd/coder
flags: [-tags=embed]
ldflags: ["-s -w -X github.com/coder/coder/cli/buildinfo.tag={{ .Version }}"]
env: [CGO_ENABLED=0]
goos: [darwin, linux, windows]
goarch: [amd64, arm64]
nfpms:
- id: packages
vendor: Coder
homepage: https://coder.com
maintainer: Coder <support@coder.com>
description: |
Provision development environments with infrastructure with code
formats:
- apk
- deb
- rpm
suggests:
- postgresql
builds:
- coder
bindir: /usr/bin
contents:
- src: coder.env
dst: /etc/coder.d/coder.env
type: "config|noreplace"
- src: coder.service
dst: /usr/lib/systemd/system/coder.service
release:
ids: [coder, packages]
snapshot:
name_template: '{{ .Version }}-devel+{{ .ShortCommit }}'
+2 -1
View File
@@ -7,6 +7,7 @@
"emeraldwalk.runonsave",
"zxh404.vscode-proto3",
"redhat.vscode-yaml",
"streetsidesoftware.code-spell-checker"
"streetsidesoftware.code-spell-checker",
"dbaeumer.vscode-eslint"
]
}
+24 -2
View File
@@ -1,10 +1,13 @@
{
"cSpell.words": [
"buildname",
"circbuf",
"cliflag",
"cliui",
"coderd",
"coderdtest",
"codersdk",
"cronstrue",
"devel",
"drpc",
"drpcconn",
@@ -12,12 +15,15 @@
"drpcserver",
"Dsts",
"fatih",
"Formik",
"goarch",
"gographviz",
"goleak",
"gossh",
"gsyslog",
"hashicorp",
"hclsyntax",
"httpapi",
"httpmw",
"idtoken",
"Iflag",
@@ -35,6 +41,7 @@
"nolint",
"nosec",
"ntqry",
"OIDC",
"oneof",
"parameterscopeid",
"pqtype",
@@ -45,25 +52,40 @@
"ptty",
"ptytest",
"retrier",
"rpty",
"sdkproto",
"Signup",
"sourcemapped",
"stretchr",
"TCGETS",
"tcpip",
"TCSETS",
"templateversions",
"testid",
"tfexec",
"tfjson",
"tfstate",
"trimprefix",
"typegen",
"unconvert",
"Untar",
"VMID",
"weblinks",
"webrtc",
"workspacebuilds",
"xerrors",
"xstate",
"yamux"
],
"emeraldwalk.runonsave": {
"commands": [
{
"match": "database/query.sql",
"match": "database/queries/*.sql",
"cmd": "make gen"
},
{
"match": "provisionerd/proto/provisionerd.proto",
"cmd": "make provisionerd/proto/provisionerd.pb.go"
}
]
},
@@ -91,5 +113,5 @@
},
// We often use a version of TypeScript that's ahead of the version shipped
// with VS Code.
"typescript.tsdk": "./site/node_modules/typescript/lib",
"typescript.tsdk": "./site/node_modules/typescript/lib"
}
+6
View File
@@ -0,0 +1,6 @@
FROM alpine
# Generated by goreleaser on `goreleaser release`
ADD coder /opt/coder
ENTRYPOINT [ "/opt/coder", "server" ]
+77 -39
View File
@@ -1,23 +1,31 @@
.DEFAULT_GOAL := build
INSTALL_DIR=$(shell go env GOPATH)/bin
GOOS=$(shell go env GOOS)
GOARCH=$(shell go env GOARCH)
bin:
goreleaser build --snapshot --rm-dist
.PHONY: bin
bin: $(shell find . -not -path './vendor/*' -type f -name '*.go') go.mod go.sum $(shell find ./examples/templates)
@echo "== This builds binaries for command-line usage."
@echo "== Use \"make build\" to embed the site."
goreleaser build --snapshot --rm-dist --single-target
build: site/out bin
build: dist/artifacts.json
.PHONY: build
# Runs migrations to output a dump of the database.
coderd/database/dump.sql: $(wildcard coderd/database/migrations/*.sql)
go run coderd/database/dump/main.go
.PHONY: coderd/database/dump.sql
# Generates Go code for querying the database.
coderd/database/generate: fmt/sql coderd/database/dump.sql $(wildcard coderd/database/queries/*.sql)
coderd/database/querier.go: coderd/database/dump.sql $(wildcard coderd/database/queries/*.sql)
coderd/database/generate.sh
.PHONY: coderd/database/generate
dev:
./scripts/develop.sh
.PHONY: dev
dist/artifacts.json: site/out/index.html $(shell find . -not -path './vendor/*' -type f -name '*.go') go.mod go.sum $(shell find ./examples/templates)
goreleaser release --snapshot --rm-dist --skip-sign
fmt/prettier:
@echo "--- prettier"
@@ -29,74 +37,104 @@ else
endif
.PHONY: fmt/prettier
fmt/sql: $(wildcard coderd/database/queries/*.sql)
# TODO: this is slightly slow
for fi in coderd/database/queries/*.sql; do \
npx sql-formatter \
--language postgresql \
--lines-between-queries 2 \
--tab-indent \
$$fi \
--output $$fi; \
done
fmt/terraform: $(wildcard *.tf)
terraform fmt -recursive
.PHONY: fmt/terraform
sed -i 's/@ /@/g' ./coderd/database/queries/*.sql
fmt/shfmt: $(shell shfmt -f .)
@echo "--- shfmt"
# Only do diff check in CI, errors on diff.
ifdef CI
shfmt -d $(shell shfmt -f .)
else
shfmt -w $(shell shfmt -f .)
endif
fmt: fmt/prettier fmt/sql
fmt: fmt/prettier fmt/terraform fmt/shfmt
.PHONY: fmt
gen: coderd/database/generate peerbroker/proto provisionersdk/proto provisionerd/proto
.PHONY: gen
gen: coderd/database/querier.go peerbroker/proto/peerbroker.pb.go provisionersdk/proto/provisioner.pb.go provisionerd/proto/provisionerd.pb.go site/src/api/typesGenerated.ts
install: bin
install: build
mkdir -p $(INSTALL_DIR)
@echo "--- Copying from bin to $(INSTALL_DIR)"
cp -r ./dist/coder_$(GOOS)_$(GOARCH)/* $(INSTALL_DIR)
cp -r ./dist/coder-$(GOOS)_$(GOOS)_$(GOARCH)*/* $(INSTALL_DIR)
@echo "-- CLI available at $(shell ls $(INSTALL_DIR)/coder*)"
.PHONY: install
lint:
golangci-lint run
.PHONY: lint
lint: lint/shellcheck lint/go
peerbroker/proto: peerbroker/proto/peerbroker.proto
lint/go:
golangci-lint run
.PHONY: lint/go
# Use shfmt to determine the shell files, takes editorconfig into consideration.
lint/shellcheck: $(shell shfmt -f .)
@echo "--- shellcheck"
shellcheck --external-sources $(shell shfmt -f .)
peerbroker/proto/peerbroker.pb.go: peerbroker/proto/peerbroker.proto
protoc \
--go_out=. \
--go_opt=paths=source_relative \
--go-drpc_out=. \
--go-drpc_opt=paths=source_relative \
./peerbroker/proto/peerbroker.proto
.PHONY: peerbroker/proto
provisionerd/proto: provisionerd/proto/provisionerd.proto
provisionerd/proto/provisionerd.pb.go: provisionerd/proto/provisionerd.proto
protoc \
--go_out=. \
--go_opt=paths=source_relative \
--go-drpc_out=. \
--go-drpc_opt=paths=source_relative \
./provisionerd/proto/provisionerd.proto
.PHONY: provisionerd/proto
provisionersdk/proto: provisionersdk/proto/provisioner.proto
provisionersdk/proto/provisioner.pb.go: provisionersdk/proto/provisioner.proto
protoc \
--go_out=. \
--go_opt=paths=source_relative \
--go-drpc_out=. \
--go-drpc_opt=paths=source_relative \
./provisionersdk/proto/provisioner.proto
.PHONY: provisionersdk/proto
release: site/out
goreleaser release --snapshot --rm-dist
.PHONY: release
site/out:
site/out/index.html: $(shell find ./site -not -path './site/node_modules/*' -type f -name '*.tsx') $(shell find ./site -not -path './site/node_modules/*' -type f -name '*.ts') site/package.json
./scripts/yarn_install.sh
cd site && yarn typegen
cd site && yarn build
# Restores GITKEEP files!
git checkout HEAD site/out
.PHONY: site/out
test:
site/src/api/typesGenerated.ts: scripts/apitypings/main.go $(shell find codersdk -type f -name '*.go')
go run scripts/apitypings/main.go > site/src/api/typesGenerated.ts
cd site && yarn run format:types
.PHONY: test
test: test-clean
gotestsum -- -v -short ./...
.PHONY: test-postgres
test-postgres: test-clean
DB=ci gotestsum --junitfile="gotests.xml" --packages="./..." -- \
-covermode=atomic -coverprofile="gotests.coverage" -timeout=5m \
-coverpkg=./...,github.com/coder/coder/codersdk \
-count=1 -parallel=1 -race -failfast
.PHONY: test-postgres-docker
test-postgres-docker:
docker run \
--env POSTGRES_PASSWORD=postgres \
--env POSTGRES_USER=postgres \
--env POSTGRES_DB=postgres \
--env PGDATA=/tmp \
--publish 5432:5432 \
--name test-postgres-docker \
--restart unless-stopped \
--detach \
postgres:11 \
-c shared_buffers=1GB \
-c max_connections=1000
.PHONY: test-clean
test-clean:
go clean -testcache
+164
View File
@@ -0,0 +1,164 @@
# Coder
[!["GitHub
Discussions"](https://img.shields.io/badge/%20GitHub-%20Discussions-gray.svg?longCache=true&logo=github&colorB=purple)](https://github.com/coder/coder/discussions)
[!["Join us on
Discord"](https://img.shields.io/badge/join-us%20on%20Discord-gray.svg?longCache=true&logo=discord&colorB=purple)](https://discord.gg/coder)
[![Twitter
Follow](https://img.shields.io/twitter/follow/CoderHQ?label=%40CoderHQ&style=social)](https://twitter.com/coderhq)
[![codecov](https://codecov.io/gh/coder/coder/branch/main/graph/badge.svg?token=TNLW3OAP6G)](https://codecov.io/gh/coder/coder)
## Run Coder *now*
```curl -L https://coder.com/install.sh | sh```
## What Coder does
Coder creates remote development machines so you can develop your code from anywhere. #coder
> **Note**:
> Coder is in an alpha state, but any serious bugs are P1 for us so [please report them](https://github.com/coder/coder/issues/new/choose).
<p align="center">
<img src="./docs/images/hero-image.png">
</p>
**Code more**
- Build and test faster
- Leveraging cloud CPUs, RAM, network speeds, etc.
- Access your environment from any place on any client (even an iPad)
- Onboard instantly then stay up to date continuously
**Manage less**
- Ensure your entire team is using the same tools and resources
- Rollout critical updates to your developers with one command
- Automatically shut down expensive cloud resources
- Keep your source code and data behind your firewall
## How it works
Coder workspaces are represented with Terraform. But, no Terraform knowledge is
required to get started. We have a database of pre-made templates built into the
product.
<p align="center">
<img src="./docs/images/providers-compute.png">
</p>
Coder workspaces don't stop at compute. You can add storage buckets, secrets, sidecars
and whatever else Terraform lets you dream up.
[Learn more about managing infrastructure.](./docs/templates.md)
## IDE Support
You can use any Web IDE ([code-server](https://github.com/coder/code-server), [projector](https://github.com/JetBrains/projector-server), [Jupyter](https://jupyter.org/), etc.), [JetBrains Gateway](https://www.jetbrains.com/remote-development/gateway/), [VS Code Remote](https://code.visualstudio.com/docs/remote/ssh-tutorial) or even a file sync such as [mutagen](https://mutagen.io/).
<p align="center">
<img src="./docs/images/ide-icons.svg" height=72>
</p>
## Installing Coder
There are a few ways to install Coder: [install script](./docs/install.md#installsh) (macOS, Linux), [docker-compose](./docs/install.md#docker-compose), or [manually](./docs/install.md#manual) via the latest release (macOS, Windows, and Linux).
If you use the install script, you can preview what occurs during the install process:
```sh
curl -fsSL https://coder.com/install.sh | sh -s -- --dry-run
```
To install, run:
```sh
curl -fsSL https://coder.com/install.sh | sh
```
Once installed, you can run a temporary deployment in dev mode (all data is in-memory and destroyed on exit):
```sh
coder server --dev
```
Use `coder --help` to get a complete list of flags and environment variables.
## Creating your first template and workspace
In a new terminal window, run the following to copy a sample template:
```bash
coder templates init
```
Follow the CLI instructions to modify and create the template specific for your
usage (e.g., a template to **Develop in Linux on Google Cloud**).
Create a workspace using your template:
```bash
coder create --template="yourTemplate" <workspaceName>
```
Connect to your workspace via SSH:
```bash
coder ssh <workspaceName>
```
## Modifying templates
You can edit the Terraform template using a sample template:
```sh
coder templates init
cd gcp-linux/
vim main.tf
coder templates update gcp-linux
```
## Documentation
- [About Coder](./docs/about.md#about-coder)
- [Why remote development](./docs/about.md#why-remote-development)
- [Why Coder](./docs/about.md#why-coder)
- [What Coder is not](./docs/about.md#what-coder-is-not)
- [Comparison: Coder vs. [product]](./docs/about.md#comparison)
- [Templates](./docs/templates.md)
- [Manage templates](./docs/templates.md#manage-templates)
- [Persistent and ephemeral
resources](./docs/templates.md#persistent-and-ephemeral-resources)
- [Parameters](./docs/templates.md#parameters)
- [Workspaces](./docs/workspaces.md)
- [Create workspaces](./docs/workspaces.md#create-workspaces)
- [Connect with SSH](./docs/workspaces.md#connect-with-ssh)
- [Editors and IDEs](./docs/workspaces.md#editors-and-ides)
- [Workspace lifecycle](./docs/workspaces.md#workspace-lifecycle)
- [Updating workspaces](./docs/workspaces.md#updating-workspaces)
## Community
Join the community on [Discord](https://discord.gg/coder) and [Twitter](https://twitter.com/coderhq) #coder!
[Suggest improvements and report problems](https://github.com/coder/coder/issues/new/choose)
## Comparison
Please file [an issue](https://github.com/coder/coder/issues/new) if any information is out of date. Also refer to: [What Coder is not](./docs/about.md#what-coder-is-not).
| Tool | Type | Delivery Model | Cost | Environments |
| :---------------------------------------------------------- | :------- | :----------------- | :---------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Coder](https://github.com/coder/coder) | Platform | OSS + Self-Managed | Pay your cloud | All [Terraform](https://www.terraform.io/registry/providers) resources, all clouds, multi-architecture: Linux, Mac, Windows, containers, VMs, amd64, arm64 |
| [code-server](https://github.com/cdr/code-server) | Web IDE | OSS + Self-Managed | Pay your cloud | Linux, Mac, Windows, containers, VMs, amd64, arm64 |
| [Coder (Classic)](https://coder.com/docs) | Platform | Self-Managed | Pay your cloud + license fees | Kubernetes Linux Containers |
| [GitHub Codespaces](https://github.com/features/codespaces) | Platform | SaaS | 2x Azure Compute | Linux containers |
---
_As of 5/27/22_
## Contributing
Read the [contributing docs](./docs/CONTRIBUTING.md).
Find our list of contributors [here](./docs/CONTRIBUTORS.md).
+568 -67
View File
@@ -4,65 +4,107 @@ import (
"context"
"crypto/rand"
"crypto/rsa"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/url"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/armon/circbuf"
"github.com/gliderlabs/ssh"
"github.com/google/uuid"
"github.com/pkg/sftp"
"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/peer"
"github.com/coder/coder/peerbroker"
"github.com/coder/coder/pty"
"github.com/coder/retry"
)
"github.com/gliderlabs/ssh"
gossh "golang.org/x/crypto/ssh"
"golang.org/x/xerrors"
const (
ProtocolReconnectingPTY = "reconnecting-pty"
ProtocolSSH = "ssh"
ProtocolDial = "dial"
)
type Options struct {
Logger slog.Logger
ReconnectingPTYTimeout time.Duration
EnvironmentVariables map[string]string
Logger slog.Logger
}
type Dialer func(ctx context.Context, options *peer.ConnOptions) (*peerbroker.Listener, error)
type Metadata struct {
OwnerEmail string `json:"owner_email"`
OwnerUsername string `json:"owner_username"`
EnvironmentVariables map[string]string `json:"environment_variables"`
StartupScript string `json:"startup_script"`
Directory string `json:"directory"`
}
func New(dialer Dialer, options *peer.ConnOptions) io.Closer {
type Dialer func(ctx context.Context, logger slog.Logger) (Metadata, *peerbroker.Listener, error)
func New(dialer Dialer, options *Options) io.Closer {
if options == nil {
options = &Options{}
}
if options.ReconnectingPTYTimeout == 0 {
options.ReconnectingPTYTimeout = 5 * time.Minute
}
ctx, cancelFunc := context.WithCancel(context.Background())
server := &agent{
clientDialer: dialer,
options: options,
closeCancel: cancelFunc,
closed: make(chan struct{}),
dialer: dialer,
reconnectingPTYTimeout: options.ReconnectingPTYTimeout,
logger: options.Logger,
closeCancel: cancelFunc,
closed: make(chan struct{}),
envVars: options.EnvironmentVariables,
}
server.init(ctx)
return server
}
type agent struct {
clientDialer Dialer
options *peer.ConnOptions
dialer Dialer
logger slog.Logger
reconnectingPTYs sync.Map
reconnectingPTYTimeout time.Duration
connCloseWait sync.WaitGroup
closeCancel context.CancelFunc
closeMutex sync.Mutex
closed chan struct{}
sshServer *ssh.Server
envVars map[string]string
// metadata is atomic because values can change after reconnection.
metadata atomic.Value
startupScript atomic.Bool
sshServer *ssh.Server
}
func (a *agent) run(ctx context.Context) {
var metadata Metadata
var peerListener *peerbroker.Listener
var err error
// An exponential back-off occurs when the connection is failing to dial.
// This is to prevent server spam in case of a coderd outage.
for retrier := retry.New(50*time.Millisecond, 10*time.Second); retrier.Wait(ctx); {
peerListener, err = a.clientDialer(ctx, a.options)
metadata, peerListener, err = a.dialer(ctx, a.logger)
if err != nil {
if errors.Is(err, context.Canceled) {
return
@@ -70,10 +112,10 @@ func (a *agent) run(ctx context.Context) {
if a.isClosed() {
return
}
a.options.Logger.Warn(context.Background(), "failed to dial", slog.Error(err))
a.logger.Warn(context.Background(), "failed to dial", slog.Error(err))
continue
}
a.options.Logger.Info(context.Background(), "connected")
a.logger.Info(context.Background(), "connected")
break
}
select {
@@ -81,6 +123,20 @@ func (a *agent) run(ctx context.Context) {
return
default:
}
a.metadata.Store(metadata)
if a.startupScript.CAS(false, true) {
// The startup script has not ran yet!
go func() {
err := a.runStartupScript(ctx, metadata.StartupScript)
if errors.Is(err, context.Canceled) {
return
}
if err != nil {
a.logger.Warn(ctx, "agent script failed", slog.Error(err))
}
}()
}
for {
conn, err := peerListener.Accept()
@@ -88,7 +144,7 @@ func (a *agent) run(ctx context.Context) {
if a.isClosed() {
return
}
a.options.Logger.Debug(ctx, "peer listener accept exited; restarting connection", slog.Error(err))
a.logger.Debug(ctx, "peer listener accept exited; restarting connection", slog.Error(err))
a.run(ctx)
return
}
@@ -99,9 +155,57 @@ func (a *agent) run(ctx context.Context) {
}
}
func (*agent) runStartupScript(ctx context.Context, script string) error {
if script == "" {
return nil
}
currentUser, err := user.Current()
if err != nil {
return xerrors.Errorf("get current user: %w", err)
}
username := currentUser.Username
shell, err := usershell.Get(username)
if err != nil {
return xerrors.Errorf("get user shell: %w", err)
}
writer, err := os.OpenFile(filepath.Join(os.TempDir(), "coder-startup-script.log"), os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
return xerrors.Errorf("open startup script log file: %w", err)
}
defer func() {
_ = writer.Close()
}()
caller := "-c"
if runtime.GOOS == "windows" {
caller = "/c"
}
cmd := exec.CommandContext(ctx, shell, caller, script)
cmd.Stdout = writer
cmd.Stderr = writer
err = cmd.Run()
if err != nil {
// cmd.Run does not return a context canceled error, it returns "signal: killed".
if ctx.Err() != nil {
return ctx.Err()
}
return xerrors.Errorf("run: %w", err)
}
return nil
}
func (a *agent) handlePeerConn(ctx context.Context, conn *peer.Conn) {
go func() {
<-conn.Closed()
select {
case <-a.closed:
case <-conn.Closed():
}
_ = conn.Close()
a.connCloseWait.Done()
}()
for {
@@ -110,15 +214,19 @@ func (a *agent) handlePeerConn(ctx context.Context, conn *peer.Conn) {
if errors.Is(err, peer.ErrClosed) || a.isClosed() {
return
}
a.options.Logger.Debug(ctx, "accept channel from peer connection", slog.Error(err))
a.logger.Debug(ctx, "accept channel from peer connection", slog.Error(err))
return
}
switch channel.Protocol() {
case "ssh":
a.sshServer.HandleConn(channel.NetConn())
case ProtocolSSH:
go a.sshServer.HandleConn(channel.NetConn())
case ProtocolReconnectingPTY:
go a.handleReconnectingPTY(ctx, channel.Label(), channel.NetConn())
case ProtocolDial:
go a.handleDial(ctx, channel.Label(), channel.NetConn())
default:
a.options.Logger.Warn(ctx, "unhandled protocol from channel",
a.logger.Warn(ctx, "unhandled protocol from channel",
slog.F("protocol", channel.Protocol()),
slog.F("label", channel.Label()),
)
@@ -138,17 +246,20 @@ func (a *agent) init(ctx context.Context) {
if err != nil {
panic(err)
}
sshLogger := a.options.Logger.Named("ssh-server")
sshLogger := a.logger.Named("ssh-server")
forwardHandler := &ssh.ForwardedTCPHandler{}
a.sshServer = &ssh.Server{
ChannelHandlers: ssh.DefaultChannelHandlers,
ChannelHandlers: map[string]ssh.ChannelHandler{
"direct-tcpip": ssh.DirectTCPIPHandler,
"session": ssh.DefaultSessionHandler,
},
ConnectionFailedCallback: func(conn net.Conn, err error) {
sshLogger.Info(ctx, "ssh connection ended", slog.Error(err))
},
Handler: func(session ssh.Session) {
err := a.handleSSHSession(session)
if err != nil {
a.options.Logger.Warn(ctx, "ssh session failed", slog.Error(err))
a.logger.Warn(ctx, "ssh session failed", slog.Error(err))
_ = session.Exit(1)
return
}
@@ -180,56 +291,117 @@ func (a *agent) init(ctx context.Context) {
NoClientAuth: true,
}
},
SubsystemHandlers: map[string]ssh.SubsystemHandler{
"sftp": func(session ssh.Session) {
server, err := sftp.NewServer(session)
if err != nil {
a.logger.Debug(session.Context(), "initialize sftp server", slog.Error(err))
return
}
defer server.Close()
err = server.Serve()
if errors.Is(err, io.EOF) {
return
}
a.logger.Debug(session.Context(), "sftp server exited with error", slog.Error(err))
},
},
}
go a.run(ctx)
}
func (a *agent) handleSSHSession(session ssh.Session) error {
var (
command string
args = []string{}
err error
)
// createCommand processes raw command input with OpenSSH-like behavior.
// If the rawCommand provided is empty, it will default to the users shell.
// This injects environment variables specified by the user at launch too.
func (a *agent) createCommand(ctx context.Context, rawCommand string, env []string) (*exec.Cmd, error) {
currentUser, err := user.Current()
if err != nil {
return xerrors.Errorf("get current user: %w", err)
return nil, xerrors.Errorf("get current user: %w", err)
}
username := currentUser.Username
// gliderlabs/ssh returns a command slice of zero
// when a shell is requested.
if len(session.Command()) == 0 {
command, err = usershell.Get(username)
if err != nil {
return xerrors.Errorf("get user shell: %w", err)
}
} else {
command = session.Command()[0]
if len(session.Command()) > 1 {
args = session.Command()[1:]
}
shell, err := usershell.Get(username)
if err != nil {
return nil, xerrors.Errorf("get user shell: %w", err)
}
signals := make(chan ssh.Signal)
breaks := make(chan bool)
defer close(signals)
defer close(breaks)
go func() {
for {
select {
case <-session.Context().Done():
return
// Ignore signals and breaks for now!
case <-signals:
case <-breaks:
}
}
}()
rawMetadata := a.metadata.Load()
if rawMetadata == nil {
return nil, xerrors.Errorf("no metadata was provided: %w", err)
}
metadata, valid := rawMetadata.(Metadata)
if !valid {
return nil, xerrors.Errorf("metadata is the wrong type: %T", metadata)
}
cmd := exec.CommandContext(session.Context(), command, args...)
cmd.Env = append(os.Environ(), session.Environ()...)
// gliderlabs/ssh returns a command slice of zero
// when a shell is requested.
command := rawCommand
if len(command) == 0 {
command = shell
}
// OpenSSH executes all commands with the users current shell.
// We replicate that behavior for IDE support.
caller := "-c"
if runtime.GOOS == "windows" {
caller = "/c"
}
cmd := exec.CommandContext(ctx, shell, caller, command)
cmd.Dir = metadata.Directory
if cmd.Dir == "" {
// Default to $HOME if a directory is not set!
cmd.Dir = os.Getenv("HOME")
}
cmd.Env = append(os.Environ(), env...)
executablePath, err := os.Executable()
if err != nil {
return nil, xerrors.Errorf("getting os executable: %w", err)
}
cmd.Env = append(cmd.Env, fmt.Sprintf("USER=%s", username))
cmd.Env = append(cmd.Env, fmt.Sprintf(`PATH=%s%c%s`, os.Getenv("PATH"), filepath.ListSeparator, filepath.Dir(executablePath)))
// 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))
// These prevent the user from having to specify _anything_ to successfully commit.
// Both author and committer must be set!
cmd.Env = append(cmd.Env, fmt.Sprintf(`GIT_AUTHOR_EMAIL=%s`, metadata.OwnerEmail))
cmd.Env = append(cmd.Env, fmt.Sprintf(`GIT_COMMITTER_EMAIL=%s`, metadata.OwnerEmail))
cmd.Env = append(cmd.Env, fmt.Sprintf(`GIT_AUTHOR_NAME=%s`, metadata.OwnerUsername))
cmd.Env = append(cmd.Env, fmt.Sprintf(`GIT_COMMITTER_NAME=%s`, metadata.OwnerUsername))
// Load environment variables passed via the agent.
// These should override all variables we manually specify.
for key, value := range metadata.EnvironmentVariables {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, value))
}
// Agent-level environment variables should take over all!
// This is used for setting agent-specific variables like "CODER_AGENT_TOKEN".
for key, value := range a.envVars {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, value))
}
return cmd, nil
}
func (a *agent) handleSSHSession(session ssh.Session) error {
cmd, err := a.createCommand(session.Context(), session.RawCommand(), session.Environ())
if err != nil {
return err
}
if ssh.AgentRequested(session) {
l, err := ssh.NewAgentListener()
if err != nil {
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()))
}
sshPty, windowSize, isPty := session.Pty()
if isPty {
@@ -238,11 +410,15 @@ func (a *agent) handleSSHSession(session ssh.Session) error {
if err != nil {
return xerrors.Errorf("start command: %w", err)
}
err = ptty.Resize(uint16(sshPty.Window.Height), uint16(sshPty.Window.Width))
if err != nil {
return xerrors.Errorf("resize ptty: %w", err)
}
go func() {
for win := range windowSize {
err = ptty.Resize(uint16(win.Width), uint16(win.Height))
err = ptty.Resize(uint16(win.Height), uint16(win.Width))
if err != nil {
a.options.Logger.Warn(context.Background(), "failed to resize tty", slog.Error(err))
a.logger.Warn(context.Background(), "failed to resize tty", slog.Error(err))
}
}
}()
@@ -258,7 +434,7 @@ func (a *agent) handleSSHSession(session ssh.Session) error {
}
cmd.Stdout = session
cmd.Stderr = 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()
@@ -272,8 +448,263 @@ func (a *agent) handleSSHSession(session ssh.Session) error {
if err != nil {
return xerrors.Errorf("start: %w", err)
}
_ = cmd.Wait()
return nil
return cmd.Wait()
}
func (a *agent) handleReconnectingPTY(ctx context.Context, rawID string, conn net.Conn) {
defer conn.Close()
// The ID format is referenced in conn.go.
// <uuid>:<height>:<width>
idParts := strings.SplitN(rawID, ":", 4)
if len(idParts) != 4 {
a.logger.Warn(ctx, "client sent invalid id format", slog.F("raw-id", rawID))
return
}
id := idParts[0]
// Enforce a consistent format for IDs.
_, err := uuid.Parse(id)
if err != nil {
a.logger.Warn(ctx, "client sent reconnection token that isn't a uuid", slog.F("id", id), slog.Error(err))
return
}
// Parse the initial terminal dimensions.
height, err := strconv.Atoi(idParts[1])
if err != nil {
a.logger.Warn(ctx, "client sent invalid height", slog.F("id", id), slog.F("height", idParts[1]))
return
}
width, err := strconv.Atoi(idParts[2])
if err != nil {
a.logger.Warn(ctx, "client sent invalid width", slog.F("id", id), slog.F("width", idParts[2]))
return
}
var rpty *reconnectingPTY
rawRPTY, ok := a.reconnectingPTYs.Load(id)
if ok {
rpty, ok = rawRPTY.(*reconnectingPTY)
if !ok {
a.logger.Warn(ctx, "found invalid type in reconnecting pty map", slog.F("id", id))
}
} else {
// Empty command will default to the users shell!
cmd, err := a.createCommand(ctx, idParts[3], nil)
if err != nil {
a.logger.Warn(ctx, "create reconnecting pty command", slog.Error(err))
return
}
cmd.Env = append(cmd.Env, "TERM=xterm-256color")
ptty, process, err := pty.Start(cmd)
if err != nil {
a.logger.Warn(ctx, "start reconnecting pty command", slog.F("id", id))
}
// Default to buffer 64KiB.
circularBuffer, err := circbuf.NewBuffer(64 << 10)
if err != nil {
a.logger.Warn(ctx, "create circular buffer", slog.Error(err))
return
}
a.closeMutex.Lock()
a.connCloseWait.Add(1)
a.closeMutex.Unlock()
ctx, cancelFunc := context.WithCancel(ctx)
rpty = &reconnectingPTY{
activeConns: make(map[string]net.Conn),
ptty: ptty,
// Timeouts created with an after func can be reset!
timeout: time.AfterFunc(a.reconnectingPTYTimeout, cancelFunc),
circularBuffer: circularBuffer,
}
a.reconnectingPTYs.Store(id, rpty)
go func() {
// CommandContext isn't respected for Windows PTYs right now,
// so we need to manually track the lifecycle.
// When the context has been completed either:
// 1. The timeout completed.
// 2. The parent context was canceled.
<-ctx.Done()
_ = process.Kill()
}()
go func() {
// If the process dies randomly, we should
// close the pty.
_, _ = process.Wait()
rpty.Close()
}()
go func() {
buffer := make([]byte, 1024)
for {
read, err := rpty.ptty.Output().Read(buffer)
if err != nil {
// When the PTY is closed, this is triggered.
break
}
part := buffer[:read]
rpty.circularBufferMutex.Lock()
_, err = rpty.circularBuffer.Write(part)
rpty.circularBufferMutex.Unlock()
if err != nil {
a.logger.Error(ctx, "reconnecting pty write buffer", slog.Error(err), slog.F("id", id))
break
}
rpty.activeConnsMutex.Lock()
for _, conn := range rpty.activeConns {
_, _ = conn.Write(part)
}
rpty.activeConnsMutex.Unlock()
}
// Cleanup the process, PTY, and delete it's
// ID from memory.
_ = process.Kill()
rpty.Close()
a.reconnectingPTYs.Delete(id)
a.connCloseWait.Done()
}()
}
// Resize the PTY to initial height + width.
err = rpty.ptty.Resize(uint16(height), uint16(width))
if err != nil {
// We can continue after this, it's not fatal!
a.logger.Error(ctx, "resize reconnecting pty", slog.F("id", id), slog.Error(err))
}
// Write any previously stored data for the TTY.
rpty.circularBufferMutex.RLock()
_, err = conn.Write(rpty.circularBuffer.Bytes())
rpty.circularBufferMutex.RUnlock()
if err != nil {
a.logger.Warn(ctx, "write reconnecting pty buffer", slog.F("id", id), slog.Error(err))
return
}
connectionID := uuid.NewString()
// Multiple connections to the same TTY are permitted.
// This could easily be used for terminal sharing, but
// we do it because it's a nice user experience to
// copy/paste a terminal URL and have it _just work_.
rpty.activeConnsMutex.Lock()
rpty.activeConns[connectionID] = conn
rpty.activeConnsMutex.Unlock()
// Resetting this timeout prevents the PTY from exiting.
rpty.timeout.Reset(a.reconnectingPTYTimeout)
ctx, cancelFunc := context.WithCancel(ctx)
defer cancelFunc()
heartbeat := time.NewTicker(a.reconnectingPTYTimeout / 2)
defer heartbeat.Stop()
go func() {
// Keep updating the activity while this
// connection is alive!
for {
select {
case <-ctx.Done():
return
case <-heartbeat.C:
}
rpty.timeout.Reset(a.reconnectingPTYTimeout)
}
}()
defer func() {
// After this connection ends, remove it from
// the PTYs active connections. If it isn't
// removed, all PTY data will be sent to it.
rpty.activeConnsMutex.Lock()
delete(rpty.activeConns, connectionID)
rpty.activeConnsMutex.Unlock()
}()
decoder := json.NewDecoder(conn)
var req ReconnectingPTYRequest
for {
err = decoder.Decode(&req)
if xerrors.Is(err, io.EOF) {
return
}
if err != nil {
a.logger.Warn(ctx, "reconnecting pty buffer read error", slog.F("id", id), slog.Error(err))
return
}
_, err = rpty.ptty.Input().Write([]byte(req.Data))
if err != nil {
a.logger.Warn(ctx, "write to reconnecting pty", slog.F("id", id), slog.Error(err))
return
}
// Check if a resize needs to happen!
if req.Height == 0 || req.Width == 0 {
continue
}
err = rpty.ptty.Resize(req.Height, req.Width)
if err != nil {
// We can continue after this, it's not fatal!
a.logger.Error(ctx, "resize reconnecting pty", slog.F("id", id), slog.Error(err))
}
}
}
// dialResponse is written to datachannels with protocol "dial" by the agent as
// the first packet to signify whether the dial succeeded or failed.
type dialResponse struct {
Error string `json:"error,omitempty"`
}
func (a *agent) handleDial(ctx context.Context, label string, conn net.Conn) {
defer conn.Close()
writeError := func(responseError error) error {
msg := ""
if responseError != nil {
msg = responseError.Error()
if !xerrors.Is(responseError, io.EOF) {
a.logger.Warn(ctx, "handle dial", slog.F("label", label), slog.Error(responseError))
}
}
b, err := json.Marshal(dialResponse{
Error: msg,
})
if err != nil {
a.logger.Warn(ctx, "write dial response", slog.F("label", label), slog.Error(err))
return xerrors.Errorf("marshal agent webrtc dial response: %w", err)
}
_, err = conn.Write(b)
return err
}
u, err := url.Parse(label)
if err != nil {
_ = writeError(xerrors.Errorf("parse URL %q: %w", label, err))
return
}
network := u.Scheme
addr := u.Host + u.Path
if strings.HasPrefix(network, "unix") {
if runtime.GOOS == "windows" {
_ = writeError(xerrors.New("Unix forwarding is not supported from Windows workspaces"))
return
}
addr, err = ExpandRelativeHomePath(addr)
if err != nil {
_ = writeError(xerrors.Errorf("expand path %q: %w", addr, err))
return
}
}
d := net.Dialer{Timeout: 3 * time.Second}
nconn, err := d.DialContext(ctx, network, addr)
if err != nil {
_ = writeError(xerrors.Errorf("dial '%v://%v': %w", network, addr, err))
return
}
err = writeError(nil)
if err != nil {
return
}
Bicopy(ctx, conn, nconn)
}
// isClosed returns whether the API is closed or not.
@@ -298,3 +729,73 @@ func (a *agent) Close() error {
a.connCloseWait.Wait()
return nil
}
type reconnectingPTY struct {
activeConnsMutex sync.Mutex
activeConns map[string]net.Conn
circularBuffer *circbuf.Buffer
circularBufferMutex sync.RWMutex
timeout *time.Timer
ptty pty.PTY
}
// Close ends all connections to the reconnecting
// PTY and clear the circular buffer.
func (r *reconnectingPTY) Close() {
r.activeConnsMutex.Lock()
defer r.activeConnsMutex.Unlock()
for _, conn := range r.activeConns {
_ = conn.Close()
}
_ = r.ptty.Close()
r.circularBuffer.Reset()
r.timeout.Stop()
}
// 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) {
defer c1.Close()
defer c2.Close()
var wg sync.WaitGroup
copyFunc := func(dst io.WriteCloser, src io.Reader) {
defer wg.Done()
_, _ = 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:
}
}
// ExpandRelativeHomePath expands the tilde at the beginning of a path to the
// current user's home directory and returns a full absolute path.
func ExpandRelativeHomePath(in string) (string, error) {
usr, err := user.Current()
if err != nil {
return "", xerrors.Errorf("get current user details: %w", err)
}
if in == "~" {
in = usr.HomeDir
} else if strings.HasPrefix(in, "~/") {
in = filepath.Join(usr.HomeDir, in[2:])
}
return filepath.Abs(in)
}
+404 -45
View File
@@ -1,15 +1,31 @@
package agent_test
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/pion/udp"
"github.com/pion/webrtc/v3"
"github.com/pkg/sftp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
"golang.org/x/crypto/ssh"
"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
@@ -29,24 +45,8 @@ func TestAgent(t *testing.T) {
t.Parallel()
t.Run("SessionExec", func(t *testing.T) {
t.Parallel()
api := setup(t)
stream, err := api.NegotiateConnection(context.Background())
require.NoError(t, err)
conn, err := peerbroker.Dial(stream, []webrtc.ICEServer{}, &peer.ConnOptions{
Logger: slogtest.Make(t, nil),
})
require.NoError(t, err)
t.Cleanup(func() {
_ = conn.Close()
})
client := agent.Conn{
Negotiator: api,
Conn: conn,
}
sshClient, err := client.SSHClient()
require.NoError(t, err)
session, err := sshClient.NewSession()
require.NoError(t, err)
session := setupSSHSession(t, agent.Metadata{})
command := "echo test"
if runtime.GOOS == "windows" {
command = "cmd.exe /c echo test"
@@ -56,33 +56,47 @@ func TestAgent(t *testing.T) {
require.Equal(t, "test", strings.TrimSpace(string(output)))
})
t.Run("GitSSH", func(t *testing.T) {
t.Parallel()
session := setupSSHSession(t, agent.Metadata{})
command := "sh -c 'echo $GIT_SSH_COMMAND'"
if runtime.GOOS == "windows" {
command = "cmd.exe /c echo %GIT_SSH_COMMAND%"
}
output, err := session.Output(command)
require.NoError(t, err)
require.True(t, strings.HasSuffix(strings.TrimSpace(string(output)), "gitssh --"))
})
t.Run("PATHHasCoder", func(t *testing.T) {
t.Parallel()
session := setupSSHSession(t, agent.Metadata{})
command := "sh -c 'echo $PATH'"
if runtime.GOOS == "windows" {
command = "cmd.exe /c echo %PATH%"
}
output, err := session.Output(command)
require.NoError(t, err)
ex, err := os.Executable()
t.Log(ex)
require.NoError(t, err)
require.True(t, strings.Contains(strings.TrimSpace(string(output)), filepath.Dir(ex)))
})
t.Run("SessionTTY", func(t *testing.T) {
t.Parallel()
api := setup(t)
stream, err := api.NegotiateConnection(context.Background())
require.NoError(t, err)
conn, err := peerbroker.Dial(stream, []webrtc.ICEServer{}, &peer.ConnOptions{
Logger: slogtest.Make(t, nil),
})
require.NoError(t, err)
t.Cleanup(func() {
_ = conn.Close()
})
client := &agent.Conn{
Negotiator: api,
Conn: conn,
if runtime.GOOS == "windows" {
// This might be our implementation, or ConPTY itself.
// It's difficult to find extensive tests for it, so
// it seems like it could be either.
t.Skip("ConPTY appears to be inconsistent on Windows.")
}
sshClient, err := client.SSHClient()
require.NoError(t, err)
session, err := sshClient.NewSession()
require.NoError(t, err)
prompt := "$"
session := setupSSHSession(t, agent.Metadata{})
command := "bash"
if runtime.GOOS == "windows" {
command = "cmd.exe"
prompt = ">"
}
err = session.RequestPty("xterm", 128, 128, ssh.TerminalModes{})
err := session.RequestPty("xterm", 128, 128, ssh.TerminalModes{})
require.NoError(t, err)
ptty := ptytest.New(t)
require.NoError(t, err)
@@ -91,26 +105,371 @@ func TestAgent(t *testing.T) {
session.Stdin = ptty.Input()
err = session.Start(command)
require.NoError(t, err)
ptty.ExpectMatch(prompt)
caret := "$"
if runtime.GOOS == "windows" {
caret = ">"
}
ptty.ExpectMatch(caret)
ptty.WriteLine("echo test")
ptty.ExpectMatch("test")
ptty.WriteLine("exit")
err = session.Wait()
require.NoError(t, err)
})
t.Run("LocalForwarding", func(t *testing.T) {
t.Parallel()
random, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
_ = random.Close()
tcpAddr, valid := random.Addr().(*net.TCPAddr)
require.True(t, valid)
randomPort := tcpAddr.Port
local, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer local.Close()
tcpAddr, valid = local.Addr().(*net.TCPAddr)
require.True(t, valid)
localPort := tcpAddr.Port
done := make(chan struct{})
go func() {
conn, err := local.Accept()
assert.NoError(t, err)
_ = conn.Close()
close(done)
}()
err = setupSSHCommand(t, []string{"-L", fmt.Sprintf("%d:127.0.0.1:%d", randomPort, localPort)}, []string{"echo", "test"}).Start()
require.NoError(t, err)
conn, err := net.Dial("tcp", "127.0.0.1:"+strconv.Itoa(localPort))
require.NoError(t, err)
conn.Close()
<-done
})
t.Run("SFTP", func(t *testing.T) {
t.Parallel()
sshClient, err := setupAgent(t, agent.Metadata{}, 0).SSHClient()
require.NoError(t, err)
client, err := sftp.NewClient(sshClient)
require.NoError(t, err)
tempFile := filepath.Join(t.TempDir(), "sftp")
file, err := client.Create(tempFile)
require.NoError(t, err)
err = file.Close()
require.NoError(t, err)
_, err = os.Stat(tempFile)
require.NoError(t, err)
})
t.Run("EnvironmentVariables", func(t *testing.T) {
t.Parallel()
key := "EXAMPLE"
value := "value"
session := setupSSHSession(t, agent.Metadata{
EnvironmentVariables: map[string]string{
key: value,
},
})
command := "sh -c 'echo $" + key + "'"
if runtime.GOOS == "windows" {
command = "cmd.exe /c echo %" + key + "%"
}
output, err := session.Output(command)
require.NoError(t, err)
require.Equal(t, value, strings.TrimSpace(string(output)))
})
t.Run("StartupScript", func(t *testing.T) {
t.Parallel()
tempPath := filepath.Join(os.TempDir(), "content.txt")
content := "somethingnice"
setupAgent(t, agent.Metadata{
StartupScript: fmt.Sprintf("echo %s > %s", content, tempPath),
}, 0)
var gotContent string
require.Eventually(t, func() bool {
content, err := os.ReadFile(tempPath)
if err != nil {
return false
}
if len(content) == 0 {
return false
}
if runtime.GOOS == "windows" {
// Windows uses UTF16! 🪟🪟🪟
content, _, err = transform.Bytes(unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder(), content)
require.NoError(t, err)
}
gotContent = string(content)
return true
}, 15*time.Second, 100*time.Millisecond)
require.Equal(t, content, strings.TrimSpace(gotContent))
})
t.Run("ReconnectingPTY", func(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
// This might be our implementation, or ConPTY itself.
// It's difficult to find extensive tests for it, so
// it seems like it could be either.
t.Skip("ConPTY appears to be inconsistent on Windows.")
}
conn := setupAgent(t, agent.Metadata{}, 0)
id := uuid.NewString()
netConn, err := conn.ReconnectingPTY(id, 100, 100, "/bin/bash")
require.NoError(t, err)
bufRead := bufio.NewReader(netConn)
// Brief pause to reduce the likelihood that we send keystrokes while
// the shell is simultaneously sending a prompt.
time.Sleep(100 * time.Millisecond)
data, err := json.Marshal(agent.ReconnectingPTYRequest{
Data: "echo test\r\n",
})
require.NoError(t, err)
_, err = netConn.Write(data)
require.NoError(t, err)
expectLine := func(matcher func(string) bool) {
for {
line, err := bufRead.ReadString('\n')
require.NoError(t, err)
if matcher(line) {
break
}
}
}
matchEchoCommand := func(line string) bool {
return strings.Contains(line, "echo test")
}
matchEchoOutput := func(line string) bool {
return strings.Contains(line, "test") && !strings.Contains(line, "echo")
}
// Once for typing the command...
expectLine(matchEchoCommand)
// And another time for the actual output.
expectLine(matchEchoOutput)
_ = netConn.Close()
netConn, err = conn.ReconnectingPTY(id, 100, 100, "/bin/bash")
require.NoError(t, err)
bufRead = bufio.NewReader(netConn)
// Same output again!
expectLine(matchEchoCommand)
expectLine(matchEchoOutput)
})
t.Run("Dial", func(t *testing.T) {
t.Parallel()
cases := []struct {
name string
setup func(t *testing.T) net.Listener
}{
{
name: "TCP",
setup: func(t *testing.T) net.Listener {
l, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err, "create TCP listener")
return l
},
},
{
name: "UDP",
setup: func(t *testing.T) net.Listener {
addr := net.UDPAddr{
IP: net.ParseIP("127.0.0.1"),
Port: 0,
}
l, err := udp.Listen("udp", &addr)
require.NoError(t, err, "create UDP listener")
return l
},
},
{
name: "Unix",
setup: func(t *testing.T) net.Listener {
if runtime.GOOS == "windows" {
t.Skip("Unix socket forwarding isn't supported on Windows")
}
tmpDir, err := os.MkdirTemp("", "coderd_agent_test_")
require.NoError(t, err, "create temp dir for unix listener")
t.Cleanup(func() {
_ = os.RemoveAll(tmpDir)
})
l, err := net.Listen("unix", filepath.Join(tmpDir, "test.sock"))
require.NoError(t, err, "create UDP listener")
return l
},
},
}
for _, c := range cases {
c := c
t.Run(c.name, func(t *testing.T) {
t.Parallel()
// Setup listener
l := c.setup(t)
defer l.Close()
go func() {
for {
c, err := l.Accept()
if err != nil {
return
}
go testAccept(t, c)
}
}()
// Dial the listener over WebRTC twice and test out of order
conn := setupAgent(t, agent.Metadata{}, 0)
conn1, err := conn.DialContext(context.Background(), l.Addr().Network(), l.Addr().String())
require.NoError(t, err)
defer conn1.Close()
conn2, err := conn.DialContext(context.Background(), l.Addr().Network(), l.Addr().String())
require.NoError(t, err)
defer conn2.Close()
testDial(t, conn2)
testDial(t, conn1)
})
}
})
t.Run("DialError", func(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
// This test uses Unix listeners so we can very easily ensure that
// no other tests decide to listen on the same random port we
// picked.
t.Skip("this test is unsupported on Windows")
return
}
tmpDir, err := os.MkdirTemp("", "coderd_agent_test_")
require.NoError(t, err, "create temp dir")
t.Cleanup(func() {
_ = os.RemoveAll(tmpDir)
})
// Try to dial the non-existent Unix socket over WebRTC
conn := setupAgent(t, agent.Metadata{}, 0)
netConn, err := conn.DialContext(context.Background(), "unix", filepath.Join(tmpDir, "test.sock"))
require.Error(t, err)
require.ErrorContains(t, err, "remote dial error")
require.ErrorContains(t, err, "no such file")
require.Nil(t, netConn)
})
}
func setup(t *testing.T) proto.DRPCPeerBrokerClient {
func setupSSHCommand(t *testing.T, beforeArgs []string, afterArgs []string) *exec.Cmd {
agentConn := setupAgent(t, agent.Metadata{}, 0)
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
go func() {
for {
conn, err := listener.Accept()
if err != nil {
return
}
ssh, err := agentConn.SSH()
assert.NoError(t, err)
go io.Copy(conn, ssh)
go io.Copy(ssh, conn)
}
}()
t.Cleanup(func() {
_ = listener.Close()
})
tcpAddr, valid := listener.Addr().(*net.TCPAddr)
require.True(t, valid)
args := append(beforeArgs,
"-o", "HostName "+tcpAddr.IP.String(),
"-o", "Port "+strconv.Itoa(tcpAddr.Port),
"-o", "StrictHostKeyChecking=no", "host")
args = append(args, afterArgs...)
return exec.Command("ssh", args...)
}
func setupSSHSession(t *testing.T, options agent.Metadata) *ssh.Session {
sshClient, err := setupAgent(t, options, 0).SSHClient()
require.NoError(t, err)
session, err := sshClient.NewSession()
require.NoError(t, err)
return session
}
func setupAgent(t *testing.T, metadata agent.Metadata, ptyTimeout time.Duration) *agent.Conn {
client, server := provisionersdk.TransportPipe()
closer := agent.New(func(ctx context.Context, opts *peer.ConnOptions) (*peerbroker.Listener, error) {
return peerbroker.Listen(server, nil, opts)
}, &peer.ConnOptions{
Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
closer := agent.New(func(ctx context.Context, logger slog.Logger) (agent.Metadata, *peerbroker.Listener, error) {
listener, err := peerbroker.Listen(server, nil)
return metadata, listener, err
}, &agent.Options{
Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
ReconnectingPTYTimeout: ptyTimeout,
})
t.Cleanup(func() {
_ = client.Close()
_ = server.Close()
_ = closer.Close()
})
return proto.NewDRPCPeerBrokerClient(provisionersdk.Conn(client))
api := proto.NewDRPCPeerBrokerClient(provisionersdk.Conn(client))
stream, err := api.NegotiateConnection(context.Background())
assert.NoError(t, err)
conn, err := peerbroker.Dial(stream, []webrtc.ICEServer{}, &peer.ConnOptions{
Logger: slogtest.Make(t, nil),
})
require.NoError(t, err)
t.Cleanup(func() {
_ = conn.Close()
})
return &agent.Conn{
Negotiator: api,
Conn: conn,
}
}
var dialTestPayload = []byte("dean-was-here123")
func testDial(t *testing.T, c net.Conn) {
t.Helper()
assertWritePayload(t, c, dialTestPayload)
assertReadPayload(t, c, dialTestPayload)
}
func testAccept(t *testing.T, c net.Conn) {
t.Helper()
defer c.Close()
assertReadPayload(t, c, dialTestPayload)
assertWritePayload(t, c, dialTestPayload)
}
func assertReadPayload(t *testing.T, r io.Reader, payload []byte) {
b := make([]byte, len(payload)+16)
n, err := r.Read(b)
assert.NoError(t, err, "read payload")
assert.Equal(t, len(payload), n, "read payload length does not match")
assert.Equal(t, payload, b[:n])
}
func assertWritePayload(t *testing.T, w io.Writer, payload []byte) {
n, err := w.Write(payload)
assert.NoError(t, err, "write payload")
assert.Equal(t, len(payload), n, "payload length does not match")
}
+64 -2
View File
@@ -2,7 +2,11 @@ package agent
import (
"context"
"encoding/json"
"fmt"
"net"
"net/url"
"strings"
"golang.org/x/crypto/ssh"
"golang.org/x/xerrors"
@@ -11,6 +15,14 @@ import (
"github.com/coder/coder/peerbroker/proto"
)
// ReconnectingPTYRequest is sent from the client to the server
// to pipe data to a PTY.
type ReconnectingPTYRequest struct {
Data string `json:"data"`
Height uint16 `json:"height"`
Width uint16 `json:"width"`
}
// Conn wraps a peer connection with helper functions to
// communicate with the agent.
type Conn struct {
@@ -20,10 +32,24 @@ type Conn struct {
*peer.Conn
}
// ReconnectingPTY returns a connection serving a TTY that can
// be reconnected to via ID.
//
// The command is optional and defaults to start a shell.
func (c *Conn) ReconnectingPTY(id string, height, width uint16, command string) (net.Conn, error) {
channel, err := c.CreateChannel(context.Background(), fmt.Sprintf("%s:%d:%d:%s", id, height, width, command), &peer.ChannelOptions{
Protocol: ProtocolReconnectingPTY,
})
if err != nil {
return nil, xerrors.Errorf("pty: %w", err)
}
return channel.NetConn(), nil
}
// SSH dials the built-in SSH server.
func (c *Conn) SSH() (net.Conn, error) {
channel, err := c.Dial(context.Background(), "ssh", &peer.ChannelOptions{
Protocol: "ssh",
channel, err := c.CreateChannel(context.Background(), "ssh", &peer.ChannelOptions{
Protocol: ProtocolSSH,
})
if err != nil {
return nil, xerrors.Errorf("dial: %w", err)
@@ -50,6 +76,42 @@ func (c *Conn) SSHClient() (*ssh.Client, error) {
return ssh.NewClient(sshConn, channels, requests), nil
}
// DialContext dials an arbitrary protocol+address from inside the workspace and
// proxies it through the provided net.Conn.
func (c *Conn) DialContext(ctx context.Context, network string, addr string) (net.Conn, error) {
u := &url.URL{
Scheme: network,
}
if strings.HasPrefix(network, "unix") {
u.Path = addr
} else {
u.Host = addr
}
channel, err := c.CreateChannel(ctx, u.String(), &peer.ChannelOptions{
Protocol: ProtocolDial,
Unordered: strings.HasPrefix(network, "udp"),
})
if err != nil {
return nil, xerrors.Errorf("create datachannel: %w", err)
}
// The first message written from the other side is a JSON payload
// containing the dial error.
dec := json.NewDecoder(channel)
var res dialResponse
err = dec.Decode(&res)
if err != nil {
return nil, xerrors.Errorf("failed to decode initial packet: %w", err)
}
if res.Error != "" {
_ = channel.Close()
return nil, xerrors.Errorf("remote dial error: %v", res.Error)
}
return channel.NetConn(), nil
}
func (c *Conn) Close() error {
_ = c.Negotiator.DRPCConn().Close()
return c.Conn.Close()
+1 -3
View File
@@ -3,8 +3,6 @@ package usershell
import "os"
// Get returns the $SHELL environment variable.
// TODO: This should use "dscl" to fetch the proper value. See:
// https://stackoverflow.com/questions/16375519/how-to-get-the-default-shell
func Get(username string) (string, error) {
func Get(_ string) (string, error) {
return os.Getenv("SHELL"), nil
}
@@ -1,7 +1,7 @@
package buildinfo
import (
"path"
"fmt"
"runtime/debug"
"sync"
"time"
@@ -14,6 +14,12 @@ var (
buildInfoValid bool
readBuildInfo sync.Once
externalURL string
readExternalURL sync.Once
version string
readVersion sync.Once
// Injected with ldflags at build!
tag string
)
@@ -21,29 +27,41 @@ var (
// Version returns the semantic version of the build.
// Use golang.org/x/mod/semver to compare versions.
func Version() string {
revision, valid := revision()
if valid {
revision = "+" + revision[:7]
}
if tag == "" {
return "v0.0.0-devel" + revision
}
if semver.Build(tag) == "" {
tag += revision
}
return "v" + tag
readVersion.Do(func() {
revision, valid := revision()
if valid {
revision = "+" + revision[:7]
}
if tag == "" {
// This occurs when the tag hasn't been injected,
// like when using "go run".
version = "v0.0.0-devel" + revision
return
}
version = "v" + tag
// The tag must be prefixed with "v" otherwise the
// semver library will return an empty string.
if semver.Build(version) == "" {
version += revision
}
})
return version
}
// ExternalURL returns a URL referencing the current Coder version.
// For production builds, this will link directly to a release.
// For development builds, this will link to a commit.
func ExternalURL() string {
repo := "https://github.com/coder/coder"
revision, valid := revision()
if !valid {
return repo
}
return path.Join(repo, "commit", revision)
readExternalURL.Do(func() {
repo := "https://github.com/coder/coder"
revision, valid := revision()
if !valid {
externalURL = repo
return
}
externalURL = fmt.Sprintf("%s/commit/%s", repo, revision)
})
return externalURL
}
// Time returns when the Git revision was published.
@@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/mod/semver"
"github.com/coder/coder/cli/buildinfo"
"github.com/coder/coder/buildinfo"
)
func TestBuildInfo(t *testing.T) {
+38 -15
View File
@@ -4,6 +4,8 @@ import (
"context"
"net/http"
"net/url"
"os"
"path/filepath"
"time"
"cloud.google.com/go/compute/metadata"
@@ -16,29 +18,35 @@ import (
"github.com/coder/coder/agent"
"github.com/coder/coder/cli/cliflag"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/peer"
"github.com/coder/retry"
"gopkg.in/natefinch/lumberjack.v2"
)
func workspaceAgent() *cobra.Command {
var (
rawURL string
auth string
token string
auth string
)
cmd := &cobra.Command{
Use: "agent",
// This command isn't useful to manually execute.
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
if rawURL == "" {
return xerrors.New("CODER_URL must be set")
rawURL, err := cmd.Flags().GetString(varAgentURL)
if err != nil {
return xerrors.Errorf("CODER_AGENT_URL must be set: %w", err)
}
coderURL, err := url.Parse(rawURL)
if err != nil {
return xerrors.Errorf("parse %q: %w", rawURL, err)
}
logger := slog.Make(sloghuman.Sink(cmd.OutOrStdout())).Leveled(slog.LevelDebug)
logWriter := &lumberjack.Logger{
Filename: filepath.Join(os.TempDir(), "coder-agent.log"),
MaxSize: 5, // MB
}
defer logWriter.Close()
logger := slog.Make(sloghuman.Sink(cmd.ErrOrStderr()), sloghuman.Sink(logWriter)).Leveled(slog.LevelDebug)
client := codersdk.New(coderURL)
// exchangeToken returns a session token.
@@ -47,8 +55,9 @@ func workspaceAgent() *cobra.Command {
var exchangeToken func(context.Context) (codersdk.WorkspaceAgentAuthenticateResponse, error)
switch auth {
case "token":
if token == "" {
return xerrors.Errorf("CODER_TOKEN must be set for token auth")
token, err := cmd.Flags().GetString(varAgentToken)
if err != nil {
return xerrors.Errorf("CODER_AGENT_TOKEN must be set for token auth: %w", err)
}
client.SessionToken = token
case "google-instance-identity":
@@ -77,7 +86,19 @@ func workspaceAgent() *cobra.Command {
return client.AuthWorkspaceAWSInstanceIdentity(ctx)
}
case "azure-instance-identity":
return xerrors.Errorf("not implemented")
// This is *only* done for testing to mock client authentication.
// This will never be set in a production scenario.
var azureClient *http.Client
azureClientRaw := cmd.Context().Value("azure-client")
if azureClientRaw != nil {
azureClient, _ = azureClientRaw.(*http.Client)
if azureClient != nil {
client.HTTPClient = azureClient
}
}
exchangeToken = func(ctx context.Context) (codersdk.WorkspaceAgentAuthenticateResponse, error) {
return client.AuthWorkspaceAzureInstanceIdentity(ctx)
}
}
if exchangeToken != nil {
@@ -104,17 +125,19 @@ func workspaceAgent() *cobra.Command {
}
}
closer := agent.New(client.ListenWorkspaceAgent, &peer.ConnOptions{
closer := agent.New(client.ListenWorkspaceAgent, &agent.Options{
Logger: logger,
EnvironmentVariables: map[string]string{
// Override the "CODER_AGENT_TOKEN" variable in all
// shells so "gitssh" works!
"CODER_AGENT_TOKEN": client.SessionToken,
},
})
<-cmd.Context().Done()
return closer.Close()
},
}
cliflag.StringVarP(cmd.Flags(), &auth, "auth", "", "CODER_AUTH", "token", "Specify the authentication type to use for the agent")
cliflag.StringVarP(cmd.Flags(), &rawURL, "url", "", "CODER_URL", "", "Specify the URL to access Coder")
cliflag.StringVarP(cmd.Flags(), &token, "token", "", "CODER_TOKEN", "", "Specifies the authentication token to access Coder")
cliflag.StringVarP(cmd.Flags(), &auth, "auth", "", "CODER_AGENT_AUTH", "token", "Specify the authentication type to use for the agent")
return cmd
}
+181
View File
@@ -0,0 +1,181 @@
package cli_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
)
func TestWorkspaceAgent(t *testing.T) {
t.Parallel()
t.Run("Azure", func(t *testing.T) {
t.Parallel()
instanceID := "instanceidentifier"
certificates, metadataClient := coderdtest.NewAzureInstanceIdentity(t, instanceID)
client := coderdtest.New(t, &coderdtest.Options{
AzureCertificates: certificates,
IncludeProvisionerD: true,
})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "somename",
Type: "someinstance",
Agents: []*proto.Agent{{
Auth: &proto.Agent_InstanceId{
InstanceId: instanceID,
},
}},
}},
},
},
}},
})
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)
cmd, _ := clitest.New(t, "agent", "--auth", "azure-instance-identity", "--agent-url", client.URL.String())
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
errC := make(chan error)
go func() {
// A linting error occurs for weakly typing the context value here.
//nolint // The above seems reasonable for a one-off test.
ctx := context.WithValue(ctx, "azure-client", metadataClient)
errC <- cmd.ExecuteContext(ctx)
}()
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
require.NoError(t, err)
dialer, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, nil)
require.NoError(t, err)
defer dialer.Close()
_, err = dialer.Ping()
require.NoError(t, err)
cancelFunc()
err = <-errC
require.NoError(t, err)
})
t.Run("AWS", func(t *testing.T) {
t.Parallel()
instanceID := "instanceidentifier"
certificates, metadataClient := coderdtest.NewAWSInstanceIdentity(t, instanceID)
client := coderdtest.New(t, &coderdtest.Options{
AWSCertificates: certificates,
IncludeProvisionerD: true,
})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "somename",
Type: "someinstance",
Agents: []*proto.Agent{{
Auth: &proto.Agent_InstanceId{
InstanceId: instanceID,
},
}},
}},
},
},
}},
})
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)
cmd, _ := clitest.New(t, "agent", "--auth", "aws-instance-identity", "--agent-url", client.URL.String())
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
errC := make(chan error)
go func() {
// A linting error occurs for weakly typing the context value here.
//nolint // The above seems reasonable for a one-off test.
ctx := context.WithValue(ctx, "aws-client", metadataClient)
errC <- cmd.ExecuteContext(ctx)
}()
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
require.NoError(t, err)
dialer, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, nil)
require.NoError(t, err)
defer dialer.Close()
_, err = dialer.Ping()
require.NoError(t, err)
cancelFunc()
err = <-errC
require.NoError(t, err)
})
t.Run("GoogleCloud", func(t *testing.T) {
t.Parallel()
instanceID := "instanceidentifier"
validator, metadata := coderdtest.NewGoogleInstanceIdentity(t, instanceID, false)
client := coderdtest.New(t, &coderdtest.Options{
GoogleTokenValidator: validator,
IncludeProvisionerD: true,
})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "somename",
Type: "someinstance",
Agents: []*proto.Agent{{
Auth: &proto.Agent_InstanceId{
InstanceId: instanceID,
},
}},
}},
},
},
}},
})
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)
cmd, _ := clitest.New(t, "agent", "--auth", "google-instance-identity", "--agent-url", client.URL.String())
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
errC := make(chan error)
go func() {
// A linting error occurs for weakly typing the context value here.
//nolint // The above seems reasonable for a one-off test.
ctx := context.WithValue(ctx, "gcp-client", metadata)
errC <- cmd.ExecuteContext(ctx)
}()
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
require.NoError(t, err)
dialer, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, nil)
require.NoError(t, err)
defer dialer.Close()
_, err = dialer.Ping()
require.NoError(t, err)
cancelFunc()
err = <-errC
require.NoError(t, err)
})
}
+155
View File
@@ -0,0 +1,155 @@
package cli
import (
"fmt"
"os"
"time"
"github.com/spf13/cobra"
"github.com/coder/coder/coderd/autobuild/schedule"
"github.com/coder/coder/codersdk"
)
const autostartDescriptionLong = `To have your workspace build automatically at a regular time you can enable autostart.
When enabling autostart, provide the minute, hour, and day(s) of week.
The default schedule is at 09:00 in your local timezone (TZ env, UTC by default).
`
func autostart() *cobra.Command {
autostartCmd := &cobra.Command{
Annotations: workspaceCommand,
Use: "autostart enable <workspace>",
Short: "schedule a workspace to automatically start at a regular time",
Long: autostartDescriptionLong,
Example: "coder autostart enable my-workspace --minute 30 --hour 9 --days 1-5 --tz Europe/Dublin",
}
autostartCmd.AddCommand(autostartShow())
autostartCmd.AddCommand(autostartEnable())
autostartCmd.AddCommand(autostartDisable())
return autostartCmd
}
func autostartShow() *cobra.Command {
cmd := &cobra.Command{
Use: "show <workspace_name>",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
return err
}
workspace, err := namedWorkspace(cmd, client, args[0])
if err != nil {
return err
}
if workspace.AutostartSchedule == nil || *workspace.AutostartSchedule == "" {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "not enabled\n")
return nil
}
validSchedule, err := schedule.Weekly(*workspace.AutostartSchedule)
if err != nil {
// This should never happen.
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "invalid autostart schedule %q for workspace %s: %s\n", *workspace.AutostartSchedule, workspace.Name, err.Error())
return nil
}
next := validSchedule.Next(time.Now())
loc, _ := time.LoadLocation(validSchedule.Timezone())
_, _ = fmt.Fprintf(cmd.OutOrStdout(),
"schedule: %s\ntimezone: %s\nnext: %s\n",
validSchedule.Cron(),
validSchedule.Timezone(),
next.In(loc),
)
return nil
},
}
return cmd
}
func autostartEnable() *cobra.Command {
// yes some of these are technically numbers but the cron library will do that work
var autostartMinute string
var autostartHour string
var autostartDayOfWeek string
var autostartTimezone string
cmd := &cobra.Command{
Use: "enable <workspace_name> <schedule>",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
return err
}
spec := fmt.Sprintf("CRON_TZ=%s %s %s * * %s", autostartTimezone, autostartMinute, autostartHour, autostartDayOfWeek)
validSchedule, err := schedule.Weekly(spec)
if err != nil {
return err
}
workspace, err := namedWorkspace(cmd, client, args[0])
if err != nil {
return err
}
err = client.UpdateWorkspaceAutostart(cmd.Context(), workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
Schedule: &spec,
})
if err != nil {
return err
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nThe %s workspace will automatically start at %s.\n\n", workspace.Name, validSchedule.Next(time.Now()))
return nil
},
}
cmd.Flags().StringVar(&autostartMinute, "minute", "0", "autostart minute")
cmd.Flags().StringVar(&autostartHour, "hour", "9", "autostart hour")
cmd.Flags().StringVar(&autostartDayOfWeek, "days", "1-5", "autostart day(s) of week")
tzEnv := os.Getenv("TZ")
if tzEnv == "" {
tzEnv = "UTC"
}
cmd.Flags().StringVar(&autostartTimezone, "tz", tzEnv, "autostart timezone")
return cmd
}
func autostartDisable() *cobra.Command {
return &cobra.Command{
Use: "disable <workspace_name>",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
return err
}
workspace, err := namedWorkspace(cmd, client, args[0])
if err != nil {
return err
}
err = client.UpdateWorkspaceAutostart(cmd.Context(), workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
Schedule: nil,
})
if err != nil {
return err
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nThe %s workspace will no longer automatically start.\n\n", workspace.Name)
return nil
},
}
}
+161
View File
@@ -0,0 +1,161 @@
package cli_test
import (
"bytes"
"context"
"fmt"
"os"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/util/ptr"
"github.com/coder/coder/codersdk"
)
func TestAutostart(t *testing.T) {
t.Parallel()
t.Run("ShowOK", func(t *testing.T) {
t.Parallel()
var (
ctx = context.Background()
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user = coderdtest.CreateFirstUser(t, client)
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID)
cmdArgs = []string{"autostart", "show", workspace.Name}
sched = "CRON_TZ=Europe/Dublin 30 17 * * 1-5"
stdoutBuf = &bytes.Buffer{}
)
err := client.UpdateWorkspaceAutostart(ctx, workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
Schedule: ptr.Ref(sched),
})
require.NoError(t, err)
cmd, root := clitest.New(t, cmdArgs...)
clitest.SetupConfig(t, client, root)
cmd.SetOut(stdoutBuf)
err = cmd.Execute()
require.NoError(t, err, "unexpected error")
// CRON_TZ gets stripped
require.Contains(t, stdoutBuf.String(), "schedule: 30 17 * * 1-5")
})
t.Run("EnableDisableOK", func(t *testing.T) {
t.Parallel()
var (
ctx = context.Background()
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user = coderdtest.CreateFirstUser(t, client)
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID)
tz = "Europe/Dublin"
cmdArgs = []string{"autostart", "enable", workspace.Name, "--minute", "30", "--hour", "9", "--days", "1-5", "--tz", tz}
sched = "CRON_TZ=Europe/Dublin 30 9 * * 1-5"
stdoutBuf = &bytes.Buffer{}
)
cmd, root := clitest.New(t, cmdArgs...)
clitest.SetupConfig(t, client, root)
cmd.SetOut(stdoutBuf)
err := cmd.Execute()
require.NoError(t, err, "unexpected error")
require.Contains(t, stdoutBuf.String(), "will automatically start at", "unexpected output")
// Ensure autostart schedule updated
updated, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "fetch updated workspace")
require.Equal(t, sched, *updated.AutostartSchedule, "expected autostart schedule to be set")
// Disable schedule
cmd, root = clitest.New(t, "autostart", "disable", workspace.Name)
clitest.SetupConfig(t, client, root)
cmd.SetOut(stdoutBuf)
err = cmd.Execute()
require.NoError(t, err, "unexpected error")
require.Contains(t, stdoutBuf.String(), "will no longer automatically start", "unexpected output")
// Ensure autostart schedule updated
updated, err = client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "fetch updated workspace")
require.Nil(t, updated.AutostartSchedule, "expected autostart schedule to not be set")
})
t.Run("Enable_NotFound", func(t *testing.T) {
t.Parallel()
var (
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user = coderdtest.CreateFirstUser(t, client)
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
)
cmd, root := clitest.New(t, "autostart", "enable", "doesnotexist")
clitest.SetupConfig(t, client, root)
err := cmd.Execute()
require.ErrorContains(t, err, "status code 403: forbidden", "unexpected error")
})
t.Run("Disable_NotFound", func(t *testing.T) {
t.Parallel()
var (
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user = coderdtest.CreateFirstUser(t, client)
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
)
cmd, root := clitest.New(t, "autostart", "disable", "doesnotexist")
clitest.SetupConfig(t, client, root)
err := cmd.Execute()
require.ErrorContains(t, err, "status code 403: forbidden", "unexpected error")
})
t.Run("Enable_DefaultSchedule", func(t *testing.T) {
t.Parallel()
var (
ctx = context.Background()
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user = coderdtest.CreateFirstUser(t, client)
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID)
)
// check current TZ env var
currTz := os.Getenv("TZ")
if currTz == "" {
currTz = "UTC"
}
expectedSchedule := fmt.Sprintf("CRON_TZ=%s 0 9 * * 1-5", currTz)
cmd, root := clitest.New(t, "autostart", "enable", workspace.Name)
clitest.SetupConfig(t, client, root)
err := cmd.Execute()
require.NoError(t, err, "unexpected error")
// Ensure nothing happened
updated, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err, "fetch updated workspace")
require.Equal(t, expectedSchedule, *updated.AutostartSchedule, "expected default autostart schedule")
})
}
+88
View File
@@ -0,0 +1,88 @@
package cli
import (
"fmt"
"strings"
"time"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
"github.com/coder/coder/codersdk"
)
const (
bumpDescriptionLong = `To extend the autostop deadline for a workspace.
If no unit is specified in the duration, we assume minutes.`
defaultBumpDuration = 90 * time.Minute
)
func bump() *cobra.Command {
bumpCmd := &cobra.Command{
Args: cobra.RangeArgs(1, 2),
Annotations: workspaceCommand,
Use: "bump <workspace-name> [duration]",
Short: "Extend the autostop deadline for a workspace.",
Long: bumpDescriptionLong,
Example: "coder bump my-workspace 90m",
RunE: func(cmd *cobra.Command, args []string) error {
bumpDuration := defaultBumpDuration
if len(args) > 1 {
d, err := tryParseDuration(args[1])
if err != nil {
return err
}
bumpDuration = d
}
if bumpDuration < time.Minute {
return xerrors.New("minimum bump duration is 1 minute")
}
client, err := createClient(cmd)
if err != nil {
return xerrors.Errorf("create client: %w", err)
}
workspace, err := namedWorkspace(cmd, client, args[0])
if err != nil {
return xerrors.Errorf("get workspace: %w", err)
}
if workspace.LatestBuild.Deadline.IsZero() {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "no deadline set\n")
return nil
}
newDeadline := workspace.LatestBuild.Deadline.Add(bumpDuration)
if err := client.PutExtendWorkspace(cmd.Context(), workspace.ID, codersdk.PutExtendWorkspaceRequest{
Deadline: newDeadline,
}); err != nil {
return err
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Workspace %q will now stop at %s\n", workspace.Name, newDeadline.Format(time.RFC3339))
return nil
},
}
return bumpCmd
}
func tryParseDuration(raw string) (time.Duration, error) {
// If the user input a raw number, assume minutes
if isDigit(raw) {
raw = raw + "m"
}
d, err := time.ParseDuration(raw)
if err != nil {
return 0, err
}
return d, nil
}
func isDigit(s string) bool {
return strings.IndexFunc(s, func(c rune) bool {
return c < '0' || c > '9'
}) == -1
}
+218
View File
@@ -0,0 +1,218 @@
package cli_test
import (
"bytes"
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
)
func TestBump(t *testing.T) {
t.Parallel()
t.Run("BumpOKDefault", func(t *testing.T) {
t.Parallel()
// Given: we have a workspace
var (
err error
ctx = context.Background()
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user = coderdtest.CreateFirstUser(t, client)
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID)
cmdArgs = []string{"bump", workspace.Name}
stdoutBuf = &bytes.Buffer{}
)
// Given: we wait for the workspace to be built
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
workspace, err = client.Workspace(ctx, workspace.ID)
require.NoError(t, err)
expectedDeadline := workspace.LatestBuild.Deadline.Add(90 * time.Minute)
// Assert test invariant: workspace build has a deadline set equal to now plus ttl
initDeadline := time.Now().Add(time.Duration(*workspace.TTLMillis) * time.Millisecond)
require.WithinDuration(t, initDeadline, workspace.LatestBuild.Deadline, time.Minute)
cmd, root := clitest.New(t, cmdArgs...)
clitest.SetupConfig(t, client, root)
cmd.SetOut(stdoutBuf)
// When: we execute `coder bump <workspace>`
err = cmd.ExecuteContext(ctx)
require.NoError(t, err, "unexpected error")
// Then: the deadline of the latest build is updated
updated, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err)
require.WithinDuration(t, expectedDeadline, updated.LatestBuild.Deadline, time.Minute)
})
t.Run("BumpSpecificDuration", func(t *testing.T) {
t.Parallel()
// Given: we have a workspace
var (
err error
ctx = context.Background()
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user = coderdtest.CreateFirstUser(t, client)
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID)
cmdArgs = []string{"bump", workspace.Name, "30"}
stdoutBuf = &bytes.Buffer{}
)
// Given: we wait for the workspace to be built
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
workspace, err = client.Workspace(ctx, workspace.ID)
require.NoError(t, err)
expectedDeadline := workspace.LatestBuild.Deadline.Add(30 * time.Minute)
// Assert test invariant: workspace build has a deadline set equal to now plus ttl
initDeadline := time.Now().Add(time.Duration(*workspace.TTLMillis) * time.Millisecond)
require.WithinDuration(t, initDeadline, workspace.LatestBuild.Deadline, time.Minute)
cmd, root := clitest.New(t, cmdArgs...)
clitest.SetupConfig(t, client, root)
cmd.SetOut(stdoutBuf)
// When: we execute `coder bump workspace <number without units>`
err = cmd.ExecuteContext(ctx)
require.NoError(t, err)
// Then: the deadline of the latest build is updated assuming the units are minutes
updated, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err)
require.WithinDuration(t, expectedDeadline, updated.LatestBuild.Deadline, time.Minute)
})
t.Run("BumpInvalidDuration", func(t *testing.T) {
t.Parallel()
// Given: we have a workspace
var (
err error
ctx = context.Background()
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user = coderdtest.CreateFirstUser(t, client)
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID)
cmdArgs = []string{"bump", workspace.Name, "kwyjibo"}
stdoutBuf = &bytes.Buffer{}
)
// Given: we wait for the workspace to be built
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
workspace, err = client.Workspace(ctx, workspace.ID)
require.NoError(t, err)
// Assert test invariant: workspace build has a deadline set equal to now plus ttl
initDeadline := time.Now().Add(time.Duration(*workspace.TTLMillis) * time.Millisecond)
require.WithinDuration(t, initDeadline, workspace.LatestBuild.Deadline, time.Minute)
cmd, root := clitest.New(t, cmdArgs...)
clitest.SetupConfig(t, client, root)
cmd.SetOut(stdoutBuf)
// When: we execute `coder bump workspace <not a number>`
err = cmd.ExecuteContext(ctx)
// Then: the command fails
require.ErrorContains(t, err, "invalid duration")
})
t.Run("BumpNoDeadline", func(t *testing.T) {
t.Parallel()
// Given: we have a workspace with no deadline set
var (
err error
ctx = context.Background()
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user = coderdtest.CreateFirstUser(t, client)
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.TTLMillis = nil
})
cmdArgs = []string{"bump", workspace.Name}
stdoutBuf = &bytes.Buffer{}
)
// Given: we wait for the workspace to build
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
workspace, err = client.Workspace(ctx, workspace.ID)
require.NoError(t, err)
// Assert test invariant: workspace has no TTL set
require.Zero(t, workspace.LatestBuild.Deadline)
require.NoError(t, err)
cmd, root := clitest.New(t, cmdArgs...)
clitest.SetupConfig(t, client, root)
cmd.SetOut(stdoutBuf)
// When: we execute `coder bump workspace``
err = cmd.ExecuteContext(ctx)
require.NoError(t, err)
// Then: nothing happens and the deadline remains unset
updated, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err)
require.Zero(t, updated.LatestBuild.Deadline)
})
t.Run("BumpMinimumDuration", func(t *testing.T) {
t.Parallel()
// Given: we have a workspace with no deadline set
var (
err error
ctx = context.Background()
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user = coderdtest.CreateFirstUser(t, client)
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace = coderdtest.CreateWorkspace(t, client, user.OrganizationID, project.ID)
cmdArgs = []string{"bump", workspace.Name, "59s"}
stdoutBuf = &bytes.Buffer{}
)
// Given: we wait for the workspace to build
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
workspace, err = client.Workspace(ctx, workspace.ID)
require.NoError(t, err)
// Assert test invariant: workspace build has a deadline set equal to now plus ttl
initDeadline := time.Now().Add(time.Duration(*workspace.TTLMillis) * time.Millisecond)
require.WithinDuration(t, initDeadline, workspace.LatestBuild.Deadline, time.Minute)
cmd, root := clitest.New(t, cmdArgs...)
clitest.SetupConfig(t, client, root)
cmd.SetOut(stdoutBuf)
// When: we execute `coder bump workspace 59s`
err = cmd.ExecuteContext(ctx)
require.ErrorContains(t, err, "minimum bump duration is 1 minute")
// Then: an error is reported and the deadline remains as before
updated, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err)
require.WithinDuration(t, workspace.LatestBuild.Deadline, updated.LatestBuild.Deadline, time.Minute)
})
}
+40
View File
@@ -14,10 +14,21 @@ import (
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/spf13/pflag"
)
// String sets a string flag on the given flag set.
func String(flagset *pflag.FlagSet, name, shorthand, env, def, usage string) {
v, ok := os.LookupEnv(env)
if !ok || v == "" {
v = def
}
flagset.StringP(name, shorthand, v, fmtUsage(usage, env))
}
// StringVarP sets a string flag on the given flag set.
func StringVarP(flagset *pflag.FlagSet, p *string, name string, shorthand string, env string, def string, usage string) {
v, ok := os.LookupEnv(env)
@@ -27,6 +38,18 @@ func StringVarP(flagset *pflag.FlagSet, p *string, name string, shorthand string
flagset.StringVarP(p, name, shorthand, v, fmtUsage(usage, env))
}
func StringArrayVarP(flagset *pflag.FlagSet, ptr *[]string, name string, shorthand string, env string, def []string, usage string) {
val, ok := os.LookupEnv(env)
if ok {
if val == "" {
def = []string{}
} else {
def = strings.Split(val, ",")
}
}
flagset.StringArrayVarP(ptr, name, shorthand, def, usage)
}
// Uint8VarP sets a uint8 flag on the given flag set.
func Uint8VarP(flagset *pflag.FlagSet, ptr *uint8, name string, shorthand string, env string, def uint8, usage string) {
val, ok := os.LookupEnv(env)
@@ -61,6 +84,23 @@ func BoolVarP(flagset *pflag.FlagSet, ptr *bool, name string, shorthand string,
flagset.BoolVarP(ptr, name, shorthand, valb, fmtUsage(usage, env))
}
// DurationVarP sets a time.Duration flag on the given flag set.
func DurationVarP(flagset *pflag.FlagSet, ptr *time.Duration, name string, shorthand string, env string, def time.Duration, usage string) {
val, ok := os.LookupEnv(env)
if !ok || val == "" {
flagset.DurationVarP(ptr, name, shorthand, def, fmtUsage(usage, env))
return
}
valb, err := time.ParseDuration(val)
if err != nil {
flagset.DurationVarP(ptr, name, shorthand, def, fmtUsage(usage, env))
return
}
flagset.DurationVarP(ptr, name, shorthand, valb, fmtUsage(usage, env))
}
func fmtUsage(u string, env string) string {
if env == "" {
return fmt.Sprintf("%s.", u)
+93 -1
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"strconv"
"testing"
"time"
"github.com/spf13/pflag"
"github.com/stretchr/testify/require"
@@ -16,6 +17,28 @@ import (
//nolint:paralleltest
func TestCliflag(t *testing.T) {
t.Run("StringDefault", func(t *testing.T) {
flagset, name, shorthand, env, usage := randomFlag()
def, _ := cryptorand.String(10)
cliflag.String(flagset, name, shorthand, env, def, usage)
got, err := flagset.GetString(name)
require.NoError(t, err)
require.Equal(t, def, got)
require.Contains(t, flagset.FlagUsages(), usage)
require.Contains(t, flagset.FlagUsages(), fmt.Sprintf(" - consumes $%s", env))
})
t.Run("StringEnvVar", func(t *testing.T) {
flagset, name, shorthand, env, usage := randomFlag()
envValue, _ := cryptorand.String(10)
t.Setenv(env, envValue)
def, _ := cryptorand.String(10)
cliflag.String(flagset, name, shorthand, env, def, usage)
got, err := flagset.GetString(name)
require.NoError(t, err)
require.Equal(t, envValue, got)
})
t.Run("StringVarPDefault", func(t *testing.T) {
var ptr string
flagset, name, shorthand, env, usage := randomFlag()
def, _ := cryptorand.String(10)
@@ -28,7 +51,7 @@ func TestCliflag(t *testing.T) {
require.Contains(t, flagset.FlagUsages(), fmt.Sprintf(" - consumes $%s", env))
})
t.Run("StringEnvVar", func(t *testing.T) {
t.Run("StringVarPEnvVar", func(t *testing.T) {
var ptr string
flagset, name, shorthand, env, usage := randomFlag()
envValue, _ := cryptorand.String(10)
@@ -54,6 +77,36 @@ func TestCliflag(t *testing.T) {
require.NotContains(t, flagset.FlagUsages(), " - consumes")
})
t.Run("StringArrayDefault", func(t *testing.T) {
var ptr []string
flagset, name, shorthand, env, usage := randomFlag()
def := []string{"hello"}
cliflag.StringArrayVarP(flagset, &ptr, name, shorthand, env, def, usage)
got, err := flagset.GetStringArray(name)
require.NoError(t, err)
require.Equal(t, def, got)
})
t.Run("StringArrayEnvVar", func(t *testing.T) {
var ptr []string
flagset, name, shorthand, env, usage := randomFlag()
t.Setenv(env, "wow,test")
cliflag.StringArrayVarP(flagset, &ptr, name, shorthand, env, nil, usage)
got, err := flagset.GetStringArray(name)
require.NoError(t, err)
require.Equal(t, []string{"wow", "test"}, got)
})
t.Run("StringArrayEnvVarEmpty", func(t *testing.T) {
var ptr []string
flagset, name, shorthand, env, usage := randomFlag()
t.Setenv(env, "")
cliflag.StringArrayVarP(flagset, &ptr, name, shorthand, env, nil, usage)
got, err := flagset.GetStringArray(name)
require.NoError(t, err)
require.Equal(t, []string{}, got)
})
t.Run("IntDefault", func(t *testing.T) {
var ptr uint8
flagset, name, shorthand, env, usage := randomFlag()
@@ -131,6 +184,45 @@ func TestCliflag(t *testing.T) {
require.NoError(t, err)
require.Equal(t, def, got)
})
t.Run("DurationDefault", func(t *testing.T) {
var ptr time.Duration
flagset, name, shorthand, env, usage := randomFlag()
def, _ := cryptorand.Duration()
cliflag.DurationVarP(flagset, &ptr, name, shorthand, env, def, usage)
got, err := flagset.GetDuration(name)
require.NoError(t, err)
require.Equal(t, def, got)
require.Contains(t, flagset.FlagUsages(), usage)
require.Contains(t, flagset.FlagUsages(), fmt.Sprintf(" - consumes $%s", env))
})
t.Run("DurationEnvVar", func(t *testing.T) {
var ptr time.Duration
flagset, name, shorthand, env, usage := randomFlag()
envValue, _ := cryptorand.Duration()
t.Setenv(env, envValue.String())
def, _ := cryptorand.Duration()
cliflag.DurationVarP(flagset, &ptr, name, shorthand, env, def, usage)
got, err := flagset.GetDuration(name)
require.NoError(t, err)
require.Equal(t, envValue, got)
})
t.Run("DurationFailParse", func(t *testing.T) {
var ptr time.Duration
flagset, name, shorthand, env, usage := randomFlag()
envValue, _ := cryptorand.String(10)
t.Setenv(env, envValue)
def, _ := cryptorand.Duration()
cliflag.DurationVarP(flagset, &ptr, name, shorthand, env, def, usage)
got, err := flagset.GetDuration(name)
require.NoError(t, err)
require.Equal(t, def, got)
})
}
func randomFlag() (*pflag.FlagSet, string, string, string, string) {
+1 -3
View File
@@ -3,7 +3,6 @@ package clitest_test
import (
"testing"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
"github.com/coder/coder/cli/clitest"
@@ -25,8 +24,7 @@ func TestCli(t *testing.T) {
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
go func() {
err := cmd.Execute()
require.NoError(t, err)
_ = cmd.Execute()
}()
pty.ExpectMatch("coder")
}
+28 -9
View File
@@ -4,6 +4,8 @@ import (
"context"
"fmt"
"io"
"os"
"os/signal"
"sync"
"time"
@@ -15,7 +17,7 @@ import (
type AgentOptions struct {
WorkspaceName string
Fetch func(context.Context) (codersdk.WorkspaceResource, error)
Fetch func(context.Context) (codersdk.WorkspaceAgent, error)
FetchInterval time.Duration
WarnInterval time.Duration
}
@@ -29,23 +31,40 @@ func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error {
opts.WarnInterval = 30 * time.Second
}
var resourceMutex sync.Mutex
resource, err := opts.Fetch(ctx)
agent, err := opts.Fetch(ctx)
if err != nil {
return xerrors.Errorf("fetch: %w", err)
}
if resource.Agent.Status == codersdk.WorkspaceAgentConnected {
if agent.Status == codersdk.WorkspaceAgentConnected {
return nil
}
if resource.Agent.Status == codersdk.WorkspaceAgentDisconnected {
if agent.Status == codersdk.WorkspaceAgentDisconnected {
opts.WarnInterval = 0
}
spin := spinner.New(spinner.CharSets[78], 100*time.Millisecond, spinner.WithColor("fgHiGreen"))
spin.Writer = writer
spin.ForceOutput = true
spin.Suffix = " Waiting for connection from " + Styles.Field.Render(resource.Type+"."+resource.Name) + "..."
spin.Suffix = " Waiting for connection from " + Styles.Field.Render(agent.Name) + "..."
spin.Start()
defer spin.Stop()
ctx, cancelFunc := context.WithCancel(ctx)
defer cancelFunc()
stopSpin := make(chan os.Signal, 1)
signal.Notify(stopSpin, os.Interrupt)
defer signal.Stop(stopSpin)
go func() {
select {
case <-ctx.Done():
return
case <-stopSpin:
}
signal.Stop(stopSpin)
spin.Stop()
// nolint:revive
os.Exit(1)
}()
ticker := time.NewTicker(opts.FetchInterval)
defer ticker.Stop()
timer := time.NewTimer(opts.WarnInterval)
@@ -59,8 +78,8 @@ func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error {
resourceMutex.Lock()
defer resourceMutex.Unlock()
message := "Don't panic, your workspace is booting up!"
if resource.Agent.Status == codersdk.WorkspaceAgentDisconnected {
message = "The workspace agent lost connection! Wait for it to reconnect or run: " + Styles.Code.Render("coder workspaces rebuild "+opts.WorkspaceName)
if agent.Status == codersdk.WorkspaceAgentDisconnected {
message = "The workspace agent lost connection! Wait for it to reconnect or run: " + Styles.Code.Render("coder rebuild "+opts.WorkspaceName)
}
// This saves the cursor position, then defers clearing from the cursor
// position to the end of the screen.
@@ -74,11 +93,11 @@ func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error {
case <-ticker.C:
}
resourceMutex.Lock()
resource, err = opts.Fetch(ctx)
agent, err = opts.Fetch(ctx)
if err != nil {
return xerrors.Errorf("fetch: %w", err)
}
if resource.Agent.Status != codersdk.WorkspaceAgentConnected {
if agent.Status != codersdk.WorkspaceAgentConnected {
resourceMutex.Unlock()
continue
}
+7 -9
View File
@@ -6,7 +6,7 @@ import (
"time"
"github.com/spf13/cobra"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/assert"
"go.uber.org/atomic"
"github.com/coder/coder/cli/cliui"
@@ -22,16 +22,14 @@ func TestAgent(t *testing.T) {
RunE: func(cmd *cobra.Command, args []string) error {
err := cliui.Agent(cmd.Context(), cmd.OutOrStdout(), cliui.AgentOptions{
WorkspaceName: "example",
Fetch: func(ctx context.Context) (codersdk.WorkspaceResource, error) {
resource := codersdk.WorkspaceResource{
Agent: &codersdk.WorkspaceAgent{
Status: codersdk.WorkspaceAgentDisconnected,
},
Fetch: func(ctx context.Context) (codersdk.WorkspaceAgent, error) {
agent := codersdk.WorkspaceAgent{
Status: codersdk.WorkspaceAgentDisconnected,
}
if disconnected.Load() {
resource.Agent.Status = codersdk.WorkspaceAgentConnected
agent.Status = codersdk.WorkspaceAgentConnected
}
return resource, nil
return agent, nil
},
FetchInterval: time.Millisecond,
WarnInterval: 10 * time.Millisecond,
@@ -45,7 +43,7 @@ func TestAgent(t *testing.T) {
go func() {
defer close(done)
err := cmd.Execute()
require.NoError(t, err)
assert.NoError(t, err)
}()
ptty.ExpectMatch("lost connection")
disconnected.Store(true)
+2
View File
@@ -26,6 +26,7 @@ var Styles = struct {
Checkmark,
Code,
Crossmark,
Error,
Field,
Keyword,
Paragraph,
@@ -41,6 +42,7 @@ var Styles = struct {
Checkmark: defaultStyles.Checkmark,
Code: defaultStyles.Code,
Crossmark: defaultStyles.Error.Copy().SetString("✘"),
Error: defaultStyles.Error,
Field: defaultStyles.Code.Copy().Foreground(lipgloss.AdaptiveColor{Light: "#000000", Dark: "#FFFFFF"}),
Keyword: defaultStyles.Keyword,
Paragraph: defaultStyles.Paragraph,
-31
View File
@@ -1,31 +0,0 @@
//go:build darwin
// +build darwin
package cliui
import (
"golang.org/x/sys/unix"
"golang.org/x/xerrors"
)
func removeLineLengthLimit(inputFD int) (func(), error) {
termios, err := unix.IoctlGetTermios(inputFD, unix.TIOCGETA)
if err != nil {
return nil, xerrors.Errorf("get termios: %w", err)
}
newState := *termios
// MacOS has a default line limit of 1024. See:
// https://unix.stackexchange.com/questions/204815/terminal-does-not-accept-pasted-or-typed-lines-of-more-than-1024-characters
//
// This removes canonical input processing, so deletes will not function
// as expected. This _seems_ fine for most use-cases, but is unfortunate.
newState.Lflag &^= unix.ICANON
err = unix.IoctlSetTermios(inputFD, unix.TIOCSETA, &newState)
if err != nil {
return nil, xerrors.Errorf("set termios: %w", err)
}
return func() {
_ = unix.IoctlSetTermios(inputFD, unix.TIOCSETA, termios)
}, nil
}
-10
View File
@@ -1,10 +0,0 @@
//go:build !darwin
// +build !darwin
package cliui
import "golang.org/x/xerrors"
func removeLineLengthLimit(_ int) (func(), error) {
return nil, xerrors.New("not implemented")
}
+19 -3
View File
@@ -10,7 +10,7 @@ import (
"github.com/coder/coder/codersdk"
)
func ParameterSchema(cmd *cobra.Command, parameterSchema codersdk.TemplateVersionParameterSchema) (string, error) {
func ParameterSchema(cmd *cobra.Command, parameterSchema codersdk.ParameterSchema) (string, error) {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), Styles.Bold.Render("var."+parameterSchema.Name))
if parameterSchema.Description != "" {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+strings.TrimSpace(strings.Join(strings.Split(parameterSchema.Description, "\n"), "\n "))+"\n")
@@ -30,6 +30,7 @@ func ParameterSchema(cmd *cobra.Command, parameterSchema codersdk.TemplateVersio
_, _ = fmt.Fprint(cmd.OutOrStdout(), "\033[1A")
value, err = Select(cmd, SelectOptions{
Options: options,
Default: parameterSchema.DefaultSourceValue,
HideSearch: true,
})
if err == nil {
@@ -37,9 +38,24 @@ func ParameterSchema(cmd *cobra.Command, parameterSchema codersdk.TemplateVersio
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+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(cmd, PromptOptions{
Text: Styles.Bold.Render("Enter a value:"),
Text: Styles.Bold.Render(text),
})
}
return value, err
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
}
+57 -32
View File
@@ -5,15 +5,14 @@ import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"os/signal"
"runtime"
"strings"
"github.com/bgentry/speakeasy"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
)
// PromptOptions supply a set of options to the prompt.
@@ -25,8 +24,21 @@ type PromptOptions struct {
Validate func(string) error
}
func AllowSkipPrompt(cmd *cobra.Command) {
cmd.Flags().BoolP("yes", "y", false, "Bypass prompts")
}
// Prompt asks the user for input.
func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
// If the cmd has a "yes" flag for skipping confirm prompts, honor it.
// If it's not a "Confirm" prompt, then don't skip. As the default value of
// "yes" makes no sense.
if opts.IsConfirm && cmd.Flags().Lookup("yes") != nil {
if skip, _ := cmd.Flags().GetBool("yes"); skip {
return "yes", nil
}
}
_, _ = fmt.Fprint(cmd.OutOrStdout(), Styles.FocusedPrompt.String()+opts.Text+" ")
if opts.IsConfirm {
opts.Default = "yes"
@@ -35,8 +47,6 @@ func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
_, _ = fmt.Fprint(cmd.OutOrStdout(), Styles.Placeholder.Render("("+opts.Default+") "))
}
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
defer signal.Stop(interrupt)
errCh := make(chan error, 1)
lineCh := make(chan string)
@@ -44,19 +54,13 @@ func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
var line string
var err error
inFile, valid := cmd.InOrStdin().(*os.File)
if opts.Secret && valid && isatty.IsTerminal(inFile.Fd()) {
inFile, isInputFile := cmd.InOrStdin().(*os.File)
if opts.Secret && isInputFile && isatty.IsTerminal(inFile.Fd()) {
// we don't install a signal handler here because speakeasy has its own
line, err = speakeasy.Ask("")
} else {
if runtime.GOOS == "darwin" && valid {
var restore func()
restore, err = removeLineLengthLimit(int(inFile.Fd()))
if err != nil {
errCh <- err
return
}
defer restore()
}
signal.Notify(interrupt, os.Interrupt)
defer signal.Stop(interrupt)
reader := bufio.NewReader(cmd.InOrStdin())
line, err = reader.ReadString('\n')
@@ -65,22 +69,7 @@ func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
// This enables multiline JSON to be pasted into an input, and have
// it parse properly.
if err == nil && (strings.HasPrefix(line, "{") || strings.HasPrefix(line, "[")) {
pipeReader, pipeWriter := io.Pipe()
defer pipeWriter.Close()
defer pipeReader.Close()
go func() {
_, _ = pipeWriter.Write([]byte(line))
_, _ = reader.WriteTo(pipeWriter)
}()
var rawMessage json.RawMessage
err := json.NewDecoder(pipeReader).Decode(&rawMessage)
if err == nil {
var buf bytes.Buffer
err = json.Compact(&buf, rawMessage)
if err == nil {
line = buf.String()
}
}
line, err = promptJSON(reader, line)
}
}
if err != nil {
@@ -99,7 +88,7 @@ func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
return "", err
case line := <-lineCh:
if opts.IsConfirm && line != "yes" && line != "y" {
return line, Canceled
return line, xerrors.Errorf("got %q: %w", line, Canceled)
}
if opts.Validate != nil {
err := opts.Validate(line)
@@ -117,3 +106,39 @@ func Prompt(cmd *cobra.Command, opts PromptOptions) (string, error) {
return "", Canceled
}
}
func promptJSON(reader *bufio.Reader, line string) (string, error) {
var data bytes.Buffer
for {
_, _ = data.WriteString(line)
var rawMessage json.RawMessage
err := json.Unmarshal(data.Bytes(), &rawMessage)
if err != nil {
if err.Error() != "unexpected end of JSON input" {
// If a real syntax error occurs in JSON,
// we want to return that partial line to the user.
err = nil
line = data.String()
break
}
// Read line-by-line. We can't use a JSON decoder
// here because it doesn't work by newline, so
// reads will block.
line, err = reader.ReadString('\n')
if err != nil {
break
}
continue
}
// Compacting the JSON makes it easier for parsing and testing.
rawJSON := data.Bytes()
data.Reset()
err = json.Compact(&data, rawJSON)
if err != nil {
return line, xerrors.Errorf("compact json: %w", err)
}
return data.String(), nil
}
return line, nil
}
+118 -12
View File
@@ -1,13 +1,20 @@
package cliui_test
import (
"bytes"
"context"
"io"
"os"
"os/exec"
"testing"
"time"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/pty"
"github.com/coder/coder/pty/ptytest"
)
@@ -20,8 +27,8 @@ func TestPrompt(t *testing.T) {
go func() {
resp, err := newPrompt(ptty, cliui.PromptOptions{
Text: "Example",
})
require.NoError(t, err)
}, nil)
assert.NoError(t, err)
msgChan <- resp
}()
ptty.ExpectMatch("Example")
@@ -37,8 +44,8 @@ func TestPrompt(t *testing.T) {
resp, err := newPrompt(ptty, cliui.PromptOptions{
Text: "Example",
IsConfirm: true,
})
require.NoError(t, err)
}, nil)
assert.NoError(t, err)
doneChan <- resp
}()
ptty.ExpectMatch("Example")
@@ -46,6 +53,47 @@ func TestPrompt(t *testing.T) {
require.Equal(t, "yes", <-doneChan)
})
t.Run("Skip", func(t *testing.T) {
t.Parallel()
ptty := ptytest.New(t)
var buf bytes.Buffer
// Copy all data written out to a buffer. When we close the ptty, we can
// no longer read from the ptty.Output(), but we can read what was
// written to the buffer.
dataRead, doneReading := context.WithTimeout(context.Background(), time.Second*2)
go func() {
// This will throw an error sometimes. The underlying ptty
// has its own cleanup routines in t.Cleanup. Instead of
// trying to control the close perfectly, just let the ptty
// double close. This error isn't important, we just
// want to know the ptty is done sending output.
_, _ = io.Copy(&buf, ptty.Output())
doneReading()
}()
doneChan := make(chan string)
go func() {
resp, err := newPrompt(ptty, cliui.PromptOptions{
Text: "ShouldNotSeeThis",
IsConfirm: true,
}, func(cmd *cobra.Command) {
cliui.AllowSkipPrompt(cmd)
cmd.SetArgs([]string{"-y"})
})
assert.NoError(t, err)
doneChan <- resp
}()
require.Equal(t, "yes", <-doneChan)
// Close the reader to end the io.Copy
require.NoError(t, ptty.Close(), "close eof reader")
// Wait for the IO copy to finish
<-dataRead.Done()
// Timeout error means the output was hanging
require.ErrorIs(t, dataRead.Err(), context.Canceled, "should be canceled")
require.Len(t, buf.Bytes(), 0, "expect no output")
})
t.Run("JSON", func(t *testing.T) {
t.Parallel()
ptty := ptytest.New(t)
@@ -53,8 +101,8 @@ func TestPrompt(t *testing.T) {
go func() {
resp, err := newPrompt(ptty, cliui.PromptOptions{
Text: "Example",
})
require.NoError(t, err)
}, nil)
assert.NoError(t, err)
doneChan <- resp
}()
ptty.ExpectMatch("Example")
@@ -69,8 +117,8 @@ func TestPrompt(t *testing.T) {
go func() {
resp, err := newPrompt(ptty, cliui.PromptOptions{
Text: "Example",
})
require.NoError(t, err)
}, nil)
assert.NoError(t, err)
doneChan <- resp
}()
ptty.ExpectMatch("Example")
@@ -85,8 +133,8 @@ func TestPrompt(t *testing.T) {
go func() {
resp, err := newPrompt(ptty, cliui.PromptOptions{
Text: "Example",
})
require.NoError(t, err)
}, nil)
assert.NoError(t, err)
doneChan <- resp
}()
ptty.ExpectMatch("Example")
@@ -97,7 +145,7 @@ func TestPrompt(t *testing.T) {
})
}
func newPrompt(ptty *ptytest.PTY, opts cliui.PromptOptions) (string, error) {
func newPrompt(ptty *ptytest.PTY, opts cliui.PromptOptions, cmdOpt func(cmd *cobra.Command)) (string, error) {
value := ""
cmd := &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
@@ -106,7 +154,65 @@ func newPrompt(ptty *ptytest.PTY, opts cliui.PromptOptions) (string, error) {
return err
},
}
cmd.SetOutput(ptty.Output())
// Optionally modify the cmd
if cmdOpt != nil {
cmdOpt(cmd)
}
cmd.SetOut(ptty.Output())
cmd.SetErr(ptty.Output())
cmd.SetIn(ptty.Input())
return value, cmd.ExecuteContext(context.Background())
}
func TestPasswordTerminalState(t *testing.T) {
if os.Getenv("TEST_SUBPROCESS") == "1" {
passwordHelper()
return
}
t.Parallel()
ptty := ptytest.New(t)
ptyWithFlags, ok := ptty.PTY.(pty.WithFlags)
if !ok {
t.Skip("unable to check PTY local echo on this platform")
}
cmd := exec.Command(os.Args[0], "-test.run=TestPasswordTerminalState") //nolint:gosec
cmd.Env = append(os.Environ(), "TEST_SUBPROCESS=1")
// connect the child process's stdio to the PTY directly, not via a pipe
cmd.Stdin = ptty.Input().Reader
cmd.Stdout = ptty.Output().Writer
cmd.Stderr = os.Stderr
err := cmd.Start()
require.NoError(t, err)
process := cmd.Process
defer process.Kill()
ptty.ExpectMatch("Password: ")
time.Sleep(100 * time.Millisecond) // wait for child process to turn off echo and start reading input
echo, err := ptyWithFlags.EchoEnabled()
require.NoError(t, err)
require.False(t, echo, "echo is on while reading password")
err = process.Signal(os.Interrupt)
require.NoError(t, err)
_, err = process.Wait()
require.NoError(t, err)
echo, err = ptyWithFlags.EchoEnabled()
require.NoError(t, err)
require.True(t, echo, "echo is off after reading password")
}
func passwordHelper() {
cmd := &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Password:",
Secret: true,
})
},
}
cmd.ExecuteContext(context.Background())
}
+33 -7
View File
@@ -1,6 +1,7 @@
package cliui
import (
"bytes"
"context"
"fmt"
"io"
@@ -12,7 +13,6 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/codersdk"
)
@@ -36,6 +36,9 @@ type ProvisionerJobOptions struct {
FetchInterval time.Duration
// Verbose determines whether debug and trace logs will be shown.
Verbose bool
// Silent determines whether log output will be shown unless there is an
// error.
Silent bool
}
// ProvisionerJob renders a provisioner job with interactive cancellation.
@@ -134,12 +137,30 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
return xerrors.Errorf("logs: %w", err)
}
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()
return err
case <-ctx.Done():
flushLogBuffer()
return ctx.Err()
case <-ticker.C:
updateJob()
@@ -161,30 +182,35 @@ func ProvisionerJob(ctx context.Context, writer io.Writer, opts ProvisionerJobOp
}
err = xerrors.New(job.Error)
jobMutex.Unlock()
flushLogBuffer()
return err
}
output := ""
switch log.Level {
case database.LogLevelTrace, database.LogLevelDebug:
case codersdk.LogLevelTrace, codersdk.LogLevelDebug:
if !opts.Verbose {
continue
}
output = Styles.Placeholder.Render(log.Output)
case database.LogLevelError:
case codersdk.LogLevelError:
output = defaultStyles.Error.Render(log.Output)
case database.LogLevelWarn:
case codersdk.LogLevelWarn:
output = Styles.Warn.Render(log.Output)
case database.LogLevelInfo:
case codersdk.LogLevelInfo:
output = log.Output
}
jobMutex.Lock()
if log.Stage != currentStage && log.Stage != "" {
updateStage(log.Stage, log.CreatedAt)
jobMutex.Unlock()
continue
}
_, _ = fmt.Fprintf(writer, "%s %s\n", Styles.Placeholder.Render(" "), output)
didLogBetweenStage = true
_, _ = fmt.Fprintf(logOutput, "%s %s\n", Styles.Placeholder.Render(" "), output)
if !opts.Silent {
didLogBetweenStage = true
}
jobMutex.Unlock()
}
}
+4 -4
View File
@@ -9,7 +9,7 @@ import (
"time"
"github.com/spf13/cobra"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/assert"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/coderd/database"
@@ -90,9 +90,9 @@ func TestProvisionerJob(t *testing.T) {
go func() {
<-test.Next
currentProcess, err := os.FindProcess(os.Getpid())
require.NoError(t, err)
assert.NoError(t, err)
err = currentProcess.Signal(os.Interrupt)
require.NoError(t, err)
assert.NoError(t, err)
<-test.Next
test.JobMutex.Lock()
test.Job.Status = codersdk.ProvisionerJobCanceled
@@ -150,7 +150,7 @@ func newProvisionerJob(t *testing.T) provisionerJobTest {
defer close(done)
err := cmd.ExecuteContext(context.Background())
if err != nil {
require.ErrorIs(t, err, cliui.Canceled)
assert.ErrorIs(t, err, cliui.Canceled)
}
}()
t.Cleanup(func() {
+141
View File
@@ -0,0 +1,141 @@
package cliui
import (
"fmt"
"io"
"sort"
"strconv"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/codersdk"
)
type WorkspaceResourcesOptions struct {
WorkspaceName string
HideAgentState bool
HideAccess bool
Title string
}
// WorkspaceResources displays the connection status and tree-view of provided resources.
// ┌────────────────────────────────────────────────────────────────────────────┐
// │ RESOURCE STATUS ACCESS │
// ├────────────────────────────────────────────────────────────────────────────┤
// │ google_compute_disk.root persistent │
// ├────────────────────────────────────────────────────────────────────────────┤
// │ google_compute_instance.dev ephemeral │
// │ └─ dev (linux, amd64) ⦾ connecting [10s] coder ssh dev.dev │
// ├────────────────────────────────────────────────────────────────────────────┤
// │ kubernetes_pod.dev ephemeral │
// │ ├─ go (linux, amd64) ⦿ connected coder ssh dev.go │
// │ └─ postgres (linux, amd64) ⦾ disconnected [4s] coder ssh dev.postgres │
// └────────────────────────────────────────────────────────────────────────────┘
func WorkspaceResources(writer io.Writer, resources []codersdk.WorkspaceResource, options WorkspaceResourcesOptions) error {
// Sort resources by type for consistent output.
sort.Slice(resources, func(i, j int) bool {
return resources[i].Type < resources[j].Type
})
// Address on stop indexes whether a resource still exists when in the stopped transition.
addressOnStop := map[string]codersdk.WorkspaceResource{}
for _, resource := range resources {
if resource.Transition != codersdk.WorkspaceTransitionStop {
continue
}
addressOnStop[resource.Type+"."+resource.Name] = resource
}
// Displayed stores whether a resource has already been shown.
// Resources can be stored with numerous states, which we
// process prior to display.
displayed := map[string]struct{}{}
tableWriter := table.NewWriter()
if options.Title != "" {
tableWriter.SetTitle(options.Title)
}
tableWriter.SetStyle(table.StyleLight)
tableWriter.Style().Options.SeparateColumns = false
row := table.Row{"Resource", "Status"}
if !options.HideAccess {
row = append(row, "Access")
}
tableWriter.AppendHeader(row)
totalAgents := 0
for _, resource := range resources {
totalAgents += len(resource.Agents)
}
for _, resource := range resources {
if resource.Type == "random_string" {
// Hide resources that aren't substantial to a user!
// This is an unfortunate case, and we should allow
// callers to hide resources eventually.
continue
}
resourceAddress := resource.Type + "." + resource.Name
if _, shown := displayed[resourceAddress]; shown {
// The same resource can have multiple transitions.
continue
}
displayed[resourceAddress] = struct{}{}
// Sort agents by name for consistent output.
sort.Slice(resource.Agents, func(i, j int) bool {
return resource.Agents[i].Name < resource.Agents[j].Name
})
_, existsOnStop := addressOnStop[resourceAddress]
resourceState := "ephemeral"
if existsOnStop {
resourceState = "persistent"
}
// Display a line for the resource.
tableWriter.AppendRow(table.Row{
Styles.Bold.Render(resourceAddress),
Styles.Placeholder.Render(resourceState),
"",
})
// Display all agents associated with the resource.
for index, agent := range resource.Agents {
sshCommand := "coder ssh " + options.WorkspaceName
if totalAgents > 1 {
sshCommand += "." + agent.Name
}
sshCommand = Styles.Code.Render(sshCommand)
var agentStatus string
if !options.HideAgentState {
switch agent.Status {
case codersdk.WorkspaceAgentConnecting:
since := database.Now().Sub(agent.CreatedAt)
agentStatus = Styles.Warn.Render("⦾ connecting") + " " +
Styles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
case codersdk.WorkspaceAgentDisconnected:
since := database.Now().Sub(*agent.DisconnectedAt)
agentStatus = Styles.Error.Render("⦾ disconnected") + " " +
Styles.Placeholder.Render("["+strconv.Itoa(int(since.Seconds()))+"s]")
case codersdk.WorkspaceAgentConnected:
agentStatus = Styles.Keyword.Render("⦿ connected")
}
}
pipe := "├"
if index == len(resource.Agents)-1 {
pipe = "└"
}
row := table.Row{
// These tree from a resource!
fmt.Sprintf("%s─ %s (%s, %s)", pipe, agent.Name, agent.OperatingSystem, agent.Architecture),
agentStatus,
}
if !options.HideAccess {
row = append(row, sshCommand)
}
tableWriter.AppendRow(row)
}
tableWriter.AppendSeparator()
}
_, err := fmt.Fprintln(writer, tableWriter.Render())
return err
}
+96
View File
@@ -0,0 +1,96 @@
package cliui_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/pty/ptytest"
)
func TestWorkspaceResources(t *testing.T) {
t.Parallel()
t.Run("SingleAgentSSH", func(t *testing.T) {
t.Parallel()
ptty := ptytest.New(t)
done := make(chan struct{})
go func() {
err := cliui.WorkspaceResources(ptty.Output(), []codersdk.WorkspaceResource{{
Type: "google_compute_instance",
Name: "dev",
Transition: codersdk.WorkspaceTransitionStart,
Agents: []codersdk.WorkspaceAgent{{
Name: "dev",
Status: codersdk.WorkspaceAgentConnected,
Architecture: "amd64",
OperatingSystem: "linux",
}},
}}, cliui.WorkspaceResourcesOptions{
WorkspaceName: "example",
})
assert.NoError(t, err)
close(done)
}()
ptty.ExpectMatch("coder ssh example")
<-done
})
t.Run("MultipleStates", func(t *testing.T) {
t.Parallel()
ptty := ptytest.New(t)
disconnected := database.Now().Add(-4 * time.Second)
done := make(chan struct{})
go func() {
err := cliui.WorkspaceResources(ptty.Output(), []codersdk.WorkspaceResource{{
Transition: codersdk.WorkspaceTransitionStart,
Type: "google_compute_disk",
Name: "root",
}, {
Transition: codersdk.WorkspaceTransitionStop,
Type: "google_compute_disk",
Name: "root",
}, {
Transition: codersdk.WorkspaceTransitionStart,
Type: "google_compute_instance",
Name: "dev",
Agents: []codersdk.WorkspaceAgent{{
CreatedAt: database.Now().Add(-10 * time.Second),
Status: codersdk.WorkspaceAgentConnecting,
Name: "dev",
OperatingSystem: "linux",
Architecture: "amd64",
}},
}, {
Transition: codersdk.WorkspaceTransitionStart,
Type: "kubernetes_pod",
Name: "dev",
Agents: []codersdk.WorkspaceAgent{{
Status: codersdk.WorkspaceAgentConnected,
Name: "go",
Architecture: "amd64",
OperatingSystem: "linux",
}, {
DisconnectedAt: &disconnected,
Status: codersdk.WorkspaceAgentDisconnected,
Name: "postgres",
Architecture: "amd64",
OperatingSystem: "linux",
}},
}}, cliui.WorkspaceResourcesOptions{
WorkspaceName: "dev",
HideAgentState: false,
HideAccess: false,
})
assert.NoError(t, err)
close(done)
}()
ptty.ExpectMatch("google_compute_disk.root")
ptty.ExpectMatch("google_compute_instance.dev")
ptty.ExpectMatch("coder ssh dev.postgres")
<-done
})
}
+15 -2
View File
@@ -1,11 +1,13 @@
package cliui
import (
"errors"
"flag"
"io"
"os"
"github.com/AlecAivazis/survey/v2"
"github.com/AlecAivazis/survey/v2/terminal"
"github.com/spf13/cobra"
)
@@ -33,7 +35,9 @@ func init() {
}
type SelectOptions struct {
Options []string
Options []string
// Default will be highlighted first if it's a valid option.
Default string
Size int
HideSearch bool
}
@@ -48,10 +52,16 @@ func Select(cmd *cobra.Command, opts SelectOptions) (string, error) {
if flag.Lookup("test.v") != nil {
return opts.Options[0], nil
}
opts.HideSearch = false
var defaultOption interface{}
if opts.Default != "" {
defaultOption = opts.Default
}
var value string
err := survey.AskOne(&survey.Select{
Options: opts.Options,
Default: defaultOption,
PageSize: opts.Size,
}, &value, survey.WithIcons(func(is *survey.IconSet) {
is.Help.Text = "Type to search"
@@ -63,6 +73,9 @@ func Select(cmd *cobra.Command, opts SelectOptions) (string, error) {
}, fileReadWriter{
Writer: cmd.OutOrStdout(),
}, cmd.OutOrStdout()))
if errors.Is(err, terminal.InterruptErr) {
return value, Canceled
}
return value, err
}
+2 -1
View File
@@ -5,6 +5,7 @@ import (
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/cliui"
@@ -21,7 +22,7 @@ func TestSelect(t *testing.T) {
resp, err := newSelect(ptty, cliui.SelectOptions{
Options: []string{"First", "Second"},
})
require.NoError(t, err)
assert.NoError(t, err)
msgChan <- resp
}()
require.Equal(t, "First", <-msgChan)
+43
View File
@@ -0,0 +1,43 @@
package cliui
import (
"strings"
"github.com/jedib0t/go-pretty/v6/table"
)
// Table creates a new table with standardized styles.
func Table() table.Writer {
tableWriter := table.NewWriter()
tableWriter.Style().Box.PaddingLeft = ""
tableWriter.Style().Box.PaddingRight = " "
tableWriter.Style().Options.DrawBorder = false
tableWriter.Style().Options.SeparateHeader = false
tableWriter.Style().Options.SeparateColumns = false
return tableWriter
}
// FilterTableColumns returns configurations to hide columns
// that are not provided in the array. If the array is empty,
// no filtering will occur!
func FilterTableColumns(header table.Row, columns []string) []table.ColumnConfig {
if len(columns) == 0 {
return nil
}
columnConfigs := make([]table.ColumnConfig, 0)
for _, headerTextRaw := range header {
headerText, _ := headerTextRaw.(string)
hidden := true
for _, column := range columns {
if strings.EqualFold(strings.ReplaceAll(column, "_", " "), headerText) {
hidden = false
break
}
}
columnConfigs = append(columnConfigs, table.ColumnConfig{
Name: headerText,
Hidden: hidden,
})
}
return columnConfigs
}
+4
View File
@@ -21,6 +21,10 @@ func (r Root) Organization() File {
return File(filepath.Join(string(r), "organization"))
}
func (r Root) DotfilesURL() File {
return File(filepath.Join(string(r), "dotfilesurl"))
}
// File provides convenience methods for interacting with *os.File.
type File string
+51 -20
View File
@@ -30,10 +30,19 @@ const sshEndToken = "# ------------END-CODER------------"
func configSSH() *cobra.Command {
var (
sshConfigFile string
sshConfigFile string
sshOptions []string
skipProxyCommand bool
)
cmd := &cobra.Command{
Use: "config-ssh",
Annotations: workspaceCommand,
Use: "config-ssh",
Short: "Populate your SSH config with Host entries for all of your workspaces",
Example: `
- You can use -o (or --ssh-option) so set SSH options to be used for all your
workspaces.
` + cliui.Styles.Code.Render("$ coder config-ssh -o ForwardAgent=yes"),
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
@@ -52,47 +61,66 @@ func configSSH() *cobra.Command {
sshConfigContent = sshConfigContent[:startIndex-1] + sshConfigContent[endIndex+len(sshEndToken):]
}
workspaces, err := client.WorkspacesByUser(cmd.Context(), codersdk.Me)
workspaces, err := client.Workspaces(cmd.Context(), codersdk.WorkspaceFilter{
Owner: codersdk.Me,
})
if err != nil {
return err
}
if len(workspaces) == 0 {
return xerrors.New("You don't have any workspaces!")
}
binPath, err := currentBinPath(cmd)
binaryFile, err := currentBinPath(cmd)
if err != nil {
return err
}
root := createConfig(cmd)
sshConfigContent += "\n" + sshStartToken + "\n" + sshStartMessage + "\n\n"
sshConfigContentMutex := sync.Mutex{}
var errGroup errgroup.Group
for _, workspace := range workspaces {
workspace := workspace
errGroup.Go(func() error {
resources, err := client.WorkspaceResourcesByBuild(cmd.Context(), workspace.LatestBuild.ID)
resources, err := client.TemplateVersionResources(cmd.Context(), workspace.LatestBuild.TemplateVersionID)
if err != nil {
return err
}
resourcesWithAgents := make([]codersdk.WorkspaceResource, 0)
for _, resource := range resources {
if resource.Agent == nil {
if resource.Transition != codersdk.WorkspaceTransitionStart {
continue
}
resourcesWithAgents = append(resourcesWithAgents, resource)
for _, agent := range resource.Agents {
sshConfigContentMutex.Lock()
hostname := workspace.Name
if len(resource.Agents) > 1 {
hostname += "." + agent.Name
}
configOptions := []string{
"Host coder." + hostname,
}
for _, option := range sshOptions {
configOptions = append(configOptions, "\t"+option)
}
configOptions = append(configOptions,
"\tHostName coder."+hostname,
"\tConnectTimeout=0",
"\tStrictHostKeyChecking=no",
// Without this, the "REMOTE HOST IDENTITY CHANGED"
// message will appear.
"\tUserKnownHostsFile=/dev/null",
// This disables the "Warning: Permanently added 'hostname' (RSA) to the list of known hosts."
// message from appearing on every SSH. This happens because we ignore the known hosts.
"\tLogLevel ERROR",
)
if !skipProxyCommand {
configOptions = append(configOptions, fmt.Sprintf("\tProxyCommand %q --global-config %q ssh --stdio %s", binaryFile, root, hostname))
}
sshConfigContent += strings.Join(configOptions, "\n") + "\n"
sshConfigContentMutex.Unlock()
}
}
sshConfigContentMutex.Lock()
defer sshConfigContentMutex.Unlock()
if len(resourcesWithAgents) == 1 {
sshConfigContent += strings.Join([]string{
"Host coder." + workspace.Name,
"\tHostName coder." + workspace.Name,
fmt.Sprintf("\tProxyCommand %q ssh --stdio %s", binPath, workspace.Name),
"\tConnectTimeout=0",
"\tStrictHostKeyChecking=no",
}, "\n") + "\n"
}
return nil
})
}
@@ -116,6 +144,9 @@ func configSSH() *cobra.Command {
},
}
cliflag.StringVarP(cmd.Flags(), &sshConfigFile, "ssh-config-file", "", "CODER_SSH_CONFIG_FILE", "~/.ssh/config", "Specifies the path to an SSH config.")
cmd.Flags().StringArrayVarP(&sshOptions, "ssh-option", "o", []string{}, "Specifies additional SSH options to embed in each host stanza.")
cmd.Flags().BoolVarP(&skipProxyCommand, "skip-proxy-command", "", false, "Specifies whether the ProxyCommand option should be skipped. Useful for testing.")
_ = cmd.Flags().MarkHidden("skip-proxy-command")
return cmd
}
+117 -25
View File
@@ -1,43 +1,135 @@
package cli_test
import (
"context"
"io"
"net"
"os"
"os/exec"
"strconv"
"strings"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"cdr.dev/slog/sloggers/slogtest"
"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"
)
func TestConfigSSH(t *testing.T) {
t.Parallel()
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
tempFile, err := os.CreateTemp(t.TempDir(), "")
require.NoError(t, err)
_ = tempFile.Close()
cmd, root := clitest.New(t, "config-ssh", "--ssh-config-file", tempFile.Name())
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
go func() {
defer close(doneChan)
err := cmd.Execute()
require.NoError(t, err)
}()
<-doneChan
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
authToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionDryRun: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
Agents: []*proto.Agent{{
Id: uuid.NewString(),
Name: "example",
}},
}},
},
},
}},
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "example",
Type: "aws_instance",
Agents: []*proto.Agent{{
Id: uuid.NewString(),
Name: "example",
Auth: &proto.Agent_Token{
Token: authToken,
},
}},
}},
},
},
}},
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
agentClient := codersdk.New(client.URL)
agentClient.SessionToken = authToken
agentCloser := agent.New(agentClient.ListenWorkspaceAgent, &agent.Options{
Logger: slogtest.Make(t, nil),
})
t.Cleanup(func() {
_ = agentCloser.Close()
})
tempFile, err := os.CreateTemp(t.TempDir(), "")
require.NoError(t, err)
_ = tempFile.Close()
resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
agentConn, err := client.DialWorkspaceAgent(context.Background(), resources[0].Agents[0].ID, nil)
require.NoError(t, err)
defer agentConn.Close()
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
t.Cleanup(func() {
_ = listener.Close()
})
go func() {
for {
conn, err := listener.Accept()
if err != nil {
return
}
ssh, err := agentConn.SSH()
assert.NoError(t, err)
go io.Copy(conn, ssh)
go io.Copy(ssh, conn)
}
}()
t.Cleanup(func() {
_ = listener.Close()
})
tcpAddr, valid := listener.Addr().(*net.TCPAddr)
require.True(t, valid)
cmd, root := clitest.New(t, "config-ssh",
"--ssh-option", "HostName "+tcpAddr.IP.String(),
"--ssh-option", "Port "+strconv.Itoa(tcpAddr.Port),
"--ssh-config-file", tempFile.Name(),
"--skip-proxy-command")
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
go func() {
defer close(doneChan)
err := cmd.Execute()
assert.NoError(t, err)
}()
<-doneChan
t.Log(tempFile.Name())
// #nosec
sshCmd := exec.Command("ssh", "-F", tempFile.Name(), "coder."+workspace.Name, "echo", "test")
sshCmd.Stderr = os.Stderr
data, err := sshCmd.Output()
require.NoError(t, err)
require.Equal(t, "test", strings.TrimSpace(string(data)))
}
+268
View File
@@ -0,0 +1,268 @@
package cli
import (
"fmt"
"time"
"github.com/spf13/cobra"
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
"github.com/coder/coder/cli/cliflag"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/coderd/autobuild/schedule"
"github.com/coder/coder/coderd/util/ptr"
"github.com/coder/coder/codersdk"
)
func create() *cobra.Command {
var (
autostartMinute string
autostartHour string
autostartDow string
parameterFile string
templateName string
ttl time.Duration
tzName string
workspaceName string
)
cmd := &cobra.Command{
Annotations: workspaceCommand,
Use: "create [name]",
Short: "Create a workspace from a template",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return err
}
if len(args) >= 1 {
workspaceName = args[0]
}
if workspaceName == "" {
workspaceName, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Specify a name for your workspace:",
Validate: func(workspaceName string) error {
_, err = client.WorkspaceByOwnerAndName(cmd.Context(), codersdk.Me, workspaceName)
if err == nil {
return xerrors.Errorf("A workspace already exists named %q!", workspaceName)
}
return nil
},
})
if err != nil {
return err
}
}
tz, err := time.LoadLocation(tzName)
if err != nil {
return xerrors.Errorf("Invalid workspace autostart timezone: %w", err)
}
schedSpec := fmt.Sprintf("CRON_TZ=%s %s %s * * %s", tz.String(), autostartMinute, autostartHour, autostartDow)
_, err = schedule.Weekly(schedSpec)
if err != nil {
return xerrors.Errorf("invalid workspace autostart schedule: %w", err)
}
if ttl == 0 {
return xerrors.Errorf("TTL must be at least 1 minute")
}
_, err = client.WorkspaceByOwnerAndName(cmd.Context(), codersdk.Me, workspaceName)
if err == nil {
return xerrors.Errorf("A workspace already exists named %q!", workspaceName)
}
var template codersdk.Template
if templateName == "" {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Wrap.Render("Select a template below to preview the provisioned infrastructure:"))
templates, err := client.TemplatesByOrganization(cmd.Context(), organization.ID)
if err != nil {
return err
}
slices.SortFunc(templates, func(a, b codersdk.Template) bool {
return a.WorkspaceOwnerCount > b.WorkspaceOwnerCount
})
templateNames := make([]string, 0, len(templates))
templateByName := make(map[string]codersdk.Template, len(templates))
for _, template := range templates {
templateName := template.Name
if template.WorkspaceOwnerCount > 0 {
developerText := "developer"
if template.WorkspaceOwnerCount != 1 {
developerText = "developers"
}
templateName += cliui.Styles.Placeholder.Render(fmt.Sprintf(" (used by %d %s)", template.WorkspaceOwnerCount, developerText))
}
templateNames = append(templateNames, templateName)
templateByName[templateName] = template
}
// Move the cursor up a single line for nicer display!
option, err := cliui.Select(cmd, cliui.SelectOptions{
Options: templateNames,
HideSearch: true,
})
if err != nil {
return err
}
template = templateByName[option]
} else {
template, err = client.TemplateByName(cmd.Context(), organization.ID, templateName)
if err != nil {
return xerrors.Errorf("get template by name: %w", err)
}
}
templateVersion, err := client.TemplateVersion(cmd.Context(), template.ActiveVersionID)
if err != nil {
return err
}
parameterSchemas, err := client.TemplateVersionSchema(cmd.Context(), templateVersion.ID)
if err != nil {
return err
}
// parameterMapFromFile can be nil if parameter file is not specified
var parameterMapFromFile map[string]string
if parameterFile != "" {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render("Attempting to read the variables from the parameter file.")+"\r\n")
parameterMapFromFile, err = createParameterMapFromFile(parameterFile)
if err != nil {
return err
}
}
disclaimerPrinted := false
parameters := make([]codersdk.CreateParameterRequest, 0)
for _, parameterSchema := range parameterSchemas {
if !parameterSchema.AllowOverrideSource {
continue
}
if !disclaimerPrinted {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), 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
}
parameterValue, err := getParameterValueFromMapOrInput(cmd, parameterMapFromFile, parameterSchema)
if err != nil {
return err
}
parameters = append(parameters, codersdk.CreateParameterRequest{
Name: parameterSchema.Name,
SourceValue: parameterValue,
SourceScheme: codersdk.ParameterSourceSchemeData,
DestinationScheme: parameterSchema.DefaultDestinationScheme,
})
}
_, _ = fmt.Fprintln(cmd.OutOrStdout())
// Run a dry-run with the given parameters to check correctness
after := time.Now()
dryRun, err := client.CreateTemplateVersionDryRun(cmd.Context(), templateVersion.ID, codersdk.CreateTemplateVersionDryRunRequest{
WorkspaceName: workspaceName,
ParameterValues: parameters,
})
if err != nil {
return xerrors.Errorf("begin workspace dry-run: %w", err)
}
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "Planning workspace...")
err = cliui.ProvisionerJob(cmd.Context(), cmd.OutOrStdout(), cliui.ProvisionerJobOptions{
Fetch: func() (codersdk.ProvisionerJob, error) {
return client.TemplateVersionDryRun(cmd.Context(), templateVersion.ID, dryRun.ID)
},
Cancel: func() error {
return client.CancelTemplateVersionDryRun(cmd.Context(), templateVersion.ID, dryRun.ID)
},
Logs: func() (<-chan codersdk.ProvisionerJobLog, error) {
return client.TemplateVersionDryRunLogsAfter(cmd.Context(), templateVersion.ID, dryRun.ID, after)
},
// Don't show log output for the dry-run unless there's an error.
Silent: true,
})
if err != nil {
// TODO (Dean): reprompt for parameter values if we deem it to
// be a validation error
return xerrors.Errorf("dry-run workspace: %w", err)
}
resources, err := client.TemplateVersionDryRunResources(cmd.Context(), templateVersion.ID, dryRun.ID)
if err != nil {
return xerrors.Errorf("get workspace dry-run resources: %w", err)
}
err = cliui.WorkspaceResources(cmd.OutOrStdout(), resources, cliui.WorkspaceResourcesOptions{
WorkspaceName: workspaceName,
// Since agent's haven't connected yet, hiding this makes more sense.
HideAgentState: true,
Title: "Workspace Preview",
})
if err != nil {
return err
}
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Confirm create?",
IsConfirm: true,
})
if err != nil {
return err
}
workspace, err := client.CreateWorkspace(cmd.Context(), organization.ID, codersdk.CreateWorkspaceRequest{
TemplateID: template.ID,
Name: workspaceName,
AutostartSchedule: &schedSpec,
TTLMillis: ptr.Ref(ttl.Milliseconds()),
ParameterValues: parameters,
})
if err != nil {
return err
}
err = cliui.WorkspaceBuild(cmd.Context(), cmd.OutOrStdout(), client, workspace.LatestBuild.ID, after)
if err != nil {
return err
}
resources, err = client.WorkspaceResourcesByBuild(cmd.Context(), workspace.LatestBuild.ID)
if err != nil {
return err
}
err = cliui.WorkspaceResources(cmd.OutOrStdout(), resources, cliui.WorkspaceResourcesOptions{
WorkspaceName: workspaceName,
})
if err != nil {
return err
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "The %s workspace has been created!\n", cliui.Styles.Keyword.Render(workspace.Name))
return nil
},
}
cliui.AllowSkipPrompt(cmd)
cliflag.StringVarP(cmd.Flags(), &templateName, "template", "t", "CODER_TEMPLATE_NAME", "", "Specify a template name.")
cliflag.StringVarP(cmd.Flags(), &parameterFile, "parameter-file", "", "CODER_PARAMETER_FILE", "", "Specify a file path with parameter values.")
cliflag.StringVarP(cmd.Flags(), &autostartMinute, "autostart-minute", "", "CODER_WORKSPACE_AUTOSTART_MINUTE", "0", "Specify the minute(s) at which the workspace should autostart (e.g. 0).")
cliflag.StringVarP(cmd.Flags(), &autostartHour, "autostart-hour", "", "CODER_WORKSPACE_AUTOSTART_HOUR", "9", "Specify the hour(s) at which the workspace should autostart (e.g. 9).")
cliflag.StringVarP(cmd.Flags(), &autostartDow, "autostart-day-of-week", "", "CODER_WORKSPACE_AUTOSTART_DOW", "MON-FRI", "Specify the days(s) on which the workspace should autostart (e.g. MON,TUE,WED,THU,FRI)")
cliflag.StringVarP(cmd.Flags(), &tzName, "tz", "", "TZ", "", "Specify your timezone location for workspace autostart (e.g. US/Central).")
cliflag.DurationVarP(cmd.Flags(), &ttl, "ttl", "", "CODER_WORKSPACE_TTL", 8*time.Hour, "Specify a time-to-live (TTL) for the workspace (e.g. 8h).")
return cmd
}
+366
View File
@@ -0,0 +1,366 @@
package cli_test
import (
"context"
"database/sql"
"fmt"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
"github.com/coder/coder/pty/ptytest"
)
func TestCreate(t *testing.T) {
t.Parallel()
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
args := []string{
"create",
"my-workspace",
"--template", template.Name,
"--tz", "US/Central",
"--autostart-minute", "0",
"--autostart-hour", "*/2",
"--autostart-day-of-week", "MON-FRI",
"--ttl", "8h",
}
cmd, root := clitest.New(t, args...)
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
go func() {
defer close(doneChan)
err := cmd.Execute()
assert.NoError(t, err)
}()
matches := []string{
"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("CreateErrInvalidTz", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
args := []string{
"create",
"my-workspace",
"--template", template.Name,
"--tz", "invalid",
}
cmd, root := clitest.New(t, args...)
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
go func() {
defer close(doneChan)
err := cmd.Execute()
assert.EqualError(t, err, "Invalid workspace autostart timezone: unknown time zone invalid")
}()
<-doneChan
})
t.Run("CreateErrInvalidTTL", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
args := []string{
"create",
"my-workspace",
"--template", template.Name,
"--ttl", "0s",
}
cmd, root := clitest.New(t, args...)
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
go func() {
defer close(doneChan)
err := cmd.Execute()
assert.EqualError(t, err, "TTL must be at least 1 minute")
}()
<-doneChan
})
t.Run("CreateFromListWithSkip", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
_ = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
cmd, root := clitest.New(t, "create", "my-workspace", "-y")
member := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
clitest.SetupConfig(t, member, root)
cmdCtx, done := context.WithTimeout(context.Background(), time.Second*3)
go func() {
defer done()
err := cmd.ExecuteContext(cmdCtx)
assert.NoError(t, err)
}()
// No pty interaction needed since we use the -y skip prompt flag
<-cmdCtx.Done()
require.ErrorIs(t, cmdCtx.Err(), context.Canceled)
})
t.Run("FromNothing", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
_ = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
cmd, root := clitest.New(t, "create", "")
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
go func() {
defer close(doneChan)
err := cmd.Execute()
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("WithParameter", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
defaultValue := "something"
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: createTestParseResponseWithDefault(defaultValue),
Provision: echo.ProvisionComplete,
ProvisionDryRun: echo.ProvisionComplete,
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
_ = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
cmd, root := clitest.New(t, "create", "")
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
go func() {
defer close(doneChan)
err := cmd.Execute()
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{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
defaultValue := "something"
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: createTestParseResponseWithDefault(defaultValue),
Provision: echo.ProvisionComplete,
ProvisionDryRun: echo.ProvisionComplete,
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
_ = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
tempDir := t.TempDir()
parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml")
_, _ = parameterFile.WriteString("region: \"bingo\"\nusername: \"boingo\"")
cmd, root := clitest.New(t, "create", "", "--parameter-file", parameterFile.Name())
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
go func() {
defer close(doneChan)
err := cmd.Execute()
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
removeTmpDirUntilSuccess(t, tempDir)
})
t.Run("WithParameterFileNotContainingTheValue", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
defaultValue := "something"
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: createTestParseResponseWithDefault(defaultValue),
Provision: echo.ProvisionComplete,
ProvisionDryRun: echo.ProvisionComplete,
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
tempDir := t.TempDir()
parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml")
_, _ = parameterFile.WriteString("zone: \"bananas\"")
cmd, root := clitest.New(t, "create", "my-workspace", "--template", template.Name, "--parameter-file", parameterFile.Name())
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
go func() {
defer close(doneChan)
err := cmd.Execute()
assert.EqualError(t, err, "Parameter value absent in parameter file for \"region\"!")
}()
<-doneChan
removeTmpDirUntilSuccess(t, tempDir)
})
t.Run("FailedDryRun", func(t *testing.T) {
t.Parallel()
client, api := coderdtest.NewWithAPI(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionDryRun: []*proto.Provision_Response{
{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Error: "test error",
},
},
},
},
})
// 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.ProvisionerJobFailed, version.Job.Status, "job is not failed")
err := api.Database.UpdateProvisionerJobWithCompleteByID(context.Background(), database.UpdateProvisionerJobWithCompleteByIDParams{
ID: version.Job.ID,
CompletedAt: sql.NullTime{
Time: time.Now(),
Valid: true,
},
UpdatedAt: time.Now(),
Error: sql.NullString{},
})
require.NoError(t, err, "update provisioner job")
_ = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
cmd, root := clitest.New(t, "create", "test")
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
err = cmd.Execute()
require.Error(t, err)
require.ErrorContains(t, err, "dry-run workspace")
})
}
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,
},
},
},
},
},
}}
}
+49
View File
@@ -0,0 +1,49 @@
package cli
import (
"time"
"github.com/spf13/cobra"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/codersdk"
)
// nolint
func delete() *cobra.Command {
cmd := &cobra.Command{
Annotations: workspaceCommand,
Use: "delete <workspace>",
Short: "Delete a workspace",
Aliases: []string{"rm"},
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
_, err := cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Confirm delete workspace?",
IsConfirm: true,
})
if err != nil {
return err
}
client, err := createClient(cmd)
if err != nil {
return err
}
workspace, err := namedWorkspace(cmd, client, args[0])
if err != nil {
return err
}
before := time.Now()
build, err := client.CreateWorkspaceBuild(cmd.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionDelete,
})
if err != nil {
return err
}
return cliui.WorkspaceBuild(cmd.Context(), cmd.OutOrStdout(), client, build.ID, before)
},
}
cliui.AllowSkipPrompt(cmd)
return cmd
}
+95
View File
@@ -0,0 +1,95 @@
package cli_test
import (
"context"
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/pty/ptytest"
)
func TestDelete(t *testing.T) {
t.Run("WithParameter", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
cmd, root := clitest.New(t, "delete", workspace.Name, "-y")
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
go func() {
defer close(doneChan)
err := cmd.Execute()
// When running with the race detector on, we sometimes get an EOF.
if err != nil {
assert.ErrorIs(t, err, io.EOF)
}
}()
pty.ExpectMatch("Cleaning Up")
<-doneChan
})
t.Run("DifferentUser", func(t *testing.T) {
t.Parallel()
adminClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
adminUser := coderdtest.CreateFirstUser(t, adminClient)
orgID := adminUser.OrganizationID
client := coderdtest.CreateAnotherUser(t, adminClient, orgID)
user, err := client.User(context.Background(), codersdk.Me)
require.NoError(t, err)
version := coderdtest.CreateTemplateVersion(t, adminClient, orgID, nil)
coderdtest.AwaitTemplateVersionJob(t, adminClient, version.ID)
template := coderdtest.CreateTemplate(t, adminClient, orgID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, orgID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
cmd, root := clitest.New(t, "delete", user.Username+"/"+workspace.Name, "-y")
clitest.SetupConfig(t, adminClient, root)
doneChan := make(chan struct{})
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
go func() {
defer close(doneChan)
err := cmd.Execute()
// When running with the race detector on, we sometimes get an EOF.
if err != nil {
assert.ErrorIs(t, err, io.EOF)
}
}()
pty.ExpectMatch("Cleaning Up")
<-doneChan
workspace, err = client.Workspace(context.Background(), workspace.ID)
require.ErrorContains(t, err, "was deleted")
})
t.Run("InvalidWorkspaceIdentifier", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
cmd, root := clitest.New(t, "delete", "a/b/c", "-y")
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
go func() {
defer close(doneChan)
err := cmd.Execute()
assert.ErrorContains(t, err, "invalid workspace name: \"a/b/c\"")
}()
<-doneChan
})
}
+279
View File
@@ -0,0 +1,279 @@
package cli
import (
"errors"
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
"github.com/coder/coder/cli/cliflag"
"github.com/coder/coder/cli/cliui"
)
func dotfiles() *cobra.Command {
var (
symlinkDir string
)
cmd := &cobra.Command{
Use: "dotfiles [git_repo_url]",
Args: cobra.ExactArgs(1),
Short: "Checkout and install a dotfiles repository.",
Example: "coder dotfiles [-y] git@github.com:example/dotfiles.git",
RunE: func(cmd *cobra.Command, args []string) error {
var (
dotfilesRepoDir = "dotfiles"
gitRepo = args[0]
cfg = createConfig(cmd)
cfgDir = string(cfg)
dotfilesDir = filepath.Join(cfgDir, dotfilesRepoDir)
// This follows the same pattern outlined by others in the market:
// https://github.com/coder/coder/pull/1696#issue-1245742312
installScriptSet = []string{
"install.sh",
"install",
"bootstrap.sh",
"bootstrap",
"script/bootstrap",
"setup.sh",
"setup",
"script/setup",
}
)
_, _ = fmt.Fprint(cmd.OutOrStdout(), "Checking if dotfiles repository already exists...\n")
dotfilesExists, err := dirExists(dotfilesDir)
if err != nil {
return xerrors.Errorf("checking dir %s: %w", dotfilesDir, err)
}
moved := false
if dotfilesExists {
du, err := cfg.DotfilesURL().Read()
if err != nil && !errors.Is(err, os.ErrNotExist) {
return xerrors.Errorf("reading dotfiles url config: %w", err)
}
// if the git url has changed we create a backup and clone fresh
if gitRepo != du {
backupDir := fmt.Sprintf("%s_backup_%s", dotfilesDir, time.Now().Format(time.RFC3339))
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: fmt.Sprintf("The dotfiles URL has changed from %q to %q.\n Coder will backup the existing repo to %s.\n\n Continue?", du, gitRepo, backupDir),
IsConfirm: true,
})
if err != nil {
return err
}
err = os.Rename(dotfilesDir, backupDir)
if err != nil {
return xerrors.Errorf("renaming dir %s: %w", dotfilesDir, err)
}
_, _ = fmt.Fprint(cmd.OutOrStdout(), "Done backup up dotfiles.\n")
dotfilesExists = false
moved = true
}
}
var (
gitCmdDir string
subcommands []string
promptText string
)
if dotfilesExists {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Found dotfiles repository at %s\n", dotfilesDir)
gitCmdDir = dotfilesDir
subcommands = []string{"pull", "--ff-only"}
promptText = fmt.Sprintf("Pulling latest from %s into directory %s.\n Continue?", gitRepo, dotfilesDir)
} else {
if !moved {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Did not find dotfiles repository at %s\n", dotfilesDir)
}
gitCmdDir = cfgDir
subcommands = []string{"clone", args[0], dotfilesRepoDir}
promptText = fmt.Sprintf("Cloning %s into directory %s.\n\n Continue?", gitRepo, dotfilesDir)
}
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: promptText,
IsConfirm: true,
})
if err != nil {
return err
}
// ensure command dir exists
err = os.MkdirAll(gitCmdDir, 0750)
if err != nil {
return xerrors.Errorf("ensuring dir at %s: %w", gitCmdDir, err)
}
// check if git ssh command already exists so we can just wrap it
gitsshCmd := os.Getenv("GIT_SSH_COMMAND")
if gitsshCmd == "" {
gitsshCmd = "ssh"
}
// clone or pull repo
c := exec.CommandContext(cmd.Context(), "git", subcommands...)
c.Dir = gitCmdDir
c.Env = append(os.Environ(), fmt.Sprintf(`GIT_SSH_COMMAND=%s -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no`, gitsshCmd))
c.Stdout = cmd.OutOrStdout()
c.Stderr = cmd.ErrOrStderr()
err = c.Run()
if err != nil {
if !dotfilesExists {
return err
}
// if the repo exists we soft fail the update operation and try to continue
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Error.Render("Failed to update repo, continuing..."))
}
// save git repo url so we can detect changes next time
err = cfg.DotfilesURL().Write(gitRepo)
if err != nil {
return xerrors.Errorf("writing dotfiles url config: %w", err)
}
files, err := os.ReadDir(dotfilesDir)
if err != nil {
return xerrors.Errorf("reading files in dir %s: %w", dotfilesDir, err)
}
var dotfiles []string
for _, f := range files {
// make sure we do not copy `.git*` files
if strings.HasPrefix(f.Name(), ".") && !strings.HasPrefix(f.Name(), ".git") {
dotfiles = append(dotfiles, f.Name())
}
}
script := findScript(installScriptSet, files)
if script != "" {
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: fmt.Sprintf("Running install script %s.\n\n Continue?", script),
IsConfirm: true,
})
if err != nil {
return err
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Running %s...\n", script)
// it is safe to use a variable command here because it's from
// a filtered list of pre-approved install scripts
// nolint:gosec
scriptCmd := exec.CommandContext(cmd.Context(), filepath.Join(dotfilesDir, script))
scriptCmd.Dir = dotfilesDir
scriptCmd.Stdout = cmd.OutOrStdout()
scriptCmd.Stderr = cmd.ErrOrStderr()
err = scriptCmd.Run()
if err != nil {
return xerrors.Errorf("running %s: %w", script, err)
}
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "Dotfiles installation complete.")
return nil
}
if len(dotfiles) == 0 {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "No install scripts or dotfiles found, nothing to do.")
return nil
}
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: "No install scripts found, symlinking dotfiles to home directory.\n\n Continue?",
IsConfirm: true,
})
if err != nil {
return err
}
if symlinkDir == "" {
symlinkDir, err = os.UserHomeDir()
if err != nil {
return xerrors.Errorf("getting user home: %w", err)
}
}
for _, df := range dotfiles {
from := filepath.Join(dotfilesDir, df)
to := filepath.Join(symlinkDir, df)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Symlinking %s to %s...\n", from, to)
isRegular, err := isRegular(to)
if err != nil {
return xerrors.Errorf("checking symlink for %s: %w", to, err)
}
// move conflicting non-symlink files to file.ext.bak
if isRegular {
backup := fmt.Sprintf("%s.bak", to)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Moving %s to %s...\n", to, backup)
err = os.Rename(to, backup)
if err != nil {
return xerrors.Errorf("renaming dir %s: %w", to, err)
}
}
err = os.Symlink(from, to)
if err != nil {
return xerrors.Errorf("symlinking %s to %s: %w", from, to, err)
}
}
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "Dotfiles installation complete.")
return nil
},
}
cliui.AllowSkipPrompt(cmd)
cliflag.StringVarP(cmd.Flags(), &symlinkDir, "symlink-dir", "", "CODER_SYMLINK_DIR", "", "Specifies the directory for the dotfiles symlink destinations. If empty will use $HOME.")
return cmd
}
// dirExists checks if the path exists and is a directory.
func dirExists(name string) (bool, error) {
fi, err := os.Stat(name)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, xerrors.Errorf("stat dir: %w", err)
}
if !fi.IsDir() {
return false, xerrors.New("exists but not a directory")
}
return true, nil
}
// findScript will find the first file that matches the script set.
func findScript(scriptSet []string, files []fs.DirEntry) string {
for _, i := range scriptSet {
for _, f := range files {
if f.Name() == i {
return f.Name()
}
}
}
return ""
}
// isRegular detects if the file exists and is not a symlink.
func isRegular(to string) (bool, error) {
fi, err := os.Lstat(to)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return false, nil
}
return false, xerrors.Errorf("lstat %s: %w", to, err)
}
return fi.Mode().IsRegular(), nil
}
+141
View File
@@ -0,0 +1,141 @@
package cli_test
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/cli/config"
"github.com/coder/coder/cryptorand"
)
// nolint:paralleltest
func TestDotfiles(t *testing.T) {
t.Run("MissingArg", func(t *testing.T) {
cmd, _ := clitest.New(t, "dotfiles")
err := cmd.Execute()
require.Error(t, err)
})
t.Run("NoInstallScript", func(t *testing.T) {
_, root := clitest.New(t)
testRepo := testGitRepo(t, root)
// nolint:gosec
err := os.WriteFile(filepath.Join(testRepo, ".bashrc"), []byte("wow"), 0750)
require.NoError(t, err)
c := exec.Command("git", "add", ".bashrc")
c.Dir = testRepo
err = c.Run()
require.NoError(t, err)
c = exec.Command("git", "commit", "-m", `"add .bashrc"`)
c.Dir = testRepo
out, err := c.CombinedOutput()
require.NoError(t, err, string(out))
cmd, _ := clitest.New(t, "dotfiles", "--global-config", string(root), "--symlink-dir", string(root), "-y", testRepo)
err = cmd.Execute()
require.NoError(t, err)
b, err := os.ReadFile(filepath.Join(string(root), ".bashrc"))
require.NoError(t, err)
require.Equal(t, string(b), "wow")
})
t.Run("InstallScript", func(t *testing.T) {
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)
// nolint:gosec
err := os.WriteFile(filepath.Join(testRepo, "install.sh"), []byte("#!/bin/bash\necho wow > "+filepath.Join(string(root), ".bashrc")), 0750)
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)
cmd, _ := clitest.New(t, "dotfiles", "--global-config", string(root), "--symlink-dir", string(root), "-y", testRepo)
err = cmd.Execute()
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) {
_, root := clitest.New(t)
testRepo := testGitRepo(t, root)
// nolint:gosec
err := os.WriteFile(filepath.Join(testRepo, ".bashrc"), []byte("wow"), 0750)
require.NoError(t, err)
// add a conflicting file at destination
// nolint:gosec
err = os.WriteFile(filepath.Join(string(root), ".bashrc"), []byte("backup"), 0750)
require.NoError(t, err)
c := exec.Command("git", "add", ".bashrc")
c.Dir = testRepo
err = c.Run()
require.NoError(t, err)
c = exec.Command("git", "commit", "-m", `"add .bashrc"`)
c.Dir = testRepo
out, err := c.CombinedOutput()
require.NoError(t, err, string(out))
cmd, _ := clitest.New(t, "dotfiles", "--global-config", string(root), "--symlink-dir", string(root), "-y", testRepo)
err = cmd.Execute()
require.NoError(t, err)
b, err := os.ReadFile(filepath.Join(string(root), ".bashrc"))
require.NoError(t, err)
require.Equal(t, string(b), "wow")
// check for backup file
b, err = os.ReadFile(filepath.Join(string(root), ".bashrc.bak"))
require.NoError(t, err)
require.Equal(t, string(b), "backup")
})
}
func testGitRepo(t *testing.T, root config.Root) string {
r, err := cryptorand.String(8)
require.NoError(t, err)
dir := filepath.Join(string(root), fmt.Sprintf("test-repo-%s", r))
err = os.MkdirAll(dir, 0750)
require.NoError(t, err)
c := exec.Command("git", "init")
c.Dir = dir
err = c.Run()
require.NoError(t, err)
c = exec.Command("git", "config", "user.email", "ci@coder.com")
c.Dir = dir
err = c.Run()
require.NoError(t, err)
c = exec.Command("git", "config", "user.name", "C I")
c.Dir = dir
err = c.Run()
require.NoError(t, err)
return dir
}
+72
View File
@@ -0,0 +1,72 @@
package cli
import (
"fmt"
"os"
"os/exec"
"strings"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
"github.com/coder/coder/cli/cliui"
)
func gitssh() *cobra.Command {
return &cobra.Command{
Use: "gitssh",
Hidden: true,
Short: `Wraps the "ssh" command and uses the coder gitssh key for authentication`,
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createAgentClient(cmd)
if err != nil {
return xerrors.Errorf("create agent client: %w", err)
}
key, err := client.AgentGitSSHKey(cmd.Context())
if err != nil {
return xerrors.Errorf("get agent git ssh token: %w", err)
}
privateKeyFile, err := os.CreateTemp("", "coder-gitsshkey-*")
if err != nil {
return xerrors.Errorf("create temp gitsshkey file: %w", err)
}
defer func() {
_ = privateKeyFile.Close()
_ = os.Remove(privateKeyFile.Name())
}()
_, err = privateKeyFile.WriteString(key.PrivateKey)
if err != nil {
return xerrors.Errorf("write to temp gitsshkey file: %w", err)
}
err = privateKeyFile.Close()
if err != nil {
return xerrors.Errorf("close temp gitsshkey file: %w", err)
}
args = append([]string{"-i", privateKeyFile.Name()}, args...)
c := exec.CommandContext(cmd.Context(), "ssh", args...)
c.Stderr = cmd.ErrOrStderr()
c.Stdout = cmd.OutOrStdout()
c.Stdin = cmd.InOrStdin()
err = c.Run()
if err != nil {
exitErr := &exec.ExitError{}
if xerrors.As(err, &exitErr) && exitErr.ExitCode() == 255 {
_, _ = fmt.Fprintln(cmd.ErrOrStderr(),
"\n"+cliui.Styles.Wrap.Render("Coder authenticates with "+cliui.Styles.Field.Render("git")+
" using the public key below. All clones with SSH are authenticated automatically 🪄.")+"\n")
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), cliui.Styles.Code.Render(strings.TrimSpace(key.PublicKey))+"\n")
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Add to GitHub and GitLab:")
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), cliui.Styles.Prompt.String()+"https://github.com/settings/ssh/new")
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), cliui.Styles.Prompt.String()+"https://gitlab.com/-/profile/keys")
_, _ = fmt.Fprintln(cmd.ErrOrStderr())
return err
}
return xerrors.Errorf("run ssh command: %w", err)
}
return nil
},
}
}
+114
View File
@@ -0,0 +1,114 @@
package cli_test
import (
"context"
"fmt"
"net"
"sync/atomic"
"testing"
"github.com/gliderlabs/ssh"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
gossh "golang.org/x/crypto/ssh"
"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"
)
func TestGitSSH(t *testing.T) {
t.Parallel()
t.Run("Dial", func(t *testing.T) {
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
// get user public key
keypair, err := client.GitSSHKey(context.Background(), codersdk.Me)
require.NoError(t, err)
publicKey, _, _, _, err := gossh.ParseAuthorizedKey([]byte(keypair.PublicKey))
require.NoError(t, err)
// setup template
agentToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionDryRun: echo.ProvisionComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "somename",
Type: "someinstance",
Agents: []*proto.Agent{{
Auth: &proto.Agent_Token{
Token: agentToken,
},
}},
}},
},
},
}},
})
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)
// start workspace agent
cmd, root := clitest.New(t, "agent", "--agent-token", agentToken, "--agent-url", client.URL.String())
agentClient := client
clitest.SetupConfig(t, agentClient, root)
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
agentErrC := make(chan error)
go func() {
agentErrC <- cmd.ExecuteContext(ctx)
}()
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
resources, err := client.WorkspaceResourcesByBuild(context.Background(), workspace.LatestBuild.ID)
require.NoError(t, err)
dialer, err := client.DialWorkspaceAgent(context.Background(), resources[0].Agents[0].ID, nil)
require.NoError(t, err)
defer dialer.Close()
_, err = dialer.Ping()
require.NoError(t, err)
// start ssh server
l, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
defer l.Close()
publicKeyOption := ssh.PublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
return ssh.KeysEqual(publicKey, key)
})
var inc int64
sshErrC := make(chan error)
go func() {
// as long as we get a successful session we don't care if the server errors
_ = ssh.Serve(l, func(s ssh.Session) {
atomic.AddInt64(&inc, 1)
t.Log("got authenticated session")
sshErrC <- s.Exit(0)
}, publicKeyOption)
}()
// start ssh session
addr, ok := l.Addr().(*net.TCPAddr)
require.True(t, ok)
// set to agent config dir
gitsshCmd, _ := clitest.New(t, "gitssh", "--agent-url", agentClient.URL.String(), "--agent-token", agentToken, "--", fmt.Sprintf("-p%d", addr.Port), "-o", "StrictHostKeyChecking=no", "-o", "IdentitiesOnly=yes", "127.0.0.1")
err = gitsshCmd.ExecuteContext(context.Background())
require.NoError(t, err)
require.EqualValues(t, 1, inc)
err = <-sshErrC
require.NoError(t, err, "error in ssh session exit")
cancelFunc()
err = <-agentErrC
require.NoError(t, err, "error in agent execute")
})
}
+169
View File
@@ -0,0 +1,169 @@
package cli
import (
"fmt"
"strings"
"time"
"github.com/google/uuid"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/coderd/autobuild/schedule"
"github.com/coder/coder/coderd/util/ptr"
"github.com/coder/coder/codersdk"
)
func list() *cobra.Command {
var (
columns []string
)
cmd := &cobra.Command{
Annotations: workspaceCommand,
Use: "list",
Short: "List all workspaces",
Aliases: []string{"ls"},
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
return err
}
workspaces, err := client.Workspaces(cmd.Context(), codersdk.WorkspaceFilter{})
if err != nil {
return err
}
if len(workspaces) == 0 {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Prompt.String()+"No workspaces found! Create one:")
_, _ = fmt.Fprintln(cmd.OutOrStdout())
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+cliui.Styles.Code.Render("coder create <name>"))
_, _ = fmt.Fprintln(cmd.OutOrStdout())
return nil
}
users, err := client.Users(cmd.Context(), codersdk.UsersRequest{})
if err != nil {
return err
}
usersByID := map[uuid.UUID]codersdk.User{}
for _, user := range users {
usersByID[user.ID] = user
}
tableWriter := cliui.Table()
header := table.Row{"workspace", "template", "status", "last built", "outdated", "autostart", "ttl"}
tableWriter.AppendHeader(header)
tableWriter.SortBy([]table.SortBy{{
Name: "workspace",
}})
tableWriter.SetColumnConfigs(cliui.FilterTableColumns(header, columns))
for _, workspace := range workspaces {
status := ""
inProgress := false
if workspace.LatestBuild.Job.Status == codersdk.ProvisionerJobRunning ||
workspace.LatestBuild.Job.Status == codersdk.ProvisionerJobCanceling {
inProgress = true
}
switch workspace.LatestBuild.Transition {
case codersdk.WorkspaceTransitionStart:
status = "Running"
if inProgress {
status = "Starting"
}
case codersdk.WorkspaceTransitionStop:
status = "Stopped"
if inProgress {
status = "Stopping"
}
case codersdk.WorkspaceTransitionDelete:
status = "Deleted"
if inProgress {
status = "Deleting"
}
}
if workspace.LatestBuild.Job.Status == codersdk.ProvisionerJobFailed {
status = "Failed"
}
duration := time.Now().UTC().Sub(workspace.LatestBuild.Job.CreatedAt).Truncate(time.Second)
autostartDisplay := "-"
if !ptr.NilOrEmpty(workspace.AutostartSchedule) {
if sched, err := schedule.Weekly(*workspace.AutostartSchedule); err == nil {
autostartDisplay = sched.Cron()
}
}
autostopDisplay := "-"
if !ptr.NilOrZero(workspace.TTLMillis) {
dur := time.Duration(*workspace.TTLMillis) * time.Millisecond
autostopDisplay = durationDisplay(dur)
if has, ext := hasExtension(workspace); has {
autostopDisplay += fmt.Sprintf(" (+%s)", durationDisplay(ext.Round(time.Minute)))
}
}
user := usersByID[workspace.OwnerID]
tableWriter.AppendRow(table.Row{
user.Username + "/" + workspace.Name,
workspace.TemplateName,
status,
durationDisplay(duration),
workspace.Outdated,
autostartDisplay,
autostopDisplay,
})
}
_, err = fmt.Fprintln(cmd.OutOrStdout(), tableWriter.Render())
return err
},
}
cmd.Flags().StringArrayVarP(&columns, "column", "c", nil,
"Specify a column to filter in the table.")
return cmd
}
func hasExtension(ws codersdk.Workspace) (bool, time.Duration) {
if ws.LatestBuild.Transition != codersdk.WorkspaceTransitionStart {
return false, 0
}
if ws.LatestBuild.Deadline.IsZero() {
return false, 0
}
if ws.TTLMillis == nil {
return false, 0
}
ttl := time.Duration(*ws.TTLMillis) * time.Millisecond
delta := ws.LatestBuild.Deadline.Add(-ttl).Sub(ws.LatestBuild.CreatedAt)
if delta < time.Minute {
return false, 0
}
return true, delta
}
func durationDisplay(d time.Duration) string {
duration := d
if duration > time.Hour {
duration = duration.Truncate(time.Hour)
}
if duration > time.Minute {
duration = duration.Truncate(time.Minute)
}
days := 0
for duration.Hours() > 24 {
days++
duration -= 24 * time.Hour
}
durationDisplay := duration.String()
if days > 0 {
durationDisplay = fmt.Sprintf("%dd%s", days, durationDisplay)
}
if strings.HasSuffix(durationDisplay, "m0s") {
durationDisplay = durationDisplay[:len(durationDisplay)-2]
}
if strings.HasSuffix(durationDisplay, "h0m") {
durationDisplay = durationDisplay[:len(durationDisplay)-2]
}
return durationDisplay
}
+42
View File
@@ -0,0 +1,42 @@
package cli_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/pty/ptytest"
)
func TestList(t *testing.T) {
t.Parallel()
t.Run("Single", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFunc()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
cmd, root := clitest.New(t, "ls")
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
errC := make(chan error)
go func() {
errC <- cmd.ExecuteContext(ctx)
}()
pty.ExpectMatch(workspace.Name)
pty.ExpectMatch("Running")
cancelFunc()
require.NoError(t, <-errC)
})
}
+44 -24
View File
@@ -8,6 +8,7 @@ import (
"os"
"os/exec"
"os/user"
"path"
"runtime"
"strings"
@@ -37,8 +38,9 @@ func init() {
func login() *cobra.Command {
return &cobra.Command{
Use: "login <url>",
Args: cobra.ExactArgs(1),
Use: "login <url>",
Short: "Authenticate with a Coder deployment",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
rawURL := args[0]
@@ -117,6 +119,19 @@ func login() *cobra.Command {
if err != nil {
return xerrors.Errorf("specify password prompt: %w", err)
}
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Confirm " + cliui.Styles.Field.Render("password") + ":",
Secret: true,
Validate: func(s string) error {
if s != password {
return xerrors.Errorf("Passwords do not match")
}
return nil
},
})
if err != nil {
return xerrors.Errorf("confirm password prompt: %w", err)
}
_, err = client.CreateFirstUser(cmd.Context(), codersdk.CreateFirstUserRequest{
Email: email,
@@ -150,32 +165,37 @@ func login() *cobra.Command {
cliui.Styles.Paragraph.Render(fmt.Sprintf("Welcome to Coder, %s! You're authenticated.", cliui.Styles.Keyword.Render(username)))+"\n")
_, _ = fmt.Fprintf(cmd.OutOrStdout(),
cliui.Styles.Paragraph.Render("Get started by creating a template: "+cliui.Styles.Code.Render("coder templates create"))+"\n")
cliui.Styles.Paragraph.Render("Get started by creating a template: "+cliui.Styles.Code.Render("coder templates init"))+"\n")
return nil
}
authURL := *serverURL
authURL.Path = serverURL.Path + "/cli-auth"
if err := openURL(cmd, authURL.String()); err != nil {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Open the following in your browser:\n\n\t%s\n\n", authURL.String())
} else {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Your browser has been opened to visit:\n\n\t%s\n\n", authURL.String())
}
sessionToken, _ := cmd.Flags().GetString(varToken)
if sessionToken == "" {
authURL := *serverURL
// Don't use filepath.Join, we don't want to use the os separator
// for a url.
authURL.Path = path.Join(serverURL.Path, "/cli-auth")
if err := openURL(cmd, authURL.String()); err != nil {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Open the following in your browser:\n\n\t%s\n\n", authURL.String())
} else {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Your browser has been opened to visit:\n\n\t%s\n\n", authURL.String())
}
sessionToken, err := cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Paste your token here:",
Secret: true,
Validate: func(token string) error {
client.SessionToken = token
_, err := client.User(cmd.Context(), codersdk.Me)
if err != nil {
return xerrors.New("That's not a valid token!")
}
return err
},
})
if err != nil {
return xerrors.Errorf("paste token prompt: %w", err)
sessionToken, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Paste your token here:",
Secret: true,
Validate: func(token string) error {
client.SessionToken = token
_, err := client.User(cmd.Context(), codersdk.Me)
if err != nil {
return xerrors.New("That's not a valid token!")
}
return err
},
})
if err != nil {
return xerrors.Errorf("paste token prompt: %w", err)
}
}
// Login to get user data - verify it is OK before persisting
+55 -3
View File
@@ -4,6 +4,7 @@ import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/clitest"
@@ -35,7 +36,7 @@ func TestLogin(t *testing.T) {
go func() {
defer close(doneChan)
err := root.Execute()
require.NoError(t, err)
assert.NoError(t, err)
}()
matches := []string{
@@ -43,6 +44,7 @@ func TestLogin(t *testing.T) {
"username", "testuser",
"email", "user@coder.com",
"password", "password",
"password", "password", // Confirm.
}
for i := 0; i < len(matches); i += 2 {
match := matches[i]
@@ -54,6 +56,44 @@ func TestLogin(t *testing.T) {
<-doneChan
})
t.Run("InitialUserTTYConfirmPasswordFailAndReprompt", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
client := coderdtest.New(t, nil)
// The --force-tty flag is required on Windows, because the `isatty` library does not
// accurately detect Windows ptys when they are not attached to a process:
// https://github.com/mattn/go-isatty/issues/59
doneChan := make(chan struct{})
root, _ := clitest.New(t, "login", "--force-tty", client.URL.String())
pty := ptytest.New(t)
root.SetIn(pty.Input())
root.SetOut(pty.Output())
go func() {
defer close(doneChan)
err := root.ExecuteContext(ctx)
assert.ErrorIs(t, err, context.Canceled)
}()
matches := []string{
"first user?", "yes",
"username", "testuser",
"email", "user@coder.com",
"password", "mypass",
"password", "wrongpass", // Confirm.
}
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
pty.WriteLine(value)
}
pty.ExpectMatch("Passwords do not match")
pty.ExpectMatch("password") // Re-prompt password.
cancel()
<-doneChan
})
t.Run("ExistingUserValidTokenTTY", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
@@ -67,7 +107,7 @@ func TestLogin(t *testing.T) {
go func() {
defer close(doneChan)
err := root.Execute()
require.NoError(t, err)
assert.NoError(t, err)
}()
pty.ExpectMatch("Paste your token here:")
@@ -92,7 +132,7 @@ func TestLogin(t *testing.T) {
defer close(doneChan)
err := root.ExecuteContext(ctx)
// An error is expected in this case, since the login wasn't successful:
require.Error(t, err)
assert.Error(t, err)
}()
pty.ExpectMatch("Paste your token here:")
@@ -101,4 +141,16 @@ func TestLogin(t *testing.T) {
cancelFunc()
<-doneChan
})
t.Run("TokenFlag", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
root, cfg := clitest.New(t, "login", client.URL.String(), "--token", client.SessionToken)
err := root.Execute()
require.NoError(t, err)
sessionFile, err := cfg.Session().Read()
require.NoError(t, err)
require.Equal(t, client.SessionToken, sessionFile)
})
}
+77
View File
@@ -0,0 +1,77 @@
package cli
import (
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
"github.com/coder/coder/cli/cliui"
)
func logout() *cobra.Command {
cmd := &cobra.Command{
Use: "logout",
Short: "Remove the local authenticated session",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
return err
}
var errors []error
config := createConfig(cmd)
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Are you sure you want to logout?",
IsConfirm: true,
Default: "yes",
})
if err != nil {
return err
}
err = client.Logout(cmd.Context())
if err != nil {
errors = append(errors, xerrors.Errorf("logout api: %w", err))
}
err = config.URL().Delete()
// Only throw error if the URL configuration file is present,
// otherwise the user is already logged out, and we proceed
if err != nil && !os.IsNotExist(err) {
errors = append(errors, xerrors.Errorf("remove URL file: %w", err))
}
err = config.Session().Delete()
// Only throw error if the session configuration file is present,
// otherwise the user is already logged out, and we proceed
if err != nil && !os.IsNotExist(err) {
errors = append(errors, xerrors.Errorf("remove session file: %w", err))
}
err = config.Organization().Delete()
// If the organization configuration file is absent, we still proceed
if err != nil && !os.IsNotExist(err) {
errors = append(errors, xerrors.Errorf("remove organization file: %w", err))
}
if len(errors) > 0 {
var errorStringBuilder strings.Builder
for _, err := range errors {
_, _ = fmt.Fprint(&errorStringBuilder, "\t"+err.Error()+"\n")
}
errorString := strings.TrimRight(errorStringBuilder.String(), "\n")
return xerrors.New("Failed to log out.\n" + errorString)
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), caret+"You are no longer logged in. You can log in using 'coder login <url>'.\n")
return nil
},
}
cliui.AllowSkipPrompt(cmd)
return cmd
}
+217
View File
@@ -0,0 +1,217 @@
package cli_test
import (
"fmt"
"os"
"regexp"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/cli/config"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/pty/ptytest"
)
func TestLogout(t *testing.T) {
t.Parallel()
t.Run("Logout", func(t *testing.T) {
t.Parallel()
pty := ptytest.New(t)
config := login(t, pty)
// Ensure session files exist.
require.FileExists(t, string(config.URL()))
require.FileExists(t, string(config.Session()))
logoutChan := make(chan struct{})
logout, _ := clitest.New(t, "logout", "--global-config", string(config))
logout.SetIn(pty.Input())
logout.SetOut(pty.Output())
go func() {
defer close(logoutChan)
err := logout.Execute()
assert.NoError(t, err)
assert.NoFileExists(t, string(config.URL()))
assert.NoFileExists(t, string(config.Session()))
}()
pty.ExpectMatch("Are you sure you want to logout?")
pty.WriteLine("yes")
pty.ExpectMatch("You are no longer logged in. You can log in using 'coder login <url>'.")
<-logoutChan
})
t.Run("SkipPrompt", func(t *testing.T) {
t.Parallel()
pty := ptytest.New(t)
config := login(t, pty)
// Ensure session files exist.
require.FileExists(t, string(config.URL()))
require.FileExists(t, string(config.Session()))
logoutChan := make(chan struct{})
logout, _ := clitest.New(t, "logout", "--global-config", string(config), "-y")
logout.SetIn(pty.Input())
logout.SetOut(pty.Output())
go func() {
defer close(logoutChan)
err := logout.Execute()
assert.NoError(t, err)
assert.NoFileExists(t, string(config.URL()))
assert.NoFileExists(t, string(config.Session()))
}()
pty.ExpectMatch("You are no longer logged in. You can log in using 'coder login <url>'.")
<-logoutChan
})
t.Run("NoURLFile", func(t *testing.T) {
t.Parallel()
pty := ptytest.New(t)
config := login(t, pty)
// Ensure session files exist.
require.FileExists(t, string(config.URL()))
require.FileExists(t, string(config.Session()))
err := os.Remove(string(config.URL()))
require.NoError(t, err)
logoutChan := make(chan struct{})
logout, _ := clitest.New(t, "logout", "--global-config", string(config))
logout.SetIn(pty.Input())
logout.SetOut(pty.Output())
go func() {
defer close(logoutChan)
err := logout.Execute()
assert.EqualError(t, err, "You are not logged in. Try logging in using 'coder login <url>'.")
}()
<-logoutChan
})
t.Run("NoSessionFile", func(t *testing.T) {
t.Parallel()
pty := ptytest.New(t)
config := login(t, pty)
// Ensure session files exist.
require.FileExists(t, string(config.URL()))
require.FileExists(t, string(config.Session()))
err := os.Remove(string(config.Session()))
require.NoError(t, err)
logoutChan := make(chan struct{})
logout, _ := clitest.New(t, "logout", "--global-config", string(config))
logout.SetIn(pty.Input())
logout.SetOut(pty.Output())
go func() {
defer close(logoutChan)
err = logout.Execute()
assert.EqualError(t, err, "You are not logged in. Try logging in using 'coder login <url>'.")
}()
<-logoutChan
})
t.Run("CannotDeleteFiles", func(t *testing.T) {
t.Parallel()
pty := ptytest.New(t)
config := login(t, pty)
// Ensure session files exist.
require.FileExists(t, string(config.URL()))
require.FileExists(t, string(config.Session()))
var (
err error
urlFile *os.File
sessionFile *os.File
)
if runtime.GOOS == "windows" {
// Opening the files so Windows does not allow deleting them.
urlFile, err = os.Open(string(config.URL()))
require.NoError(t, err)
sessionFile, err = os.Open(string(config.Session()))
require.NoError(t, err)
} else {
// Changing the permissions to throw error during deletion.
err = os.Chmod(string(config), 0500)
require.NoError(t, err)
}
t.Cleanup(func() {
if runtime.GOOS == "windows" {
// Closing the opened files for cleanup.
err = urlFile.Close()
require.NoError(t, err)
err = sessionFile.Close()
require.NoError(t, err)
} else {
// Setting the permissions back for cleanup.
err = os.Chmod(string(config), 0700)
require.NoError(t, err)
}
})
logoutChan := make(chan struct{})
logout, _ := clitest.New(t, "logout", "--global-config", string(config))
logout.SetIn(pty.Input())
logout.SetOut(pty.Output())
go func() {
defer close(logoutChan)
err := logout.Execute()
assert.NotNil(t, err)
var errorMessage string
if runtime.GOOS == "windows" {
errorMessage = "The process cannot access the file because it is being used by another process."
} else {
errorMessage = "permission denied"
}
errRegex := regexp.MustCompile(fmt.Sprintf("Failed to log out.\n\tremove URL file: .+: %s\n\tremove session file: .+: %s", errorMessage, errorMessage))
assert.Regexp(t, errRegex, err.Error())
}()
pty.ExpectMatch("Are you sure you want to logout?")
pty.WriteLine("yes")
<-logoutChan
})
}
func login(t *testing.T, pty *ptytest.PTY) config.Root {
t.Helper()
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
doneChan := make(chan struct{})
root, cfg := clitest.New(t, "login", "--force-tty", client.URL.String(), "--no-open")
root.SetIn(pty.Input())
root.SetOut(pty.Output())
go func() {
defer close(doneChan)
err := root.Execute()
assert.NoError(t, err)
}()
pty.ExpectMatch("Paste your token here:")
pty.WriteLine(client.SessionToken)
pty.ExpectMatch("Welcome to Coder")
<-doneChan
return cfg
}
+57
View File
@@ -0,0 +1,57 @@
package cli
import (
"os"
"golang.org/x/xerrors"
"gopkg.in/yaml.v3"
"github.com/spf13/cobra"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/codersdk"
)
// Reads a YAML file and populates a string -> string map.
// Throws an error if the file name is empty.
func createParameterMapFromFile(parameterFile string) (map[string]string, error) {
if parameterFile != "" {
parameterMap := make(map[string]string)
parameterFileContents, err := os.ReadFile(parameterFile)
if err != nil {
return nil, err
}
err = yaml.Unmarshal(parameterFileContents, &parameterMap)
if err != nil {
return nil, err
}
return parameterMap, nil
}
return nil, xerrors.Errorf("Parameter file name is not specified")
}
// Returns a parameter value from a given map, if the map exists, else takes input from the user.
// Throws an error if the map exists but does not include a value for the parameter.
func getParameterValueFromMapOrInput(cmd *cobra.Command, parameterMap map[string]string, parameterSchema codersdk.ParameterSchema) (string, error) {
var parameterValue string
if parameterMap != nil {
var ok bool
parameterValue, ok = parameterMap[parameterSchema.Name]
if !ok {
return "", xerrors.Errorf("Parameter value absent in parameter file for %q!", parameterSchema.Name)
}
} else {
var err error
parameterValue, err = cliui.ParameterSchema(cmd, parameterSchema)
if err != nil {
return "", err
}
}
return parameterValue, nil
}
+79
View File
@@ -0,0 +1,79 @@
package cli
import (
"os"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCreateParameterMapFromFile(t *testing.T) {
t.Parallel()
t.Run("CreateParameterMapFromFile", func(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml")
_, _ = parameterFile.WriteString("region: \"bananas\"\ndisk: \"20\"\n")
parameterMapFromFile, err := createParameterMapFromFile(parameterFile.Name())
expectedMap := map[string]string{
"region": "bananas",
"disk": "20",
}
assert.Equal(t, expectedMap, parameterMapFromFile)
assert.Nil(t, err)
removeTmpDirUntilSuccess(t, tempDir)
})
t.Run("WithEmptyFilename", func(t *testing.T) {
t.Parallel()
parameterMapFromFile, err := createParameterMapFromFile("")
assert.Nil(t, parameterMapFromFile)
assert.EqualError(t, err, "Parameter file name is not specified")
})
t.Run("WithInvalidFilename", func(t *testing.T) {
t.Parallel()
parameterMapFromFile, err := createParameterMapFromFile("invalidFile.yaml")
assert.Nil(t, parameterMapFromFile)
// On Unix based systems, it is: `open invalidFile.yaml: no such file or directory`
// On Windows, it is `open invalidFile.yaml: The system cannot find the file specified.`
if runtime.GOOS == "windows" {
assert.EqualError(t, err, "open invalidFile.yaml: The system cannot find the file specified.")
} else {
assert.EqualError(t, err, "open invalidFile.yaml: no such file or directory")
}
})
t.Run("WithInvalidYAML", func(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml")
_, _ = parameterFile.WriteString("region = \"bananas\"\ndisk = \"20\"\n")
parameterMapFromFile, err := createParameterMapFromFile(parameterFile.Name())
assert.Nil(t, parameterMapFromFile)
assert.EqualError(t, err, "yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `region ...` into map[string]string")
removeTmpDirUntilSuccess(t, tempDir)
})
}
// Need this for Windows because of a known issue with Go:
// https://github.com/golang/go/issues/52986
func removeTmpDirUntilSuccess(t *testing.T, tempDir string) {
t.Helper()
t.Cleanup(func() {
err := os.RemoveAll(tempDir)
for err != nil {
err = os.RemoveAll(tempDir)
}
})
}
-75
View File
@@ -1,75 +0,0 @@
package cli
import (
"fmt"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/codersdk"
)
func parameterCreate() *cobra.Command {
var (
name string
value string
scheme string
)
cmd := &cobra.Command{
Use: "create <scope> [name]",
Aliases: []string{"mk"},
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return err
}
scopeName := ""
if len(args) >= 2 {
scopeName = args[1]
}
scope, scopeID, err := parseScopeAndID(cmd.Context(), client, organization, args[0], scopeName)
if err != nil {
return err
}
scheme, err := parseParameterScheme(scheme)
if err != nil {
return err
}
_, err = client.CreateParameter(cmd.Context(), scope, scopeID, codersdk.CreateParameterRequest{
Name: name,
SourceValue: value,
SourceScheme: database.ParameterSourceSchemeData,
DestinationScheme: scheme,
})
if err != nil {
return err
}
_, _ = fmt.Printf("Created!\n")
return nil
},
}
cmd.Flags().StringVarP(&name, "name", "n", "", "Name for a parameter.")
_ = cmd.MarkFlagRequired("name")
cmd.Flags().StringVarP(&value, "value", "v", "", "Value for a parameter.")
_ = cmd.MarkFlagRequired("value")
cmd.Flags().StringVarP(&scheme, "scheme", "s", "var", `Scheme for the parameter ("var" or "env").`)
return cmd
}
func parseParameterScheme(scheme string) (database.ParameterDestinationScheme, error) {
switch scheme {
case "env":
return database.ParameterDestinationSchemeEnvironmentVariable, nil
case "var":
return database.ParameterDestinationSchemeProvisionerVariable, nil
}
return database.ParameterDestinationSchemeNone, xerrors.Errorf("scheme %q not recognized", scheme)
}
-13
View File
@@ -1,13 +0,0 @@
package cli
import "github.com/spf13/cobra"
func parameterDelete() *cobra.Command {
return &cobra.Command{
Use: "delete",
Aliases: []string{"rm"},
RunE: func(cmd *cobra.Command, args []string) error {
return nil
},
}
}
-50
View File
@@ -1,50 +0,0 @@
package cli
import (
"fmt"
"text/tabwriter"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
func parameterList() *cobra.Command {
return &cobra.Command{
Use: "list <scope> <scope-id>",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return err
}
name := ""
if len(args) >= 2 {
name = args[1]
}
scope, scopeID, err := parseScopeAndID(cmd.Context(), client, organization, args[0], name)
if err != nil {
return err
}
params, err := client.Parameters(cmd.Context(), scope, scopeID)
if err != nil {
return err
}
writer := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 4, ' ', 0)
_, _ = fmt.Fprintf(writer, "%s\t%s\t%s\n",
color.HiBlackString("Parameter"),
color.HiBlackString("Created"),
color.HiBlackString("Scheme"))
for _, param := range params {
_, _ = fmt.Fprintf(writer, "%s\t%s\t%s\n",
color.New(color.FgHiCyan).Sprint(param.Name),
color.WhiteString(param.UpdatedAt.Format("January 2, 2006")),
color.New(color.FgHiWhite).Sprint(param.DestinationScheme))
}
return writer.Flush()
},
}
}
-78
View File
@@ -1,78 +0,0 @@
package cli
import (
"context"
"github.com/google/uuid"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
"github.com/coder/coder/codersdk"
)
func parameters() *cobra.Command {
cmd := &cobra.Command{
Use: "parameters",
Aliases: []string{"params"},
}
cmd.AddCommand(parameterCreate(), parameterList(), parameterDelete())
return cmd
}
func parseScopeAndID(ctx context.Context, client *codersdk.Client, organization codersdk.Organization, rawScope string, name string) (codersdk.ParameterScope, uuid.UUID, error) {
scope, err := parseParameterScope(rawScope)
if err != nil {
return scope, uuid.Nil, err
}
var scopeID uuid.UUID
switch scope {
case codersdk.ParameterOrganization:
if name == "" {
scopeID = organization.ID
} else {
org, err := client.OrganizationByName(ctx, codersdk.Me, name)
if err != nil {
return scope, uuid.Nil, err
}
scopeID = org.ID
}
case codersdk.ParameterTemplate:
template, err := client.TemplateByName(ctx, organization.ID, name)
if err != nil {
return scope, uuid.Nil, err
}
scopeID = template.ID
case codersdk.ParameterUser:
uid, _ := uuid.Parse(name)
user, err := client.User(ctx, uid)
if err != nil {
return scope, uuid.Nil, err
}
scopeID = user.ID
case codersdk.ParameterWorkspace:
workspace, err := client.WorkspaceByName(ctx, codersdk.Me, name)
if err != nil {
return scope, uuid.Nil, err
}
scopeID = workspace.ID
}
return scope, scopeID, nil
}
func parseParameterScope(scope string) (codersdk.ParameterScope, error) {
switch scope {
case string(codersdk.ParameterOrganization):
return codersdk.ParameterOrganization, nil
case string(codersdk.ParameterTemplate):
return codersdk.ParameterTemplate, nil
case string(codersdk.ParameterUser):
return codersdk.ParameterUser, nil
case string(codersdk.ParameterWorkspace):
return codersdk.ParameterWorkspace, nil
}
return codersdk.ParameterOrganization, xerrors.Errorf("no scope found by name %q", scope)
}
+374
View File
@@ -0,0 +1,374 @@
package cli
import (
"context"
"fmt"
"net"
"os"
"os/signal"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
"github.com/pion/udp"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
coderagent "github.com/coder/coder/agent"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/codersdk"
)
func portForward() *cobra.Command {
var (
tcpForwards []string // <port>:<port>
udpForwards []string // <port>:<port>
unixForwards []string // <path>:<path> OR <port>:<path>
)
cmd := &cobra.Command{
Use: "port-forward <workspace>",
Aliases: []string{"tunnel"},
Args: cobra.ExactArgs(1),
Example: `
- Port forward a single TCP port from 1234 in the workspace to port 5678 on
your local machine
` + cliui.Styles.Code.Render("$ coder port-forward <workspace> --tcp 5678:1234") + `
- Port forward a single UDP port from port 9000 to port 9000 on your local
machine
` + cliui.Styles.Code.Render("$ coder port-forward <workspace> --udp 9000") + `
- Forward a Unix socket in the workspace to a local Unix socket
` + cliui.Styles.Code.Render("$ coder port-forward <workspace> --unix ./local.sock:~/remote.sock") + `
- Forward a Unix socket in the workspace to a local TCP port
` + cliui.Styles.Code.Render("$ coder port-forward <workspace> --unix 8080:~/remote.sock") + `
- Port forward multiple TCP ports and a UDP port
` + cliui.Styles.Code.Render("$ coder port-forward <workspace> --tcp 8080:8080 --tcp 9000:3000 --udp 5353:53"),
RunE: func(cmd *cobra.Command, args []string) error {
specs, err := parsePortForwards(tcpForwards, udpForwards, unixForwards)
if err != nil {
return xerrors.Errorf("parse port-forward specs: %w", err)
}
if len(specs) == 0 {
err = cmd.Help()
if err != nil {
return xerrors.Errorf("generate help output: %w", err)
}
return xerrors.New("no port-forwards requested")
}
client, err := createClient(cmd)
if err != nil {
return err
}
workspace, agent, err := getWorkspaceAndAgent(cmd, client, codersdk.Me, args[0], false)
if err != nil {
return err
}
if workspace.LatestBuild.Transition != codersdk.WorkspaceTransitionStart {
return xerrors.New("workspace must be in start transition to port-forward")
}
if workspace.LatestBuild.Job.CompletedAt == nil {
err = cliui.WorkspaceBuild(cmd.Context(), cmd.ErrOrStderr(), client, workspace.LatestBuild.ID, workspace.CreatedAt)
if err != nil {
return err
}
}
err = cliui.Agent(cmd.Context(), cmd.ErrOrStderr(), cliui.AgentOptions{
WorkspaceName: workspace.Name,
Fetch: func(ctx context.Context) (codersdk.WorkspaceAgent, error) {
return client.WorkspaceAgent(ctx, agent.ID)
},
})
if err != nil {
return xerrors.Errorf("await agent: %w", err)
}
conn, err := client.DialWorkspaceAgent(cmd.Context(), agent.ID, nil)
if err != nil {
return xerrors.Errorf("dial workspace agent: %w", err)
}
defer conn.Close()
// Start all listeners.
var (
ctx, cancel = context.WithCancel(cmd.Context())
wg = new(sync.WaitGroup)
listeners = make([]net.Listener, len(specs))
closeAllListeners = func() {
for _, l := range listeners {
if l == nil {
continue
}
_ = l.Close()
}
}
)
defer cancel()
for i, spec := range specs {
l, err := listenAndPortForward(ctx, cmd, conn, wg, spec)
if err != nil {
closeAllListeners()
return err
}
listeners[i] = l
}
// Wait for the context to be canceled or for a signal and close
// all listeners.
var closeErr error
go func() {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
select {
case <-ctx.Done():
closeErr = ctx.Err()
case <-sigs:
_, _ = fmt.Fprintln(cmd.OutOrStderr(), "Received signal, closing all listeners and active connections")
closeErr = xerrors.New("signal received")
}
cancel()
closeAllListeners()
}()
_, _ = fmt.Fprintln(cmd.OutOrStderr(), "Ready!")
wg.Wait()
return closeErr
},
}
cmd.Flags().StringArrayVarP(&tcpForwards, "tcp", "p", []string{}, "Forward a TCP port from the workspace to the local machine")
cmd.Flags().StringArrayVar(&udpForwards, "udp", []string{}, "Forward a UDP port from the workspace to the local machine. The UDP connection has TCP-like semantics to support stateful UDP protocols")
cmd.Flags().StringArrayVar(&unixForwards, "unix", []string{}, "Forward a Unix socket in the workspace to a local Unix socket or TCP port")
return cmd
}
func listenAndPortForward(ctx context.Context, cmd *cobra.Command, conn *coderagent.Conn, wg *sync.WaitGroup, spec portForwardSpec) (net.Listener, error) {
_, _ = fmt.Fprintf(cmd.OutOrStderr(), "Forwarding '%v://%v' locally to '%v://%v' in the workspace\n", spec.listenNetwork, spec.listenAddress, spec.dialNetwork, spec.dialAddress)
var (
l net.Listener
err error
)
switch spec.listenNetwork {
case "tcp":
l, err = net.Listen(spec.listenNetwork, spec.listenAddress)
case "udp":
var host, port string
host, port, err = net.SplitHostPort(spec.listenAddress)
if err != nil {
return nil, xerrors.Errorf("split %q: %w", spec.listenAddress, err)
}
var portInt int
portInt, err = strconv.Atoi(port)
if err != nil {
return nil, xerrors.Errorf("parse port %v from %q as int: %w", port, spec.listenAddress, err)
}
l, err = udp.Listen(spec.listenNetwork, &net.UDPAddr{
IP: net.ParseIP(host),
Port: portInt,
})
case "unix":
l, err = net.Listen(spec.listenNetwork, spec.listenAddress)
default:
return nil, xerrors.Errorf("unknown listen network %q", spec.listenNetwork)
}
if err != nil {
return nil, xerrors.Errorf("listen '%v://%v': %w", spec.listenNetwork, spec.listenAddress, err)
}
wg.Add(1)
go func(spec portForwardSpec) {
defer wg.Done()
for {
netConn, err := l.Accept()
if err != nil {
_, _ = fmt.Fprintf(cmd.OutOrStderr(), "Error accepting connection from '%v://%v': %+v\n", spec.listenNetwork, spec.listenAddress, err)
_, _ = fmt.Fprintln(cmd.OutOrStderr(), "Killing listener")
return
}
go func(netConn net.Conn) {
defer netConn.Close()
remoteConn, err := conn.DialContext(ctx, spec.dialNetwork, spec.dialAddress)
if err != nil {
_, _ = fmt.Fprintf(cmd.OutOrStderr(), "Failed to dial '%v://%v' in workspace: %s\n", spec.dialNetwork, spec.dialAddress, err)
return
}
defer remoteConn.Close()
coderagent.Bicopy(ctx, netConn, remoteConn)
}(netConn)
}
}(spec)
return l, nil
}
type portForwardSpec struct {
listenNetwork string // tcp, udp, unix
listenAddress string // <ip>:<port> or path
dialNetwork string // tcp, udp, unix
dialAddress string // <ip>:<port> or path
}
func parsePortForwards(tcpSpecs, udpSpecs, unixSpecs []string) ([]portForwardSpec, error) {
specs := []portForwardSpec{}
for _, spec := range tcpSpecs {
local, remote, err := parsePortPort(spec)
if err != nil {
return nil, xerrors.Errorf("failed to parse TCP port-forward specification %q: %w", spec, err)
}
specs = append(specs, portForwardSpec{
listenNetwork: "tcp",
listenAddress: fmt.Sprintf("127.0.0.1:%v", local),
dialNetwork: "tcp",
dialAddress: fmt.Sprintf("127.0.0.1:%v", remote),
})
}
for _, spec := range udpSpecs {
local, remote, err := parsePortPort(spec)
if err != nil {
return nil, xerrors.Errorf("failed to parse UDP port-forward specification %q: %w", spec, err)
}
specs = append(specs, portForwardSpec{
listenNetwork: "udp",
listenAddress: fmt.Sprintf("127.0.0.1:%v", local),
dialNetwork: "udp",
dialAddress: fmt.Sprintf("127.0.0.1:%v", remote),
})
}
for _, specStr := range unixSpecs {
localPath, localTCP, remotePath, err := parseUnixUnix(specStr)
if err != nil {
return nil, xerrors.Errorf("failed to parse Unix port-forward specification %q: %w", specStr, err)
}
spec := portForwardSpec{
dialNetwork: "unix",
dialAddress: remotePath,
}
if localPath == "" {
spec.listenNetwork = "tcp"
spec.listenAddress = fmt.Sprintf("127.0.0.1:%v", localTCP)
} else {
if runtime.GOOS == "windows" {
return nil, xerrors.Errorf("Unix port-forwarding is not supported on Windows")
}
spec.listenNetwork = "unix"
spec.listenAddress = localPath
}
specs = append(specs, spec)
}
// Check for duplicate entries.
locals := map[string]struct{}{}
for _, spec := range specs {
localStr := fmt.Sprintf("%v:%v", spec.listenNetwork, spec.listenAddress)
if _, ok := locals[localStr]; ok {
return nil, xerrors.Errorf("local %v %v is specified twice", spec.listenNetwork, spec.listenAddress)
}
locals[localStr] = struct{}{}
}
return specs, nil
}
func parsePort(in string) (uint16, error) {
port, err := strconv.ParseUint(strings.TrimSpace(in), 10, 16)
if err != nil {
return 0, xerrors.Errorf("parse port %q: %w", in, err)
}
if port == 0 {
return 0, xerrors.New("port cannot be 0")
}
return uint16(port), nil
}
func parseUnixPath(in string) (string, error) {
path, err := coderagent.ExpandRelativeHomePath(strings.TrimSpace(in))
if err != nil {
return "", xerrors.Errorf("tidy path %q: %w", in, err)
}
return path, nil
}
func parsePortPort(in string) (local uint16, remote uint16, err error) {
parts := strings.Split(in, ":")
if len(parts) > 2 {
return 0, 0, xerrors.Errorf("invalid port specification %q", in)
}
if len(parts) == 1 {
// Duplicate the single part
parts = append(parts, parts[0])
}
local, err = parsePort(parts[0])
if err != nil {
return 0, 0, xerrors.Errorf("parse local port from %q: %w", in, err)
}
remote, err = parsePort(parts[1])
if err != nil {
return 0, 0, xerrors.Errorf("parse remote port from %q: %w", in, err)
}
return local, remote, nil
}
func parsePortOrUnixPath(in string) (string, uint16, error) {
port, err := parsePort(in)
if err == nil {
return "", port, nil
}
path, err := parseUnixPath(in)
if err != nil {
return "", 0, xerrors.Errorf("could not parse port or unix path %q: %w", in, err)
}
return path, 0, nil
}
func parseUnixUnix(in string) (string, uint16, string, error) {
parts := strings.Split(in, ":")
if len(parts) > 2 {
return "", 0, "", xerrors.Errorf("invalid port-forward specification %q", in)
}
if len(parts) == 1 {
// Duplicate the single part
parts = append(parts, parts[0])
}
localPath, localPort, err := parsePortOrUnixPath(parts[0])
if err != nil {
return "", 0, "", xerrors.Errorf("parse local part of spec %q: %w", in, err)
}
// We don't really touch the remote path at all since it gets cleaned
// up/expanded on the remote.
return localPath, localPort, parts[1], nil
}
+551
View File
@@ -0,0 +1,551 @@
package cli_test
import (
"bytes"
"context"
"fmt"
"io"
"net"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"time"
"github.com/google/uuid"
"github.com/pion/udp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/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"
)
func TestPortForward(t *testing.T) {
t.Parallel()
t.Run("None", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
cmd, root := clitest.New(t, "port-forward", "blah")
clitest.SetupConfig(t, client, root)
buf := newThreadSafeBuffer()
cmd.SetOut(buf)
err := cmd.Execute()
require.Error(t, err)
require.ErrorContains(t, err, "no port-forwards")
// Check that the help was printed.
require.Contains(t, buf.String(), "port-forward <workspace>")
})
cases := []struct {
name string
network string
// The flag to pass to `coder port-forward X` to port-forward this type
// of connection. Has two format args (both strings), the first is the
// local address and the second is the remote address.
flag string
// setupRemote creates a "remote" listener to emulate a service in the
// workspace.
setupRemote func(t *testing.T) net.Listener
// setupLocal returns an available port or Unix socket path that the
// port-forward command will listen on "locally". Returns the address
// you pass to net.Dial, and the port/path you pass to `coder
// port-forward`.
setupLocal func(t *testing.T) (string, string)
}{
{
name: "TCP",
network: "tcp",
flag: "--tcp=%v:%v",
setupRemote: func(t *testing.T) net.Listener {
l, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err, "create TCP listener")
return l
},
setupLocal: func(t *testing.T) (string, string) {
l, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err, "create TCP listener to generate random port")
defer l.Close()
_, port, err := net.SplitHostPort(l.Addr().String())
require.NoErrorf(t, err, "split TCP address %q", l.Addr().String())
return l.Addr().String(), port
},
},
{
name: "UDP",
network: "udp",
flag: "--udp=%v:%v",
setupRemote: func(t *testing.T) net.Listener {
addr := net.UDPAddr{
IP: net.ParseIP("127.0.0.1"),
Port: 0,
}
l, err := udp.Listen("udp", &addr)
require.NoError(t, err, "create UDP listener")
return l
},
setupLocal: func(t *testing.T) (string, string) {
addr := net.UDPAddr{
IP: net.ParseIP("127.0.0.1"),
Port: 0,
}
l, err := udp.Listen("udp", &addr)
require.NoError(t, err, "create UDP listener to generate random port")
defer l.Close()
_, port, err := net.SplitHostPort(l.Addr().String())
require.NoErrorf(t, err, "split UDP address %q", l.Addr().String())
return l.Addr().String(), port
},
},
{
name: "Unix",
network: "unix",
flag: "--unix=%v:%v",
setupRemote: func(t *testing.T) net.Listener {
if runtime.GOOS == "windows" {
t.Skip("Unix socket forwarding isn't supported on Windows")
}
tmpDir, err := os.MkdirTemp("", "coderd_agent_test_")
require.NoError(t, err, "create temp dir for unix listener")
t.Cleanup(func() {
_ = os.RemoveAll(tmpDir)
})
l, err := net.Listen("unix", filepath.Join(tmpDir, "test.sock"))
require.NoError(t, err, "create UDP listener")
return l
},
setupLocal: func(t *testing.T) (string, string) {
tmpDir, err := os.MkdirTemp("", "coderd_agent_test_")
require.NoError(t, err, "create temp dir for unix listener")
t.Cleanup(func() {
_ = os.RemoveAll(tmpDir)
})
path := filepath.Join(tmpDir, "test.sock")
return path, path
},
},
}
for _, c := range cases { //nolint:paralleltest // the `c := c` confuses the linter
c := c
// Avoid parallel test here because setupLocal reserves
// a free open port which is not guaranteed to be free
// after the listener closes.
//nolint:paralleltest
t.Run(c.name, func(t *testing.T) {
//nolint:paralleltest
t.Run("OnePort", func(t *testing.T) {
var (
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user = coderdtest.CreateFirstUser(t, client)
_, workspace = runAgent(t, client, user.UserID)
p1 = setupTestListener(t, c.setupRemote(t))
)
// Create a flag that forwards from local to listener 1.
localAddress, localFlag := c.setupLocal(t)
flag := fmt.Sprintf(c.flag, localFlag, p1)
// Launch port-forward in a goroutine so we can start dialing
// the "local" listener.
cmd, root := clitest.New(t, "port-forward", workspace.Name, flag)
clitest.SetupConfig(t, client, root)
buf := newThreadSafeBuffer()
cmd.SetOut(io.MultiWriter(buf, os.Stderr))
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
errC := make(chan error)
go func() {
errC <- cmd.ExecuteContext(ctx)
}()
waitForPortForwardReady(t, buf)
// Open two connections simultaneously and test them out of
// sync.
d := net.Dialer{Timeout: 3 * time.Second}
c1, err := d.DialContext(ctx, c.network, localAddress)
require.NoError(t, err, "open connection 1 to 'local' listener")
defer c1.Close()
c2, err := d.DialContext(ctx, c.network, localAddress)
require.NoError(t, err, "open connection 2 to 'local' listener")
defer c2.Close()
testDial(t, c2)
testDial(t, c1)
cancel()
err = <-errC
require.ErrorIs(t, err, context.Canceled)
})
//nolint:paralleltest
t.Run("TwoPorts", func(t *testing.T) {
var (
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user = coderdtest.CreateFirstUser(t, client)
_, workspace = runAgent(t, client, user.UserID)
p1 = setupTestListener(t, c.setupRemote(t))
p2 = setupTestListener(t, c.setupRemote(t))
)
// Create a flags for listener 1 and listener 2.
localAddress1, localFlag1 := c.setupLocal(t)
localAddress2, localFlag2 := c.setupLocal(t)
flag1 := fmt.Sprintf(c.flag, localFlag1, p1)
flag2 := fmt.Sprintf(c.flag, localFlag2, p2)
// Launch port-forward in a goroutine so we can start dialing
// the "local" listeners.
cmd, root := clitest.New(t, "port-forward", workspace.Name, flag1, flag2)
clitest.SetupConfig(t, client, root)
buf := newThreadSafeBuffer()
cmd.SetOut(io.MultiWriter(buf, os.Stderr))
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
errC := make(chan error)
go func() {
errC <- cmd.ExecuteContext(ctx)
}()
waitForPortForwardReady(t, buf)
// Open a connection to both listener 1 and 2 simultaneously and
// then test them out of order.
d := net.Dialer{Timeout: 3 * time.Second}
c1, err := d.DialContext(ctx, c.network, localAddress1)
require.NoError(t, err, "open connection 1 to 'local' listener 1")
defer c1.Close()
c2, err := d.DialContext(ctx, c.network, localAddress2)
require.NoError(t, err, "open connection 2 to 'local' listener 2")
defer c2.Close()
testDial(t, c2)
testDial(t, c1)
cancel()
err = <-errC
require.ErrorIs(t, err, context.Canceled)
})
})
}
// Test doing a TCP -> Unix forward.
//nolint:paralleltest
t.Run("TCP2Unix", func(t *testing.T) {
var (
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user = coderdtest.CreateFirstUser(t, client)
_, workspace = runAgent(t, client, user.UserID)
// Find the TCP and Unix cases so we can use their setupLocal and
// setupRemote methods respectively.
tcpCase = cases[0]
unixCase = cases[2]
// Setup remote Unix listener.
p1 = setupTestListener(t, unixCase.setupRemote(t))
)
// Create a flag that forwards from local TCP to Unix listener 1.
// Notably this is a --unix flag.
localAddress, localFlag := tcpCase.setupLocal(t)
flag := fmt.Sprintf(unixCase.flag, localFlag, p1)
// Launch port-forward in a goroutine so we can start dialing
// the "local" listener.
cmd, root := clitest.New(t, "port-forward", workspace.Name, flag)
clitest.SetupConfig(t, client, root)
buf := newThreadSafeBuffer()
cmd.SetOut(io.MultiWriter(buf, os.Stderr))
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
errC := make(chan error)
go func() {
errC <- cmd.ExecuteContext(ctx)
}()
waitForPortForwardReady(t, buf)
// Open two connections simultaneously and test them out of
// sync.
d := net.Dialer{Timeout: 3 * time.Second}
c1, err := d.DialContext(ctx, tcpCase.network, localAddress)
require.NoError(t, err, "open connection 1 to 'local' listener")
defer c1.Close()
c2, err := d.DialContext(ctx, tcpCase.network, localAddress)
require.NoError(t, err, "open connection 2 to 'local' listener")
defer c2.Close()
testDial(t, c2)
testDial(t, c1)
cancel()
err = <-errC
require.ErrorIs(t, err, context.Canceled)
})
// Test doing TCP, UDP and Unix at the same time.
//nolint:paralleltest
t.Run("All", func(t *testing.T) {
var (
client = coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user = coderdtest.CreateFirstUser(t, client)
_, workspace = runAgent(t, client, user.UserID)
// These aren't fixed size because we exclude Unix on Windows.
dials = []addr{}
flags = []string{}
)
// Start listeners and populate arrays with the cases.
for _, c := range cases {
if strings.HasPrefix(c.network, "unix") && runtime.GOOS == "windows" {
// Unix isn't supported on Windows, but we can still
// test other protocols together.
continue
}
p := setupTestListener(t, c.setupRemote(t))
localAddress, localFlag := c.setupLocal(t)
dials = append(dials, addr{
network: c.network,
addr: localAddress,
})
flags = append(flags, fmt.Sprintf(c.flag, localFlag, p))
}
// Launch port-forward in a goroutine so we can start dialing
// the "local" listeners.
cmd, root := clitest.New(t, append([]string{"port-forward", workspace.Name}, flags...)...)
clitest.SetupConfig(t, client, root)
buf := newThreadSafeBuffer()
cmd.SetOut(io.MultiWriter(buf, os.Stderr))
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
errC := make(chan error)
go func() {
errC <- cmd.ExecuteContext(ctx)
}()
waitForPortForwardReady(t, buf)
// Open connections to all items in the "dial" array.
var (
d = net.Dialer{Timeout: 3 * time.Second}
conns = make([]net.Conn, len(dials))
)
for i, a := range dials {
c, err := d.DialContext(ctx, a.network, a.addr)
require.NoErrorf(t, err, "open connection %v to 'local' listener %v", i+1, i+1)
t.Cleanup(func() {
_ = c.Close()
})
conns[i] = c
}
// Test each connection in reverse order.
for i := len(conns) - 1; i >= 0; i-- {
testDial(t, conns[i])
}
cancel()
err := <-errC
require.ErrorIs(t, err, context.Canceled)
})
}
// runAgent creates a fake workspace and starts an agent locally for that
// workspace. The agent will be cleaned up on test completion.
func runAgent(t *testing.T, client *codersdk.Client, userID uuid.UUID) ([]codersdk.WorkspaceResource, codersdk.Workspace) {
ctx := context.Background()
user, err := client.User(ctx, userID.String())
require.NoError(t, err, "specified user does not exist")
require.Greater(t, len(user.OrganizationIDs), 0, "user has no organizations")
orgID := user.OrganizationIDs[0]
// Setup template
agentToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, orgID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionDryRun: echo.ProvisionComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "somename",
Type: "someinstance",
Agents: []*proto.Agent{{
Auth: &proto.Agent_Token{
Token: agentToken,
},
}},
}},
},
},
}},
})
// Create template and workspace
template := coderdtest.CreateTemplate(t, client, orgID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, orgID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
// Start workspace agent in a goroutine
cmd, root := clitest.New(t, "agent", "--agent-token", agentToken, "--agent-url", client.URL.String())
clitest.SetupConfig(t, client, root)
errC := make(chan error)
agentCtx, agentCancel := context.WithCancel(ctx)
t.Cleanup(func() {
agentCancel()
err := <-errC
require.NoError(t, err)
})
go func() {
errC <- cmd.ExecuteContext(agentCtx)
}()
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
resources, err := client.WorkspaceResourcesByBuild(context.Background(), workspace.LatestBuild.ID)
require.NoError(t, err)
return resources, workspace
}
// setupTestListener starts accepting connections and echoing a single packet.
// Returns the listener and the listen port or Unix path.
func setupTestListener(t *testing.T, l net.Listener) string {
// Wait for listener to completely exit before releasing.
done := make(chan struct{})
t.Cleanup(func() {
_ = l.Close()
<-done
})
go func() {
defer close(done)
// Guard against testAccept running require after test completion.
var wg sync.WaitGroup
defer wg.Wait()
for {
c, err := l.Accept()
if err != nil {
return
}
wg.Add(1)
go func() {
testAccept(t, c)
wg.Done()
}()
}
}()
addr := l.Addr().String()
if !strings.HasPrefix(l.Addr().Network(), "unix") {
_, port, err := net.SplitHostPort(addr)
require.NoErrorf(t, err, "split non-Unix listen path %q", addr)
addr = port
}
return addr
}
var dialTestPayload = []byte("dean-was-here123")
func testDial(t *testing.T, c net.Conn) {
t.Helper()
assertWritePayload(t, c, dialTestPayload)
assertReadPayload(t, c, dialTestPayload)
}
func testAccept(t *testing.T, c net.Conn) {
t.Helper()
defer c.Close()
assertReadPayload(t, c, dialTestPayload)
assertWritePayload(t, c, dialTestPayload)
}
func assertReadPayload(t *testing.T, r io.Reader, payload []byte) {
b := make([]byte, len(payload)+16)
n, err := r.Read(b)
assert.NoError(t, err, "read payload")
assert.Equal(t, len(payload), n, "read payload length does not match")
assert.Equal(t, payload, b[:n])
}
func assertWritePayload(t *testing.T, w io.Writer, payload []byte) {
n, err := w.Write(payload)
assert.NoError(t, err, "write payload")
assert.Equal(t, len(payload), n, "payload length does not match")
}
func waitForPortForwardReady(t *testing.T, output *threadSafeBuffer) {
for i := 0; i < 100; i++ {
time.Sleep(250 * time.Millisecond)
data := output.String()
if strings.Contains(data, "Ready!") {
return
}
}
t.Fatal("port-forward command did not become ready in time")
}
type addr struct {
network string
addr string
}
type threadSafeBuffer struct {
b *bytes.Buffer
mut *sync.RWMutex
}
func newThreadSafeBuffer() *threadSafeBuffer {
return &threadSafeBuffer{
b: bytes.NewBuffer(nil),
mut: new(sync.RWMutex),
}
}
var (
_ io.Reader = &threadSafeBuffer{}
_ io.Writer = &threadSafeBuffer{}
)
// Read implements io.Reader.
func (b *threadSafeBuffer) Read(p []byte) (int, error) {
b.mut.RLock()
defer b.mut.RUnlock()
return b.b.Read(p)
}
// Write implements io.Writer.
func (b *threadSafeBuffer) Write(p []byte) (int, error) {
b.mut.Lock()
defer b.mut.Unlock()
return b.b.Write(p)
}
func (b *threadSafeBuffer) String() string {
b.mut.RLock()
defer b.mut.RUnlock()
return b.b.String()
}
+70
View File
@@ -0,0 +1,70 @@
package cli
import (
"strings"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/codersdk"
)
func publickey() *cobra.Command {
var (
reset bool
)
cmd := &cobra.Command{
Use: "publickey",
Aliases: []string{"pubkey"},
Short: "Output your public key for Git operations",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
return xerrors.Errorf("create codersdk client: %w", err)
}
if reset {
// Confirm prompt if using --reset. We don't want to accidentally
// reset our public key.
_, err := cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Confirm regenerate a new sshkey for your workspaces? This will require updating the key " +
"on any services it is registered with. This action cannot be reverted.",
IsConfirm: true,
})
if err != nil {
return err
}
// Reset the public key, let the retrieve re-read it.
_, err = client.RegenerateGitSSHKey(cmd.Context(), codersdk.Me)
if err != nil {
return err
}
}
key, err := client.GitSSHKey(cmd.Context(), codersdk.Me)
if err != nil {
return xerrors.Errorf("create codersdk client: %w", err)
}
cmd.Println(cliui.Styles.Wrap.Render(
"This is your public key for using " + cliui.Styles.Field.Render("git") + " in " +
"Coder. All clones with SSH will be authenticated automatically 🪄.",
))
cmd.Println()
cmd.Println(cliui.Styles.Code.Render(strings.TrimSpace(key.PublicKey)))
cmd.Println()
cmd.Println("Add to GitHub and GitLab:")
cmd.Println(cliui.Styles.Prompt.String() + "https://github.com/settings/ssh/new")
cmd.Println(cliui.Styles.Prompt.String() + "https://gitlab.com/-/profile/keys")
return nil
},
}
cmd.Flags().BoolVar(&reset, "reset", false, "Regenerate your public key. This will require updating the key on any services it's registered with.")
cliui.AllowSkipPrompt(cmd)
return cmd
}
+27
View File
@@ -0,0 +1,27 @@
package cli_test
import (
"bytes"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
)
func TestPublicKey(t *testing.T) {
t.Parallel()
t.Run("OK", func(t *testing.T) {
client := coderdtest.New(t, nil)
_ = coderdtest.CreateFirstUser(t, client)
cmd, root := clitest.New(t, "publickey")
clitest.SetupConfig(t, client, root)
buf := new(bytes.Buffer)
cmd.SetOut(buf)
err := cmd.Execute()
require.NoError(t, err)
publicKey := buf.String()
require.NotEmpty(t, publicKey)
})
}
+90
View File
@@ -0,0 +1,90 @@
package cli
import (
"database/sql"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
"github.com/coder/coder/cli/cliflag"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/userpassword"
)
func resetPassword() *cobra.Command {
var (
postgresURL string
)
root := &cobra.Command{
Use: "reset-password <username>",
Short: "Reset a user's password by directly updating the database",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
username := args[0]
sqlDB, err := sql.Open("postgres", postgresURL)
if err != nil {
return xerrors.Errorf("dial postgres: %w", err)
}
defer sqlDB.Close()
err = sqlDB.Ping()
if err != nil {
return xerrors.Errorf("ping postgres: %w", err)
}
err = database.EnsureClean(sqlDB)
if err != nil {
return xerrors.Errorf("database needs migration: %w", err)
}
db := database.New(sqlDB)
user, err := db.GetUserByEmailOrUsername(cmd.Context(), database.GetUserByEmailOrUsernameParams{
Username: username,
})
if err != nil {
return xerrors.Errorf("retrieving user: %w", err)
}
password, err := cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Enter new " + cliui.Styles.Field.Render("password") + ":",
Secret: true,
Validate: cliui.ValidateNotEmpty,
})
if err != nil {
return xerrors.Errorf("password prompt: %w", err)
}
confirmedPassword, err := cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Confirm " + cliui.Styles.Field.Render("password") + ":",
Secret: true,
Validate: cliui.ValidateNotEmpty,
})
if err != nil {
return xerrors.Errorf("confirm password prompt: %w", err)
}
if password != confirmedPassword {
return xerrors.New("Passwords do not match")
}
hashedPassword, err := userpassword.Hash(password)
if err != nil {
return xerrors.Errorf("hash password: %w", err)
}
err = db.UpdateUserHashedPassword(cmd.Context(), database.UpdateUserHashedPasswordParams{
ID: user.ID,
HashedPassword: []byte(hashedPassword),
})
if err != nil {
return xerrors.Errorf("updating password: %w", err)
}
return nil
},
}
cliflag.StringVarP(root.Flags(), &postgresURL, "postgres-url", "", "CODER_PG_CONNECTION_URL", "", "URL of a PostgreSQL database to connect to")
return root
}
+109
View File
@@ -0,0 +1,109 @@
package cli_test
import (
"context"
"net/url"
"runtime"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/database/postgres"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/pty/ptytest"
)
// nolint:paralleltest
func TestResetPassword(t *testing.T) {
// postgres.Open() seems to be creating race conditions when run in parallel.
// t.Parallel()
if runtime.GOOS != "linux" || testing.Short() {
// Skip on non-Linux because it spawns a PostgreSQL instance.
t.SkipNow()
}
const email = "some@one.com"
const username = "example"
const oldPassword = "password"
const newPassword = "password2"
// start postgres and coder server processes
connectionURL, closeFunc, err := postgres.Open()
require.NoError(t, err)
defer closeFunc()
ctx, cancelFunc := context.WithCancel(context.Background())
serverDone := make(chan struct{})
serverCmd, cfg := clitest.New(t, "server", "--address", ":0", "--postgres-url", connectionURL)
go func() {
defer close(serverDone)
err = serverCmd.ExecuteContext(ctx)
assert.ErrorIs(t, err, context.Canceled)
}()
var client *codersdk.Client
require.Eventually(t, func() bool {
rawURL, err := cfg.URL().Read()
if err != nil {
return false
}
accessURL, err := url.Parse(rawURL)
require.NoError(t, err)
client = codersdk.New(accessURL)
return true
}, 15*time.Second, 25*time.Millisecond)
_, err = client.CreateFirstUser(ctx, codersdk.CreateFirstUserRequest{
Email: email,
Username: username,
Password: oldPassword,
OrganizationName: "example",
})
require.NoError(t, err)
// reset the password
resetCmd, cmdCfg := clitest.New(t, "reset-password", "--postgres-url", connectionURL, username)
clitest.SetupConfig(t, client, cmdCfg)
cmdDone := make(chan struct{})
pty := ptytest.New(t)
resetCmd.SetIn(pty.Input())
resetCmd.SetOut(pty.Output())
go func() {
defer close(cmdDone)
err = resetCmd.Execute()
assert.NoError(t, err)
}()
matches := []struct {
output string
input string
}{
{"Enter new", newPassword},
{"Confirm", newPassword},
}
for _, match := range matches {
pty.ExpectMatch(match.output)
pty.WriteLine(match.input)
}
<-cmdDone
// now try logging in
_, err = client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
Email: email,
Password: oldPassword,
})
require.Error(t, err)
_, err = client.LoginWithPassword(ctx, codersdk.LoginWithPasswordRequest{
Email: email,
Password: newPassword,
})
require.NoError(t, err)
cancelFunc()
<-serverDone
}
+197 -52
View File
@@ -1,17 +1,20 @@
package cli
import (
"fmt"
"net/url"
"os"
"strings"
"time"
"github.com/charmbracelet/lipgloss"
"golang.org/x/xerrors"
"github.com/kirsle/configdir"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
"github.com/coder/coder/cli/buildinfo"
"github.com/coder/coder/buildinfo"
"github.com/coder/coder/cli/cliflag"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/cli/config"
"github.com/coder/coder/codersdk"
@@ -19,80 +22,133 @@ import (
var (
caret = cliui.Styles.Prompt.String()
// Applied as annotations to workspace commands
// so they display in a separated "help" section.
workspaceCommand = map[string]string{
"workspaces": " ",
}
)
const (
varGlobalConfig = "global-config"
varNoOpen = "no-open"
varForceTty = "force-tty"
varURL = "url"
varToken = "token"
varAgentToken = "agent-token"
varAgentURL = "agent-url"
varGlobalConfig = "global-config"
varNoOpen = "no-open"
varForceTty = "force-tty"
notLoggedInMessage = "You are not logged in. Try logging in using 'coder login <url>'."
)
func init() {
// Customizes the color of headings to make subcommands more visually
// appealing.
header := cliui.Styles.Placeholder
cobra.AddTemplateFunc("usageHeader", func(s string) string {
return header.Render(s)
})
}
func Root() *cobra.Command {
cmd := &cobra.Command{
Use: "coder",
Version: buildinfo.Version(),
SilenceUsage: true,
Long: ` ▄█▀ ▀█▄
▄▄ ▀▀▀ █▌ ██▀▀█▄ ▐█
▄▄██▀▀█▄▄▄ ██ ██ █▀▀█ ▐█▀▀██ ▄█▀▀█ █▀▀
█▌ ▄▌ ▐█ █▌ ▀█▄▄▄█▌ █ █ ▐█ ██ ██▀▀ █
██████▀▄█ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀ ▀▀▀▀ ▀
` + lipgloss.NewStyle().Underline(true).Render("Self-hosted developer workspaces on your infra") + `
Use: "coder",
Version: buildinfo.Version(),
SilenceErrors: true,
SilenceUsage: true,
Long: `Coder — A tool for provisioning self-hosted development environments.
`,
Example: cliui.Styles.Paragraph.Render(`Start Coder in "dev" mode. This dev-mode requires no further setup, and your local `+cliui.Styles.Code.Render("coder")+` CLI will be authenticated to talk to it. This makes it easy to experiment with Coder.`) + `
Example: ` Start Coder in "dev" mode. This dev-mode requires no further setup, and your local ` + cliui.Styles.Code.Render("coder") + ` CLI will be authenticated to talk to it. This makes it easy to experiment with Coder.
` + cliui.Styles.Code.Render("$ coder server --dev") + `
` + cliui.Styles.Code.Render("$ coder start --dev") + `
` + cliui.Styles.Paragraph.Render("Get started by creating a template from an example.") + `
` + cliui.Styles.Code.Render("$ coder templates init"),
Get started by creating a template from an example.
` + cliui.Styles.Code.Render("$ coder templates init"),
}
// Customizes the color of headings to make subcommands
// more visually appealing.
header := cliui.Styles.Placeholder
cmd.SetUsageTemplate(strings.NewReplacer(
`Usage:`, header.Render("Usage:"),
`Examples:`, header.Render("Examples:"),
`Available Commands:`, header.Render("Commands:"),
`Global Flags:`, header.Render("Global Flags:"),
`Flags:`, header.Render("Flags:"),
`Additional help topics:`, header.Render("Additional help:"),
).Replace(cmd.UsageTemplate()))
cmd.SetVersionTemplate(versionTemplate())
cmd.AddCommand(
autostart(),
bump(),
configSSH(),
start(),
create(),
delete(),
dotfiles(),
gitssh(),
list(),
login(),
parameters(),
templates(),
users(),
workspaces(),
logout(),
publickey(),
resetPassword(),
server(),
show(),
start(),
state(),
stop(),
ssh(),
workspaceTunnel(),
templates(),
ttl(),
update(),
users(),
portForward(),
workspaceAgent(),
)
cmd.PersistentFlags().String(varGlobalConfig, configdir.LocalConfig("coderv2"), "Path to the global `coder` config directory")
cmd.PersistentFlags().Bool(varForceTty, false, "Force the `coder` command to run as if connected to a TTY")
err := cmd.PersistentFlags().MarkHidden(varForceTty)
if err != nil {
// This should never return an error, because we just added the `--force-tty`` flag prior to calling MarkHidden.
panic(err)
}
cmd.SetUsageTemplate(usageTemplate())
cmd.SetVersionTemplate(versionTemplate())
cmd.PersistentFlags().String(varURL, "", "Specify the URL to your deployment.")
cmd.PersistentFlags().String(varToken, "", "Specify an authentication token.")
cliflag.String(cmd.PersistentFlags(), varAgentToken, "", "CODER_AGENT_TOKEN", "", "Specify an agent authentication token.")
_ = cmd.PersistentFlags().MarkHidden(varAgentToken)
cliflag.String(cmd.PersistentFlags(), varAgentURL, "", "CODER_AGENT_URL", "", "Specify the URL for an agent to access your deployment.")
_ = cmd.PersistentFlags().MarkHidden(varAgentURL)
cliflag.String(cmd.PersistentFlags(), varGlobalConfig, "", "CODER_CONFIG_DIR", configdir.LocalConfig("coderv2"), "Specify the path to the global `coder` config directory.")
cmd.PersistentFlags().Bool(varForceTty, false, "Force the `coder` command to run as if connected to a TTY.")
_ = cmd.PersistentFlags().MarkHidden(varForceTty)
cmd.PersistentFlags().Bool(varNoOpen, false, "Block automatically opening URLs in the browser.")
err = cmd.PersistentFlags().MarkHidden(varNoOpen)
if err != nil {
panic(err)
}
_ = cmd.PersistentFlags().MarkHidden(varNoOpen)
return cmd
}
// createClient returns a new client from the command context.
// The configuration directory will be read from the global flag.
// It reads from global configuration files if flags are not set.
func createClient(cmd *cobra.Command) (*codersdk.Client, error) {
root := createConfig(cmd)
rawURL, err := root.URL().Read()
rawURL, err := cmd.Flags().GetString(varURL)
if err != nil || rawURL == "" {
rawURL, err = root.URL().Read()
if err != nil {
// If the configuration files are absent, the user is logged out
if os.IsNotExist(err) {
return nil, xerrors.New(notLoggedInMessage)
}
return nil, err
}
}
serverURL, err := url.Parse(strings.TrimSpace(rawURL))
if err != nil {
return nil, err
}
token, err := cmd.Flags().GetString(varToken)
if err != nil || token == "" {
token, err = root.Session().Read()
if err != nil {
// If the configuration files are absent, the user is logged out
if os.IsNotExist(err) {
return nil, xerrors.New(notLoggedInMessage)
}
return nil, err
}
}
client := codersdk.New(serverURL)
client.SessionToken = strings.TrimSpace(token)
return client, nil
}
// createAgentClient returns a new client from the command context.
// It works just like createClient, but uses the agent token and URL instead.
func createAgentClient(cmd *cobra.Command) (*codersdk.Client, error) {
rawURL, err := cmd.Flags().GetString(varAgentURL)
if err != nil {
return nil, err
}
@@ -100,7 +156,7 @@ func createClient(cmd *cobra.Command) (*codersdk.Client, error) {
if err != nil {
return nil, err
}
token, err := root.Session().Read()
token, err := cmd.Flags().GetString(varAgentToken)
if err != nil {
return nil, err
}
@@ -120,6 +176,27 @@ func currentOrganization(cmd *cobra.Command, client *codersdk.Client) (codersdk.
return orgs[0], nil
}
// namedWorkspace fetches and returns a workspace by an identifier, which may be either
// a bare name (for a workspace owned by the current user) or a "user/workspace" combination,
// where user is either a username or UUID.
func namedWorkspace(cmd *cobra.Command, client *codersdk.Client, identifier string) (codersdk.Workspace, error) {
parts := strings.Split(identifier, "/")
var owner, name string
switch len(parts) {
case 1:
owner = codersdk.Me
name = parts[0]
case 2:
owner = parts[0]
name = parts[1]
default:
return codersdk.Workspace{}, xerrors.Errorf("invalid workspace name: %q", identifier)
}
return client.WorkspaceByOwnerAndName(cmd.Context(), owner, name)
}
// createConfig consumes the global configuration flag to produce a config root.
func createConfig(cmd *cobra.Command) config.Root {
globalRoot, err := cmd.Flags().GetString(varGlobalConfig)
@@ -147,6 +224,68 @@ func isTTY(cmd *cobra.Command) bool {
return isatty.IsTerminal(file.Fd())
}
func usageTemplate() string {
// usageHeader is defined in init().
return `{{usageHeader "Usage:"}}
{{- if .Runnable}}
{{.UseLine}}
{{end}}
{{- if .HasAvailableSubCommands}}
{{.CommandPath}} [command]
{{end}}
{{- if gt (len .Aliases) 0}}
{{usageHeader "Aliases:"}}
{{.NameAndAliases}}
{{end}}
{{- if .HasExample}}
{{usageHeader "Get Started:"}}
{{.Example}}
{{end}}
{{- if .HasAvailableSubCommands}}
{{usageHeader "Commands:"}}
{{- range .Commands}}
{{- if (or (and .IsAvailableCommand (eq (len .Annotations) 0)) (eq .Name "help"))}}
{{rpad .Name .NamePadding }} {{.Short}}
{{- end}}
{{- end}}
{{end}}
{{- if and (not .HasParent) .HasAvailableSubCommands}}
{{usageHeader "Workspace Commands:"}}
{{- range .Commands}}
{{- if (and .IsAvailableCommand (ne (index .Annotations "workspaces") ""))}}
{{rpad .Name .NamePadding }} {{.Short}}
{{- end}}
{{- end}}
{{end}}
{{- if .HasAvailableLocalFlags}}
{{usageHeader "Flags:"}}
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}
{{end}}
{{- if .HasAvailableInheritedFlags}}
{{usageHeader "Global Flags:"}}
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}
{{end}}
{{- if .HasHelpSubCommands}}
{{usageHeader "Additional help topics:"}}
{{- range .Commands}}
{{- if .IsAdditionalHelpTopicCommand}}
{{rpad .CommandPath .CommandPathPadding}} {{.Short}}
{{- end}}
{{- end}}
{{end}}
{{- if .HasAvailableSubCommands}}
Use "{{.CommandPath}} [command] --help" for more information about a command.
{{end}}`
}
func versionTemplate() string {
template := `Coder {{printf "%s" .Version}}`
buildTime, valid := buildinfo.Time()
@@ -157,3 +296,9 @@ func versionTemplate() string {
template += "\r\n"
return template
}
// FormatCobraError colorizes and adds "--help" docs to cobra commands.
func FormatCobraError(err error, cmd *cobra.Command) string {
helpErrMsg := fmt.Sprintf("Run '%s --help' for usage.", cmd.CommandPath())
return cliui.Styles.Error.Render(err.Error() + "\n" + helpErrMsg)
}
+22
View File
@@ -0,0 +1,22 @@
package cli_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli"
"github.com/coder/coder/cli/clitest"
)
func TestRoot(t *testing.T) {
t.Run("FormatCobraError", func(t *testing.T) {
t.Parallel()
cmd, _ := clitest.New(t, "delete")
cmd, err := cmd.ExecuteC()
errStr := cli.FormatCobraError(err, cmd)
require.Contains(t, errStr, "Run 'coder delete --help' for usage.")
})
}
+801
View File
@@ -0,0 +1,801 @@
package cli
import (
"context"
"crypto/tls"
"crypto/x509"
"database/sql"
"encoding/pem"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"net/http/pprof"
"net/url"
"os"
"os/signal"
"path/filepath"
"time"
"github.com/coder/coder/buildinfo"
"github.com/coder/coder/provisioner/echo"
"github.com/briandowns/spinner"
"github.com/coreos/go-systemd/daemon"
"github.com/google/go-github/v43/github"
"github.com/pion/turn/v2"
"github.com/pion/webrtc/v3"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/spf13/cobra"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"golang.org/x/mod/semver"
"golang.org/x/oauth2"
xgithub "golang.org/x/oauth2/github"
"golang.org/x/xerrors"
"google.golang.org/api/idtoken"
"google.golang.org/api/option"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
"github.com/coder/coder/cli/cliflag"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/cli/config"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/autobuild/executor"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/databasefake"
"github.com/coder/coder/coderd/devtunnel"
"github.com/coder/coder/coderd/gitsshkey"
"github.com/coder/coder/coderd/tracing"
"github.com/coder/coder/coderd/turnconn"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/cryptorand"
"github.com/coder/coder/provisioner/terraform"
"github.com/coder/coder/provisionerd"
"github.com/coder/coder/provisionersdk"
"github.com/coder/coder/provisionersdk/proto"
)
// nolint:gocyclo
func server() *cobra.Command {
var (
accessURL string
address string
autobuildPollInterval time.Duration
promEnabled bool
promAddress string
pprofEnabled bool
pprofAddress string
cacheDir string
dev bool
devUserEmail string
devUserPassword string
postgresURL string
// provisionerDaemonCount is a uint8 to ensure a number > 0.
provisionerDaemonCount uint8
oauth2GithubClientID string
oauth2GithubClientSecret string
oauth2GithubAllowedOrganizations []string
oauth2GithubAllowSignups bool
tlsCertFile string
tlsClientCAFile string
tlsClientAuth string
tlsEnable bool
tlsKeyFile string
tlsMinVersion string
turnRelayAddress string
tunnel bool
stunServers []string
trace bool
secureAuthCookie bool
sshKeygenAlgorithmRaw string
spooky bool
verbose bool
)
root := &cobra.Command{
Use: "server",
Short: "Start a Coder server",
RunE: func(cmd *cobra.Command, args []string) error {
logger := slog.Make(sloghuman.Sink(os.Stderr))
buildModeDev := semver.Prerelease(buildinfo.Version()) == "-devel"
if verbose || buildModeDev {
logger = logger.Leveled(slog.LevelDebug)
}
var (
tracerProvider *sdktrace.TracerProvider
err error
sqlDriver = "postgres"
)
if trace {
tracerProvider, err = tracing.TracerProvider(cmd.Context(), "coderd")
if err != nil {
logger.Warn(cmd.Context(), "failed to start telemetry exporter", slog.Error(err))
} else {
defer func() {
// allow time for traces to flush even if command context is canceled
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = tracerProvider.Shutdown(ctx)
}()
d, err := tracing.PostgresDriver(tracerProvider, "coderd.database")
if err != nil {
logger.Warn(cmd.Context(), "failed to start postgres tracing driver", slog.Error(err))
} else {
sqlDriver = d
}
}
}
printLogo(cmd, spooky)
listener, err := net.Listen("tcp", address)
if err != nil {
return xerrors.Errorf("listen %q: %w", address, err)
}
defer listener.Close()
if tlsEnable {
listener, err = configureTLS(listener, tlsMinVersion, tlsClientAuth, tlsCertFile, tlsKeyFile, tlsClientCAFile)
if err != nil {
return xerrors.Errorf("configure tls: %w", err)
}
}
tcpAddr, valid := listener.Addr().(*net.TCPAddr)
if !valid {
return xerrors.New("must be listening on tcp")
}
// If just a port is specified, assume localhost.
if tcpAddr.IP.IsUnspecified() {
tcpAddr.IP = net.IPv4(127, 0, 0, 1)
}
localURL := &url.URL{
Scheme: "http",
Host: tcpAddr.String(),
}
if tlsEnable {
localURL.Scheme = "https"
}
if accessURL == "" {
accessURL = localURL.String()
} else {
// If an access URL is specified, always skip tunneling.
tunnel = false
}
var (
tunnelErrChan <-chan error
ctxTunnel, closeTunnel = context.WithCancel(cmd.Context())
)
defer closeTunnel()
// If we're attempting to tunnel in dev-mode, the access URL
// needs to be changed to use the tunnel.
if dev && tunnel {
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), cliui.Styles.Wrap.Render(
"Coder requires a URL accessible by workspaces you provision. "+
"A free tunnel can be created for simple setup. This will "+
"expose your Coder deployment to a publicly accessible URL. "+
cliui.Styles.Field.Render("--access-url")+" can be specified instead.\n",
))
// This skips the prompt if the flag is explicitly specified.
if !cmd.Flags().Changed("tunnel") {
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Would you like to start a tunnel for simple setup?",
IsConfirm: true,
})
if errors.Is(err, cliui.Canceled) {
return err
}
}
if err == nil {
accessURL, tunnelErrChan, err = devtunnel.New(ctxTunnel, localURL)
if err != nil {
return xerrors.Errorf("create tunnel: %w", err)
}
}
_, _ = fmt.Fprintln(cmd.ErrOrStderr())
}
validator, err := idtoken.NewValidator(cmd.Context(), option.WithoutAuthentication())
if err != nil {
return err
}
accessURLParsed, err := url.Parse(accessURL)
if err != nil {
return xerrors.Errorf("parse access url %q: %w", accessURL, err)
}
sshKeygenAlgorithm, err := gitsshkey.ParseAlgorithm(sshKeygenAlgorithmRaw)
if err != nil {
return xerrors.Errorf("parse ssh keygen algorithm %s: %w", sshKeygenAlgorithmRaw, err)
}
turnServer, err := turnconn.New(&turn.RelayAddressGeneratorStatic{
RelayAddress: net.ParseIP(turnRelayAddress),
Address: turnRelayAddress,
})
if err != nil {
return xerrors.Errorf("create turn server: %w", err)
}
iceServers := make([]webrtc.ICEServer, 0)
for _, stunServer := range stunServers {
iceServers = append(iceServers, webrtc.ICEServer{
URLs: []string{stunServer},
})
}
options := &coderd.Options{
AccessURL: accessURLParsed,
ICEServers: iceServers,
Logger: logger.Named("coderd"),
Database: databasefake.New(),
Pubsub: database.NewPubsubInMemory(),
GoogleTokenValidator: validator,
SecureAuthCookie: secureAuthCookie,
SSHKeygenAlgorithm: sshKeygenAlgorithm,
TURNServer: turnServer,
TracerProvider: tracerProvider,
}
if oauth2GithubClientSecret != "" {
options.GithubOAuth2Config, err = configureGithubOAuth2(accessURLParsed, oauth2GithubClientID, oauth2GithubClientSecret, oauth2GithubAllowSignups, oauth2GithubAllowedOrganizations)
if err != nil {
return xerrors.Errorf("configure github oauth2: %w", err)
}
}
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "access-url: %s\n", accessURL)
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "provisioner-daemons: %d\n", provisionerDaemonCount)
_, _ = fmt.Fprintln(cmd.ErrOrStderr())
if !dev {
sqlDB, err := sql.Open(sqlDriver, postgresURL)
if err != nil {
return xerrors.Errorf("dial postgres: %w", err)
}
err = sqlDB.Ping()
if err != nil {
return xerrors.Errorf("ping postgres: %w", err)
}
err = database.MigrateUp(sqlDB)
if err != nil {
return xerrors.Errorf("migrate up: %w", err)
}
options.Database = database.New(sqlDB)
options.Pubsub, err = database.NewPubsub(cmd.Context(), sqlDB, postgresURL)
if err != nil {
return xerrors.Errorf("create pubsub: %w", err)
}
}
coderAPI := coderd.New(options)
client := codersdk.New(localURL)
if tlsEnable {
// Secure transport isn't needed for locally communicating!
client.HTTPClient.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
//nolint:gosec
InsecureSkipVerify: true,
},
}
}
// This prevents the pprof import from being accidentally deleted.
var _ = pprof.Handler
if pprofEnabled {
//nolint:revive
defer serveHandler(cmd.Context(), logger, nil, pprofAddress, "pprof")()
}
if promEnabled {
//nolint:revive
defer serveHandler(cmd.Context(), logger, promhttp.Handler(), promAddress, "prometheus")()
}
errCh := make(chan error, 1)
provisionerDaemons := make([]*provisionerd.Server, 0)
for i := 0; uint8(i) < provisionerDaemonCount; i++ {
daemonClose, err := newProvisionerDaemon(cmd.Context(), coderAPI, logger, cacheDir, errCh, dev)
if err != nil {
return xerrors.Errorf("create provisioner daemon: %w", err)
}
provisionerDaemons = append(provisionerDaemons, daemonClose)
}
defer func() {
for _, provisionerDaemon := range provisionerDaemons {
_ = provisionerDaemon.Close()
}
}()
shutdownConnsCtx, shutdownConns := context.WithCancel(cmd.Context())
defer shutdownConns()
go func() {
defer close(errCh)
server := http.Server{
// These errors are typically noise like "TLS: EOF". Vault does similar:
// https://github.com/hashicorp/vault/blob/e2490059d0711635e529a4efcbaa1b26998d6e1c/command/server.go#L2714
ErrorLog: log.New(io.Discard, "", 0),
Handler: coderAPI.Handler,
BaseContext: func(_ net.Listener) context.Context {
return shutdownConnsCtx
},
}
errCh <- server.Serve(listener)
}()
config := createConfig(cmd)
if dev {
if devUserPassword == "" {
devUserPassword, err = cryptorand.String(10)
if err != nil {
return xerrors.Errorf("generate random admin password for dev: %w", err)
}
}
restorePreviousSession, err := createFirstUser(logger, cmd, client, config, devUserEmail, devUserPassword)
if err != nil {
return xerrors.Errorf("create first user: %w", err)
}
defer restorePreviousSession()
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "email: %s\n", devUserEmail)
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "password: %s\n", devUserPassword)
_, _ = fmt.Fprintln(cmd.ErrOrStderr())
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), cliui.Styles.Wrap.Render(`Started in dev mode. All data is in-memory! `+cliui.Styles.Bold.Render("Do not use in production")+`. Press `+
cliui.Styles.Field.Render("ctrl+c")+` to clean up provisioned infrastructure.`)+"\n\n")
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), cliui.Styles.Wrap.Render(`Run `+cliui.Styles.Code.Render("coder templates init")+
" in a new terminal to start creating workspaces.")+"\n")
} else {
// This is helpful for tests, but can be silently ignored.
// Coder may be ran as users that don't have permission to write in the homedir,
// such as via the systemd service.
_ = config.URL().Write(client.URL.String())
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), cliui.Styles.Paragraph.Render(cliui.Styles.Wrap.Render(cliui.Styles.Prompt.String()+`Started in `+
cliui.Styles.Field.Render("production")+` mode. All data is stored in the PostgreSQL provided! Press `+cliui.Styles.Field.Render("ctrl+c")+` to gracefully shutdown.`))+"\n")
hasFirstUser, err := client.HasFirstUser(cmd.Context())
if !hasFirstUser && err == nil {
// This could fail for a variety of TLS-related reasons.
// This is a helpful starter message, and not critical for user interaction.
_, _ = fmt.Fprint(cmd.ErrOrStderr(), cliui.Styles.Paragraph.Render(cliui.Styles.Wrap.Render(cliui.Styles.FocusedPrompt.String()+`Run `+cliui.Styles.Code.Render("coder login "+accessURL)+" in a new terminal to get started.\n")))
}
}
// Updates the systemd status from activating to activated.
_, err = daemon.SdNotify(false, daemon.SdNotifyReady)
if err != nil {
return xerrors.Errorf("notify systemd: %w", err)
}
autobuildPoller := time.NewTicker(autobuildPollInterval)
defer autobuildPoller.Stop()
autobuildExecutor := executor.New(cmd.Context(), options.Database, logger, autobuildPoller.C)
autobuildExecutor.Run()
// Because the graceful shutdown includes cleaning up workspaces in dev mode, we're
// going to make it harder to accidentally skip the graceful shutdown by hitting ctrl+c
// two or more times. So the stopChan is unlimited in size and we don't call
// signal.Stop() until graceful shutdown finished--this means we swallow additional
// SIGINT after the first. To get out of a graceful shutdown, the user can send SIGQUIT
// with ctrl+\ or SIGTERM with `kill`.
stopChan := make(chan os.Signal, 1)
defer signal.Stop(stopChan)
signal.Notify(stopChan, os.Interrupt)
select {
case <-cmd.Context().Done():
coderAPI.Close()
return cmd.Context().Err()
case err := <-tunnelErrChan:
if err != nil {
return err
}
case err := <-errCh:
shutdownConns()
coderAPI.Close()
return err
case <-stopChan:
}
_, err = daemon.SdNotify(false, daemon.SdNotifyStopping)
if err != nil {
return xerrors.Errorf("notify systemd: %w", err)
}
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "\n\n"+
cliui.Styles.Bold.Render(
"Interrupt caught, gracefully exiting. Use ctrl+\\ to force quit"))
if dev {
workspaces, err := client.Workspaces(cmd.Context(), codersdk.WorkspaceFilter{
Owner: codersdk.Me,
})
if err != nil {
return xerrors.Errorf("get workspaces: %w", err)
}
for _, workspace := range workspaces {
before := time.Now()
build, err := client.CreateWorkspaceBuild(cmd.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionDelete,
})
if err != nil {
return xerrors.Errorf("delete workspace: %w", err)
}
err = cliui.WorkspaceBuild(cmd.Context(), cmd.OutOrStdout(), client, build.ID, before)
if err != nil {
return xerrors.Errorf("delete workspace %s: %w", workspace.Name, err)
}
}
}
for _, provisionerDaemon := range provisionerDaemons {
spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond)
spin.Writer = cmd.OutOrStdout()
spin.Suffix = cliui.Styles.Keyword.Render(" Shutting down provisioner daemon...")
spin.Start()
err = provisionerDaemon.Shutdown(cmd.Context())
if err != nil {
spin.FinalMSG = cliui.Styles.Prompt.String() + "Failed to shutdown provisioner daemon: " + err.Error()
spin.Stop()
}
err = provisionerDaemon.Close()
if err != nil {
spin.Stop()
return xerrors.Errorf("close provisioner daemon: %w", err)
}
spin.FinalMSG = cliui.Styles.Prompt.String() + "Gracefully shut down provisioner daemon!\n"
spin.Stop()
}
if dev && tunnel {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), cliui.Styles.Prompt.String()+"Waiting for dev tunnel to close...\n")
closeTunnel()
<-tunnelErrChan
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), cliui.Styles.Prompt.String()+"Waiting for WebSocket connections to close...\n")
shutdownConns()
coderAPI.Close()
return nil
},
}
cliflag.DurationVarP(root.Flags(), &autobuildPollInterval, "autobuild-poll-interval", "", "CODER_AUTOBUILD_POLL_INTERVAL", time.Minute, "Specifies the interval at which to poll for and execute automated workspace build operations.")
cliflag.StringVarP(root.Flags(), &accessURL, "access-url", "", "CODER_ACCESS_URL", "", "Specifies the external URL to access Coder.")
cliflag.StringVarP(root.Flags(), &address, "address", "a", "CODER_ADDRESS", "127.0.0.1:3000", "The address to serve the API and dashboard.")
cliflag.BoolVarP(root.Flags(), &promEnabled, "prometheus-enable", "", "CODER_PROMETHEUS_ENABLE", false, "Enable serving prometheus metrics on the addressdefined by --prometheus-address.")
cliflag.StringVarP(root.Flags(), &promAddress, "prometheus-address", "", "CODER_PROMETHEUS_ADDRESS", "127.0.0.1:2112", "The address to serve prometheus metrics.")
cliflag.BoolVarP(root.Flags(), &pprofEnabled, "pprof-enable", "", "CODER_PPROF_ENABLE", false, "Enable serving pprof metrics on the address defined by --pprof-address.")
cliflag.StringVarP(root.Flags(), &pprofAddress, "pprof-address", "", "CODER_PPROF_ADDRESS", "127.0.0.1:6060", "The address to serve pprof.")
// systemd uses the CACHE_DIRECTORY environment variable!
cliflag.StringVarP(root.Flags(), &cacheDir, "cache-dir", "", "CACHE_DIRECTORY", filepath.Join(os.TempDir(), "coder-cache"), "Specifies a directory to cache binaries for provision operations.")
cliflag.BoolVarP(root.Flags(), &dev, "dev", "", "CODER_DEV_MODE", false, "Serve Coder in dev mode for tinkering")
cliflag.StringVarP(root.Flags(), &devUserEmail, "dev-admin-email", "", "CODER_DEV_ADMIN_EMAIL", "admin@coder.com", "Specifies the admin email to be used in dev mode (--dev)")
cliflag.StringVarP(root.Flags(), &devUserPassword, "dev-admin-password", "", "CODER_DEV_ADMIN_PASSWORD", "", "Specifies the admin password to be used in dev mode (--dev) instead of a randomly generated one")
cliflag.StringVarP(root.Flags(), &postgresURL, "postgres-url", "", "CODER_PG_CONNECTION_URL", "", "URL of a PostgreSQL database to connect to")
cliflag.Uint8VarP(root.Flags(), &provisionerDaemonCount, "provisioner-daemons", "", "CODER_PROVISIONER_DAEMONS", 3, "The amount of provisioner daemons to create on start.")
cliflag.StringVarP(root.Flags(), &oauth2GithubClientID, "oauth2-github-client-id", "", "CODER_OAUTH2_GITHUB_CLIENT_ID", "",
"Specifies a client ID to use for oauth2 with GitHub.")
cliflag.StringVarP(root.Flags(), &oauth2GithubClientSecret, "oauth2-github-client-secret", "", "CODER_OAUTH2_GITHUB_CLIENT_SECRET", "",
"Specifies a client secret to use for oauth2 with GitHub.")
cliflag.StringArrayVarP(root.Flags(), &oauth2GithubAllowedOrganizations, "oauth2-github-allowed-orgs", "", "CODER_OAUTH2_GITHUB_ALLOWED_ORGS", nil,
"Specifies organizations the user must be a member of to authenticate with GitHub.")
cliflag.BoolVarP(root.Flags(), &oauth2GithubAllowSignups, "oauth2-github-allow-signups", "", "CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS", false,
"Specifies whether new users can sign up with GitHub.")
cliflag.BoolVarP(root.Flags(), &tlsEnable, "tls-enable", "", "CODER_TLS_ENABLE", false, "Specifies if TLS will be enabled")
cliflag.StringVarP(root.Flags(), &tlsCertFile, "tls-cert-file", "", "CODER_TLS_CERT_FILE", "",
"Specifies the path to the certificate for TLS. It requires a PEM-encoded file. "+
"To configure the listener to use a CA certificate, concatenate the primary certificate "+
"and the CA certificate together. The primary certificate should appear first in the combined file")
cliflag.StringVarP(root.Flags(), &tlsClientCAFile, "tls-client-ca-file", "", "CODER_TLS_CLIENT_CA_FILE", "",
"PEM-encoded Certificate Authority file used for checking the authenticity of client")
cliflag.StringVarP(root.Flags(), &tlsClientAuth, "tls-client-auth", "", "CODER_TLS_CLIENT_AUTH", "request",
`Specifies the policy the server will follow for TLS Client Authentication. `+
`Accepted values are "none", "request", "require-any", "verify-if-given", or "require-and-verify"`)
cliflag.StringVarP(root.Flags(), &tlsKeyFile, "tls-key-file", "", "CODER_TLS_KEY_FILE", "",
"Specifies the path to the private key for the certificate. It requires a PEM-encoded file")
cliflag.StringVarP(root.Flags(), &tlsMinVersion, "tls-min-version", "", "CODER_TLS_MIN_VERSION", "tls12",
`Specifies the minimum supported version of TLS. Accepted values are "tls10", "tls11", "tls12" or "tls13"`)
cliflag.BoolVarP(root.Flags(), &tunnel, "tunnel", "", "CODER_DEV_TUNNEL", true,
"Specifies whether the dev tunnel will be enabled or not. If specified, the interactive prompt will not display.")
cliflag.StringArrayVarP(root.Flags(), &stunServers, "stun-server", "", "CODER_STUN_SERVERS", []string{
"stun:stun.l.google.com:19302",
}, "Specify URLs for STUN servers to enable P2P connections.")
cliflag.BoolVarP(root.Flags(), &trace, "trace", "", "CODER_TRACE", false, "Specifies if application tracing data is collected")
cliflag.StringVarP(root.Flags(), &turnRelayAddress, "turn-relay-address", "", "CODER_TURN_RELAY_ADDRESS", "127.0.0.1",
"Specifies the address to bind TURN connections.")
cliflag.BoolVarP(root.Flags(), &secureAuthCookie, "secure-auth-cookie", "", "CODER_SECURE_AUTH_COOKIE", false, "Specifies if the 'Secure' property is set on browser session cookies")
cliflag.StringVarP(root.Flags(), &sshKeygenAlgorithmRaw, "ssh-keygen-algorithm", "", "CODER_SSH_KEYGEN_ALGORITHM", "ed25519", "Specifies the algorithm to use for generating ssh keys. "+
`Accepted values are "ed25519", "ecdsa", or "rsa4096"`)
cliflag.BoolVarP(root.Flags(), &spooky, "spooky", "", "", false, "Specifies spookiness level")
cliflag.BoolVarP(root.Flags(), &verbose, "verbose", "v", "CODER_VERBOSE", false, "Enables verbose logging.")
_ = root.Flags().MarkHidden("spooky")
return root
}
// createFirstUser creates the first user and sets a valid session.
// Caller must call restorePreviousSession on server exit.
func createFirstUser(logger slog.Logger, cmd *cobra.Command, client *codersdk.Client, cfg config.Root, email, password string) (func(), error) {
if email == "" {
return nil, xerrors.New("email is empty")
}
if password == "" {
return nil, xerrors.New("password is empty")
}
_, err := client.CreateFirstUser(cmd.Context(), codersdk.CreateFirstUserRequest{
Email: email,
Username: "developer",
Password: password,
OrganizationName: "acme-corp",
})
if err != nil {
return nil, xerrors.Errorf("create first user: %w", err)
}
token, err := client.LoginWithPassword(cmd.Context(), codersdk.LoginWithPasswordRequest{
Email: email,
Password: password,
})
if err != nil {
return nil, xerrors.Errorf("login with first user: %w", err)
}
client.SessionToken = token.SessionToken
// capture the current session and if exists recover session on server exit
restorePreviousSession := func() {}
oldURL, _ := cfg.URL().Read()
oldSession, _ := cfg.Session().Read()
if oldURL != "" && oldSession != "" {
restorePreviousSession = func() {
currentURL, err := cfg.URL().Read()
if err != nil {
logger.Error(cmd.Context(), "failed to read current session url", slog.Error(err))
return
}
currentSession, err := cfg.Session().Read()
if err != nil {
logger.Error(cmd.Context(), "failed to read current session token", slog.Error(err))
return
}
// if it's changed since we wrote to it don't restore session
if currentURL != client.URL.String() ||
currentSession != token.SessionToken {
return
}
err = cfg.URL().Write(oldURL)
if err != nil {
logger.Error(cmd.Context(), "failed to recover previous session url", slog.Error(err))
return
}
err = cfg.Session().Write(oldSession)
if err != nil {
logger.Error(cmd.Context(), "failed to recover previous session token", slog.Error(err))
return
}
}
}
err = cfg.URL().Write(client.URL.String())
if err != nil {
return nil, xerrors.Errorf("write local url: %w", err)
}
err = cfg.Session().Write(token.SessionToken)
if err != nil {
return nil, xerrors.Errorf("write session token: %w", err)
}
return restorePreviousSession, nil
}
// nolint:revive
func newProvisionerDaemon(ctx context.Context, coderAPI *coderd.API,
logger slog.Logger, cacheDir string, errChan chan error, dev bool) (*provisionerd.Server, error) {
err := os.MkdirAll(cacheDir, 0700)
if err != nil {
return nil, xerrors.Errorf("mkdir %q: %w", cacheDir, err)
}
terraformClient, terraformServer := provisionersdk.TransportPipe()
go func() {
err := terraform.Serve(ctx, &terraform.ServeOptions{
ServeOptions: &provisionersdk.ServeOptions{
Listener: terraformServer,
},
CachePath: cacheDir,
Logger: logger,
})
if err != nil {
errChan <- err
}
}()
tempDir, err := os.MkdirTemp("", "provisionerd")
if err != nil {
return nil, err
}
provisioners := provisionerd.Provisioners{
string(database.ProvisionerTypeTerraform): proto.NewDRPCProvisionerClient(provisionersdk.Conn(terraformClient)),
}
// include echo provisioner when in dev mode
if dev {
echoClient, echoServer := provisionersdk.TransportPipe()
go func() {
err := echo.Serve(ctx, &provisionersdk.ServeOptions{Listener: echoServer})
if err != nil {
errChan <- err
}
}()
provisioners[string(database.ProvisionerTypeEcho)] = proto.NewDRPCProvisionerClient(provisionersdk.Conn(echoClient))
}
return provisionerd.New(coderAPI.ListenProvisionerDaemon, &provisionerd.Options{
Logger: logger,
PollInterval: 500 * time.Millisecond,
UpdateInterval: 500 * time.Millisecond,
Provisioners: provisioners,
WorkDirectory: tempDir,
}), nil
}
// nolint: revive
func printLogo(cmd *cobra.Command, spooky bool) {
if spooky {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), `
▄████▄ ▒█████ ▓█████▄ ▓█████ ██▀███
▒██▀ ▀█ ▒██▒ ██▒▒██▀ ██▌▓█ ▀ ▓██ ▒ ██▒
▒▓█ ▄ ▒██░ ██▒░██ █▌▒███ ▓██ ░▄█ ▒
▒▓▓▄ ▄██▒▒██ ██░░▓█▄ ▌▒▓█ ▄ ▒██▀▀█▄
▒ ▓███▀ ░░ ████▓▒░░▒████▓ ░▒████▒░██▓ ▒██▒
░ ░▒ ▒ ░░ ▒░▒░▒░ ▒▒▓ ▒ ░░ ▒░ ░░ ▒▓ ░▒▓░
░ ▒ ░ ▒ ▒░ ░ ▒ ▒ ░ ░ ░ ░▒ ░ ▒░
░ ░ ░ ░ ▒ ░ ░ ░ ░ ░░ ░
░ ░ ░ ░ ░ ░ ░ ░
░ ░
`)
return
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), ` ▄█▀ ▀█▄
▄▄ ▀▀▀ █▌ ██▀▀█▄ ▐█
▄▄██▀▀█▄▄▄ ██ ██ █▀▀█ ▐█▀▀██ ▄█▀▀█ █▀▀
█▌ ▄▌ ▐█ █▌ ▀█▄▄▄█▌ █ █ ▐█ ██ ██▀▀ █
██████▀▄█ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀ ▀▀▀▀ ▀
`)
}
func configureTLS(listener net.Listener, tlsMinVersion, tlsClientAuth, tlsCertFile, tlsKeyFile, tlsClientCAFile string) (net.Listener, error) {
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
}
switch tlsMinVersion {
case "tls10":
tlsConfig.MinVersion = tls.VersionTLS10
case "tls11":
tlsConfig.MinVersion = tls.VersionTLS11
case "tls12":
tlsConfig.MinVersion = tls.VersionTLS12
case "tls13":
tlsConfig.MinVersion = tls.VersionTLS13
default:
return nil, xerrors.Errorf("unrecognized tls version: %q", tlsMinVersion)
}
switch tlsClientAuth {
case "none":
tlsConfig.ClientAuth = tls.NoClientCert
case "request":
tlsConfig.ClientAuth = tls.RequestClientCert
case "require-any":
tlsConfig.ClientAuth = tls.RequireAnyClientCert
case "verify-if-given":
tlsConfig.ClientAuth = tls.VerifyClientCertIfGiven
case "require-and-verify":
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
default:
return nil, xerrors.Errorf("unrecognized tls client auth: %q", tlsClientAuth)
}
if tlsCertFile == "" {
return nil, xerrors.New("tls-cert-file is required when tls is enabled")
}
if tlsKeyFile == "" {
return nil, xerrors.New("tls-key-file is required when tls is enabled")
}
certPEMBlock, err := os.ReadFile(tlsCertFile)
if err != nil {
return nil, xerrors.Errorf("read file %q: %w", tlsCertFile, err)
}
keyPEMBlock, err := os.ReadFile(tlsKeyFile)
if err != nil {
return nil, xerrors.Errorf("read file %q: %w", tlsKeyFile, err)
}
keyBlock, _ := pem.Decode(keyPEMBlock)
if keyBlock == nil {
return nil, xerrors.New("decoded pem is blank")
}
cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
if err != nil {
return nil, xerrors.Errorf("create key pair: %w", err)
}
tlsConfig.GetCertificate = func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
return &cert, nil
}
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(certPEMBlock)
tlsConfig.RootCAs = certPool
if tlsClientCAFile != "" {
caPool := x509.NewCertPool()
data, err := os.ReadFile(tlsClientCAFile)
if err != nil {
return nil, xerrors.Errorf("read %q: %w", tlsClientCAFile, err)
}
if !caPool.AppendCertsFromPEM(data) {
return nil, xerrors.Errorf("failed to parse CA certificate in tls-client-ca-file")
}
tlsConfig.ClientCAs = caPool
}
return tls.NewListener(listener, tlsConfig), nil
}
func configureGithubOAuth2(accessURL *url.URL, clientID, clientSecret string, allowSignups bool, allowOrgs []string) (*coderd.GithubOAuth2Config, error) {
redirectURL, err := accessURL.Parse("/api/v2/users/oauth2/github/callback")
if err != nil {
return nil, xerrors.Errorf("parse github oauth callback url: %w", err)
}
return &coderd.GithubOAuth2Config{
OAuth2Config: &oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
Endpoint: xgithub.Endpoint,
RedirectURL: redirectURL.String(),
Scopes: []string{
"read:user",
"read:org",
"user:email",
},
},
AllowSignups: allowSignups,
AllowOrganizations: allowOrgs,
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
user, _, err := github.NewClient(client).Users.Get(ctx, "")
return user, err
},
ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) {
emails, _, err := github.NewClient(client).Users.ListEmails(ctx, &github.ListOptions{})
return emails, err
},
ListOrganizationMemberships: func(ctx context.Context, client *http.Client) ([]*github.Membership, error) {
memberships, _, err := github.NewClient(client).Organizations.ListOrgMemberships(ctx, &github.ListOrgMembershipsOptions{
State: "active",
})
return memberships, err
},
}, nil
}
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))
srv := &http.Server{Addr: addr, Handler: handler}
go func() {
err := srv.ListenAndServe()
if err != nil && !xerrors.Is(err, http.ErrServerClosed) {
logger.Error(ctx, "http server listen", slog.F("name", name), slog.Error(err))
}
}()
return func() { _ = srv.Close() }
}
+111 -33
View File
@@ -9,15 +9,18 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"net"
"net/http"
"net/url"
"os"
"runtime"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
@@ -28,10 +31,11 @@ import (
)
// This cannot be ran in parallel because it uses a signal.
// nolint:tparallel
func TestStart(t *testing.T) {
// nolint:paralleltest
func TestServer(t *testing.T) {
t.Run("Production", func(t *testing.T) {
t.Parallel()
// postgres.Open() seems to be creating race conditions when run in parallel.
// t.Parallel()
if runtime.GOOS != "linux" || testing.Short() {
// Skip on non-Linux because it spawns a PostgreSQL instance.
t.SkipNow()
@@ -40,12 +44,11 @@ func TestStart(t *testing.T) {
require.NoError(t, err)
defer closeFunc()
ctx, cancelFunc := context.WithCancel(context.Background())
done := make(chan struct{})
root, cfg := clitest.New(t, "start", "--address", ":0", "--postgres-url", connectionURL)
defer cancelFunc()
root, cfg := clitest.New(t, "server", "--address", ":0", "--postgres-url", connectionURL)
errC := make(chan error)
go func() {
defer close(done)
err = root.ExecuteContext(ctx)
require.ErrorIs(t, err, context.Canceled)
errC <- root.ExecuteContext(ctx)
}()
var client *codersdk.Client
require.Eventually(t, func() bool {
@@ -54,7 +57,7 @@ func TestStart(t *testing.T) {
return false
}
accessURL, err := url.Parse(rawURL)
require.NoError(t, err)
assert.NoError(t, err)
client = codersdk.New(accessURL)
return true
}, 15*time.Second, 25*time.Millisecond)
@@ -66,17 +69,77 @@ func TestStart(t *testing.T) {
})
require.NoError(t, err)
cancelFunc()
<-done
require.ErrorIs(t, <-errC, context.Canceled)
})
t.Run("Development", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
root, cfg := clitest.New(t, "start", "--dev", "--tunnel=false", "--address", ":0")
wantEmail := "admin@coder.com"
root, cfg := clitest.New(t, "server", "--dev", "--tunnel=false", "--address", ":0")
var buf strings.Builder
errC := make(chan error)
root.SetOutput(&buf)
go func() {
err := root.ExecuteContext(ctx)
require.ErrorIs(t, err, context.Canceled)
errC <- root.ExecuteContext(ctx)
}()
var token string
require.Eventually(t, func() bool {
var err error
token, err = cfg.Session().Read()
return err == nil && token != ""
}, 15*time.Second, 25*time.Millisecond)
// Verify that authentication was properly set in dev-mode.
accessURL, err := cfg.URL().Read()
require.NoError(t, err)
parsed, err := url.Parse(accessURL)
require.NoError(t, err)
client := codersdk.New(parsed)
client.SessionToken = token
_, err = client.User(ctx, codersdk.Me)
require.NoError(t, err, "token:", token)
cancelFunc()
require.ErrorIs(t, <-errC, context.Canceled)
// Verify that credentials were output to the terminal.
assert.Contains(t, buf.String(), fmt.Sprintf("email: %s", wantEmail), "expected output %q; got no match", wantEmail)
// Check that the password line is output and that it's non-empty.
if _, after, found := strings.Cut(buf.String(), "password: "); found {
before, _, _ := strings.Cut(after, "\n")
before = strings.Trim(before, "\r") // Ensure no control character is left.
assert.NotEmpty(t, before, "expected non-empty password; got empty")
} else {
t.Error("expected password line output; got no match")
}
})
// Duplicated test from "Development" above to test setting email/password via env.
// Cannot run parallel due to os.Setenv.
//nolint:paralleltest
t.Run("Development with email and password from env", func(t *testing.T) {
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
wantEmail := "myadmin@coder.com"
wantPassword := "testpass42"
t.Setenv("CODER_DEV_ADMIN_EMAIL", wantEmail)
t.Setenv("CODER_DEV_ADMIN_PASSWORD", wantPassword)
root, cfg := clitest.New(t, "server", "--dev", "--tunnel=false", "--address", ":0")
var buf strings.Builder
root.SetOutput(&buf)
errC := make(chan error)
go func() {
errC <- root.ExecuteContext(ctx)
}()
var token string
require.Eventually(t, func() bool {
var err error
@@ -92,12 +155,19 @@ func TestStart(t *testing.T) {
client.SessionToken = token
_, err = client.User(ctx, codersdk.Me)
require.NoError(t, err)
cancelFunc()
require.ErrorIs(t, <-errC, context.Canceled)
// Verify that credentials were output to the terminal.
assert.Contains(t, buf.String(), fmt.Sprintf("email: %s", wantEmail), "expected output %q; got no match", wantEmail)
assert.Contains(t, buf.String(), fmt.Sprintf("password: %s", wantPassword), "expected output %q; got no match", wantPassword)
})
t.Run("TLSBadVersion", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
root, _ := clitest.New(t, "start", "--dev", "--tunnel=false", "--address", ":0",
root, _ := clitest.New(t, "server", "--dev", "--tunnel=false", "--address", ":0",
"--tls-enable", "--tls-min-version", "tls9")
err := root.ExecuteContext(ctx)
require.Error(t, err)
@@ -106,7 +176,7 @@ func TestStart(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
root, _ := clitest.New(t, "start", "--dev", "--tunnel=false", "--address", ":0",
root, _ := clitest.New(t, "server", "--dev", "--tunnel=false", "--address", ":0",
"--tls-enable", "--tls-client-auth", "something")
err := root.ExecuteContext(ctx)
require.Error(t, err)
@@ -115,7 +185,7 @@ func TestStart(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
root, _ := clitest.New(t, "start", "--dev", "--tunnel=false", "--address", ":0",
root, _ := clitest.New(t, "server", "--dev", "--tunnel=false", "--address", ":0",
"--tls-enable")
err := root.ExecuteContext(ctx)
require.Error(t, err)
@@ -126,12 +196,14 @@ func TestStart(t *testing.T) {
defer cancelFunc()
certPath, keyPath := generateTLSCertificate(t)
root, cfg := clitest.New(t, "start", "--dev", "--tunnel=false", "--address", ":0",
root, cfg := clitest.New(t, "server", "--dev", "--tunnel=false", "--address", ":0",
"--tls-enable", "--tls-cert-file", certPath, "--tls-key-file", keyPath)
errC := make(chan error)
go func() {
err := root.ExecuteContext(ctx)
require.ErrorIs(t, err, context.Canceled)
errC <- root.ExecuteContext(ctx)
}()
// Verify HTTPS
var accessURLRaw string
require.Eventually(t, func() bool {
var err error
@@ -152,6 +224,9 @@ func TestStart(t *testing.T) {
}
_, err = client.HasFirstUser(ctx)
require.NoError(t, err)
cancelFunc()
require.ErrorIs(t, <-errC, context.Canceled)
})
// This cannot be ran in parallel because it uses a signal.
//nolint:paralleltest
@@ -162,12 +237,11 @@ func TestStart(t *testing.T) {
}
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
root, cfg := clitest.New(t, "start", "--dev", "--tunnel=false", "--address", ":0", "--provisioner-daemons", "0")
done := make(chan struct{})
root, cfg := clitest.New(t, "server", "--dev", "--tunnel=false", "--address", ":0", "--provisioner-daemons", "1")
serverErr := make(chan error)
go func() {
defer close(done)
err := root.ExecuteContext(ctx)
require.NoError(t, err)
serverErr <- err
}()
var token string
require.Eventually(t, func() bool {
@@ -184,13 +258,12 @@ func TestStart(t *testing.T) {
client.SessionToken = token
orgs, err := client.OrganizationsByUser(ctx, codersdk.Me)
require.NoError(t, err)
coderdtest.NewProvisionerDaemon(t, client)
// Create a workspace so the cleanup occurs!
version := coderdtest.CreateTemplateVersion(t, client, orgs[0].ID, nil)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, orgs[0].ID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID)
workspace := coderdtest.CreateWorkspace(t, client, orgs[0].ID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
require.NoError(t, err)
@@ -198,21 +271,26 @@ func TestStart(t *testing.T) {
require.NoError(t, err)
err = currentProcess.Signal(os.Interrupt)
require.NoError(t, err)
<-done
// Send a two more signal, which should be ignored. Send 2 because the channel has a buffer
// of 1 and we want to make sure that nothing strange happens if we exceed the buffer.
err = currentProcess.Signal(os.Interrupt)
require.NoError(t, err)
err = currentProcess.Signal(os.Interrupt)
require.NoError(t, err)
err = <-serverErr
require.NoError(t, err)
})
t.Run("DatadogTracerNoLeak", func(t *testing.T) {
t.Run("TracerNoLeak", func(t *testing.T) {
t.Parallel()
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
root, _ := clitest.New(t, "start", "--dev", "--tunnel=false", "--address", ":0", "--trace-datadog=true")
done := make(chan struct{})
root, _ := clitest.New(t, "server", "--dev", "--tunnel=false", "--address", ":0", "--trace=true")
errC := make(chan error)
go func() {
defer close(done)
err := root.ExecuteContext(ctx)
require.ErrorIs(t, err, context.Canceled)
errC <- root.ExecuteContext(ctx)
}()
cancelFunc()
<-done
require.ErrorIs(t, <-errC, context.Canceled)
require.Error(t, goleak.Find())
})
}
+34
View File
@@ -0,0 +1,34 @@
package cli
import (
"github.com/spf13/cobra"
"golang.org/x/xerrors"
"github.com/coder/coder/cli/cliui"
)
func show() *cobra.Command {
return &cobra.Command{
Annotations: workspaceCommand,
Use: "show",
Short: "Show details of a workspace's resources and agents",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
return err
}
workspace, err := namedWorkspace(cmd, client, args[0])
if err != nil {
return xerrors.Errorf("get workspace: %w", err)
}
resources, err := client.WorkspaceResourcesByBuild(cmd.Context(), workspace.LatestBuild.ID)
if err != nil {
return xerrors.Errorf("get workspace resources: %w", err)
}
return cliui.WorkspaceResources(cmd.OutOrStdout(), resources, cliui.WorkspaceResourcesOptions{
WorkspaceName: workspace.Name,
})
},
}
}
+195 -78
View File
@@ -2,112 +2,91 @@ package cli
import (
"context"
"fmt"
"io"
"net"
"os"
"path/filepath"
"strings"
"time"
"github.com/gen2brain/beeep"
"github.com/gofrs/flock"
"github.com/google/uuid"
"github.com/mattn/go-isatty"
"github.com/pion/webrtc/v3"
"github.com/spf13/cobra"
gossh "golang.org/x/crypto/ssh"
gosshagent "golang.org/x/crypto/ssh/agent"
"golang.org/x/term"
"golang.org/x/xerrors"
"github.com/coder/coder/cli/cliflag"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/autobuild/notify"
"github.com/coder/coder/coderd/util/ptr"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/cryptorand"
)
var workspacePollInterval = time.Minute
var autostopNotifyCountdown = []time.Duration{30 * time.Minute}
func ssh() *cobra.Command {
var (
stdio bool
stdio bool
shuffle bool
forwardAgent bool
identityAgent string
wsPollInterval time.Duration
)
cmd := &cobra.Command{
Use: "ssh <workspace> [resource]",
Annotations: workspaceCommand,
Use: "ssh <workspace>",
Short: "SSH into a workspace",
Args: cobra.ArbitraryArgs,
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
return err
}
workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0])
if err != nil {
return err
}
if workspace.LatestBuild.Transition != database.WorkspaceTransitionStart {
return xerrors.New("workspace must be in start transition to ssh")
}
if workspace.LatestBuild.Job.CompletedAt == nil {
err = cliui.WorkspaceBuild(cmd.Context(), cmd.ErrOrStderr(), client, workspace.LatestBuild.ID, workspace.CreatedAt)
if shuffle {
err := cobra.ExactArgs(0)(cmd, args)
if err != nil {
return err
}
} else {
err := cobra.MinimumNArgs(1)(cmd, args)
if err != nil {
return err
}
}
if workspace.LatestBuild.Transition == database.WorkspaceTransitionDelete {
return xerrors.New("workspace is deleting...")
}
resources, err := client.WorkspaceResourcesByBuild(cmd.Context(), workspace.LatestBuild.ID)
workspace, agent, err := getWorkspaceAndAgent(cmd, client, codersdk.Me, args[0], shuffle)
if err != nil {
return err
}
resourceByAddress := make(map[string]codersdk.WorkspaceResource)
for _, resource := range resources {
if resource.Agent == nil {
continue
}
resourceByAddress[resource.Address] = resource
}
var resourceAddress string
if len(args) >= 2 {
resourceAddress = args[1]
} else {
// No resource name was provided!
if len(resourceByAddress) > 1 {
// List available resources to connect into?
return xerrors.Errorf("multiple agents")
}
for _, resource := range resourceByAddress {
resourceAddress = resource.Address
break
}
}
resource, exists := resourceByAddress[resourceAddress]
if !exists {
resourceKeys := make([]string, 0)
for resourceKey := range resourceByAddress {
resourceKeys = append(resourceKeys, resourceKey)
}
return xerrors.Errorf("no sshable agent with address %q: %+v", resourceAddress, resourceKeys)
}
// OpenSSH passes stderr directly to the calling TTY.
// This is required in "stdio" mode so a connecting indicator can be displayed.
err = cliui.Agent(cmd.Context(), cmd.ErrOrStderr(), cliui.AgentOptions{
WorkspaceName: workspace.Name,
Fetch: func(ctx context.Context) (codersdk.WorkspaceResource, error) {
return client.WorkspaceResource(ctx, resource.ID)
Fetch: func(ctx context.Context) (codersdk.WorkspaceAgent, error) {
return client.WorkspaceAgent(ctx, agent.ID)
},
})
if err != nil {
return xerrors.Errorf("await agent: %w", err)
}
conn, err := client.DialWorkspaceAgent(cmd.Context(), resource.ID, []webrtc.ICEServer{{
URLs: []string{"stun:stun.l.google.com:19302"},
}}, nil)
conn, err := client.DialWorkspaceAgent(cmd.Context(), agent.ID, nil)
if err != nil {
return err
}
defer conn.Close()
stopPolling := tryPollWorkspaceAutostop(cmd.Context(), client, workspace)
defer stopPolling()
if stdio {
rawSSH, err := conn.SSH()
if err != nil {
@@ -129,7 +108,22 @@ func ssh() *cobra.Command {
return err
}
if isatty.IsTerminal(os.Stdout.Fd()) {
if identityAgent == "" {
identityAgent = os.Getenv("SSH_AUTH_SOCK")
}
if forwardAgent && identityAgent != "" {
err = gosshagent.ForwardToRemote(sshClient, identityAgent)
if err != nil {
return xerrors.Errorf("forward agent failed: %w", err)
}
err = gosshagent.RequestAgentForwarding(sshSession)
if err != nil {
return xerrors.Errorf("request agent forwarding failed: %w", err)
}
}
stdoutFile, valid := cmd.OutOrStdout().(*os.File)
if valid && isatty.IsTerminal(stdoutFile.Fd()) {
state, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
return err
@@ -137,6 +131,19 @@ func ssh() *cobra.Command {
defer func() {
_ = term.Restore(int(os.Stdin.Fd()), state)
}()
windowChange := listenWindowSize(cmd.Context())
go func() {
for {
select {
case <-cmd.Context().Done():
return
case <-windowChange:
}
width, height, _ := term.GetSize(int(stdoutFile.Fd()))
_ = sshSession.WindowChange(height, width)
}
}()
}
err = sshSession.RequestPty("xterm-256color", 128, 128, gossh.TerminalModes{})
@@ -162,35 +169,145 @@ func ssh() *cobra.Command {
},
}
cliflag.BoolVarP(cmd.Flags(), &stdio, "stdio", "", "CODER_SSH_STDIO", false, "Specifies whether to emit SSH output over stdin/stdout.")
cliflag.BoolVarP(cmd.Flags(), &shuffle, "shuffle", "", "CODER_SSH_SHUFFLE", false, "Specifies whether to choose a random workspace")
_ = cmd.Flags().MarkHidden("shuffle")
cliflag.BoolVarP(cmd.Flags(), &forwardAgent, "forward-agent", "A", "CODER_SSH_FORWARD_AGENT", false, "Specifies whether to forward the SSH agent specified in $SSH_AUTH_SOCK")
cliflag.StringVarP(cmd.Flags(), &identityAgent, "identity-agent", "", "CODER_SSH_IDENTITY_AGENT", "", "Specifies which identity agent to use (overrides $SSH_AUTH_SOCK), forward agent must also be enabled")
cliflag.DurationVarP(cmd.Flags(), &wsPollInterval, "workspace-poll-interval", "", "CODER_WORKSPACE_POLL_INTERVAL", workspacePollInterval, "Specifies how often to poll for workspace automated shutdown.")
return cmd
}
type stdioConn struct {
io.Reader
io.Writer
// getWorkspaceAgent returns the workspace and agent selected using either the
// `<workspace>[.<agent>]` syntax via `in` or picks a random workspace and agent
// if `shuffle` is true.
func getWorkspaceAndAgent(cmd *cobra.Command, client *codersdk.Client, userID string, in string, shuffle bool) (codersdk.Workspace, codersdk.WorkspaceAgent, error) { //nolint:revive
ctx := cmd.Context()
var (
workspace codersdk.Workspace
workspaceParts = strings.Split(in, ".")
err error
)
if shuffle {
workspaces, err := client.Workspaces(cmd.Context(), codersdk.WorkspaceFilter{
Owner: codersdk.Me,
})
if err != nil {
return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, err
}
if len(workspaces) == 0 {
return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, xerrors.New("no workspaces to shuffle")
}
workspace, err = cryptorand.Element(workspaces)
if err != nil {
return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, err
}
} else {
workspace, err = namedWorkspace(cmd, client, workspaceParts[0])
if err != nil {
return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, err
}
}
if workspace.LatestBuild.Transition != codersdk.WorkspaceTransitionStart {
return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, xerrors.New("workspace must be in start transition to ssh")
}
if workspace.LatestBuild.Job.CompletedAt == nil {
err := cliui.WorkspaceBuild(ctx, cmd.ErrOrStderr(), client, workspace.LatestBuild.ID, workspace.CreatedAt)
if err != nil {
return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, err
}
}
if workspace.LatestBuild.Transition == codersdk.WorkspaceTransitionDelete {
return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, xerrors.Errorf("workspace %q is being deleted", workspace.Name)
}
resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID)
if err != nil {
return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, xerrors.Errorf("fetch workspace resources: %w", err)
}
agents := make([]codersdk.WorkspaceAgent, 0)
for _, resource := range resources {
agents = append(agents, resource.Agents...)
}
if len(agents) == 0 {
return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, xerrors.Errorf("workspace %q has no agents", workspace.Name)
}
var agent codersdk.WorkspaceAgent
if len(workspaceParts) >= 2 {
for _, otherAgent := range agents {
if otherAgent.Name != workspaceParts[1] {
continue
}
agent = otherAgent
break
}
if agent.ID == uuid.Nil {
return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, xerrors.Errorf("agent not found by name %q", workspaceParts[1])
}
}
if agent.ID == uuid.Nil {
if len(agents) > 1 {
if !shuffle {
return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, xerrors.New("you must specify the name of an agent")
}
agent, err = cryptorand.Element(agents)
if err != nil {
return codersdk.Workspace{}, codersdk.WorkspaceAgent{}, err
}
} else {
agent = agents[0]
}
}
return workspace, agent, nil
}
func (*stdioConn) Close() (err error) {
return nil
// Attempt to poll workspace autostop. We write a per-workspace lockfile to
// avoid spamming the user with notifications in case of multiple instances
// of the CLI running simultaneously.
func tryPollWorkspaceAutostop(ctx context.Context, client *codersdk.Client, workspace codersdk.Workspace) (stop func()) {
lock := flock.New(filepath.Join(os.TempDir(), "coder-autostop-notify-"+workspace.ID.String()))
condition := notifyCondition(ctx, client, workspace.ID, lock)
return notify.Notify(condition, workspacePollInterval, autostopNotifyCountdown...)
}
func (*stdioConn) LocalAddr() net.Addr {
return nil
}
// Notify the user if the workspace is due to shutdown.
func notifyCondition(ctx context.Context, client *codersdk.Client, workspaceID uuid.UUID, lock *flock.Flock) notify.Condition {
return func(now time.Time) (deadline time.Time, callback func()) {
// Keep trying to regain the lock.
locked, err := lock.TryLockContext(ctx, workspacePollInterval)
if err != nil || !locked {
return time.Time{}, nil
}
func (*stdioConn) RemoteAddr() net.Addr {
return nil
}
ws, err := client.Workspace(ctx, workspaceID)
if err != nil {
return time.Time{}, nil
}
func (*stdioConn) SetDeadline(_ time.Time) error {
return nil
}
if ptr.NilOrZero(ws.TTLMillis) {
return time.Time{}, nil
}
func (*stdioConn) SetReadDeadline(_ time.Time) error {
return nil
}
func (*stdioConn) SetWriteDeadline(_ time.Time) error {
return nil
deadline = ws.LatestBuild.Deadline
callback = func() {
ttl := deadline.Sub(now)
var title, body string
if ttl > time.Minute {
title = fmt.Sprintf(`Workspace %s stopping soon`, ws.Name)
body = fmt.Sprintf(
`Your Coder workspace %s is scheduled to stop in %.0f mins`, ws.Name, ttl.Minutes())
} else {
title = fmt.Sprintf("Workspace %s stopping!", ws.Name)
body = fmt.Sprintf("Your Coder workspace %s is stopping any time now!", ws.Name)
}
// notify user with a native system notification (best effort)
_ = beeep.Notify(title, body, "")
}
return deadline.Truncate(time.Minute), callback
}
}
+22
View File
@@ -0,0 +1,22 @@
//go:build !windows
// +build !windows
package cli
import (
"context"
"os"
"os/signal"
"golang.org/x/sys/unix"
)
func listenWindowSize(ctx context.Context) <-chan os.Signal {
windowSize := make(chan os.Signal, 1)
signal.Notify(windowSize, unix.SIGWINCH)
go func() {
<-ctx.Done()
signal.Stop(windowSize)
}()
return windowSize
}
+197 -87
View File
@@ -1,145 +1,127 @@
package cli_test
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"errors"
"io"
"net"
"path/filepath"
"runtime"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"
gosshagent "golang.org/x/crypto/ssh/agent"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
"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/peer"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
"github.com/coder/coder/pty/ptytest"
)
func setupWorkspaceForSSH(t *testing.T) (*codersdk.Client, codersdk.Workspace, string) {
t.Helper()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
agentToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionDryRun: echo.ProvisionComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "dev",
Type: "google_compute_instance",
Agents: []*proto.Agent{{
Id: uuid.NewString(),
Auth: &proto.Agent_Token{
Token: agentToken,
},
}},
}},
},
},
}},
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
return client, workspace, agentToken
}
func TestSSH(t *testing.T) {
t.Parallel()
t.Run("ImmediateExit", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
agentToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionDryRun: echo.ProvisionComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "dev",
Type: "google_compute_instance",
Agent: &proto.Agent{
Id: uuid.NewString(),
Auth: &proto.Agent_Token{
Token: agentToken,
},
},
}},
},
},
}},
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID)
go func() {
// Run this async so the SSH command has to wait for
// the build and agent to connect!
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
agentClient := codersdk.New(client.URL)
agentClient.SessionToken = agentToken
agentCloser := agent.New(agentClient.ListenWorkspaceAgent, &peer.ConnOptions{
Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
})
t.Cleanup(func() {
_ = agentCloser.Close()
})
}()
client, workspace, agentToken := setupWorkspaceForSSH(t)
cmd, root := clitest.New(t, "ssh", workspace.Name)
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetErr(pty.Output())
cmd.SetOut(pty.Output())
go func() {
defer close(doneChan)
cmdDone := tGo(t, func() {
err := cmd.Execute()
require.NoError(t, err)
}()
assert.NoError(t, err)
})
pty.ExpectMatch("Waiting")
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
agentClient := codersdk.New(client.URL)
agentClient.SessionToken = agentToken
agentCloser := agent.New(agentClient.ListenWorkspaceAgent, &agent.Options{
Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
})
t.Cleanup(func() {
_ = agentCloser.Close()
})
// Shells on Mac, Windows, and Linux all exit shells with the "exit" command.
pty.WriteLine("exit")
<-doneChan
<-cmdDone
})
t.Run("Stdio", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
coderdtest.NewProvisionerDaemon(t, client)
agentToken := uuid.NewString()
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionDryRun: echo.ProvisionComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "dev",
Type: "google_compute_instance",
Agent: &proto.Agent{
Id: uuid.NewString(),
Auth: &proto.Agent_Token{
Token: agentToken,
},
},
}},
},
},
}},
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID)
go func() {
client, workspace, agentToken := setupWorkspaceForSSH(t)
_, _ = tGoContext(t, func(ctx context.Context) {
// Run this async so the SSH command has to wait for
// the build and agent to connect!
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
agentClient := codersdk.New(client.URL)
agentClient.SessionToken = agentToken
agentCloser := agent.New(agentClient.ListenWorkspaceAgent, &peer.ConnOptions{
agentCloser := agent.New(agentClient.ListenWorkspaceAgent, &agent.Options{
Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
})
t.Cleanup(func() {
_ = agentCloser.Close()
})
}()
<-ctx.Done()
_ = agentCloser.Close()
})
clientOutput, clientInput := io.Pipe()
serverOutput, serverInput := io.Pipe()
cmd, root := clitest.New(t, "ssh", "--stdio", workspace.Name)
clitest.SetupConfig(t, client, root)
doneChan := make(chan struct{})
cmd.SetIn(clientOutput)
cmd.SetOut(serverInput)
cmd.SetErr(io.Discard)
go func() {
defer close(doneChan)
cmdDone := tGo(t, func() {
err := cmd.Execute()
require.NoError(t, err)
}()
assert.NoError(t, err)
})
conn, channels, requests, err := ssh.NewClientConn(&stdioConn{
Reader: serverOutput,
@@ -161,8 +143,136 @@ func TestSSH(t *testing.T) {
err = sshClient.Close()
require.NoError(t, err)
_ = clientOutput.Close()
<-doneChan
<-cmdDone
})
t.Run("ForwardAgent", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Test not supported on windows")
}
t.Parallel()
client, workspace, agentToken := setupWorkspaceForSSH(t)
_, _ = tGoContext(t, func(ctx context.Context) {
// Run this async so the SSH command has to wait for
// the build and agent to connect!
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
agentClient := codersdk.New(client.URL)
agentClient.SessionToken = agentToken
agentCloser := agent.New(agentClient.ListenWorkspaceAgent, &agent.Options{
Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
})
<-ctx.Done()
_ = agentCloser.Close()
})
// Generate private key.
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
kr := gosshagent.NewKeyring()
kr.Add(gosshagent.AddedKey{
PrivateKey: privateKey,
})
// Start up ssh agent listening on unix socket.
tmpdir := t.TempDir()
agentSock := filepath.Join(tmpdir, "agent.sock")
l, err := net.Listen("unix", agentSock)
require.NoError(t, err)
defer l.Close()
_ = tGo(t, func() {
for {
fd, err := l.Accept()
if err != nil {
if !errors.Is(err, net.ErrClosed) {
t.Logf("accept error: %v", err)
}
return
}
err = gosshagent.ServeAgent(kr, fd)
if !errors.Is(err, io.EOF) {
assert.NoError(t, err)
}
}
})
cmd, root := clitest.New(t,
"ssh",
workspace.Name,
"--forward-agent",
"--identity-agent", agentSock, // Overrides $SSH_AUTH_SOCK.
)
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
cmd.SetErr(io.Discard)
cmdDone := tGo(t, func() {
err := cmd.Execute()
assert.NoError(t, err)
})
// Ensure that SSH_AUTH_SOCK is set.
// Linux: /tmp/auth-agent3167016167/listener.sock
// macOS: /var/folders/ng/m1q0wft14hj0t3rtjxrdnzsr0000gn/T/auth-agent3245553419/listener.sock
pty.WriteLine("env")
pty.ExpectMatch("SSH_AUTH_SOCK=")
// Ensure that ssh-add lists our key.
pty.WriteLine("ssh-add -L")
keys, err := kr.List()
require.NoError(t, err)
pty.ExpectMatch(keys[0].String())
// And we're done.
pty.WriteLine("exit")
<-cmdDone
})
}
// tGoContext runs fn in a goroutine passing a context that will be
// canceled on test completion and wait until fn has finished executing.
// Done and cancel are returned for optionally waiting until completion
// or early cancellation.
//
// NOTE(mafredri): This could be moved to a helper library.
func tGoContext(t *testing.T, fn func(context.Context)) (done <-chan struct{}, cancel context.CancelFunc) {
t.Helper()
ctx, cancel := context.WithCancel(context.Background())
doneC := make(chan struct{})
t.Cleanup(func() {
cancel()
<-done
})
go func() {
fn(ctx)
close(doneC)
}()
return doneC, cancel
}
// tGo runs fn in a goroutine and waits until fn has completed before
// test completion. Done is returned for optionally waiting for fn to
// exit.
//
// NOTE(mafredri): This could be moved to a helper library.
func tGo(t *testing.T, fn func()) (done <-chan struct{}) {
t.Helper()
doneC := make(chan struct{})
t.Cleanup(func() {
<-doneC
})
go func() {
fn()
close(doneC)
}()
return doneC
}
type stdioConn struct {
+27
View File
@@ -0,0 +1,27 @@
//go:build windows
// +build windows
package cli
import (
"context"
"os"
"time"
)
func listenWindowSize(ctx context.Context) <-chan os.Signal {
windowSize := make(chan os.Signal, 3)
ticker := time.NewTicker(time.Second)
go func() {
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
}
windowSize <- nil
}
}()
return windowSize
}
+21 -481
View File
@@ -1,507 +1,47 @@
package cli
import (
"context"
"crypto/tls"
"crypto/x509"
"database/sql"
"encoding/pem"
"fmt"
"net"
"net/http"
"net/url"
"os"
"os/signal"
"path/filepath"
"time"
"github.com/briandowns/spinner"
"github.com/coreos/go-systemd/daemon"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
"google.golang.org/api/idtoken"
"google.golang.org/api/option"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
"github.com/coder/coder/cli/cliflag"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/cli/config"
"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/database/databasefake"
"github.com/coder/coder/coderd/gitsshkey"
"github.com/coder/coder/coderd/tunnel"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisioner/terraform"
"github.com/coder/coder/provisionerd"
"github.com/coder/coder/provisionersdk"
"github.com/coder/coder/provisionersdk/proto"
)
func start() *cobra.Command {
var (
accessURL string
address string
cacheDir string
dev bool
postgresURL string
// provisionerDaemonCount is a uint8 to ensure a number > 0.
provisionerDaemonCount uint8
tlsCertFile string
tlsClientCAFile string
tlsClientAuth string
tlsEnable bool
tlsKeyFile string
tlsMinVersion string
useTunnel bool
traceDatadog bool
secureAuthCookie bool
sshKeygenAlgorithmRaw string
)
root := &cobra.Command{
Use: "start",
cmd := &cobra.Command{
Annotations: workspaceCommand,
Use: "start <workspace>",
Short: "Build a workspace with the start state",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if traceDatadog {
tracer.Start()
defer tracer.Stop()
}
printLogo(cmd)
listener, err := net.Listen("tcp", address)
if err != nil {
return xerrors.Errorf("listen %q: %w", address, err)
}
defer listener.Close()
if tlsEnable {
listener, err = configureTLS(listener, tlsMinVersion, tlsClientAuth, tlsCertFile, tlsKeyFile, tlsClientCAFile)
if err != nil {
return xerrors.Errorf("configure tls: %w", err)
}
}
tcpAddr, valid := listener.Addr().(*net.TCPAddr)
if !valid {
return xerrors.New("must be listening on tcp")
}
// If just a port is specified, assume localhost.
if tcpAddr.IP.IsUnspecified() {
tcpAddr.IP = net.IPv4(127, 0, 0, 1)
}
localURL := &url.URL{
Scheme: "http",
Host: tcpAddr.String(),
}
if tlsEnable {
localURL.Scheme = "https"
}
if accessURL == "" {
accessURL = localURL.String()
}
var tunnelErr <-chan error
// If we're attempting to tunnel in dev-mode, the access URL
// needs to be changed to use the tunnel.
if dev && useTunnel {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render("Coder requires a network endpoint that can be accessed by provisioned workspaces. In dev mode, a free tunnel can be created for you. This will expose your Coder deployment to the internet.")+"\n")
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Would you like Coder to start a tunnel for simple setup?",
IsConfirm: true,
})
if err == nil {
accessURL, tunnelErr, err = tunnel.New(cmd.Context(), localURL.String())
if err != nil {
return xerrors.Errorf("create tunnel: %w", err)
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render(cliui.Styles.Wrap.Render(cliui.Styles.Prompt.String()+`Tunnel started. Your deployment is accessible at:`))+"\n "+cliui.Styles.Field.Render(accessURL)+"\n")
}
}
validator, err := idtoken.NewValidator(cmd.Context(), option.WithoutAuthentication())
_, err := cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Confirm start workspace?",
IsConfirm: true,
})
if err != nil {
return err
}
accessURLParsed, err := url.Parse(accessURL)
client, err := createClient(cmd)
if err != nil {
return xerrors.Errorf("parse access url %q: %w", accessURL, err)
}
sshKeygenAlgorithm, err := gitsshkey.ParseAlgorithm(sshKeygenAlgorithmRaw)
if err != nil {
return xerrors.Errorf("parse ssh keygen algorithm %s: %w", sshKeygenAlgorithmRaw, err)
}
logger := slog.Make(sloghuman.Sink(os.Stderr))
options := &coderd.Options{
AccessURL: accessURLParsed,
Logger: logger.Named("coderd"),
Database: databasefake.New(),
Pubsub: database.NewPubsubInMemory(),
GoogleTokenValidator: validator,
SecureAuthCookie: secureAuthCookie,
SSHKeygenAlgorithm: sshKeygenAlgorithm,
}
if !dev {
sqlDB, err := sql.Open("postgres", postgresURL)
if err != nil {
return xerrors.Errorf("dial postgres: %w", err)
}
err = sqlDB.Ping()
if err != nil {
return xerrors.Errorf("ping postgres: %w", err)
}
err = database.MigrateUp(sqlDB)
if err != nil {
return xerrors.Errorf("migrate up: %w", err)
}
options.Database = database.New(sqlDB)
options.Pubsub, err = database.NewPubsub(cmd.Context(), sqlDB, postgresURL)
if err != nil {
return xerrors.Errorf("create pubsub: %w", err)
}
}
handler, closeCoderd := coderd.New(options)
client := codersdk.New(localURL)
if tlsEnable {
// Secure transport isn't needed for locally communicating!
client.HTTPClient.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
//nolint:gosec
InsecureSkipVerify: true,
},
}
}
provisionerDaemons := make([]*provisionerd.Server, 0)
for i := 0; uint8(i) < provisionerDaemonCount; i++ {
daemonClose, err := newProvisionerDaemon(cmd.Context(), client, logger, cacheDir)
if err != nil {
return xerrors.Errorf("create provisioner daemon: %w", err)
}
provisionerDaemons = append(provisionerDaemons, daemonClose)
}
defer func() {
for _, provisionerDaemon := range provisionerDaemons {
_ = provisionerDaemon.Close()
}
}()
errCh := make(chan error, 1)
shutdownConnsCtx, shutdownConns := context.WithCancel(cmd.Context())
defer shutdownConns()
go func() {
defer close(errCh)
server := http.Server{
Handler: handler,
BaseContext: func(_ net.Listener) context.Context {
return shutdownConnsCtx
},
}
errCh <- server.Serve(listener)
}()
config := createConfig(cmd)
if dev {
err = createFirstUser(cmd, client, config)
if err != nil {
return xerrors.Errorf("create first user: %w", err)
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render(cliui.Styles.Wrap.Render(cliui.Styles.Prompt.String()+`Started in `+
cliui.Styles.Field.Render("dev")+` mode. All data is in-memory! Do not use in production. Press `+cliui.Styles.Field.Render("ctrl+c")+` to clean up provisioned infrastructure.`))+
`
`+
cliui.Styles.Paragraph.Render(cliui.Styles.Wrap.Render(cliui.Styles.Prompt.String()+`Run `+cliui.Styles.Code.Render("coder templates init")+" in a new terminal to get started.\n"))+`
`)
} else {
// This is helpful for tests, but can be silently ignored.
// Coder may be ran as users that don't have permission to write in the homedir,
// such as via the systemd service.
_ = config.URL().Write(client.URL.String())
_, _ = fmt.Fprintf(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render(cliui.Styles.Wrap.Render(cliui.Styles.Prompt.String()+`Started in `+
cliui.Styles.Field.Render("production")+` mode. All data is stored in the PostgreSQL provided! Press `+cliui.Styles.Field.Render("ctrl+c")+` to gracefully shutdown.`))+"\n")
hasFirstUser, err := client.HasFirstUser(cmd.Context())
if !hasFirstUser && err == nil {
// This could fail for a variety of TLS-related reasons.
// This is a helpful starter message, and not critical for user interaction.
_, _ = fmt.Fprint(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render(cliui.Styles.Wrap.Render(cliui.Styles.FocusedPrompt.String()+`Run `+cliui.Styles.Code.Render("coder login "+client.URL.String())+" in a new terminal to get started.\n")))
}
}
// Updates the systemd status from activating to activated.
_, err = daemon.SdNotify(false, daemon.SdNotifyReady)
if err != nil {
return xerrors.Errorf("notify systemd: %w", err)
}
stopChan := make(chan os.Signal, 1)
defer signal.Stop(stopChan)
signal.Notify(stopChan, os.Interrupt)
select {
case <-cmd.Context().Done():
closeCoderd()
return cmd.Context().Err()
case err := <-tunnelErr:
return err
case err := <-errCh:
closeCoderd()
return err
case <-stopChan:
}
signal.Stop(stopChan)
_, err = daemon.SdNotify(false, daemon.SdNotifyStopping)
workspace, err := namedWorkspace(cmd, client, args[0])
if err != nil {
return xerrors.Errorf("notify systemd: %w", err)
return err
}
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "\n\n"+cliui.Styles.Bold.Render("Interrupt caught. Gracefully exiting..."))
if dev {
workspaces, err := client.WorkspacesByUser(cmd.Context(), codersdk.Me)
if err != nil {
return xerrors.Errorf("get workspaces: %w", err)
}
for _, workspace := range workspaces {
before := time.Now()
build, err := client.CreateWorkspaceBuild(cmd.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
Transition: database.WorkspaceTransitionDelete,
})
if err != nil {
return xerrors.Errorf("delete workspace: %w", err)
}
err = cliui.ProvisionerJob(cmd.Context(), cmd.OutOrStdout(), cliui.ProvisionerJobOptions{
Fetch: func() (codersdk.ProvisionerJob, error) {
build, err := client.WorkspaceBuild(cmd.Context(), build.ID)
return build.Job, err
},
Cancel: func() error {
return client.CancelWorkspaceBuild(cmd.Context(), build.ID)
},
Logs: func() (<-chan codersdk.ProvisionerJobLog, error) {
return client.WorkspaceBuildLogsAfter(cmd.Context(), build.ID, before)
},
})
if err != nil {
return xerrors.Errorf("delete workspace %s: %w", workspace.Name, err)
}
}
before := time.Now()
build, err := client.CreateWorkspaceBuild(cmd.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionStart,
})
if err != nil {
return err
}
for _, provisionerDaemon := range provisionerDaemons {
spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond)
spin.Writer = cmd.OutOrStdout()
spin.Suffix = cliui.Styles.Keyword.Render(" Shutting down provisioner daemon...")
spin.Start()
err = provisionerDaemon.Shutdown(cmd.Context())
if err != nil {
spin.FinalMSG = cliui.Styles.Prompt.String() + "Failed to shutdown provisioner daemon: " + err.Error()
spin.Stop()
}
err = provisionerDaemon.Close()
if err != nil {
spin.Stop()
return xerrors.Errorf("close provisioner daemon: %w", err)
}
spin.FinalMSG = cliui.Styles.Prompt.String() + "Gracefully shut down provisioner daemon!\n"
spin.Stop()
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), cliui.Styles.Prompt.String()+"Waiting for WebSocket connections to close...\n")
shutdownConns()
closeCoderd()
return nil
return cliui.WorkspaceBuild(cmd.Context(), cmd.OutOrStdout(), client, build.ID, before)
},
}
cliflag.StringVarP(root.Flags(), &accessURL, "access-url", "", "CODER_ACCESS_URL", "", "Specifies the external URL to access Coder")
cliflag.StringVarP(root.Flags(), &address, "address", "a", "CODER_ADDRESS", "127.0.0.1:3000", "The address to serve the API and dashboard")
// systemd uses the CACHE_DIRECTORY environment variable!
cliflag.StringVarP(root.Flags(), &cacheDir, "cache-dir", "", "CACHE_DIRECTORY", filepath.Join(os.TempDir(), "coder-cache"), "Specifies a directory to cache binaries for provision operations.")
cliflag.BoolVarP(root.Flags(), &dev, "dev", "", "CODER_DEV_MODE", false, "Serve Coder in dev mode for tinkering")
cliflag.StringVarP(root.Flags(), &postgresURL, "postgres-url", "", "CODER_PG_CONNECTION_URL", "", "URL of a PostgreSQL database to connect to")
cliflag.Uint8VarP(root.Flags(), &provisionerDaemonCount, "provisioner-daemons", "", "CODER_PROVISIONER_DAEMONS", 1, "The amount of provisioner daemons to create on start.")
cliflag.BoolVarP(root.Flags(), &tlsEnable, "tls-enable", "", "CODER_TLS_ENABLE", false, "Specifies if TLS will be enabled")
cliflag.StringVarP(root.Flags(), &tlsCertFile, "tls-cert-file", "", "CODER_TLS_CERT_FILE", "",
"Specifies the path to the certificate for TLS. It requires a PEM-encoded file. "+
"To configure the listener to use a CA certificate, concatenate the primary certificate "+
"and the CA certificate together. The primary certificate should appear first in the combined file")
cliflag.StringVarP(root.Flags(), &tlsClientCAFile, "tls-client-ca-file", "", "CODER_TLS_CLIENT_CA_FILE", "",
"PEM-encoded Certificate Authority file used for checking the authenticity of client")
cliflag.StringVarP(root.Flags(), &tlsClientAuth, "tls-client-auth", "", "CODER_TLS_CLIENT_AUTH", "request",
`Specifies the policy the server will follow for TLS Client Authentication. `+
`Accepted values are "none", "request", "require-any", "verify-if-given", or "require-and-verify"`)
cliflag.StringVarP(root.Flags(), &tlsKeyFile, "tls-key-file", "", "CODER_TLS_KEY_FILE", "",
"Specifies the path to the private key for the certificate. It requires a PEM-encoded file")
cliflag.StringVarP(root.Flags(), &tlsMinVersion, "tls-min-version", "", "CODER_TLS_MIN_VERSION", "tls12",
`Specifies the minimum supported version of TLS. Accepted values are "tls10", "tls11", "tls12" or "tls13"`)
cliflag.BoolVarP(root.Flags(), &useTunnel, "tunnel", "", "CODER_DEV_TUNNEL", true, "Serve dev mode through a Cloudflare Tunnel for easy setup")
_ = root.Flags().MarkHidden("tunnel")
cliflag.BoolVarP(root.Flags(), &traceDatadog, "trace-datadog", "", "CODER_TRACE_DATADOG", false, "Send tracing data to a datadog agent")
cliflag.BoolVarP(root.Flags(), &secureAuthCookie, "secure-auth-cookie", "", "CODER_SECURE_AUTH_COOKIE", false, "Specifies if the 'Secure' property is set on browser session cookies")
cliflag.StringVarP(root.Flags(), &sshKeygenAlgorithmRaw, "ssh-keygen-algorithm", "", "CODER_SSH_KEYGEN_ALGORITHM", "ed25519", "Specifies the algorithm to use for generating ssh keys. "+
`Accepted values are "ed25519", "ecdsa", or "rsa4096"`)
return root
}
func createFirstUser(cmd *cobra.Command, client *codersdk.Client, cfg config.Root) error {
_, err := client.CreateFirstUser(cmd.Context(), codersdk.CreateFirstUserRequest{
Email: "admin@coder.com",
Username: "developer",
Password: "password",
OrganizationName: "acme-corp",
})
if err != nil {
return xerrors.Errorf("create first user: %w", err)
}
token, err := client.LoginWithPassword(cmd.Context(), codersdk.LoginWithPasswordRequest{
Email: "admin@coder.com",
Password: "password",
})
if err != nil {
return xerrors.Errorf("login with first user: %w", err)
}
client.SessionToken = token.SessionToken
err = cfg.URL().Write(client.URL.String())
if err != nil {
return xerrors.Errorf("write local url: %w", err)
}
err = cfg.Session().Write(token.SessionToken)
if err != nil {
return xerrors.Errorf("write session token: %w", err)
}
return nil
}
func newProvisionerDaemon(ctx context.Context, client *codersdk.Client, logger slog.Logger, cacheDir string) (*provisionerd.Server, error) {
err := os.MkdirAll(cacheDir, 0700)
if err != nil {
return nil, xerrors.Errorf("mkdir %q: %w", cacheDir, err)
}
terraformClient, terraformServer := provisionersdk.TransportPipe()
go func() {
err := terraform.Serve(ctx, &terraform.ServeOptions{
ServeOptions: &provisionersdk.ServeOptions{
Listener: terraformServer,
},
CachePath: cacheDir,
Logger: logger,
})
if err != nil {
panic(err)
}
}()
tempDir, err := os.MkdirTemp("", "provisionerd")
if err != nil {
return nil, err
}
return provisionerd.New(client.ListenProvisionerDaemon, &provisionerd.Options{
Logger: logger,
PollInterval: 50 * time.Millisecond,
UpdateInterval: 50 * time.Millisecond,
Provisioners: provisionerd.Provisioners{
string(database.ProvisionerTypeTerraform): proto.NewDRPCProvisionerClient(provisionersdk.Conn(terraformClient)),
},
WorkDirectory: tempDir,
}), nil
}
func printLogo(cmd *cobra.Command) {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), ` ▄█▀ ▀█▄
▄▄ ▀▀▀ █▌ ██▀▀█▄ ▐█
▄▄██▀▀█▄▄▄ ██ ██ █▀▀█ ▐█▀▀██ ▄█▀▀█ █▀▀
█▌ ▄▌ ▐█ █▌ ▀█▄▄▄█▌ █ █ ▐█ ██ ██▀▀ █
██████▀▄█ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀▀ ▀▀▀▀ ▀
`)
}
func configureTLS(listener net.Listener, tlsMinVersion, tlsClientAuth, tlsCertFile, tlsKeyFile, tlsClientCAFile string) (net.Listener, error) {
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
}
switch tlsMinVersion {
case "tls10":
tlsConfig.MinVersion = tls.VersionTLS10
case "tls11":
tlsConfig.MinVersion = tls.VersionTLS11
case "tls12":
tlsConfig.MinVersion = tls.VersionTLS12
case "tls13":
tlsConfig.MinVersion = tls.VersionTLS13
default:
return nil, xerrors.Errorf("unrecognized tls version: %q", tlsMinVersion)
}
switch tlsClientAuth {
case "none":
tlsConfig.ClientAuth = tls.NoClientCert
case "request":
tlsConfig.ClientAuth = tls.RequestClientCert
case "require-any":
tlsConfig.ClientAuth = tls.RequireAnyClientCert
case "verify-if-given":
tlsConfig.ClientAuth = tls.VerifyClientCertIfGiven
case "require-and-verify":
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
default:
return nil, xerrors.Errorf("unrecognized tls client auth: %q", tlsClientAuth)
}
if tlsCertFile == "" {
return nil, xerrors.New("tls-cert-file is required when tls is enabled")
}
if tlsKeyFile == "" {
return nil, xerrors.New("tls-key-file is required when tls is enabled")
}
certPEMBlock, err := os.ReadFile(tlsCertFile)
if err != nil {
return nil, xerrors.Errorf("read file %q: %w", tlsCertFile, err)
}
keyPEMBlock, err := os.ReadFile(tlsKeyFile)
if err != nil {
return nil, xerrors.Errorf("read file %q: %w", tlsKeyFile, err)
}
keyBlock, _ := pem.Decode(keyPEMBlock)
if keyBlock == nil {
return nil, xerrors.New("decoded pem is blank")
}
cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
if err != nil {
return nil, xerrors.Errorf("create key pair: %w", err)
}
tlsConfig.GetCertificate = func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
return &cert, nil
}
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(certPEMBlock)
tlsConfig.RootCAs = certPool
if tlsClientCAFile != "" {
caPool := x509.NewCertPool()
data, err := os.ReadFile(tlsClientCAFile)
if err != nil {
return nil, xerrors.Errorf("read %q: %w", tlsClientCAFile, err)
}
if !caPool.AppendCertsFromPEM(data) {
return nil, xerrors.Errorf("failed to parse CA certificate in tls-client-ca-file")
}
tlsConfig.ClientCAs = caPool
}
return tls.NewListener(listener, tlsConfig), nil
cliui.AllowSkipPrompt(cmd)
return cmd
}
+112
View File
@@ -0,0 +1,112 @@
package cli
import (
"io"
"os"
"time"
"github.com/spf13/cobra"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/codersdk"
)
func state() *cobra.Command {
cmd := &cobra.Command{
Use: "state",
Short: "Manually manage Terraform state to fix broken workspaces",
}
cmd.AddCommand(statePull(), statePush())
return cmd
}
func statePull() *cobra.Command {
var buildName string
cmd := &cobra.Command{
Use: "pull <workspace> [file]",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
return err
}
workspace, err := namedWorkspace(cmd, client, args[0])
if err != nil {
return err
}
var build codersdk.WorkspaceBuild
if buildName == "latest" {
build = workspace.LatestBuild
} else {
build, err = client.WorkspaceBuildByName(cmd.Context(), workspace.ID, buildName)
if err != nil {
return err
}
}
state, err := client.WorkspaceBuildState(cmd.Context(), build.ID)
if err != nil {
return err
}
if len(args) < 2 {
cmd.Println(string(state))
return nil
}
return os.WriteFile(args[1], state, 0600)
},
}
cmd.Flags().StringVarP(&buildName, "build", "b", "latest", "Specify a workspace build to target by name.")
return cmd
}
func statePush() *cobra.Command {
var buildName string
cmd := &cobra.Command{
Use: "push <workspace> <file>",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
return err
}
workspace, err := namedWorkspace(cmd, client, args[0])
if err != nil {
return err
}
var build codersdk.WorkspaceBuild
if buildName == "latest" {
build = workspace.LatestBuild
} else {
build, err = client.WorkspaceBuildByName(cmd.Context(), workspace.ID, buildName)
if err != nil {
return err
}
}
var state []byte
if args[1] == "-" {
state, err = io.ReadAll(cmd.InOrStdin())
} else {
state, err = os.ReadFile(args[1])
}
if err != nil {
return err
}
before := time.Now()
build, err = client.CreateWorkspaceBuild(cmd.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: build.TemplateVersionID,
Transition: build.Transition,
ProvisionerState: state,
})
if err != nil {
return err
}
return cliui.WorkspaceBuild(cmd.Context(), cmd.OutOrStderr(), client, build.ID, before)
},
}
cmd.Flags().StringVarP(&buildName, "build", "b", "latest", "Specify a workspace build to target by name.")
return cmd
}
+125
View File
@@ -0,0 +1,125 @@
package cli_test
import (
"bytes"
"io"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
)
func TestStatePull(t *testing.T) {
t.Parallel()
t.Run("File", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
wantState := []byte("some state")
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
State: wantState,
},
},
}},
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
statefilePath := filepath.Join(t.TempDir(), "state")
cmd, root := clitest.New(t, "state", "pull", workspace.Name, statefilePath)
clitest.SetupConfig(t, client, root)
err := cmd.Execute()
require.NoError(t, err)
gotState, err := os.ReadFile(statefilePath)
require.NoError(t, err)
require.Equal(t, wantState, gotState)
})
t.Run("Stdout", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
wantState := []byte("some state")
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
Provision: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
State: wantState,
},
},
}},
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
cmd, root := clitest.New(t, "state", "pull", workspace.Name)
var gotState bytes.Buffer
cmd.SetOut(&gotState)
clitest.SetupConfig(t, client, root)
err := cmd.Execute()
require.NoError(t, err)
require.Equal(t, wantState, bytes.TrimSpace(gotState.Bytes()))
})
}
func TestStatePush(t *testing.T) {
t.Parallel()
t.Run("File", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
Provision: echo.ProvisionComplete,
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
stateFile, err := os.CreateTemp(t.TempDir(), "")
require.NoError(t, err)
wantState := []byte("some magic state")
_, err = stateFile.Write(wantState)
require.NoError(t, err)
err = stateFile.Close()
require.NoError(t, err)
cmd, root := clitest.New(t, "state", "push", workspace.Name, stateFile.Name())
cmd.SetErr(io.Discard)
cmd.SetOut(io.Discard)
clitest.SetupConfig(t, client, root)
err = cmd.Execute()
require.NoError(t, err)
})
t.Run("Stdin", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
Provision: echo.ProvisionComplete,
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
cmd, root := clitest.New(t, "state", "push", "--build", workspace.LatestBuild.Name, workspace.Name, "-")
clitest.SetupConfig(t, client, root)
cmd.SetIn(strings.NewReader("some magic state"))
err := cmd.Execute()
require.NoError(t, err)
})
}
+47
View File
@@ -0,0 +1,47 @@
package cli
import (
"time"
"github.com/spf13/cobra"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/codersdk"
)
func stop() *cobra.Command {
cmd := &cobra.Command{
Annotations: workspaceCommand,
Use: "stop <workspace>",
Short: "Build a workspace with the stop state",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
_, err := cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Confirm stop workspace?",
IsConfirm: true,
})
if err != nil {
return err
}
client, err := createClient(cmd)
if err != nil {
return err
}
workspace, err := namedWorkspace(cmd, client, args[0])
if err != nil {
return err
}
before := time.Now()
build, err := client.CreateWorkspaceBuild(cmd.Context(), workspace.ID, codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionStop,
})
if err != nil {
return err
}
return cliui.WorkspaceBuild(cmd.Context(), cmd.OutOrStdout(), client, build.ID, before)
},
}
cliui.AllowSkipPrompt(cmd)
return cmd
}
+88 -39
View File
@@ -1,15 +1,14 @@
package cli
import (
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/briandowns/spinner"
"github.com/manifoldco/promptui"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
@@ -22,18 +21,20 @@ import (
func templateCreate() *cobra.Command {
var (
yes bool
directory string
provisioner string
directory string
provisioner string
parameterFile string
)
cmd := &cobra.Command{
Use: "create [name]",
Short: "Create a template from the current directory",
Short: "Create a template from the current directory or as specified by flag",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return err
@@ -45,17 +46,29 @@ func templateCreate() *cobra.Command {
} else {
templateName = args[0]
}
_, err = client.TemplateByName(cmd.Context(), organization.ID, templateName)
if err == nil {
return xerrors.Errorf("A template already exists named %q!", templateName)
}
// Confirm upload of the directory.
prettyDir := prettyDirectoryPath(directory)
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: fmt.Sprintf("Create and upload %q?", prettyDir),
IsConfirm: true,
Default: "yes",
})
if err != nil {
return err
}
spin := spinner.New(spinner.CharSets[5], 100*time.Millisecond)
spin.Writer = cmd.OutOrStdout()
spin.Suffix = cliui.Styles.Keyword.Render(" Uploading current directory...")
spin.Suffix = cliui.Styles.Keyword.Render(" Uploading directory...")
spin.Start()
defer spin.Stop()
archive, err := provisionersdk.Tar(directory)
archive, err := provisionersdk.Tar(directory, provisionersdk.TemplateArchiveLimit)
if err != nil {
return err
}
@@ -66,26 +79,17 @@ func templateCreate() *cobra.Command {
}
spin.Stop()
spin = spinner.New(spinner.CharSets[5], 100*time.Millisecond)
spin.Writer = cmd.OutOrStdout()
spin.Suffix = cliui.Styles.Keyword.Render("Something")
job, parameters, err := createValidTemplateVersion(cmd, client, organization, database.ProvisionerType(provisioner), resp.Hash)
job, parameters, err := createValidTemplateVersion(cmd, client, organization, database.ProvisionerType(provisioner), resp.Hash, parameterFile)
if err != nil {
return err
}
if !yes {
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Create template?",
IsConfirm: true,
Default: "yes",
})
if err != nil {
if errors.Is(err, promptui.ErrAbort) {
return nil
}
return err
}
_, err = cliui.Prompt(cmd, cliui.PromptOptions{
Text: "Confirm create?",
IsConfirm: true,
})
if err != nil {
return err
}
_, err = client.CreateTemplate(cmd.Context(), organization.ID, codersdk.CreateTemplateRequest{
@@ -97,28 +101,35 @@ func templateCreate() *cobra.Command {
return err
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "The %s template has been created!\n", templateName)
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "\n"+cliui.Styles.Wrap.Render(
"The "+cliui.Styles.Keyword.Render(templateName)+" template has been created! "+
"Developers can provision a workspace with this template using:")+"\n")
_, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+cliui.Styles.Code.Render(fmt.Sprintf("coder create --template=%q [workspace name]", templateName)))
_, _ = fmt.Fprintln(cmd.OutOrStdout())
return nil
},
}
currentDirectory, _ := os.Getwd()
cmd.Flags().StringVarP(&directory, "directory", "d", currentDirectory, "Specify the directory to create from")
cmd.Flags().StringVarP(&provisioner, "provisioner", "p", "terraform", "Customize the provisioner backend")
cmd.Flags().StringVarP(&provisioner, "test.provisioner", "", "terraform", "Customize the provisioner backend")
cmd.Flags().StringVarP(&parameterFile, "parameter-file", "", "", "Specify a file path with parameter values.")
// This is for testing!
err := cmd.Flags().MarkHidden("provisioner")
err := cmd.Flags().MarkHidden("test.provisioner")
if err != nil {
panic(err)
}
cmd.Flags().BoolVarP(&yes, "yes", "y", false, "Bypass prompts")
cliui.AllowSkipPrompt(cmd)
return cmd
}
func createValidTemplateVersion(cmd *cobra.Command, client *codersdk.Client, organization codersdk.Organization, provisioner database.ProvisionerType, hash string, parameters ...codersdk.CreateParameterRequest) (*codersdk.TemplateVersion, []codersdk.CreateParameterRequest, error) {
func createValidTemplateVersion(cmd *cobra.Command, client *codersdk.Client, organization codersdk.Organization, provisioner database.ProvisionerType, hash string, parameterFile string, parameters ...codersdk.CreateParameterRequest) (*codersdk.TemplateVersion, []codersdk.CreateParameterRequest, error) {
before := time.Now()
version, err := client.CreateTemplateVersion(cmd.Context(), organization.ID, codersdk.CreateTemplateVersionRequest{
StorageMethod: database.ProvisionerStorageMethodFile,
StorageMethod: codersdk.ProvisionerStorageMethodFile,
StorageSource: hash,
Provisioner: provisioner,
Provisioner: codersdk.ProvisionerType(provisioner),
ParameterValues: parameters,
})
if err != nil {
@@ -163,7 +174,7 @@ func createValidTemplateVersion(cmd *cobra.Command, client *codersdk.Client, org
sort.Slice(parameterSchemas, func(i, j int) bool {
return parameterSchemas[i].Name < parameterSchemas[j].Name
})
missingSchemas := make([]codersdk.TemplateVersionParameterSchema, 0)
missingSchemas := make([]codersdk.ParameterSchema, 0)
for _, parameterSchema := range parameterSchemas {
_, ok := valuesBySchemaID[parameterSchema.ID.String()]
if ok {
@@ -172,31 +183,69 @@ func createValidTemplateVersion(cmd *cobra.Command, client *codersdk.Client, org
missingSchemas = append(missingSchemas, parameterSchema)
}
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render("This template has required variables! They are scoped to the template, and not viewable after being set.")+"\r\n")
// parameterMapFromFile can be nil if parameter file is not specified
var parameterMapFromFile map[string]string
if parameterFile != "" {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), cliui.Styles.Paragraph.Render("Attempting to read the variables from the parameter file.")+"\r\n")
parameterMapFromFile, err = createParameterMapFromFile(parameterFile)
if err != nil {
return nil, nil, err
}
}
for _, parameterSchema := range missingSchemas {
value, err := cliui.ParameterSchema(cmd, parameterSchema)
parameterValue, err := getParameterValueFromMapOrInput(cmd, parameterMapFromFile, parameterSchema)
if err != nil {
return nil, nil, err
}
parameters = append(parameters, codersdk.CreateParameterRequest{
Name: parameterSchema.Name,
SourceValue: value,
SourceScheme: database.ParameterSourceSchemeData,
SourceValue: parameterValue,
SourceScheme: codersdk.ParameterSourceSchemeData,
DestinationScheme: parameterSchema.DefaultDestinationScheme,
})
_, _ = fmt.Fprintln(cmd.OutOrStdout())
}
return createValidTemplateVersion(cmd, client, organization, provisioner, hash, parameters...)
// This recursion is only 1 level deep in practice.
// The first pass populates the missing parameters, so it does not enter this `if` block again.
return createValidTemplateVersion(cmd, client, organization, provisioner, hash, parameterFile, parameters...)
}
if version.Job.Status != codersdk.ProvisionerJobSucceeded {
return nil, nil, xerrors.New(version.Job.Error)
}
_, _ = fmt.Fprintf(cmd.OutOrStdout(), cliui.Styles.Checkmark.String()+" Successfully imported template source!\n")
resources, err := client.TemplateVersionResources(cmd.Context(), version.ID)
if err != nil {
return nil, nil, err
}
return &version, parameters, displayTemplateVersionInfo(cmd, resources)
err = cliui.WorkspaceResources(cmd.OutOrStdout(), resources, cliui.WorkspaceResourcesOptions{
HideAgentState: true,
HideAccess: true,
Title: "Template Preview",
})
if err != nil {
return nil, nil, xerrors.Errorf("preview template resources: %w", err)
}
return &version, parameters, nil
}
// prettyDirectoryPath returns a prettified path when inside the users
// home directory. Falls back to dir if the users home directory cannot
// discerned. This function calls filepath.Clean on the result.
func prettyDirectoryPath(dir string) string {
dir = filepath.Clean(dir)
homeDir, err := os.UserHomeDir()
if err != nil {
return dir
}
pretty := dir
if strings.HasPrefix(pretty, homeDir) {
pretty = strings.TrimPrefix(pretty, homeDir)
pretty = "~" + pretty
}
return pretty
}
+161 -15
View File
@@ -1,6 +1,7 @@
package cli_test
import (
"os"
"testing"
"github.com/stretchr/testify/require"
@@ -9,6 +10,7 @@ import (
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
"github.com/coder/coder/pty/ptytest"
)
@@ -16,33 +18,177 @@ func TestTemplateCreate(t *testing.T) {
t.Parallel()
t.Run("Create", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
coderdtest.CreateFirstUser(t, client)
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
Parse: echo.ParseComplete,
Provision: echo.ProvisionComplete,
})
cmd, root := clitest.New(t, "templates", "create", "my-template", "--directory", source, "--provisioner", string(database.ProvisionerTypeEcho))
cmd, root := clitest.New(t, "templates", "create", "my-template", "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho))
clitest.SetupConfig(t, client, root)
_ = coderdtest.NewProvisionerDaemon(t, client)
doneChan := make(chan struct{})
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
execDone := make(chan error)
go func() {
defer close(doneChan)
err := cmd.Execute()
require.NoError(t, err)
execDone <- cmd.Execute()
}()
matches := []string{
"Create template?", "yes",
matches := []struct {
match string
write string
}{
{match: "Create and upload", write: "yes"},
{match: "Confirm create?", write: "yes"},
}
for i := 0; i < len(matches); i += 2 {
match := matches[i]
value := matches[i+1]
pty.ExpectMatch(match)
pty.WriteLine(value)
for _, m := range matches {
pty.ExpectMatch(m.match)
pty.WriteLine(m.write)
}
require.NoError(t, <-execDone)
})
t.Run("WithParameter", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
coderdtest.CreateFirstUser(t, client)
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
Parse: createTestParseResponse(),
Provision: echo.ProvisionComplete,
ProvisionDryRun: echo.ProvisionComplete,
})
cmd, root := clitest.New(t, "templates", "create", "my-template", "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho))
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
execDone := make(chan error)
go func() {
execDone <- cmd.Execute()
}()
matches := []struct {
match string
write string
}{
{match: "Create and upload", write: "yes"},
{match: "Enter a value:", write: "bananas"},
{match: "Confirm create?", write: "yes"},
}
for _, m := range matches {
pty.ExpectMatch(m.match)
pty.WriteLine(m.write)
}
require.NoError(t, <-execDone)
})
t.Run("WithParameterFileContainingTheValue", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
coderdtest.CreateFirstUser(t, client)
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
Parse: createTestParseResponse(),
Provision: echo.ProvisionComplete,
ProvisionDryRun: echo.ProvisionComplete,
})
tempDir := t.TempDir()
parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml")
_, _ = parameterFile.WriteString("region: \"bananas\"")
cmd, root := clitest.New(t, "templates", "create", "my-template", "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), "--parameter-file", parameterFile.Name())
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
execDone := make(chan error)
go func() {
execDone <- cmd.Execute()
}()
matches := []struct {
match string
write string
}{
{match: "Create and upload", write: "yes"},
{match: "Confirm create?", write: "yes"},
}
for _, m := range matches {
pty.ExpectMatch(m.match)
pty.WriteLine(m.write)
}
require.NoError(t, <-execDone)
removeTmpDirUntilSuccess(t, tempDir)
})
t.Run("WithParameterFileNotContainingTheValue", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
coderdtest.CreateFirstUser(t, client)
source := clitest.CreateTemplateVersionSource(t, &echo.Responses{
Parse: createTestParseResponse(),
Provision: echo.ProvisionComplete,
ProvisionDryRun: echo.ProvisionComplete,
})
tempDir := t.TempDir()
parameterFile, _ := os.CreateTemp(tempDir, "testParameterFile*.yaml")
_, _ = parameterFile.WriteString("zone: \"bananas\"")
cmd, root := clitest.New(t, "templates", "create", "my-template", "--directory", source, "--test.provisioner", string(database.ProvisionerTypeEcho), "--parameter-file", parameterFile.Name())
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
execDone := make(chan error)
go func() {
execDone <- cmd.Execute()
}()
matches := []struct {
match string
write string
}{
{match: "Create and upload", write: "yes"},
}
for _, m := range matches {
pty.ExpectMatch(m.match)
pty.WriteLine(m.write)
}
require.EqualError(t, <-execDone, "Parameter value absent in parameter file for \"region\"!")
removeTmpDirUntilSuccess(t, tempDir)
})
}
func createTestParseResponse() []*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",
DefaultDestination: &proto.ParameterDestination{
Scheme: proto.ParameterDestination_PROVISIONER_VARIABLE,
},
}},
},
},
}}
}
// Need this for Windows because of a known issue with Go:
// https://github.com/golang/go/issues/52986
func removeTmpDirUntilSuccess(t *testing.T, tempDir string) {
t.Helper()
t.Cleanup(func() {
err := os.RemoveAll(tempDir)
for err != nil {
err = os.RemoveAll(tempDir)
}
<-doneChan
})
}
+85
View File
@@ -0,0 +1,85 @@
package cli
import (
"fmt"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/codersdk"
)
func templateDelete() *cobra.Command {
return &cobra.Command{
Use: "delete [name...]",
Short: "Delete templates",
RunE: func(cmd *cobra.Command, args []string) error {
var (
ctx = cmd.Context()
templateNames = []string{}
templates = []codersdk.Template{}
)
client, err := createClient(cmd)
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return err
}
if len(args) > 0 {
templateNames = args
} else {
allTemplates, err := client.TemplatesByOrganization(ctx, organization.ID)
if err != nil {
return xerrors.Errorf("get templates by organization: %w", err)
}
if len(allTemplates) == 0 {
return xerrors.Errorf("no templates exist in the current organization %q", organization.Name)
}
opts := make([]string, 0, len(allTemplates))
for _, template := range allTemplates {
opts = append(opts, template.Name)
}
selection, err := cliui.Select(cmd, cliui.SelectOptions{
Options: opts,
})
if err != nil {
return xerrors.Errorf("select template: %w", err)
}
for _, template := range allTemplates {
if template.Name == selection {
templates = append(templates, template)
}
}
}
for _, templateName := range templateNames {
template, err := client.TemplateByName(ctx, organization.ID, templateName)
if err != nil {
return xerrors.Errorf("get template by name: %w", err)
}
templates = append(templates, template)
}
for _, template := range templates {
err := client.DeleteTemplate(ctx, template.ID)
if err != nil {
return xerrors.Errorf("delete template %q: %w", template.Name, err)
}
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Deleted template "+cliui.Styles.Code.Render(template.Name)+"!")
}
return nil
},
}
}
+89
View File
@@ -0,0 +1,89 @@
package cli_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/pty/ptytest"
)
func TestTemplateDelete(t *testing.T) {
t.Parallel()
t.Run("Ok", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
cmd, root := clitest.New(t, "templates", "delete", template.Name)
clitest.SetupConfig(t, client, root)
require.NoError(t, cmd.Execute())
_, err := client.Template(context.Background(), template.ID)
require.Error(t, err, "template should not exist")
})
t.Run("Multiple", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
templates := []codersdk.Template{
coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID),
coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID),
coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID),
}
templateNames := []string{}
for _, template := range templates {
templateNames = append(templateNames, template.Name)
}
cmd, root := clitest.New(t, append([]string{"templates", "delete"}, templateNames...)...)
clitest.SetupConfig(t, client, root)
require.NoError(t, cmd.Execute())
for _, template := range templates {
_, err := client.Template(context.Background(), template.ID)
require.Error(t, err, "template should not exist")
}
})
t.Run("Selector", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerD: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
cmd, root := clitest.New(t, "templates", "delete")
clitest.SetupConfig(t, client, root)
pty := ptytest.New(t)
cmd.SetIn(pty.Input())
cmd.SetOut(pty.Output())
execDone := make(chan error)
go func() {
execDone <- cmd.Execute()
}()
pty.WriteLine("docker-local")
require.NoError(t, <-execDone)
_, err := client.Template(context.Background(), template.ID)
require.Error(t, err, "template should not exist")
})
}
-12
View File
@@ -1,12 +0,0 @@
package cli
import "github.com/spf13/cobra"
func templateEdit() *cobra.Command {
return &cobra.Command{
Use: "edit",
RunE: func(cmd *cobra.Command, args []string) error {
return nil
},
}
}

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