From f256a23a773e235213f6fc53cf5dcf527c0601d8 Mon Sep 17 00:00:00 2001 From: Sas Swart Date: Wed, 30 Jul 2025 15:28:56 +0200 Subject: [PATCH 1/4] feat: validate presets on template import (#18844) Typos and other errors often result in invalid presets in a template. Coder would import these broken templates and present them to users when they create workspaces. An unsuspecting user who chooses a broken preset would then experience a failed workspace build with no obvious error message. This PR adds additional validation beyond what is possible in the Terraform provider schema. Coder will now present a more helpful error message to template authors when they upload a new template version: Screenshot 2025-07-14 at 12 22 49 The frontend warning is less helpful right now, but I'd like to address that in a follow-up since I need frontend help: image closes https://github.com/coder/coder/issues/17333 ## Summary by CodeRabbit * **New Features** * Improved validation and error reporting for template presets, providing clearer feedback when presets cannot be parsed or reference undefined parameters. * **Bug Fixes** * Enhanced error handling during template version creation to better detect and report issues with presets. * **Tests** * Added new tests to verify validation of both valid and invalid Terraform presets during template version creation. * Improved test reliability by enabling dynamic control over error injection in database-related tests. * **Chores** * Updated a dependency to the latest version for improved stability and features. --- coderd/dynamicparameters/error.go | 8 ++ coderd/dynamicparameters/presets.go | 28 +++++++ coderd/dynamicparameters/tags.go | 4 + coderd/templateversions.go | 8 ++ coderd/templateversions_test.go | 113 ++++++++++++++++++++++++++++ 5 files changed, 161 insertions(+) create mode 100644 coderd/dynamicparameters/presets.go diff --git a/coderd/dynamicparameters/error.go b/coderd/dynamicparameters/error.go index 4c27905bfa832..ae2217936b9dd 100644 --- a/coderd/dynamicparameters/error.go +++ b/coderd/dynamicparameters/error.go @@ -26,6 +26,14 @@ func tagValidationError(diags hcl.Diagnostics) *DiagnosticError { } } +func presetValidationError(diags hcl.Diagnostics) *DiagnosticError { + return &DiagnosticError{ + Message: "Unable to validate presets", + Diagnostics: diags, + KeyedDiagnostics: make(map[string]hcl.Diagnostics), + } +} + type DiagnosticError struct { // Message is the human-readable message that will be returned to the user. Message string diff --git a/coderd/dynamicparameters/presets.go b/coderd/dynamicparameters/presets.go new file mode 100644 index 0000000000000..24974962e029f --- /dev/null +++ b/coderd/dynamicparameters/presets.go @@ -0,0 +1,28 @@ +package dynamicparameters + +import ( + "github.com/hashicorp/hcl/v2" + + "github.com/coder/preview" +) + +// CheckPresets extracts the preset related diagnostics from a template version preset +func CheckPresets(output *preview.Output, diags hcl.Diagnostics) *DiagnosticError { + de := presetValidationError(diags) + if output == nil { + return de + } + + presets := output.Presets + for _, preset := range presets { + if hcl.Diagnostics(preset.Diagnostics).HasErrors() { + de.Extend(preset.Name, hcl.Diagnostics(preset.Diagnostics)) + } + } + + if de.HasError() { + return de + } + + return nil +} diff --git a/coderd/dynamicparameters/tags.go b/coderd/dynamicparameters/tags.go index 38a9bf4691571..d9037db5dd909 100644 --- a/coderd/dynamicparameters/tags.go +++ b/coderd/dynamicparameters/tags.go @@ -11,6 +11,10 @@ import ( func CheckTags(output *preview.Output, diags hcl.Diagnostics) *DiagnosticError { de := tagValidationError(diags) + if output == nil { + return de + } + failedTags := output.WorkspaceTags.UnusableTags() if len(failedTags) == 0 && !de.HasError() { return nil // No errors, all is good! diff --git a/coderd/templateversions.go b/coderd/templateversions.go index cc106b390f73c..2a6e09d94978e 100644 --- a/coderd/templateversions.go +++ b/coderd/templateversions.go @@ -1822,6 +1822,14 @@ func (api *API) dynamicTemplateVersionTags(ctx context.Context, rw http.Response return nil, false } + // Fails early if presets are invalid to prevent downstream workspace creation errors + presetErr := dynamicparameters.CheckPresets(output, nil) + if presetErr != nil { + code, resp := presetErr.Response() + httpapi.Write(ctx, rw, code, resp) + return nil, false + } + return output.WorkspaceTags.Tags(), true } diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index 912bca1c5fec1..0b5bf6fcf2302 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -620,6 +620,119 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) { }) } }) + + t.Run("Presets", func(t *testing.T) { + t.Parallel() + store, ps := dbtestutil.NewDB(t) + client := coderdtest.New(t, &coderdtest.Options{ + Database: store, + Pubsub: ps, + }) + owner := coderdtest.CreateFirstUser(t, client) + templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin()) + + for _, tt := range []struct { + name string + files map[string]string + expectError string + }{ + { + name: "valid preset", + files: map[string]string{ + `main.tf`: ` + terraform { + required_providers { + coder = { + source = "coder/coder" + version = "2.8.0" + } + } + } + data "coder_parameter" "valid_parameter" { + name = "valid_parameter_name" + default = "valid_option_value" + option { + name = "valid_option_name" + value = "valid_option_value" + } + } + data "coder_workspace_preset" "valid_preset" { + name = "valid_preset" + parameters = { + "valid_parameter_name" = "valid_option_value" + } + } + `, + }, + }, + { + name: "invalid preset", + files: map[string]string{ + `main.tf`: ` + terraform { + required_providers { + coder = { + source = "coder/coder" + version = "2.8.0" + } + } + } + data "coder_parameter" "valid_parameter" { + name = "valid_parameter_name" + default = "valid_option_value" + option { + name = "valid_option_name" + value = "valid_option_value" + } + } + data "coder_workspace_preset" "invalid_parameter_name" { + name = "invalid_parameter_name" + parameters = { + "invalid_parameter_name" = "irrelevant_value" + } + } + `, + }, + expectError: "Undefined Parameter", + }, + } { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + + // Create an archive from the files provided in the test case. + tarFile := testutil.CreateTar(t, tt.files) + + // Post the archive file + fi, err := templateAdmin.Upload(ctx, "application/x-tar", bytes.NewReader(tarFile)) + require.NoError(t, err) + + // Create a template version from the archive + tvName := testutil.GetRandomNameHyphenated(t) + tv, err := templateAdmin.CreateTemplateVersion(ctx, owner.OrganizationID, codersdk.CreateTemplateVersionRequest{ + Name: tvName, + StorageMethod: codersdk.ProvisionerStorageMethodFile, + Provisioner: codersdk.ProvisionerTypeTerraform, + FileID: fi.ID, + }) + + if tt.expectError == "" { + require.NoError(t, err) + // Assert the expected provisioner job is created from the template version import + pj, err := store.GetProvisionerJobByID(ctx, tv.Job.ID) + require.NoError(t, err) + require.NotNil(t, pj) + // Also assert that we get the expected information back from the API endpoint + require.Zero(t, tv.MatchedProvisioners.Count) + require.Zero(t, tv.MatchedProvisioners.Available) + require.Zero(t, tv.MatchedProvisioners.MostRecentlySeen.Time) + } else { + require.ErrorContains(t, err, tt.expectError) + require.Equal(t, tv.Job.ID, uuid.Nil) + } + }) + } + }) } func TestPatchCancelTemplateVersion(t *testing.T) { From 428ec351fe8f959e9f9abb7a1d3b5c6070e65ac3 Mon Sep 17 00:00:00 2001 From: Edward Angert Date: Wed, 30 Jul 2025 12:35:27 -0400 Subject: [PATCH 2/4] docs: add code-server/vs code web comparison table (#19104) closes #18815 adds a doc with comparison table and links to main documentation for code-server [preview](https://coder.com/docs/@18815-code-server-vs/user-guides/workspace-access/code-server) --------- Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> --- docs/manifest.json | 5 ++++ .../workspace-access/code-server.md | 29 +++++++++++++++++++ docs/user-guides/workspace-access/index.md | 9 +++--- docs/user-guides/workspace-access/vscode.md | 12 ++++---- 4 files changed, 46 insertions(+), 9 deletions(-) create mode 100644 docs/user-guides/workspace-access/code-server.md diff --git a/docs/manifest.json b/docs/manifest.json index 0305105c029fd..04e10f4387949 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -251,6 +251,11 @@ "description": "Access your workspace with IDEs in the browser", "path": "./user-guides/workspace-access/web-ides.md" }, + { + "title": "code-server", + "description": "Access your workspace with code-server", + "path": "./user-guides/workspace-access/code-server.md" + }, { "title": "Zed", "description": "Access your workspace with Zed", diff --git a/docs/user-guides/workspace-access/code-server.md b/docs/user-guides/workspace-access/code-server.md new file mode 100644 index 0000000000000..baa36b010c0c0 --- /dev/null +++ b/docs/user-guides/workspace-access/code-server.md @@ -0,0 +1,29 @@ +# code-server + +[code-server](https://github.com/coder/code-server) is our supported method of running VS Code in the web browser. + +![code-server in a workspace](../../images/code-server-ide.png) + +## Differences between code-server and VS Code Web + +Some of the key differences between code-server and VS Code Web are: + +| Feature | code-server | VS Code Web | +|--------------------------|-----------------------------------------------------------------------------|-------------------------------------------------------------------| +| Authentication | Optional login form | No built-in auth | +| Built-in proxy | Includes development proxy (not needed with Coder) | No built-in development proxy | +| Clipboard integration | Supports piping text from terminal (similar to `xclip`) | More limited | +| Display languages | Supports language pack extensions | Limited language support | +| File operations | Options to disable downloads and uploads | No built-in restrictions | +| Health endpoint | Provides `/healthz` endpoint | Limited health monitoring | +| Marketplace | Open VSX by default, configurable via flags/env vars | Uses Microsoft marketplace; modify `product.json` to use your own | +| Path-based routing | Has fixes for state collisions when used path-based | May have issues with path-based routing in certain configurations | +| Proposed API | Always enabled for all extensions | Only Microsoft extensions without configuration | +| Proxy integration | Integrates with Coder's proxy for ports panel | Integration is more limited | +| Sourcemaps | Loads locally | Uses CDN | +| Telemetry | Configurable endpoint | Does not allow a configurable endpoint | +| Terminal access to files | You can use a terminal outside of the integrated one to interact with files | Limited to integrated terminal access | +| User settings | Stored on remote disk | Stored in browser | +| Web views | Self-contained | Uses Microsoft CDN | + +For more information about code-server, visit the [code-server FAQ](https://coder.com/docs/code-server/FAQ). diff --git a/docs/user-guides/workspace-access/index.md b/docs/user-guides/workspace-access/index.md index 1bf4d9d8c9927..266e76e94757f 100644 --- a/docs/user-guides/workspace-access/index.md +++ b/docs/user-guides/workspace-access/index.md @@ -78,12 +78,12 @@ Your workspace is now accessible via `ssh coder.` ## Visual Studio Code You can develop in your Coder workspace remotely with -[VSCode](https://code.visualstudio.com/download). We support connecting with the -desktop client and VSCode in the browser with [code-server](#code-server). +[VS Code](https://code.visualstudio.com/download). +We support connecting with the desktop client and VS Code in the browser with [code-server](#code-server). ![Demo](https://github.com/coder/vscode-coder/raw/main/demo.gif?raw=true) -Read more details on [using VSCode in your workspace](./vscode.md). +Read more details on [using VS Code in your workspace](./vscode.md). ## Cursor @@ -118,7 +118,8 @@ on connecting your JetBrains IDEs. ## code-server [code-server](https://github.com/coder/code-server) is our supported method of -running VS Code in the web browser. You can read more in our +running VS Code in the web browser. +Learn more about [what makes code-server different from VS Code web](./code-server.md) or visit the [documentation for code-server](https://coder.com/docs/code-server/latest). ![code-server in a workspace](../../images/code-server-ide.png) diff --git a/docs/user-guides/workspace-access/vscode.md b/docs/user-guides/workspace-access/vscode.md index cd67c2a775bbd..3f89ac8e258bb 100644 --- a/docs/user-guides/workspace-access/vscode.md +++ b/docs/user-guides/workspace-access/vscode.md @@ -1,13 +1,15 @@ # Visual Studio Code You can develop in your Coder workspace remotely with -[VSCode](https://code.visualstudio.com/download). We support connecting with the -desktop client and VSCode in the browser with +[VS Code](https://code.visualstudio.com/download). +We support connecting with the desktop client and VS Code in the browser with [code-server](https://github.com/coder/code-server). +Learn more about how VS Code Web and code-server compare in the +[code-server doc](./code-server.md). -## VSCode Desktop +## VS Code Desktop -VSCode desktop is a default app for workspaces. +VS Code desktop is a default app for workspaces. Click `VS Code Desktop` in the dashboard to one-click enter a workspace. This automatically installs the [Coder Remote](https://github.com/coder/vscode-coder) @@ -21,7 +23,7 @@ extension, authenticates with Coder, and connects to the workspace. ### Manual Installation -You can install our extension manually in VSCode using the command palette. +You can install our extension manually in VS Code using the command palette. Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter. From 998fbdfbb37828196bfa91a5f36e274582b7dd76 Mon Sep 17 00:00:00 2001 From: Edward Angert Date: Wed, 30 Jul 2025 12:35:45 -0400 Subject: [PATCH 3/4] docs: use CODER_LOG_FILTER instead of CODER_VERBOSE (#19105) closes #18833 replace suggestions to use the now-deprecated `CODER_VERBOSE` with more specific `CODER_LOG_FILTER` thanks @UnicornyRainbow! --------- Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> --- docs/admin/monitoring/notifications/index.md | 2 +- docs/admin/users/idp-sync.md | 3 +-- docs/admin/users/oidc-auth/index.md | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/admin/monitoring/notifications/index.md b/docs/admin/monitoring/notifications/index.md index fc2bc41968d78..70279dcb16bf1 100644 --- a/docs/admin/monitoring/notifications/index.md +++ b/docs/admin/monitoring/notifications/index.md @@ -282,7 +282,7 @@ troubleshoot: 1. Review the logs. Search for the term `notifications` for diagnostic information. - If you do not see any relevant logs, set - `CODER_VERBOSE=true` or `--verbose` to output debug logs. + `CODER_LOG_FILTER=".*notifications.*"` to filter for notification-related logs. 1. If you are on version 2.15.x, notifications must be enabled using the `notifications` [experiment](../../../install/releases/feature-stages.md#early-access-features). diff --git a/docs/admin/users/idp-sync.md b/docs/admin/users/idp-sync.md index e893bf91bb8ef..3c7ec708be3f9 100644 --- a/docs/admin/users/idp-sync.md +++ b/docs/admin/users/idp-sync.md @@ -203,7 +203,7 @@ Visit the Coder UI to confirm these changes: ### Group allowlist You can limit which groups from your identity provider can log in to Coder with -[CODER_OIDC_ALLOWED_GROUPS](https://coder.com/docs/cli/server#--oidc-allowed-groups). +[CODER_OIDC_ALLOWED_GROUPS](../../reference/cli/server.md#--oidc-allowed-groups). Users who are not in a matching group will see the following error: Unauthorized group error @@ -419,7 +419,6 @@ If you are running into issues with a sync: 1. To reduce noise, you can filter for only logs related to group/role sync: ```sh - CODER_VERBOSE=true CODER_LOG_FILTER=".*userauth.*|.*groups returned.*" ``` diff --git a/docs/admin/users/oidc-auth/index.md b/docs/admin/users/oidc-auth/index.md index dd674d21606f5..ae225d66ca0be 100644 --- a/docs/admin/users/oidc-auth/index.md +++ b/docs/admin/users/oidc-auth/index.md @@ -27,7 +27,7 @@ claims from the ID token and the claims obtained from hitting the upstream provider's `userinfo` endpoint, and use the resulting data as a basis for creating a new user or looking up an existing user. -To troubleshoot claims, set `CODER_VERBOSE=true` and follow the logs while +To troubleshoot claims, set `CODER_LOG_FILTER=".*got oidc claims.*"` and follow the logs while signing in via OIDC as a new user. Coder will log the claim fields returned by the upstream identity provider in a message containing the string `got oidc claims`, as well as the user info returned. From 96e32d60a2ec90d6d96bf8e8f76b0dd33d719426 Mon Sep 17 00:00:00 2001 From: Susana Ferreira Date: Wed, 30 Jul 2025 18:02:59 +0100 Subject: [PATCH 4/4] chore(site): add preset combobox to dynamic parameters page (#19100) ## Description This PR updates the `CreateWorkspacePageViewExperimental` page to use the `Combobox` React component for preset selection. This aligns it with the implementation used in the standard `CreateWorkspacePageView`, ensuring consistency in UI behavior and component usage across both pages. Screenshot 2025-07-30 at 13 58 23 Related to `CreateWorkspacePageView` changes: https://github.com/coder/coder/pull/19063 --- .../CreateWorkspacePageViewExperimental.tsx | 74 +++++++++---------- 1 file changed, 33 insertions(+), 41 deletions(-) diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx index c478acd50ee14..b0298630776d2 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.tsx @@ -5,16 +5,10 @@ import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Avatar } from "components/Avatar/Avatar"; import { Badge } from "components/Badge/Badge"; import { Button } from "components/Button/Button"; +import { Combobox } from "components/Combobox/Combobox"; import { Input } from "components/Input/Input"; import { Label } from "components/Label/Label"; import { Link } from "components/Link/Link"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "components/Select/Select"; import { Spinner } from "components/Spinner/Spinner"; import { Switch } from "components/Switch/Switch"; import { @@ -186,30 +180,31 @@ export const CreateWorkspacePageViewExperimental: FC< }, [form.submitCount, form.errors]); const [presetOptions, setPresetOptions] = useState([ - { label: "None", value: "None" }, + { displayName: "None", value: "undefined", icon: "", description: "" }, ]); + const [selectedPresetIndex, setSelectedPresetIndex] = useState(0); + // Build options and keep default label/value in sync useEffect(() => { - setPresetOptions([ - { label: "None", value: "None" }, + const options = [ + { displayName: "None", value: "undefined", icon: "", description: "" }, ...presets.map((preset) => ({ - label: preset.Default ? `${preset.Name} (Default)` : preset.Name, + displayName: preset.Default ? `${preset.Name} (Default)` : preset.Name, value: preset.ID, + icon: preset.Icon, + description: preset.Description, })), - ]); - }, [presets]); - - const [selectedPresetIndex, setSelectedPresetIndex] = useState(0); - - // Set default preset when presets are loaded - useEffect(() => { - const defaultPreset = presets.find((preset) => preset.Default); + ]; + setPresetOptions(options); + const defaultPreset = presets.find((p) => p.Default); if (defaultPreset) { - // +1 because "None" is at index 0 - const defaultIndex = - presets.findIndex((preset) => preset.ID === defaultPreset.ID) + 1; - setSelectedPresetIndex(defaultIndex); + const idx = presets.indexOf(defaultPreset) + 1; // +1 for "None" + setSelectedPresetIndex(idx); + form.setFieldValue("template_version_preset_id", defaultPreset.ID); + } else { + setSelectedPresetIndex(0); // Explicitly set to "None" + form.setFieldValue("template_version_preset_id", undefined); } - }, [presets]); + }, [presets, form.setFieldValue]); const [presetParameterNames, setPresetParameterNames] = useState( [], @@ -572,11 +567,15 @@ export const CreateWorkspacePageViewExperimental: FC<
- + />
{/* Only show the preset parameter visibility toggle if preset parameters are actually being modified, otherwise it is ineffectual */} {presetParameterNames.length > 0 && (