From c106aee0d64083a262e2fd270b0c6b9c01eb1959 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Wed, 23 Apr 2025 15:20:00 +0300 Subject: [PATCH 001/220] fix(scripts/release): handle cherry-pick bot titles in check commit metadata (#17535) --- scripts/release/check_commit_metadata.sh | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/scripts/release/check_commit_metadata.sh b/scripts/release/check_commit_metadata.sh index f53de8e107430..1368425d00639 100755 --- a/scripts/release/check_commit_metadata.sh +++ b/scripts/release/check_commit_metadata.sh @@ -118,6 +118,23 @@ main() { title2=${parts2[*]:2} fi + # Handle cherry-pick bot, it turns "chore: foo bar (#42)" to + # "chore: foo bar (cherry-pick #42) (#43)". + if [[ ${title1} == *"(cherry-pick #"* ]]; then + title1=${title1%" ("*} + pr=${title1##*#} + pr=${pr%)} + title1=${title1%" ("*} + title1="${title1} (#${pr})"$'\n' + fi + if [[ ${title2} == *"(cherry-pick #"* ]]; then + title2=${title2%" ("*} + pr=${title2##*#} + pr=${pr%)} + title2=${title2%" ("*} + title2="${title2} (#${pr})"$'\n' + fi + if [[ ${title1} != "${title2}" ]]; then log "Invariant failed, cherry-picked commits have different titles: \"${title1%$'\n'}\" != \"${title2%$'\n'}\", attempting to check commit body for cherry-pick information..." From 71dbd0c888906ed9b9f61a79f101a4627fda25f2 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 23 Apr 2025 08:45:26 -0500 Subject: [PATCH 002/220] fix: nil ptr deref when removing OIDC from deployment and accessing old users (#17501) If OIDC is removed from a deployment, trying to create a workspace for a previous user on OIDC would panic. --- .../provisionerdserver/provisionerdserver.go | 4 +- coderd/workspaces_test.go | 48 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 78f597fa55369..22bc720736148 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -515,7 +515,9 @@ func (s *server) acquireProtoJob(ctx context.Context, job database.ProvisionerJo } var workspaceOwnerOIDCAccessToken string - if s.OIDCConfig != nil { + // The check `s.OIDCConfig != nil` is not as strict, since it can be an interface + // pointing to a typed nil. + if !reflect.ValueOf(s.OIDCConfig).IsNil() { workspaceOwnerOIDCAccessToken, err = obtainOIDCAccessToken(ctx, s.Database, s.OIDCConfig, owner.ID) if err != nil { return nil, failJob(fmt.Sprintf("obtain OIDC access token: %s", err)) diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index 3101346f5b43a..e5a5a1e513633 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -4349,3 +4349,51 @@ func TestWorkspaceTimings(t *testing.T) { require.Contains(t, err.Error(), "not found") }) } + +// TestOIDCRemoved emulates a user logging in with OIDC, then that OIDC +// auth method being removed. +func TestOIDCRemoved(t *testing.T) { + t.Parallel() + + owner, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }) + first := coderdtest.CreateFirstUser(t, owner) + + user, userData := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.ScopedRoleOrgAdmin(first.OrganizationID)) + + ctx := testutil.Context(t, testutil.WaitMedium) + //nolint:gocritic // unit test + _, err := db.UpdateUserLoginType(dbauthz.AsSystemRestricted(ctx), database.UpdateUserLoginTypeParams{ + NewLoginType: database.LoginTypeOIDC, + UserID: userData.ID, + }) + require.NoError(t, err) + + //nolint:gocritic // unit test + _, err = db.InsertUserLink(dbauthz.AsSystemRestricted(ctx), database.InsertUserLinkParams{ + UserID: userData.ID, + LoginType: database.LoginTypeOIDC, + LinkedID: "random", + OAuthAccessToken: "foobar", + OAuthAccessTokenKeyID: sql.NullString{}, + OAuthRefreshToken: "refresh", + OAuthRefreshTokenKeyID: sql.NullString{}, + OAuthExpiry: time.Now().Add(time.Hour * -1), + Claims: database.UserLinkClaims{}, + }) + require.NoError(t, err) + + version := coderdtest.CreateTemplateVersion(t, owner, first.OrganizationID, nil) + _ = coderdtest.AwaitTemplateVersionJobCompleted(t, owner, version.ID) + template := coderdtest.CreateTemplate(t, owner, first.OrganizationID, version.ID) + + wrk := coderdtest.CreateWorkspace(t, user, template.ID) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, owner, wrk.LatestBuild.ID) + + deleteBuild, err := owner.CreateWorkspaceBuild(ctx, wrk.ID, codersdk.CreateWorkspaceBuildRequest{ + Transition: codersdk.WorkspaceTransitionDelete, + }) + require.NoError(t, err, "delete the workspace") + coderdtest.AwaitWorkspaceBuildJobCompleted(t, owner, deleteBuild.ID) +} From 88589ef32fe10e008ee0a21b2b1eba056f148cee Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 23 Apr 2025 11:34:08 -0300 Subject: [PATCH 003/220] fix: fix build timeline scale for longer builds (#17514) Fix https://github.com/coder/coder/issues/15374 --- .../workspaces/WorkspaceTiming/Chart/utils.ts | 39 ++++++++++++++++++- .../WorkspaceTimings.stories.tsx | 26 +++++++++++++ .../WorkspaceTiming/WorkspaceTimings.tsx | 9 ++++- 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/site/src/modules/workspaces/WorkspaceTiming/Chart/utils.ts b/site/src/modules/workspaces/WorkspaceTiming/Chart/utils.ts index 45c6f5bf681d1..55df5b9ffad48 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/Chart/utils.ts +++ b/site/src/modules/workspaces/WorkspaceTiming/Chart/utils.ts @@ -29,7 +29,20 @@ export const calcDuration = (range: TimeRange): number => { // data in 200ms intervals. However, if the total time is 1 minute, we should // display the data in 5 seconds intervals. To achieve this, we define the // dimensions object that contains the time intervals for the chart. -const scales = [5_000, 500, 100]; +const second = 1_000; +const minute = 60 * second; +const hour = 60 * minute; +const day = 24 * hour; +const scales = [ + day, + hour, + 5 * minute, + minute, + 10 * second, + 5 * second, + 500, + 100, +]; const pickScale = (totalTime: number): number => { for (const s of scales) { @@ -48,7 +61,29 @@ export const makeTicks = (time: number) => { }; export const formatTime = (time: number): string => { - return `${time.toLocaleString()}ms`; + const seconds = Math.floor(time / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + const days = Math.floor(hours / 24); + + const parts: string[] = []; + if (days > 0) { + parts.push(`${days}d`); + } + if (hours > 0) { + parts.push(`${hours % 24}h`); + } + if (minutes > 0) { + parts.push(`${minutes % 60}m`); + } + if (seconds > 0) { + parts.push(`${seconds % 60}s`); + } + if (time % 1000 > 0) { + parts.push(`${time % 1000}ms`); + } + + return parts.join(" "); }; export const calcOffset = (range: TimeRange, baseRange: TimeRange): number => { diff --git a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx index 0210353488257..9c93b4bf6806e 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.stories.tsx @@ -126,3 +126,29 @@ export const LoadingWhenAgentScriptTimingsAreEmpty: Story = { agentScriptTimings: undefined, }, }; + +export const LongTimeRange = { + args: { + provisionerTimings: [ + { + ...WorkspaceTimingsResponse.provisioner_timings[0], + started_at: "2021-09-01T00:00:00Z", + ended_at: "2021-09-01T00:10:00Z", + }, + ], + agentConnectionTimings: [ + { + ...WorkspaceTimingsResponse.agent_connection_timings[0], + started_at: "2021-09-01T00:10:00Z", + ended_at: "2021-09-01T00:35:00Z", + }, + ], + agentScriptTimings: [ + { + ...WorkspaceTimingsResponse.agent_script_timings[0], + started_at: "2021-09-01T00:35:00Z", + ended_at: "2021-09-01T01:00:00Z", + }, + ], + }, +}; diff --git a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx index 63fc03ad2a3de..2cb0a7c20d5d8 100644 --- a/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx +++ b/site/src/modules/workspaces/WorkspaceTiming/WorkspaceTimings.tsx @@ -12,7 +12,12 @@ import type { import sortBy from "lodash/sortBy"; import uniqBy from "lodash/uniqBy"; import { type FC, useState } from "react"; -import { type TimeRange, calcDuration, mergeTimeRanges } from "./Chart/utils"; +import { + type TimeRange, + calcDuration, + formatTime, + mergeTimeRanges, +} from "./Chart/utils"; import { ResourcesChart, isCoderResource } from "./ResourcesChart"; import { ScriptsChart } from "./ScriptsChart"; import { @@ -85,7 +90,7 @@ export const WorkspaceTimings: FC = ({ const displayProvisioningTime = () => { const totalRange = mergeTimeRanges(timings.map(toTimeRange)); const totalDuration = calcDuration(totalRange); - return humanizeDuration(totalDuration); + return formatTime(totalDuration); }; return ( From 9dea568027f9b305b1551df534946e1eb05ca97a Mon Sep 17 00:00:00 2001 From: Edward Angert Date: Wed, 23 Apr 2025 11:14:15 -0400 Subject: [PATCH 004/220] docs: document GIT_ASKPASS for OAuth connections (#17457) closes #17375 from @ericpaulsen > a prospect recently inquired about how our OAuth integration with GitLab works, and I realized we do not have any information on `GIT_ASKPASS` is used to retreive the OAuth token for users when they run `git` operations. Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> --- docs/admin/external-auth.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/admin/external-auth.md b/docs/admin/external-auth.md index 6c91a5891f2db..0540a5fa92eaa 100644 --- a/docs/admin/external-auth.md +++ b/docs/admin/external-auth.md @@ -80,11 +80,24 @@ If no tokens are available, it defaults to SSH authentication. ### OAuth (external auth) -For Git providers configured with [external authentication](#configuration), Coder can use OAuth tokens for Git operations. +For Git providers configured with [external authentication](#configuration), Coder can use OAuth tokens for Git operations over HTTPS. +When using SSH URLs (like `git@github.com:organization/repo.git`), Coder uses SSH keys as described in the [SSH Authentication](#ssh-authentication) section instead. -When Git operations require authentication, and no SSH key is configured, Coder will automatically use the appropriate external auth provider based on the repository URL. +For Git operations over HTTPS, Coder automatically uses the appropriate external auth provider +token based on the repository URL. +This works through Git's `GIT_ASKPASS` mechanism, which Coder configures in each workspace. -For example, if you've configured a GitHub external auth provider and attempt to clone a GitHub repository, Coder will use the OAuth token from that provider for authentication. +To use OAuth tokens for Git authentication over HTTPS: + +1. Complete the OAuth authentication flow (**Login with GitHub**, **Login with GitLab**). +1. Use HTTPS URLs when interacting with repositories (`https://github.com/organization/repo.git`). +1. Coder automatically handles authentication. You can perform your Git operations as you normally would. + +Behind the scenes, Coder: + +- Stores your OAuth token securely in its database +- Sets up `GIT_ASKPASS` at `/tmp/coder./coder` in your workspaces +- Retrieves and injects the appropriate token when Git operations require authentication To manually access these tokens within a workspace: From 5a7d531aefdc1a383790d3d24b09a825a46dd472 Mon Sep 17 00:00:00 2001 From: Edward Angert Date: Wed, 23 Apr 2025 11:14:57 -0400 Subject: [PATCH 005/220] docs: edit the ai agents doc (#17521) general edit and adding some highlights as I work through the section [preview](https://coder.com/docs/@ai-coder-edit/ai-coder/agents) --------- Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> --- docs/ai-coder/agents.md | 103 +++++++++++++++++++++++++++------------- 1 file changed, 71 insertions(+), 32 deletions(-) diff --git a/docs/ai-coder/agents.md b/docs/ai-coder/agents.md index 009629cc67082..98d453e5d7dda 100644 --- a/docs/ai-coder/agents.md +++ b/docs/ai-coder/agents.md @@ -1,4 +1,4 @@ -# Coding Agents +# AI Coding Agents > [!NOTE] > @@ -7,50 +7,89 @@ > Please [open an issue](https://github.com/coder/coder/issues/new) or submit a > pull request if you'd like to see your favorite agent added or updated. -There are several types of coding agents emerging: +Coding agents are rapidly emerging to help developers tackle repetitive tasks, +explore codebases, and generate solutions with increasing effectiveness. -- **Headless agents** can run without an IDE open and are great for rapid - prototyping, background tasks, and chat-based supervision. -- **In-IDE agents** require developers keep their IDE opens and are great for - interactive, focused coding on more complex tasks. +You can run these agents in Coder workspaces to leverage the power of cloud resources +and deep integration with your existing development workflows. -## Headless agents +## Why Run AI Coding Agents in Coder? -Headless agents can run without an IDE open, or alongside any IDE. They -typically run as CLI commands or web apps. With Coder, developers can interact -with agents via any preferred tool such as via PR comments, within the IDE, -inside the Coder UI, or even via the REST API or an MCP client such as Claude -Desktop or Cursor. +Coder provides unique advantages for running AI coding agents: -| Agent | Supported Models | Coder Support | Limitations | -|---------------|---------------------------------------------------------|---------------------------|---------------------------------------------------------| -| Claude Code ⭐ | Anthropic Models Only (+ AWS Bedrock and GCP Vertex AI) | First class integration ✅ | Beta (research preview) | -| Goose | Most popular AI models + gateways | First class integration ✅ | Less effective compared to Claude Code | -| Aider | Most popular AI models + gateways | In progress ⏳ | Can only run 1-2 defined commands (e.g. build and test) | -| OpenHands | Most popular AI models + gateways | In progress ⏳ ⏳ | Challenging setup, no MCP support | +- **Consistent environments**: Agents work in the same standardized environments as your developers. +- **Resource optimization**: Leverage powerful cloud resources without taxing local machines. +- **Security and isolation**: Keep sensitive code, API keys, and secrets in controlled environments. +- **Seamless collaboration**: Multiple developers can observe and interact with agent activity. +- **Deep integration**: Status reporting and task management directly in the Coder UI. +- **Scalability**: Run multiple agents across multiple projects simultaneously. +- **Persistent sessions**: Agents can continue working even when developers disconnect. + +## Types of Coding Agents + +AI coding agents generally fall into two categories, both fully supported in Coder: + +### Headless Agents + +Headless agents can run without an IDE open, making them ideal for: + +- **Background automation**: Execute repetitive tasks without supervision. +- **Resource-efficient development**: Work on projects without keeping an IDE running. +- **CI/CD integration**: Generate code, tests, or documentation as part of automated workflows. +- **Multi-project management**: Monitor and contribute to multiple repositories simultaneously. + +Additionally, with Coder, headless agents benefit from: + +- Status reporting directly to the Coder dashboard. +- Workspace lifecycle management (auto-stop). +- Resource monitoring and limits to prevent runaway processes. +- API-driven management for enterprise automation. + +| Agent | Supported models | Coder integration | Notes | +|---------------|---------------------------------------------------------|---------------------------|-----------------------------------------------------------------------------------------------| +| Claude Code ⭐ | Anthropic Models Only (+ AWS Bedrock and GCP Vertex AI) | First class integration ✅ | Enhanced security through workspace isolation, resource optimization, task status in Coder UI | +| Goose | Most popular AI models + gateways | First class integration ✅ | Simplified setup with Terraform module, environment consistency | +| Aider | Most popular AI models + gateways | In progress ⏳ | Coming soon with workspace resource optimization | +| OpenHands | Most popular AI models + gateways | In progress ⏳ ⏳ | Coming soon | [Claude Code](https://github.com/anthropics/claude-code) is our recommended coding agent due to its strong performance on complex programming tasks. -> Note: Any agent can run in a Coder workspace via our -> [MCP integration](./headless.md). +> [!INFO] +> Any agent can run in a Coder workspace via our [MCP integration](./headless.md), +> even if we don't have a specific module for it yet. + +### In-IDE agents + +In-IDE agents run within development environments like VS Code, Cursor, or Windsurf. + +These are ideal for exploring new codebases, complex problem solving, pair programming, +or rubber-ducking. + +| Agent | Supported Models | Coder integration | Coder key advantages | +|-----------------------------|-----------------------------------|--------------------------------------------------------------|----------------------------------------------------------------| +| Cursor (Agent Mode) | Most popular AI models + gateways | ✅ [Cursor Module](https://registry.coder.com/modules/cursor) | Pre-configured environment, containerized dependencies | +| Windsurf (Agents and Flows) | Most popular AI models + gateways | ✅ via Remote SSH | Consistent setup across team, powerful cloud compute | +| Cline | Most popular AI models + gateways | ✅ via VS Code Extension | Enterprise-friendly API key management, consistent environment | + +## Agent status reports in the Coder dashboard + +Claude Code and Goose can report their status directly to the Coder dashboard: -## In-IDE agents +- Task progress appears in the workspace overview. +- Completion status is visible without opening the terminal. +- Error states are highlighted. -Coding agents can also run within an IDE, such as VS Code, Cursor or Windsurf. -These editors and extensions are fully supported in Coder and work well for more -complex and focused tasks where an IDE is strictly required. +## Get started -| Agent | Supported Models | Coder Support | -|-----------------------------|-----------------------------------|--------------------------------------------------------------| -| Cursor (Agent Mode) | Most popular AI models + gateways | ✅ [Cursor Module](https://registry.coder.com/modules/cursor) | -| Windsurf (Agents and Flows) | Most popular AI models + gateways | ✅ via Remote SSH | -| Cline | Most popular AI models + gateways | ✅ via VS Code Extension | +Ready to deploy AI coding agents in your Coder deployment? -In-IDE agents do not require a special template as they are not used in a -headless fashion. However, they can still be run in isolated Coder workspaces -and report activity to the Coder UI. +1. [Create a Coder template for agents](./create-template.md). +1. Configure your chosen agent with appropriate API keys and permissions. +1. Start monitoring agent activity in the Coder dashboard. ## Next Steps - [Create a Coder template for agents](./create-template.md) +- [Integrate with your issue tracker](./issue-tracker.md) +- [Learn about MCP and adding AI tools](./best-practices.md) From 3b4343ddf3c46404b18583bc8407f941179a8b86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B1=E3=82=A4=E3=83=A9?= Date: Wed, 23 Apr 2025 08:44:36 -0700 Subject: [PATCH 006/220] fix: fix workspace creation on template page (#17518) --- site/src/pages/TemplatePage/TemplateLayout.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/site/src/pages/TemplatePage/TemplateLayout.tsx b/site/src/pages/TemplatePage/TemplateLayout.tsx index 19c628ab03f10..1aa0253da9a33 100644 --- a/site/src/pages/TemplatePage/TemplateLayout.tsx +++ b/site/src/pages/TemplatePage/TemplateLayout.tsx @@ -85,9 +85,14 @@ export const TemplateLayout: FC = ({ queryFn: () => fetchTemplate(organizationName, templateName), }); const workspacePermissionsQuery = useQuery( - checkAuthorization({ - checks: workspacePermissionChecks(organizationName, me.id), - }), + data + ? checkAuthorization({ + checks: workspacePermissionChecks( + data.template.organization_id, + me.id, + ), + }) + : { enabled: false }, ); const location = useLocation(); From 36a72a2b25553bb3b89b33dd27aee9606d78c8da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B1=E3=82=A4=E3=83=A9?= Date: Wed, 23 Apr 2025 09:15:49 -0700 Subject: [PATCH 007/220] chore: loosen static validation when using dynamic parameters (#17516) Co-authored-by: Steven Masley --- coderd/apidoc/docs.go | 3 ++ coderd/apidoc/swagger.json | 3 ++ coderd/workspaces.go | 3 ++ coderd/wsbuilder/wsbuilder.go | 41 +++++++++++++------ codersdk/organizations.go | 1 + codersdk/richparameters.go | 20 +++++++++ docs/reference/api/schemas.md | 2 + docs/reference/api/workspaces.md | 2 + site/src/api/typesGenerated.ts | 1 + .../CreateWorkspacePageExperimental.tsx | 1 + 10 files changed, 64 insertions(+), 13 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 268cfd7a894ba..62f91a858247d 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -11453,6 +11453,9 @@ const docTemplate = `{ "autostart_schedule": { "type": "string" }, + "enable_dynamic_parameters": { + "type": "boolean" + }, "name": { "type": "string" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index e973f11849547..e9e0470462b39 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -10211,6 +10211,9 @@ "autostart_schedule": { "type": "string" }, + "enable_dynamic_parameters": { + "type": "boolean" + }, "name": { "type": "string" }, diff --git a/coderd/workspaces.go b/coderd/workspaces.go index a654597faeadd..c1c8b1745c106 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -676,6 +676,9 @@ func createWorkspace( if req.TemplateVersionID != uuid.Nil { builder = builder.VersionID(req.TemplateVersionID) } + if req.EnableDynamicParameters && api.Experiments.Enabled(codersdk.ExperimentDynamicParameters) { + builder = builder.UsingDynamicParameters() + } workspaceBuild, provisionerJob, provisionerDaemons, err = builder.Build( ctx, diff --git a/coderd/wsbuilder/wsbuilder.go b/coderd/wsbuilder/wsbuilder.go index fa7c00861202d..5ac0f54639a06 100644 --- a/coderd/wsbuilder/wsbuilder.go +++ b/coderd/wsbuilder/wsbuilder.go @@ -51,10 +51,11 @@ type Builder struct { logLevel string deploymentValues *codersdk.DeploymentValues - richParameterValues []codersdk.WorkspaceBuildParameter - initiator uuid.UUID - reason database.BuildReason - templateVersionPresetID uuid.UUID + richParameterValues []codersdk.WorkspaceBuildParameter + dynamicParametersEnabled bool + initiator uuid.UUID + reason database.BuildReason + templateVersionPresetID uuid.UUID // used during build, makes function arguments less verbose ctx context.Context @@ -178,6 +179,11 @@ func (b Builder) MarkPrebuild() Builder { return b } +func (b Builder) UsingDynamicParameters() Builder { + b.dynamicParametersEnabled = true + return b +} + // SetLastWorkspaceBuildInTx prepopulates the Builder's cache with the last workspace build. This allows us // to avoid a repeated database query when the Builder's caller also needs the workspace build, e.g. auto-start & // auto-stop. @@ -578,6 +584,7 @@ func (b *Builder) getParameters() (names, values []string, err error) { if err != nil { return nil, nil, BuildError{http.StatusBadRequest, "Unable to build workspace with unsupported parameters", err} } + resolver := codersdk.ParameterResolver{ Rich: db2sdk.WorkspaceBuildParameters(lastBuildParameters), } @@ -586,16 +593,24 @@ func (b *Builder) getParameters() (names, values []string, err error) { if err != nil { return nil, nil, BuildError{http.StatusInternalServerError, "failed to convert template version parameter", err} } - value, err := resolver.ValidateResolve( - tvp, - b.findNewBuildParameterValue(templateVersionParameter.Name), - ) - if err != nil { - // At this point, we've queried all the data we need from the database, - // so the only errors are problems with the request (missing data, failed - // validation, immutable parameters, etc.) - return nil, nil, BuildError{http.StatusBadRequest, fmt.Sprintf("Unable to validate parameter %q", templateVersionParameter.Name), err} + + var value string + if !b.dynamicParametersEnabled { + var err error + value, err = resolver.ValidateResolve( + tvp, + b.findNewBuildParameterValue(templateVersionParameter.Name), + ) + if err != nil { + // At this point, we've queried all the data we need from the database, + // so the only errors are problems with the request (missing data, failed + // validation, immutable parameters, etc.) + return nil, nil, BuildError{http.StatusBadRequest, fmt.Sprintf("Unable to validate parameter %q", templateVersionParameter.Name), err} + } + } else { + value = resolver.Resolve(tvp, b.findNewBuildParameterValue(templateVersionParameter.Name)) } + names = append(names, templateVersionParameter.Name) values = append(values, value) } diff --git a/codersdk/organizations.go b/codersdk/organizations.go index b880f25e15a2c..dd2eab50cf57e 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -227,6 +227,7 @@ type CreateWorkspaceRequest struct { RichParameterValues []WorkspaceBuildParameter `json:"rich_parameter_values,omitempty"` AutomaticUpdates AutomaticUpdates `json:"automatic_updates,omitempty"` TemplateVersionPresetID uuid.UUID `json:"template_version_preset_id,omitempty" format:"uuid"` + EnableDynamicParameters bool `json:"enable_dynamic_parameters,omitempty"` } func (c *Client) OrganizationByName(ctx context.Context, name string) (Organization, error) { diff --git a/codersdk/richparameters.go b/codersdk/richparameters.go index 24609bea0e68c..2ddd5d00f6c41 100644 --- a/codersdk/richparameters.go +++ b/codersdk/richparameters.go @@ -190,6 +190,26 @@ func (r *ParameterResolver) ValidateResolve(p TemplateVersionParameter, v *Works return resolvedValue.Value, nil } +// Resolve returns the value of the parameter. It does not do any validation, +// and is meant for use with the new dynamic parameters code path. +func (r *ParameterResolver) Resolve(p TemplateVersionParameter, v *WorkspaceBuildParameter) string { + prevV := r.findLastValue(p) + // First, the provided value + resolvedValue := v + // Second, previous value if not ephemeral + if resolvedValue == nil && !p.Ephemeral { + resolvedValue = prevV + } + // Last, default value + if resolvedValue == nil { + resolvedValue = &WorkspaceBuildParameter{ + Name: p.Name, + Value: p.DefaultValue, + } + } + return resolvedValue.Value +} + // findLastValue finds the value from the previous build and returns it, or nil if the parameter had no value in the // last build. func (r *ParameterResolver) findLastValue(p TemplateVersionParameter) *WorkspaceBuildParameter { diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 79d7a411bf98c..dd8ffd1971cb8 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -1462,6 +1462,7 @@ None { "automatic_updates": "always", "autostart_schedule": "string", + "enable_dynamic_parameters": true, "name": "string", "rich_parameter_values": [ { @@ -1484,6 +1485,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o |------------------------------|-------------------------------------------------------------------------------|----------|--------------|---------------------------------------------------------------------------------------------------------| | `automatic_updates` | [codersdk.AutomaticUpdates](#codersdkautomaticupdates) | false | | | | `autostart_schedule` | string | false | | | +| `enable_dynamic_parameters` | boolean | false | | | | `name` | string | true | | | | `rich_parameter_values` | array of [codersdk.WorkspaceBuildParameter](#codersdkworkspacebuildparameter) | false | | Rich parameter values allows for additional parameters to be provided during the initial provision. | | `template_id` | string | false | | Template ID specifies which template should be used for creating the workspace. | diff --git a/docs/reference/api/workspaces.md b/docs/reference/api/workspaces.md index 5e727cee297fe..5d09c46a01d30 100644 --- a/docs/reference/api/workspaces.md +++ b/docs/reference/api/workspaces.md @@ -25,6 +25,7 @@ of the template will be used. { "automatic_updates": "always", "autostart_schedule": "string", + "enable_dynamic_parameters": true, "name": "string", "rich_parameter_values": [ { @@ -605,6 +606,7 @@ of the template will be used. { "automatic_updates": "always", "autostart_schedule": "string", + "enable_dynamic_parameters": true, "name": "string", "rich_parameter_values": [ { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index d160b7683e92e..025ed9f1933cf 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -467,6 +467,7 @@ export interface CreateWorkspaceRequest { readonly rich_parameter_values?: readonly WorkspaceBuildParameter[]; readonly automatic_updates?: AutomaticUpdates; readonly template_version_preset_id?: string; + readonly enable_dynamic_parameters?: boolean; } // From codersdk/deployment.go diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx index 0c513a1733ec9..03da3bd477745 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx @@ -282,6 +282,7 @@ const CreateWorkspacePageExperimental: FC = () => { const workspace = await createWorkspaceMutation.mutateAsync({ ...workspaceRequest, + enable_dynamic_parameters: true, userId: owner.id, }); onCreateWorkspace(workspace); From c1162eb9a86547f7d456815a997624c7c898fee5 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 23 Apr 2025 18:01:02 +0100 Subject: [PATCH 008/220] fix: only highlight checkbox on hover when checkbox is enabled (#17526) resolves #17503 --- site/src/components/Checkbox/Checkbox.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/components/Checkbox/Checkbox.tsx b/site/src/components/Checkbox/Checkbox.tsx index 1278fb12ea899..e4e5bc813cc02 100644 --- a/site/src/components/Checkbox/Checkbox.tsx +++ b/site/src/components/Checkbox/Checkbox.tsx @@ -24,7 +24,7 @@ export const Checkbox = React.forwardRef< disabled:cursor-not-allowed disabled:bg-surface-primary disabled:data-[state=checked]:bg-surface-tertiary data-[state=unchecked]:bg-surface-primary data-[state=checked]:bg-surface-invert-primary data-[state=checked]:text-content-invert - hover:border-border-hover hover:data-[state=checked]:bg-surface-invert-secondary`, + hover:enabled:border-border-hover hover:data-[state=checked]:bg-surface-invert-secondary`, className, )} {...props} From 3306f0f2a2d938233b0824be9be138efea5bebc5 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 23 Apr 2025 18:01:36 +0100 Subject: [PATCH 009/220] fix: fix broken img layout (#17525) resolves #17507 Before Screenshot 2025-04-23 at 11 01 55 After Screenshot 2025-04-23 at 11 02 45 --- .../workspaces/DynamicParameter/DynamicParameter.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx index 92ec2edbaec12..c09317094a33c 100644 --- a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx +++ b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx @@ -86,13 +86,11 @@ const ParameterLabel: FC = ({ parameter, isPreset }) => { return (
{parameter.icon && ( - - - + )}
From 03eeb012479ae6717f5e0097cad356f77bcd48ea Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 23 Apr 2025 18:02:14 +0100 Subject: [PATCH 010/220] chore: use new table design for markdown rendering (#17530) resolves #17502 Screenshot 2025-04-23 at 11 26 19 --- site/src/components/Markdown/Markdown.tsx | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/site/src/components/Markdown/Markdown.tsx b/site/src/components/Markdown/Markdown.tsx index 7e9ee30246c28..a9bac7c6ad43a 100644 --- a/site/src/components/Markdown/Markdown.tsx +++ b/site/src/components/Markdown/Markdown.tsx @@ -1,11 +1,12 @@ import type { Interpolation, Theme } from "@emotion/react"; import Link from "@mui/material/Link"; -import Table from "@mui/material/Table"; -import TableBody from "@mui/material/TableBody"; -import TableCell from "@mui/material/TableCell"; -import TableContainer from "@mui/material/TableContainer"; -import TableHead from "@mui/material/TableHead"; -import TableRow from "@mui/material/TableRow"; +import { + Table, + TableBody, + TableCell, + TableHeader, + TableRow, +} from "components/Table/Table"; import isEqual from "lodash/isEqual"; import { type FC, memo } from "react"; import ReactMarkdown, { type Options } from "react-markdown"; @@ -90,11 +91,7 @@ export const Markdown: FC = (props) => { }, table: ({ children }) => { - return ( - - {children}
-
- ); + return {children}
; }, tr: ({ children }) => { @@ -102,7 +99,7 @@ export const Markdown: FC = (props) => { }, thead: ({ children }) => { - return {children}; + return {children}; }, tbody: ({ children }) => { From e6facaa41b8b4125c06b11e4779622b13c012327 Mon Sep 17 00:00:00 2001 From: Edward Angert Date: Wed, 23 Apr 2025 13:13:08 -0400 Subject: [PATCH 011/220] docs: clarify that parameter autofill requires experimental flag (#17546) Update documentation to indicate that parameter autofill requires `--experiments=auto-fill-parameters` enabled Fixes #14673 --------- Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> --- docs/admin/templates/extending-templates/parameters.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/admin/templates/extending-templates/parameters.md b/docs/admin/templates/extending-templates/parameters.md index 5db1473cec3ec..676b79d72c36f 100644 --- a/docs/admin/templates/extending-templates/parameters.md +++ b/docs/admin/templates/extending-templates/parameters.md @@ -376,6 +376,15 @@ data "coder_parameter" "jetbrains_ide" { When the template doesn't specify default values, Coder may still autofill parameters. +You need to enable `auto-fill-parameters` first: + +```shell +coder server --experiments=auto-fill-parameters +``` + +Or set the [environment variable](../../setup/index.md), `CODER_EXPERIMENTS=auto-fill-parameters` +With the feature enabled: + 1. Coder will look for URL query parameters with form `param.=`. This feature enables platform teams to create pre-filled template creation links. From 3567d455a7a85cd1480dbc68e236134ec90b324b Mon Sep 17 00:00:00 2001 From: Edward Angert Date: Wed, 23 Apr 2025 16:13:13 -0400 Subject: [PATCH 012/220] docs: fix ssh coder example in testing-templates doc (#17550) from @NickSquangler and @angrycub on Slack Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> --- docs/tutorials/testing-templates.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/testing-templates.md b/docs/tutorials/testing-templates.md index c3572286049e0..45250a6a71aac 100644 --- a/docs/tutorials/testing-templates.md +++ b/docs/tutorials/testing-templates.md @@ -105,7 +105,7 @@ jobs: coder create -t $TEMPLATE_NAME --template-version ${{ steps.name.outputs.version_name }} test-${{ steps.name.outputs.version_name }} --yes coder config-ssh --yes # run some example commands - coder ssh test-${{ steps.name.outputs.version_name }} -- make build + ssh coder.test-${{ steps.name.outputs.version_name }} -- make build - name: Delete the test workspace if: always() From ef6e6e41ff77bd6c71ecadfca830c1a1643a463e Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 24 Apr 2025 00:19:25 +0100 Subject: [PATCH 013/220] fix: add websocket close handling (#17548) resolves #17508 Display an error in the UI that the websocket closed if the user is still interacting with the dynamic parameters form Screenshot 2025-04-23 at 17 57 25 --- site/src/api/api.ts | 6 +++ .../CreateWorkspacePageExperimental.tsx | 13 +++++- ...eWorkspacePageViewExperimental.stories.tsx | 40 +++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.stories.tsx diff --git a/site/src/api/api.ts b/site/src/api/api.ts index fa62afadee608..b3ce8bd0cf471 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1015,9 +1015,11 @@ class ApiMethods { { onMessage, onError, + onClose, }: { onMessage: (response: TypesGen.DynamicParametersResponse) => void; onError: (error: Error) => void; + onClose: () => void; }, ): WebSocket => { const socket = createWebSocket( @@ -1033,6 +1035,10 @@ class ApiMethods { socket.close(); }); + socket.addEventListener("close", () => { + onClose(); + }); + return socket; }; diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx index 03da3bd477745..c02529c5d9446 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageExperimental.tsx @@ -1,4 +1,4 @@ -import type { ApiErrorResponse } from "api/errors"; +import { type ApiErrorResponse, DetailedError } from "api/errors"; import { checkAuthorization } from "api/queries/authCheck"; import { templateByName, @@ -107,6 +107,15 @@ const CreateWorkspacePageExperimental: FC = () => { onError: (error) => { setWsError(error); }, + onClose: () => { + // There is no reason for the websocket to close while a user is on the page + setWsError( + new DetailedError( + "Websocket connection for dynamic parameters unexpectedly closed.", + "Refresh the page to reset the form.", + ), + ); + }, }, ); @@ -141,7 +150,7 @@ const CreateWorkspacePageExperimental: FC = () => { } = useExternalAuth(realizedVersionId); const isLoadingFormData = - ws.current?.readyState !== WebSocket.OPEN || + ws.current?.readyState === WebSocket.CONNECTING || templateQuery.isLoading || permissionsQuery.isLoading; const loadFormDataError = templateQuery.error ?? permissionsQuery.error; diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.stories.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.stories.tsx new file mode 100644 index 0000000000000..a41e3a48c0ad9 --- /dev/null +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageViewExperimental.stories.tsx @@ -0,0 +1,40 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { DetailedError } from "api/errors"; +import { chromatic } from "testHelpers/chromatic"; +import { MockTemplate, MockUser } from "testHelpers/entities"; +import { CreateWorkspacePageViewExperimental } from "./CreateWorkspacePageViewExperimental"; + +const meta: Meta = { + title: "Pages/CreateWorkspacePageViewExperimental", + parameters: { chromatic }, + component: CreateWorkspacePageViewExperimental, + args: { + autofillParameters: [], + diagnostics: [], + defaultName: "", + defaultOwner: MockUser, + externalAuth: [], + externalAuthPollingState: "idle", + hasAllRequiredExternalAuth: true, + mode: "form", + parameters: [], + permissions: { + createWorkspaceForAny: true, + }, + presets: [], + sendMessage: () => {}, + template: MockTemplate, + }, +}; + +export default meta; +type Story = StoryObj; + +export const WebsocketError: Story = { + args: { + error: new DetailedError( + "Websocket connection for dynamic parameters unexpectedly closed.", + "Refresh the page to reset the form.", + ), + }, +}; From 4f70b596dcf4e5707c94dc24e9d31b1693fd4548 Mon Sep 17 00:00:00 2001 From: Ethan <39577870+ethanndickson@users.noreply.github.com> Date: Thu, 24 Apr 2025 13:02:57 +1000 Subject: [PATCH 014/220] ci: move go install tools to separate action (#17552) I think using an older version of mockgen on the schmoder CI broke the workflow, so I'm gonna sync it via this action, like we do with the other `make build` dependencies. --- .github/actions/setup-go-tools/action.yaml | 14 ++++++++++++++ .github/workflows/ci.yaml | 14 ++------------ 2 files changed, 16 insertions(+), 12 deletions(-) create mode 100644 .github/actions/setup-go-tools/action.yaml diff --git a/.github/actions/setup-go-tools/action.yaml b/.github/actions/setup-go-tools/action.yaml new file mode 100644 index 0000000000000..9c08a7d417b13 --- /dev/null +++ b/.github/actions/setup-go-tools/action.yaml @@ -0,0 +1,14 @@ +name: "Setup Go tools" +description: | + Set up tools for `make gen`, `offlinedocs` and Schmoder CI. +runs: + using: "composite" + steps: + - name: go install tools + shell: bash + run: | + go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30 + go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.34 + go install golang.org/x/tools/cmd/goimports@v0.31.0 + go install github.com/mikefarah/yq/v4@v4.44.3 + go install go.uber.org/mock/mockgen@v0.5.0 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 54239330f2a4f..6a0d3b621cf0f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -249,12 +249,7 @@ jobs: uses: ./.github/actions/setup-tf - name: go install tools - run: | - go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30 - go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.34 - go install golang.org/x/tools/cmd/goimports@v0.31.0 - go install github.com/mikefarah/yq/v4@v4.44.3 - go install go.uber.org/mock/mockgen@v0.5.0 + uses: ./.github/actions/setup-go-tools - name: Install Protoc run: | @@ -860,12 +855,7 @@ jobs: uses: ./.github/actions/setup-go - name: Install go tools - run: | - go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30 - go install storj.io/drpc/cmd/protoc-gen-go-drpc@v0.0.34 - go install golang.org/x/tools/cmd/goimports@v0.31.0 - go install github.com/mikefarah/yq/v4@v4.44.3 - go install go.uber.org/mock/mockgen@v0.5.0 + uses: ./.github/actions/setup-go-tools - name: Setup sqlc uses: ./.github/actions/setup-sqlc From 4759e17acd670d4c831b98d084998a8a30a505a9 Mon Sep 17 00:00:00 2001 From: M Atif Ali Date: Thu, 24 Apr 2025 12:21:31 +0500 Subject: [PATCH 015/220] chore(dogfood): allow provider minor version updates (#17554) --- dogfood/coder/main.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dogfood/coder/main.tf b/dogfood/coder/main.tf index e9df01a4a12f3..275a4f4b6f9fc 100644 --- a/dogfood/coder/main.tf +++ b/dogfood/coder/main.tf @@ -2,11 +2,11 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "2.3.0" + version = "~> 2.0" } docker = { source = "kreuzwerker/docker" - version = "~> 3.0.0" + version = "~> 3.0" } } } From 614a7d0d586e42d20215f078efc9956cd0766a8c Mon Sep 17 00:00:00 2001 From: Aericio <16523741+Aericio@users.noreply.github.com> Date: Wed, 23 Apr 2025 21:21:35 -1000 Subject: [PATCH 016/220] fix(examples/templates/docker-devcontainer): update folder path and provider version constraint (#17553) Co-authored-by: M Atif Ali --- examples/templates/docker-devcontainer/main.tf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/templates/docker-devcontainer/main.tf b/examples/templates/docker-devcontainer/main.tf index d0f328ea46f38..52877214caa7c 100644 --- a/examples/templates/docker-devcontainer/main.tf +++ b/examples/templates/docker-devcontainer/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "~> 1.0.0" + version = "~> 2.0" } docker = { source = "kreuzwerker/docker" @@ -340,11 +340,11 @@ module "jetbrains_gateway" { source = "registry.coder.com/modules/jetbrains-gateway/coder" # JetBrains IDEs to make available for the user to select - jetbrains_ides = ["IU", "PY", "WS", "PS", "RD", "CL", "GO", "RM"] + jetbrains_ides = ["IU", "PS", "WS", "PY", "CL", "GO", "RM", "RD", "RR"] default = "IU" # Default folder to open when starting a JetBrains IDE - folder = "/home/coder" + folder = "/workspaces" # This ensures that the latest version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production. version = ">= 1.0.0" From 9922240fd4c0b38d763f9b47e889639175438973 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Thu, 24 Apr 2025 09:54:00 +0200 Subject: [PATCH 017/220] feat: enable masking password inputs instead of blocking echo (#17469) Closes #17059 --- cli/cliui/prompt.go | 73 +++++++++++++++++++++++++++++++++++----- cli/cliui/prompt_test.go | 66 +++++++++++++++++++++++++++--------- go.mod | 1 - go.sum | 2 -- 4 files changed, 116 insertions(+), 26 deletions(-) diff --git a/cli/cliui/prompt.go b/cli/cliui/prompt.go index b432f75afeaaf..264ebf2939780 100644 --- a/cli/cliui/prompt.go +++ b/cli/cliui/prompt.go @@ -1,6 +1,7 @@ package cliui import ( + "bufio" "bytes" "encoding/json" "fmt" @@ -8,19 +9,21 @@ import ( "os" "os/signal" "strings" + "unicode" - "github.com/bgentry/speakeasy" "github.com/mattn/go-isatty" "golang.org/x/xerrors" + "github.com/coder/coder/v2/pty" "github.com/coder/pretty" "github.com/coder/serpent" ) // PromptOptions supply a set of options to the prompt. type PromptOptions struct { - Text string - Default string + Text string + Default string + // When true, the input will be masked with asterisks. Secret bool IsConfirm bool Validate func(string) error @@ -88,14 +91,13 @@ func Prompt(inv *serpent.Invocation, opts PromptOptions) (string, error) { var line string var err error + signal.Notify(interrupt, os.Interrupt) + defer signal.Stop(interrupt) + inFile, isInputFile := inv.Stdin.(*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("") + line, err = readSecretInput(inFile, inv.Stdout) } else { - signal.Notify(interrupt, os.Interrupt) - defer signal.Stop(interrupt) - line, err = readUntil(inv.Stdin, '\n') // Check if the first line beings with JSON object or array chars. @@ -204,3 +206,58 @@ func readUntil(r io.Reader, delim byte) (string, error) { } } } + +// readSecretInput reads secret input from the terminal rune-by-rune, +// masking each character with an asterisk. +func readSecretInput(f *os.File, w io.Writer) (string, error) { + // Put terminal into raw mode (no echo, no line buffering). + oldState, err := pty.MakeInputRaw(f.Fd()) + if err != nil { + return "", err + } + defer func() { + _ = pty.RestoreTerminal(f.Fd(), oldState) + }() + + reader := bufio.NewReader(f) + var runes []rune + + for { + r, _, err := reader.ReadRune() + if err != nil { + return "", err + } + + switch { + case r == '\r' || r == '\n': + // Finish on Enter + if _, err := fmt.Fprint(w, "\r\n"); err != nil { + return "", err + } + return string(runes), nil + + case r == 3: + // Ctrl+C + return "", ErrCanceled + + case r == 127 || r == '\b': + // Backspace/Delete: remove last rune + if len(runes) > 0 { + // Erase the last '*' on the screen + if _, err := fmt.Fprint(w, "\b \b"); err != nil { + return "", err + } + runes = runes[:len(runes)-1] + } + + default: + // Only mask printable, non-control runes + if !unicode.IsControl(r) { + runes = append(runes, r) + if _, err := fmt.Fprint(w, "*"); err != nil { + return "", err + } + } + } + } +} diff --git a/cli/cliui/prompt_test.go b/cli/cliui/prompt_test.go index 5ac0d906caae8..8b5a3e98ea1f7 100644 --- a/cli/cliui/prompt_test.go +++ b/cli/cliui/prompt_test.go @@ -6,6 +6,7 @@ import ( "io" "os" "os/exec" + "runtime" "testing" "github.com/stretchr/testify/assert" @@ -13,7 +14,6 @@ import ( "golang.org/x/xerrors" "github.com/coder/coder/v2/cli/cliui" - "github.com/coder/coder/v2/pty" "github.com/coder/coder/v2/pty/ptytest" "github.com/coder/coder/v2/testutil" "github.com/coder/serpent" @@ -181,6 +181,48 @@ func TestPrompt(t *testing.T) { resp := testutil.TryReceive(ctx, t, doneChan) require.Equal(t, "valid", resp) }) + + t.Run("MaskedSecret", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + ptty := ptytest.New(t) + doneChan := make(chan string) + go func() { + resp, err := newPrompt(ctx, ptty, cliui.PromptOptions{ + Text: "Password:", + Secret: true, + }, nil) + assert.NoError(t, err) + doneChan <- resp + }() + ptty.ExpectMatch("Password: ") + + ptty.WriteLine("test") + + resp := testutil.TryReceive(ctx, t, doneChan) + require.Equal(t, "test", resp) + }) + + t.Run("UTF8Password", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + ptty := ptytest.New(t) + doneChan := make(chan string) + go func() { + resp, err := newPrompt(ctx, ptty, cliui.PromptOptions{ + Text: "Password:", + Secret: true, + }, nil) + assert.NoError(t, err) + doneChan <- resp + }() + ptty.ExpectMatch("Password: ") + + ptty.WriteLine("和製漢字") + + resp := testutil.TryReceive(ctx, t, doneChan) + require.Equal(t, "和製漢字", resp) + }) } func newPrompt(ctx context.Context, ptty *ptytest.PTY, opts cliui.PromptOptions, invOpt func(inv *serpent.Invocation)) (string, error) { @@ -209,13 +251,12 @@ func TestPasswordTerminalState(t *testing.T) { passwordHelper() return } + if runtime.GOOS == "windows" { + t.Skip("Skipping on windows. PTY doesn't read ptty.Write correctly.") + } 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") @@ -229,21 +270,16 @@ func TestPasswordTerminalState(t *testing.T) { defer process.Kill() ptty.ExpectMatch("Password: ") - - require.Eventually(t, func() bool { - echo, err := ptyWithFlags.EchoEnabled() - return err == nil && !echo - }, testutil.WaitShort, testutil.IntervalMedium, "echo is on while reading password") + ptty.Write('t') + ptty.Write('e') + ptty.Write('s') + ptty.Write('t') + ptty.ExpectMatch("****") err = process.Signal(os.Interrupt) require.NoError(t, err) _, err = process.Wait() require.NoError(t, err) - - require.Eventually(t, func() bool { - echo, err := ptyWithFlags.EchoEnabled() - return err == nil && echo - }, testutil.WaitShort, testutil.IntervalMedium, "echo is off after reading password") } // nolint:unused diff --git a/go.mod b/go.mod index 521ff2c44ddf6..59dcaf99a291e 100644 --- a/go.mod +++ b/go.mod @@ -83,7 +83,6 @@ require ( github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 github.com/awalterschulze/gographviz v2.0.3+incompatible github.com/aws/smithy-go v1.22.3 - github.com/bgentry/speakeasy v0.2.0 github.com/bramvdbogaerde/go-scp v1.5.0 github.com/briandowns/spinner v1.23.0 github.com/cakturk/go-netstat v0.0.0-20200220111822-e5b49efee7a5 diff --git a/go.sum b/go.sum index fdcc9bc5b0296..809be5163de62 100644 --- a/go.sum +++ b/go.sum @@ -815,8 +815,6 @@ github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bgentry/speakeasy v0.2.0 h1:tgObeVOf8WAvtuAX6DhJ4xks4CFNwPDZiqzGqIHE51E= -github.com/bgentry/speakeasy v0.2.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= From ad38a3bddce85816ca1d30a696f4d0c5c165667f Mon Sep 17 00:00:00 2001 From: M Atif Ali Date: Thu, 24 Apr 2025 12:56:07 +0500 Subject: [PATCH 018/220] fix(examples/templates/kubernetes-devcontainer): update coder provider (#17555) --- examples/templates/kubernetes-devcontainer/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/templates/kubernetes-devcontainer/main.tf b/examples/templates/kubernetes-devcontainer/main.tf index c9a86f08df6d2..69e53565d3c78 100644 --- a/examples/templates/kubernetes-devcontainer/main.tf +++ b/examples/templates/kubernetes-devcontainer/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "~> 1.0.0" + version = "~> 2.0" } kubernetes = { source = "hashicorp/kubernetes" From 166d88e279632f8476053a5d0d278067d551771d Mon Sep 17 00:00:00 2001 From: M Atif Ali Date: Thu, 24 Apr 2025 13:52:34 +0500 Subject: [PATCH 019/220] docs: add automatic release calendar updates in docs (#17531) --- .github/workflows/release.yaml | 52 ++++++++ docs/install/releases/index.md | 24 ++-- scripts/update-release-calendar.sh | 205 +++++++++++++++++++++++++++++ 3 files changed, 269 insertions(+), 12 deletions(-) create mode 100755 scripts/update-release-calendar.sh diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 94d7b6f9ae5e4..040054eb84cbc 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -924,3 +924,55 @@ jobs: continue-on-error: true run: | make sqlc-push + + update-calendar: + name: "Update release calendar in docs" + runs-on: "ubuntu-latest" + needs: [release, publish-homebrew, publish-winget, publish-sqlc] + if: ${{ !inputs.dry_run }} + permissions: + contents: write + pull-requests: write + steps: + - name: Harden Runner + uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 # Needed to get all tags for version calculation + + - name: Set up Git + run: | + git config user.name "Coder CI" + git config user.email "cdrci@coder.com" + + - name: Run update script + run: | + ./scripts/update-release-calendar.sh + make fmt/markdown + + - name: Check for changes + id: check_changes + run: | + if git diff --quiet docs/install/releases/index.md; then + echo "No changes detected in release calendar." + echo "changes=false" >> $GITHUB_OUTPUT + else + echo "Changes detected in release calendar." + echo "changes=true" >> $GITHUB_OUTPUT + fi + + - name: Create Pull Request + if: steps.check_changes.outputs.changes == 'true' + uses: peter-evans/create-pull-request@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0 + with: + commit-message: "docs: update release calendar" + title: "docs: update release calendar" + body: | + This PR automatically updates the release calendar in the docs. + branch: bot/update-release-calendar + delete-branch: true + labels: docs diff --git a/docs/install/releases/index.md b/docs/install/releases/index.md index d0ab0d1a05d5e..09aca9f37cb9b 100644 --- a/docs/install/releases/index.md +++ b/docs/install/releases/index.md @@ -53,18 +53,18 @@ Best practices for installing Coder can be found on our [install](../index.md) pages. ## Release schedule - -| Release name | Release Date | Status | -|--------------|--------------------|------------------| -| 2.12.x | June 04, 2024 | Not Supported | -| 2.13.x | July 02, 2024 | Not Supported | -| 2.14.x | August 06, 2024 | Not Supported | -| 2.15.x | September 03, 2024 | Not Supported | -| 2.16.x | October 01, 2024 | Not Supported | -| 2.17.x | November 05, 2024 | Not Supported | -| 2.18.x | December 03, 2024 | Security Support | -| 2.19.x | February 04, 2024 | Stable | -| 2.20.x | March 05, 2024 | Mainline | + + +| Release name | Release Date | Status | Latest Release | +|------------------------------------------------|-------------------|------------------|----------------------------------------------------------------| +| [2.16](https://coder.com/changelog/coder-2-16) | November 05, 2024 | Not Supported | [v2.16.1](https://github.com/coder/coder/releases/tag/v2.16.1) | +| [2.17](https://coder.com/changelog/coder-2-17) | December 03, 2024 | Not Supported | [v2.17.3](https://github.com/coder/coder/releases/tag/v2.17.3) | +| [2.18](https://coder.com/changelog/coder-2-18) | February 04, 2025 | Not Supported | [v2.18.5](https://github.com/coder/coder/releases/tag/v2.18.5) | +| [2.19](https://coder.com/changelog/coder-2-19) | February 04, 2025 | Security Support | [v2.19.1](https://github.com/coder/coder/releases/tag/v2.19.1) | +| [2.20](https://coder.com/changelog/coder-2-20) | March 04, 2025 | Stable | [v2.20.2](https://github.com/coder/coder/releases/tag/v2.20.2) | +| [2.21](https://coder.com/changelog/coder-2-21) | April 01, 2025 | Mainline | [v2.21.1](https://github.com/coder/coder/releases/tag/v2.21.1) | +| 2.22 | May 07, 2024 | Not Released | N/A | + > [!TIP] > We publish a diff --git a/scripts/update-release-calendar.sh b/scripts/update-release-calendar.sh new file mode 100755 index 0000000000000..a9fe6e54d605d --- /dev/null +++ b/scripts/update-release-calendar.sh @@ -0,0 +1,205 @@ +#!/bin/bash + +set -euo pipefail + +# This script automatically updates the release calendar in docs/install/releases/index.md +# It calculates the releases based on the first Tuesday of each month rule +# and updates the status of each release (Not Supported, Security Support, Stable, Mainline, Not Released) + +DOCS_FILE="docs/install/releases/index.md" + +# Define unique markdown comments as anchors +CALENDAR_START_MARKER="" +CALENDAR_END_MARKER="" + +# Get current date +current_date=$(date +"%Y-%m-%d") +current_month=$(date +"%m") +current_year=$(date +"%Y") + +# Function to get the first Tuesday of a given month and year +get_first_tuesday() { + local year=$1 + local month=$2 + local first_day + local days_until_tuesday + local first_tuesday + + # Find the first day of the month + first_day=$(date -d "$year-$month-01" +"%u") + + # Calculate days until first Tuesday (if day 1 is Tuesday, first_day=2) + days_until_tuesday=$((first_day == 2 ? 0 : (9 - first_day) % 7)) + + # Get the date of the first Tuesday + first_tuesday=$(date -d "$year-$month-01 +$days_until_tuesday days" +"%Y-%m-%d") + + echo "$first_tuesday" +} + +# Function to format date as "Month DD, YYYY" +format_date() { + date -d "$1" +"%B %d, %Y" +} + +# Function to get the latest patch version for a minor release +get_latest_patch() { + local version_major=$1 + local version_minor=$2 + local tags + local latest + + # Get all tags for this minor version + tags=$(cd "$(git rev-parse --show-toplevel)" && git tag | grep "^v$version_major\\.$version_minor\\." | sort -V) + + # Get the latest one + latest=$(echo "$tags" | tail -1) + + if [ -z "$latest" ]; then + # If no tags found, return empty + echo "" + else + # Return without the v prefix + echo "${latest#v}" + fi +} + +# Generate releases table showing: +# - 3 previous unsupported releases +# - 1 security support release (n-2) +# - 1 stable release (n-1) +# - 1 mainline release (n) +# - 1 next release (n+1) +generate_release_calendar() { + local result="" + local version_major=2 + local latest_version + local version_minor + local start_minor + + # Find the current minor version by looking at the last mainline release tag + latest_version=$(cd "$(git rev-parse --show-toplevel)" && git tag | grep '^v[0-9]*\.[0-9]*\.[0-9]*$' | sort -V | tail -1) + version_minor=$(echo "$latest_version" | cut -d. -f2) + + # Start with 3 unsupported releases back + start_minor=$((version_minor - 5)) + + # Initialize the calendar table with an additional column for latest release + result="| Release name | Release Date | Status | Latest Release |\n" + result+="|--------------|--------------|--------|----------------|\n" + + # Generate rows for each release (7 total: 3 unsupported, 1 security, 1 stable, 1 mainline, 1 next) + for i in {0..6}; do + # Calculate release minor version + local rel_minor=$((start_minor + i)) + # Format release name without the .x + local version_name="$version_major.$rel_minor" + local release_date + local formatted_date + local latest_patch + local patch_link + local status + local formatted_version_name + + # Calculate release month and year based on release pattern + # This is a simplified calculation assuming monthly releases + local rel_month=$(((current_month - (5 - i) + 12) % 12)) + [[ $rel_month -eq 0 ]] && rel_month=12 + local rel_year=$current_year + if [[ $rel_month -gt $current_month ]]; then + rel_year=$((rel_year - 1)) + fi + if [[ $rel_month -lt $current_month && $i -gt 5 ]]; then + rel_year=$((rel_year + 1)) + fi + + # Skip January releases starting from 2025 + if [[ $rel_month -eq 1 && $rel_year -ge 2025 ]]; then + rel_month=2 + # No need to reassign rel_year to itself + fi + + # Get release date (first Tuesday of the month) + release_date=$(get_first_tuesday "$rel_year" "$(printf "%02d" "$rel_month")") + formatted_date=$(format_date "$release_date") + + # Get latest patch version + latest_patch=$(get_latest_patch "$version_major" "$rel_minor") + if [ -n "$latest_patch" ]; then + patch_link="[v${latest_patch}](https://github.com/coder/coder/releases/tag/v${latest_patch})" + else + patch_link="N/A" + fi + + # Determine status + if [[ "$release_date" > "$current_date" ]]; then + status="Not Released" + elif [[ $i -eq 6 ]]; then + status="Not Released" + elif [[ $i -eq 5 ]]; then + status="Mainline" + elif [[ $i -eq 4 ]]; then + status="Stable" + elif [[ $i -eq 3 ]]; then + status="Security Support" + else + status="Not Supported" + fi + + # Format version name and patch link based on release status + # No links for unreleased versions + if [[ "$status" == "Not Released" ]]; then + formatted_version_name="$version_name" + patch_link="N/A" + else + formatted_version_name="[$version_name](https://coder.com/changelog/coder-$version_major-$rel_minor)" + fi + + # Add row to table + result+="| $formatted_version_name | $formatted_date | $status | $patch_link |\n" + done + + echo -e "$result" +} + +# Check if the markdown comments exist in the file +if ! grep -q "$CALENDAR_START_MARKER" "$DOCS_FILE" || ! grep -q "$CALENDAR_END_MARKER" "$DOCS_FILE"; then + echo "Error: Markdown comment anchors not found in $DOCS_FILE" + echo "Please add the following anchors around the release calendar table:" + echo " $CALENDAR_START_MARKER" + echo " $CALENDAR_END_MARKER" + exit 1 +fi + +# Generate the new calendar table content +NEW_CALENDAR=$(generate_release_calendar) + +# Update the file while preserving the rest of the content +awk -v start_marker="$CALENDAR_START_MARKER" \ + -v end_marker="$CALENDAR_END_MARKER" \ + -v new_calendar="$NEW_CALENDAR" \ + ' + BEGIN { found_start = 0; found_end = 0; print_line = 1; } + $0 ~ start_marker { + print; + print new_calendar; + found_start = 1; + print_line = 0; + next; + } + $0 ~ end_marker { + found_end = 1; + print_line = 1; + print; + next; + } + print_line || !found_start || found_end { print } + ' "$DOCS_FILE" >"${DOCS_FILE}.new" + +# Replace the original file with the updated version +mv "${DOCS_FILE}.new" "$DOCS_FILE" + +# run make fmt/markdown +make fmt/markdown + +echo "Successfully updated release calendar in $DOCS_FILE" From c45343aa998ffa4779f30a4562a198a2f9ba4563 Mon Sep 17 00:00:00 2001 From: M Atif Ali Date: Thu, 24 Apr 2025 14:14:50 +0500 Subject: [PATCH 020/220] chore(dogfood): add windsurf module (#17558) --- dogfood/coder/main.tf | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dogfood/coder/main.tf b/dogfood/coder/main.tf index 275a4f4b6f9fc..92f25cb13f62b 100644 --- a/dogfood/coder/main.tf +++ b/dogfood/coder/main.tf @@ -225,6 +225,14 @@ module "cursor" { folder = local.repo_dir } +module "windsurf" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/windsurf/coder" + version = ">= 1.0.0" + agent_id = coder_agent.dev.id + folder = local.repo_dir +} + module "zed" { count = data.coder_workspace.me.start_count source = "./zed" From 25dacd39e7edda0206fd9f3cb06239e65a3f4309 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Apr 2025 09:45:15 +0000 Subject: [PATCH 021/220] chore: bump github.com/prometheus-community/pro-bing from 0.6.0 to 0.7.0 (#17378) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github.com/prometheus-community/pro-bing](https://github.com/prometheus-community/pro-bing) from 0.6.0 to 0.7.0.
Release notes

Sourced from github.com/prometheus-community/pro-bing's releases.

v0.7.0

What's Changed

Full Changelog: https://github.com/prometheus-community/pro-bing/compare/v0.6.1...v0.7.0

v0.6.1

What's Changed

New Contributors

Full Changelog: https://github.com/prometheus-community/pro-bing/compare/v0.6.0...v0.6.1

Commits
  • 85df87e Merge pull request #153 from prometheus-community/dependabot/go_modules/golan...
  • 4df7cf6 Bump golang.org/x/sync from 0.11.0 to 0.13.0
  • 0748554 Merge pull request #150 from prometheus-community/dependabot/go_modules/golan...
  • 0a802c0 Bump golang.org/x/net from 0.35.0 to 0.38.0
  • a184532 Merge pull request #152 from prometheus-community/superq/bump_go
  • ed8beb8 Update Go
  • c9b2c13 Merge pull request #148 from prometheus-community/dependabot/go_modules/golan...
  • b318089 Bump golang.org/x/sync from 0.10.0 to 0.11.0
  • ba53383 Merge pull request #147 from prometheus-community/dependabot/go_modules/golan...
  • a683c09 Bump golang.org/x/net from 0.34.0 to 0.35.0
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/prometheus-community/pro-bing&package-manager=go_modules&previous-version=0.6.0&new-version=0.7.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 59dcaf99a291e..230c911779b2f 100644 --- a/go.mod +++ b/go.mod @@ -164,7 +164,7 @@ require ( github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e github.com/pkg/sftp v1.13.7 - github.com/prometheus-community/pro-bing v0.6.0 + github.com/prometheus-community/pro-bing v0.7.0 github.com/prometheus/client_golang v1.22.0 github.com/prometheus/client_model v0.6.1 github.com/prometheus/common v0.63.0 diff --git a/go.sum b/go.sum index 809be5163de62..acdc4d34c8286 100644 --- a/go.sum +++ b/go.sum @@ -1671,8 +1671,8 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/prometheus-community/pro-bing v0.6.0 h1:04SZ/092gONTE1XUFzYFWqgB4mKwcdkqNChLMFedwhg= -github.com/prometheus-community/pro-bing v0.6.0/go.mod h1:jNCOI3D7pmTCeaoF41cNS6uaxeFY/Gmc3ffwbuJVzAQ= +github.com/prometheus-community/pro-bing v0.7.0 h1:KFYFbxC2f2Fp6c+TyxbCOEarf7rbnzr9Gw8eIb0RfZA= +github.com/prometheus-community/pro-bing v0.7.0/go.mod h1:Moob9dvlY50Bfq6i88xIwfyw7xLFHH69LUgx9n5zqCE= github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= From 118f12ac3abc7e0d607392654b6e85bcf0c8af8c Mon Sep 17 00:00:00 2001 From: Yevhenii Shcherbina Date: Thu, 24 Apr 2025 09:39:38 -0400 Subject: [PATCH 022/220] feat: implement claiming of prebuilt workspaces (#17458) Signed-off-by: Danny Kopping Co-authored-by: Danny Kopping Co-authored-by: Danny Kopping Co-authored-by: Edward Angert Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> Co-authored-by: Jaayden Halko Co-authored-by: Ethan <39577870+ethanndickson@users.noreply.github.com> Co-authored-by: M Atif Ali Co-authored-by: Aericio <16523741+Aericio@users.noreply.github.com> Co-authored-by: M Atif Ali Co-authored-by: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> --- coderd/coderd.go | 3 + coderd/prebuilds/api.go | 10 + coderd/prebuilds/noop.go | 15 + .../provisionerdserver/provisionerdserver.go | 9 +- coderd/workspaces.go | 90 ++- coderd/wsbuilder/wsbuilder.go | 16 +- enterprise/coderd/prebuilds/claim.go | 53 ++ enterprise/coderd/prebuilds/claim_test.go | 564 ++++++++++++++++++ 8 files changed, 731 insertions(+), 29 deletions(-) create mode 100644 enterprise/coderd/prebuilds/claim.go create mode 100644 enterprise/coderd/prebuilds/claim_test.go diff --git a/coderd/coderd.go b/coderd/coderd.go index cb069fd6bf29d..4a9e3e61d9cf5 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -45,6 +45,7 @@ import ( "github.com/coder/coder/v2/coderd/entitlements" "github.com/coder/coder/v2/coderd/files" "github.com/coder/coder/v2/coderd/idpsync" + "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/coderd/runtimeconfig" "github.com/coder/coder/v2/coderd/webpush" @@ -595,6 +596,7 @@ func New(options *Options) *API { f := appearance.NewDefaultFetcher(api.DeploymentValues.DocsURL.String()) api.AppearanceFetcher.Store(&f) api.PortSharer.Store(&portsharing.DefaultPortSharer) + api.PrebuildsClaimer.Store(&prebuilds.DefaultClaimer) buildInfo := codersdk.BuildInfoResponse{ ExternalURL: buildinfo.ExternalURL(), Version: buildinfo.Version(), @@ -1569,6 +1571,7 @@ type API struct { AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore] PortSharer atomic.Pointer[portsharing.PortSharer] FileCache files.Cache + PrebuildsClaimer atomic.Pointer[prebuilds.Claimer] UpdatesProvider tailnet.WorkspaceUpdatesProvider diff --git a/coderd/prebuilds/api.go b/coderd/prebuilds/api.go index 6ebfb8acced44..ebc4c39c89b50 100644 --- a/coderd/prebuilds/api.go +++ b/coderd/prebuilds/api.go @@ -2,8 +2,13 @@ package prebuilds import ( "context" + + "github.com/google/uuid" + "golang.org/x/xerrors" ) +var ErrNoClaimablePrebuiltWorkspaces = xerrors.New("no claimable prebuilt workspaces found") + // ReconciliationOrchestrator manages the lifecycle of prebuild reconciliation. // It runs a continuous loop to check and reconcile prebuild states, and can be stopped gracefully. type ReconciliationOrchestrator interface { @@ -25,3 +30,8 @@ type Reconciler interface { // in parallel, creating or deleting prebuilds as needed to reach their desired states. ReconcileAll(ctx context.Context) error } + +type Claimer interface { + Claim(ctx context.Context, userID uuid.UUID, name string, presetID uuid.UUID) (*uuid.UUID, error) + Initiator() uuid.UUID +} diff --git a/coderd/prebuilds/noop.go b/coderd/prebuilds/noop.go index ffe4e7b442af9..d122a61ebb9c6 100644 --- a/coderd/prebuilds/noop.go +++ b/coderd/prebuilds/noop.go @@ -3,6 +3,8 @@ package prebuilds import ( "context" + "github.com/google/uuid" + "github.com/coder/coder/v2/coderd/database" ) @@ -33,3 +35,16 @@ func (NoopReconciler) CalculateActions(context.Context, PresetSnapshot) (*Reconc } var _ ReconciliationOrchestrator = NoopReconciler{} + +type AGPLPrebuildClaimer struct{} + +func (AGPLPrebuildClaimer) Claim(context.Context, uuid.UUID, string, uuid.UUID) (*uuid.UUID, error) { + // Not entitled to claim prebuilds in AGPL version. + return nil, ErrNoClaimablePrebuiltWorkspaces +} + +func (AGPLPrebuildClaimer) Initiator() uuid.UUID { + return uuid.Nil +} + +var DefaultClaimer Claimer = AGPLPrebuildClaimer{} diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 22bc720736148..9362d2f3e5a85 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -2471,10 +2471,11 @@ type TemplateVersionImportJob struct { // WorkspaceProvisionJob is the payload for the "workspace_provision" job type. type WorkspaceProvisionJob struct { - WorkspaceBuildID uuid.UUID `json:"workspace_build_id"` - DryRun bool `json:"dry_run"` - IsPrebuild bool `json:"is_prebuild,omitempty"` - LogLevel string `json:"log_level,omitempty"` + WorkspaceBuildID uuid.UUID `json:"workspace_build_id"` + DryRun bool `json:"dry_run"` + IsPrebuild bool `json:"is_prebuild,omitempty"` + PrebuildClaimedByUser uuid.UUID `json:"prebuild_claimed_by,omitempty"` + LogLevel string `json:"log_level,omitempty"` } // TemplateVersionDryRunJob is the payload for the "template_version_dry_run" job type. diff --git a/coderd/workspaces.go b/coderd/workspaces.go index c1c8b1745c106..12b3787acf3d8 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -18,6 +18,7 @@ import ( "golang.org/x/xerrors" "cdr.dev/slog" + "github.com/coder/coder/v2/agent/proto" "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/database" @@ -28,6 +29,7 @@ import ( "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" "github.com/coder/coder/v2/coderd/notifications" + "github.com/coder/coder/v2/coderd/prebuilds" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" "github.com/coder/coder/v2/coderd/schedule" @@ -636,33 +638,57 @@ func createWorkspace( workspaceBuild *database.WorkspaceBuild provisionerDaemons []database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow ) + err = api.Database.InTx(func(db database.Store) error { - now := dbtime.Now() - // Workspaces are created without any versions. - minimumWorkspace, err := db.InsertWorkspace(ctx, database.InsertWorkspaceParams{ - ID: uuid.New(), - CreatedAt: now, - UpdatedAt: now, - OwnerID: owner.ID, - OrganizationID: template.OrganizationID, - TemplateID: template.ID, - Name: req.Name, - AutostartSchedule: dbAutostartSchedule, - NextStartAt: nextStartAt, - Ttl: dbTTL, - // The workspaces page will sort by last used at, and it's useful to - // have the newly created workspace at the top of the list! - LastUsedAt: dbtime.Now(), - AutomaticUpdates: dbAU, - }) - if err != nil { - return xerrors.Errorf("insert workspace: %w", err) + var ( + workspaceID uuid.UUID + claimedWorkspace *database.Workspace + prebuildsClaimer = *api.PrebuildsClaimer.Load() + ) + + // If a template preset was chosen, try claim a prebuilt workspace. + if req.TemplateVersionPresetID != uuid.Nil { + // Try and claim an eligible prebuild, if available. + claimedWorkspace, err = claimPrebuild(ctx, prebuildsClaimer, db, api.Logger, req, owner) + if err != nil && !errors.Is(err, prebuilds.ErrNoClaimablePrebuiltWorkspaces) { + return xerrors.Errorf("claim prebuild: %w", err) + } + } + + // No prebuild found; regular flow. + if claimedWorkspace == nil { + now := dbtime.Now() + // Workspaces are created without any versions. + minimumWorkspace, err := db.InsertWorkspace(ctx, database.InsertWorkspaceParams{ + ID: uuid.New(), + CreatedAt: now, + UpdatedAt: now, + OwnerID: owner.ID, + OrganizationID: template.OrganizationID, + TemplateID: template.ID, + Name: req.Name, + AutostartSchedule: dbAutostartSchedule, + NextStartAt: nextStartAt, + Ttl: dbTTL, + // The workspaces page will sort by last used at, and it's useful to + // have the newly created workspace at the top of the list! + LastUsedAt: dbtime.Now(), + AutomaticUpdates: dbAU, + }) + if err != nil { + return xerrors.Errorf("insert workspace: %w", err) + } + workspaceID = minimumWorkspace.ID + } else { + // Prebuild found! + workspaceID = claimedWorkspace.ID + initiatorID = prebuildsClaimer.Initiator() } // We have to refetch the workspace for the joined in fields. // TODO: We can use WorkspaceTable for the builder to not require // this extra fetch. - workspace, err = db.GetWorkspaceByID(ctx, minimumWorkspace.ID) + workspace, err = db.GetWorkspaceByID(ctx, workspaceID) if err != nil { return xerrors.Errorf("get workspace by ID: %w", err) } @@ -676,6 +702,13 @@ func createWorkspace( if req.TemplateVersionID != uuid.Nil { builder = builder.VersionID(req.TemplateVersionID) } + if req.TemplateVersionPresetID != uuid.Nil { + builder = builder.TemplateVersionPresetID(req.TemplateVersionPresetID) + } + if claimedWorkspace != nil { + builder = builder.MarkPrebuildClaimedBy(owner.ID) + } + if req.EnableDynamicParameters && api.Experiments.Enabled(codersdk.ExperimentDynamicParameters) { builder = builder.UsingDynamicParameters() } @@ -842,6 +875,21 @@ func requestTemplate(ctx context.Context, rw http.ResponseWriter, req codersdk.C return template, true } +func claimPrebuild(ctx context.Context, claimer prebuilds.Claimer, db database.Store, logger slog.Logger, req codersdk.CreateWorkspaceRequest, owner workspaceOwner) (*database.Workspace, error) { + claimedID, err := claimer.Claim(ctx, owner.ID, req.Name, req.TemplateVersionPresetID) + if err != nil { + // TODO: enhance this by clarifying whether this *specific* prebuild failed or whether there are none to claim. + return nil, xerrors.Errorf("claim prebuild: %w", err) + } + + lookup, err := db.GetWorkspaceByID(ctx, *claimedID) + if err != nil { + logger.Error(ctx, "unable to find claimed workspace by ID", slog.Error(err), slog.F("claimed_prebuild_id", claimedID.String())) + return nil, xerrors.Errorf("find claimed workspace by ID %q: %w", claimedID.String(), err) + } + return &lookup, nil +} + func (api *API) notifyWorkspaceCreated( ctx context.Context, receiverID uuid.UUID, diff --git a/coderd/wsbuilder/wsbuilder.go b/coderd/wsbuilder/wsbuilder.go index 5ac0f54639a06..942829004309c 100644 --- a/coderd/wsbuilder/wsbuilder.go +++ b/coderd/wsbuilder/wsbuilder.go @@ -76,7 +76,8 @@ type Builder struct { parameterValues *[]string templateVersionPresetParameterValues []database.TemplateVersionPresetParameter - prebuild bool + prebuild bool + prebuildClaimedBy uuid.UUID verifyNoLegacyParametersOnce bool } @@ -179,6 +180,12 @@ func (b Builder) MarkPrebuild() Builder { return b } +func (b Builder) MarkPrebuildClaimedBy(userID uuid.UUID) Builder { + // nolint: revive + b.prebuildClaimedBy = userID + return b +} + func (b Builder) UsingDynamicParameters() Builder { b.dynamicParametersEnabled = true return b @@ -315,9 +322,10 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object workspaceBuildID := uuid.New() input, err := json.Marshal(provisionerdserver.WorkspaceProvisionJob{ - WorkspaceBuildID: workspaceBuildID, - LogLevel: b.logLevel, - IsPrebuild: b.prebuild, + WorkspaceBuildID: workspaceBuildID, + LogLevel: b.logLevel, + IsPrebuild: b.prebuild, + PrebuildClaimedByUser: b.prebuildClaimedBy, }) if err != nil { return nil, nil, nil, BuildError{ diff --git a/enterprise/coderd/prebuilds/claim.go b/enterprise/coderd/prebuilds/claim.go new file mode 100644 index 0000000000000..f040ee756e678 --- /dev/null +++ b/enterprise/coderd/prebuilds/claim.go @@ -0,0 +1,53 @@ +package prebuilds + +import ( + "context" + "database/sql" + "errors" + + "github.com/google/uuid" + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/prebuilds" +) + +type EnterpriseClaimer struct { + store database.Store +} + +func NewEnterpriseClaimer(store database.Store) *EnterpriseClaimer { + return &EnterpriseClaimer{ + store: store, + } +} + +func (c EnterpriseClaimer) Claim( + ctx context.Context, + userID uuid.UUID, + name string, + presetID uuid.UUID, +) (*uuid.UUID, error) { + result, err := c.store.ClaimPrebuiltWorkspace(ctx, database.ClaimPrebuiltWorkspaceParams{ + NewUserID: userID, + NewName: name, + PresetID: presetID, + }) + if err != nil { + switch { + // No eligible prebuilds found + case errors.Is(err, sql.ErrNoRows): + return nil, prebuilds.ErrNoClaimablePrebuiltWorkspaces + default: + return nil, xerrors.Errorf("claim prebuild for user %q: %w", userID.String(), err) + } + } + + return &result.ID, nil +} + +func (EnterpriseClaimer) Initiator() uuid.UUID { + return prebuilds.SystemUserID +} + +var _ prebuilds.Claimer = &EnterpriseClaimer{} diff --git a/enterprise/coderd/prebuilds/claim_test.go b/enterprise/coderd/prebuilds/claim_test.go new file mode 100644 index 0000000000000..4f398724b8265 --- /dev/null +++ b/enterprise/coderd/prebuilds/claim_test.go @@ -0,0 +1,564 @@ +package prebuilds_test + +import ( + "context" + "database/sql" + "slices" + "strings" + "sync/atomic" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "golang.org/x/xerrors" + + "github.com/coder/quartz" + + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/dbtestutil" + agplprebuilds "github.com/coder/coder/v2/coderd/prebuilds" + "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/enterprise/coderd/coderdenttest" + "github.com/coder/coder/v2/enterprise/coderd/prebuilds" + "github.com/coder/coder/v2/provisioner/echo" + "github.com/coder/coder/v2/provisionersdk/proto" + "github.com/coder/coder/v2/testutil" +) + +type storeSpy struct { + database.Store + + claims *atomic.Int32 + claimParams *atomic.Pointer[database.ClaimPrebuiltWorkspaceParams] + claimedWorkspace *atomic.Pointer[database.ClaimPrebuiltWorkspaceRow] +} + +func newStoreSpy(db database.Store) *storeSpy { + return &storeSpy{ + Store: db, + claims: &atomic.Int32{}, + claimParams: &atomic.Pointer[database.ClaimPrebuiltWorkspaceParams]{}, + claimedWorkspace: &atomic.Pointer[database.ClaimPrebuiltWorkspaceRow]{}, + } +} + +func (m *storeSpy) InTx(fn func(store database.Store) error, opts *database.TxOptions) error { + // Pass spy down into transaction store. + return m.Store.InTx(func(store database.Store) error { + spy := newStoreSpy(store) + spy.claims = m.claims + spy.claimParams = m.claimParams + spy.claimedWorkspace = m.claimedWorkspace + + return fn(spy) + }, opts) +} + +func (m *storeSpy) ClaimPrebuiltWorkspace(ctx context.Context, arg database.ClaimPrebuiltWorkspaceParams) (database.ClaimPrebuiltWorkspaceRow, error) { + m.claims.Add(1) + m.claimParams.Store(&arg) + result, err := m.Store.ClaimPrebuiltWorkspace(ctx, arg) + if err == nil { + m.claimedWorkspace.Store(&result) + } + return result, err +} + +type errorStore struct { + claimingErr error + + database.Store +} + +func newErrorStore(db database.Store, claimingErr error) *errorStore { + return &errorStore{ + Store: db, + claimingErr: claimingErr, + } +} + +func (es *errorStore) InTx(fn func(store database.Store) error, opts *database.TxOptions) error { + // Pass failure store down into transaction store. + return es.Store.InTx(func(store database.Store) error { + newES := newErrorStore(store, es.claimingErr) + + return fn(newES) + }, opts) +} + +func (es *errorStore) ClaimPrebuiltWorkspace(ctx context.Context, arg database.ClaimPrebuiltWorkspaceParams) (database.ClaimPrebuiltWorkspaceRow, error) { + return database.ClaimPrebuiltWorkspaceRow{}, es.claimingErr +} + +func TestClaimPrebuild(t *testing.T) { + t.Parallel() + + if !dbtestutil.WillUsePostgres() { + t.Skip("This test requires postgres") + } + + const ( + desiredInstances = 1 + presetCount = 2 + ) + + cases := map[string]struct { + expectPrebuildClaimed bool + markPrebuildsClaimable bool + }{ + "no eligible prebuilds to claim": { + expectPrebuildClaimed: false, + markPrebuildsClaimable: false, + }, + "claiming an eligible prebuild should succeed": { + expectPrebuildClaimed: true, + markPrebuildsClaimable: true, + }, + } + + for name, tc := range cases { + tc := tc + + t.Run(name, func(t *testing.T) { + t.Parallel() + + // Setup. + ctx := testutil.Context(t, testutil.WaitSuperLong) + db, pubsub := dbtestutil.NewDB(t) + spy := newStoreSpy(db) + expectedPrebuildsCount := desiredInstances * presetCount + + logger := testutil.Logger(t) + client, _, api, owner := coderdenttest.NewWithAPI(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + IncludeProvisionerDaemon: true, + Database: spy, + Pubsub: pubsub, + }, + + EntitlementsUpdateInterval: time.Second, + }) + + reconciler := prebuilds.NewStoreReconciler(spy, pubsub, codersdk.PrebuildsConfig{}, logger, quartz.NewMock(t)) + var claimer agplprebuilds.Claimer = prebuilds.NewEnterpriseClaimer(spy) + api.AGPL.PrebuildsClaimer.Store(&claimer) + + version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, templateWithAgentAndPresetsWithPrebuilds(desiredInstances)) + _ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) + presets, err := client.TemplateVersionPresets(ctx, version.ID) + require.NoError(t, err) + require.Len(t, presets, presetCount) + + userClient, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleMember()) + + // Given: the reconciliation state is snapshot. + state, err := reconciler.SnapshotState(ctx, spy) + require.NoError(t, err) + require.Len(t, state.Presets, presetCount) + + // When: a reconciliation is setup for each preset. + for _, preset := range presets { + ps, err := state.FilterByPreset(preset.ID) + require.NoError(t, err) + require.NotNil(t, ps) + actions, err := reconciler.CalculateActions(ctx, *ps) + require.NoError(t, err) + require.NotNil(t, actions) + + require.NoError(t, reconciler.ReconcilePreset(ctx, *ps)) + } + + // Given: a set of running, eligible prebuilds eventually starts up. + runningPrebuilds := make(map[uuid.UUID]database.GetRunningPrebuiltWorkspacesRow, desiredInstances*presetCount) + require.Eventually(t, func() bool { + rows, err := spy.GetRunningPrebuiltWorkspaces(ctx) + if err != nil { + return false + } + + for _, row := range rows { + runningPrebuilds[row.CurrentPresetID.UUID] = row + + if !tc.markPrebuildsClaimable { + continue + } + + agents, err := db.GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx, row.ID) + if err != nil { + return false + } + + // Workspaces are eligible once its agent is marked "ready". + for _, agent := range agents { + err = db.UpdateWorkspaceAgentLifecycleStateByID(ctx, database.UpdateWorkspaceAgentLifecycleStateByIDParams{ + ID: agent.ID, + LifecycleState: database.WorkspaceAgentLifecycleStateReady, + StartedAt: sql.NullTime{Time: time.Now().Add(time.Hour), Valid: true}, + ReadyAt: sql.NullTime{Time: time.Now().Add(-1 * time.Hour), Valid: true}, + }) + if err != nil { + return false + } + } + } + + t.Logf("found %d running prebuilds so far, want %d", len(runningPrebuilds), expectedPrebuildsCount) + + return len(runningPrebuilds) == expectedPrebuildsCount + }, testutil.WaitSuperLong, testutil.IntervalSlow) + + // When: a user creates a new workspace with a preset for which prebuilds are configured. + workspaceName := strings.ReplaceAll(testutil.GetRandomName(t), "_", "-") + params := database.ClaimPrebuiltWorkspaceParams{ + NewUserID: user.ID, + NewName: workspaceName, + PresetID: presets[0].ID, + } + userWorkspace, err := userClient.CreateUserWorkspace(ctx, user.Username, codersdk.CreateWorkspaceRequest{ + TemplateVersionID: version.ID, + Name: workspaceName, + TemplateVersionPresetID: presets[0].ID, + }) + + require.NoError(t, err) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, userWorkspace.LatestBuild.ID) + + // Then: a prebuild should have been claimed. + require.EqualValues(t, spy.claims.Load(), 1) + require.EqualValues(t, *spy.claimParams.Load(), params) + + if !tc.expectPrebuildClaimed { + require.Nil(t, spy.claimedWorkspace.Load()) + return + } + + require.NotNil(t, spy.claimedWorkspace.Load()) + claimed := *spy.claimedWorkspace.Load() + require.NotEqual(t, claimed.ID, uuid.Nil) + + // Then: the claimed prebuild must now be owned by the requester. + workspace, err := spy.GetWorkspaceByID(ctx, claimed.ID) + require.NoError(t, err) + require.Equal(t, user.ID, workspace.OwnerID) + + // Then: the number of running prebuilds has changed since one was claimed. + currentPrebuilds, err := spy.GetRunningPrebuiltWorkspaces(ctx) + require.NoError(t, err) + require.Equal(t, expectedPrebuildsCount-1, len(currentPrebuilds)) + + // Then: the claimed prebuild is now missing from the running prebuilds set. + found := slices.ContainsFunc(currentPrebuilds, func(prebuild database.GetRunningPrebuiltWorkspacesRow) bool { + return prebuild.ID == claimed.ID + }) + require.False(t, found, "claimed prebuild should not still be considered a running prebuild") + + // Then: reconciling at this point will provision a new prebuild to replace the claimed one. + { + // Given: the reconciliation state is snapshot. + state, err = reconciler.SnapshotState(ctx, spy) + require.NoError(t, err) + + // When: a reconciliation is setup for each preset. + for _, preset := range presets { + ps, err := state.FilterByPreset(preset.ID) + require.NoError(t, err) + + // Then: the reconciliation takes place without error. + require.NoError(t, reconciler.ReconcilePreset(ctx, *ps)) + } + } + + require.Eventually(t, func() bool { + rows, err := spy.GetRunningPrebuiltWorkspaces(ctx) + if err != nil { + return false + } + + t.Logf("found %d running prebuilds so far, want %d", len(rows), expectedPrebuildsCount) + + return len(runningPrebuilds) == expectedPrebuildsCount + }, testutil.WaitSuperLong, testutil.IntervalSlow) + + // Then: when restarting the created workspace (which claimed a prebuild), it should not try and claim a new prebuild. + // Prebuilds should ONLY be used for net-new workspaces. + // This is expected by default anyway currently since new workspaces and operations on existing workspaces + // take different code paths, but it's worth validating. + + spy.claims.Store(0) // Reset counter because we need to check if any new claim requests happen. + + wp, err := userClient.WorkspaceBuildParameters(ctx, userWorkspace.LatestBuild.ID) + require.NoError(t, err) + + stopBuild, err := userClient.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ + TemplateVersionID: version.ID, + Transition: codersdk.WorkspaceTransitionStop, + RichParameterValues: wp, + }) + require.NoError(t, err) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, stopBuild.ID) + + startBuild, err := userClient.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ + TemplateVersionID: version.ID, + Transition: codersdk.WorkspaceTransitionStart, + RichParameterValues: wp, + }) + require.NoError(t, err) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, startBuild.ID) + + require.Zero(t, spy.claims.Load()) + }) + } +} + +func TestClaimPrebuild_CheckDifferentErrors(t *testing.T) { + t.Parallel() + + if !dbtestutil.WillUsePostgres() { + t.Skip("This test requires postgres") + } + + const ( + desiredInstances = 1 + presetCount = 2 + + expectedPrebuildsCount = desiredInstances * presetCount + ) + + cases := map[string]struct { + claimingErr error + checkFn func( + t *testing.T, + ctx context.Context, + store database.Store, + userClient *codersdk.Client, + user codersdk.User, + templateVersionID uuid.UUID, + presetID uuid.UUID, + ) + }{ + "ErrNoClaimablePrebuiltWorkspaces is returned": { + claimingErr: agplprebuilds.ErrNoClaimablePrebuiltWorkspaces, + checkFn: func( + t *testing.T, + ctx context.Context, + store database.Store, + userClient *codersdk.Client, + user codersdk.User, + templateVersionID uuid.UUID, + presetID uuid.UUID, + ) { + // When: a user creates a new workspace with a preset for which prebuilds are configured. + workspaceName := strings.ReplaceAll(testutil.GetRandomName(t), "_", "-") + userWorkspace, err := userClient.CreateUserWorkspace(ctx, user.Username, codersdk.CreateWorkspaceRequest{ + TemplateVersionID: templateVersionID, + Name: workspaceName, + TemplateVersionPresetID: presetID, + }) + + require.NoError(t, err) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, userWorkspace.LatestBuild.ID) + + // Then: the number of running prebuilds hasn't changed because claiming prebuild is failed and we fallback to creating new workspace. + currentPrebuilds, err := store.GetRunningPrebuiltWorkspaces(ctx) + require.NoError(t, err) + require.Equal(t, expectedPrebuildsCount, len(currentPrebuilds)) + }, + }, + "unexpected error during claim is returned": { + claimingErr: xerrors.New("unexpected error during claim"), + checkFn: func( + t *testing.T, + ctx context.Context, + store database.Store, + userClient *codersdk.Client, + user codersdk.User, + templateVersionID uuid.UUID, + presetID uuid.UUID, + ) { + // When: a user creates a new workspace with a preset for which prebuilds are configured. + workspaceName := strings.ReplaceAll(testutil.GetRandomName(t), "_", "-") + _, err := userClient.CreateUserWorkspace(ctx, user.Username, codersdk.CreateWorkspaceRequest{ + TemplateVersionID: templateVersionID, + Name: workspaceName, + TemplateVersionPresetID: presetID, + }) + + // Then: unexpected error happened and was propagated all the way to the caller + require.Error(t, err) + require.ErrorContains(t, err, "unexpected error during claim") + + // Then: the number of running prebuilds hasn't changed because claiming prebuild is failed. + currentPrebuilds, err := store.GetRunningPrebuiltWorkspaces(ctx) + require.NoError(t, err) + require.Equal(t, expectedPrebuildsCount, len(currentPrebuilds)) + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + // Setup. + ctx := testutil.Context(t, testutil.WaitSuperLong) + db, pubsub := dbtestutil.NewDB(t) + errorStore := newErrorStore(db, tc.claimingErr) + + logger := testutil.Logger(t) + client, _, api, owner := coderdenttest.NewWithAPI(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + IncludeProvisionerDaemon: true, + Database: errorStore, + Pubsub: pubsub, + }, + + EntitlementsUpdateInterval: time.Second, + }) + + reconciler := prebuilds.NewStoreReconciler(errorStore, pubsub, codersdk.PrebuildsConfig{}, logger, quartz.NewMock(t)) + var claimer agplprebuilds.Claimer = prebuilds.NewEnterpriseClaimer(errorStore) + api.AGPL.PrebuildsClaimer.Store(&claimer) + + version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, templateWithAgentAndPresetsWithPrebuilds(desiredInstances)) + _ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID) + presets, err := client.TemplateVersionPresets(ctx, version.ID) + require.NoError(t, err) + require.Len(t, presets, presetCount) + + userClient, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleMember()) + + // Given: the reconciliation state is snapshot. + state, err := reconciler.SnapshotState(ctx, errorStore) + require.NoError(t, err) + require.Len(t, state.Presets, presetCount) + + // When: a reconciliation is setup for each preset. + for _, preset := range presets { + ps, err := state.FilterByPreset(preset.ID) + require.NoError(t, err) + require.NotNil(t, ps) + actions, err := reconciler.CalculateActions(ctx, *ps) + require.NoError(t, err) + require.NotNil(t, actions) + + require.NoError(t, reconciler.ReconcilePreset(ctx, *ps)) + } + + // Given: a set of running, eligible prebuilds eventually starts up. + runningPrebuilds := make(map[uuid.UUID]database.GetRunningPrebuiltWorkspacesRow, desiredInstances*presetCount) + require.Eventually(t, func() bool { + rows, err := errorStore.GetRunningPrebuiltWorkspaces(ctx) + if err != nil { + return false + } + + for _, row := range rows { + runningPrebuilds[row.CurrentPresetID.UUID] = row + + agents, err := db.GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx, row.ID) + if err != nil { + return false + } + + // Workspaces are eligible once its agent is marked "ready". + for _, agent := range agents { + err = db.UpdateWorkspaceAgentLifecycleStateByID(ctx, database.UpdateWorkspaceAgentLifecycleStateByIDParams{ + ID: agent.ID, + LifecycleState: database.WorkspaceAgentLifecycleStateReady, + StartedAt: sql.NullTime{Time: time.Now().Add(time.Hour), Valid: true}, + ReadyAt: sql.NullTime{Time: time.Now().Add(-1 * time.Hour), Valid: true}, + }) + if err != nil { + return false + } + } + } + + t.Logf("found %d running prebuilds so far, want %d", len(runningPrebuilds), expectedPrebuildsCount) + + return len(runningPrebuilds) == expectedPrebuildsCount + }, testutil.WaitSuperLong, testutil.IntervalSlow) + + tc.checkFn(t, ctx, errorStore, userClient, user, version.ID, presets[0].ID) + }) + } +} + +func templateWithAgentAndPresetsWithPrebuilds(desiredInstances int32) *echo.Responses { + return &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: []*proto.Response{ + { + Type: &proto.Response_Plan{ + Plan: &proto.PlanComplete{ + Resources: []*proto.Resource{ + { + Type: "compute", + Name: "main", + Agents: []*proto.Agent{ + { + Name: "smith", + OperatingSystem: "linux", + Architecture: "i386", + }, + }, + }, + }, + Presets: []*proto.Preset{ + { + Name: "preset-a", + Parameters: []*proto.PresetParameter{ + { + Name: "k1", + Value: "v1", + }, + }, + Prebuild: &proto.Prebuild{ + Instances: desiredInstances, + }, + }, + { + Name: "preset-b", + Parameters: []*proto.PresetParameter{ + { + Name: "k1", + Value: "v2", + }, + }, + Prebuild: &proto.Prebuild{ + Instances: desiredInstances, + }, + }, + }, + }, + }, + }, + }, + ProvisionApply: []*proto.Response{ + { + Type: &proto.Response_Apply{ + Apply: &proto.ApplyComplete{ + Resources: []*proto.Resource{ + { + Type: "compute", + Name: "main", + Agents: []*proto.Agent{ + { + Name: "smith", + OperatingSystem: "linux", + Architecture: "i386", + }, + }, + }, + }, + }, + }, + }, + }, + } +} From fc921a584f872b2402361a7baadd6faad1791211 Mon Sep 17 00:00:00 2001 From: M Atif Ali Date: Thu, 24 Apr 2025 19:42:33 +0500 Subject: [PATCH 023/220] chore(docs): update release calendar dates and next release calculation (#17560) --- docs/install/releases/index.md | 10 +-- scripts/update-release-calendar.sh | 102 ++++++++++++++++++++--------- 2 files changed, 77 insertions(+), 35 deletions(-) diff --git a/docs/install/releases/index.md b/docs/install/releases/index.md index 09aca9f37cb9b..806b80eae3101 100644 --- a/docs/install/releases/index.md +++ b/docs/install/releases/index.md @@ -57,13 +57,13 @@ pages. | Release name | Release Date | Status | Latest Release | |------------------------------------------------|-------------------|------------------|----------------------------------------------------------------| -| [2.16](https://coder.com/changelog/coder-2-16) | November 05, 2024 | Not Supported | [v2.16.1](https://github.com/coder/coder/releases/tag/v2.16.1) | -| [2.17](https://coder.com/changelog/coder-2-17) | December 03, 2024 | Not Supported | [v2.17.3](https://github.com/coder/coder/releases/tag/v2.17.3) | -| [2.18](https://coder.com/changelog/coder-2-18) | February 04, 2025 | Not Supported | [v2.18.5](https://github.com/coder/coder/releases/tag/v2.18.5) | +| [2.16](https://coder.com/changelog/coder-2-16) | October 01, 2024 | Not Supported | [v2.16.1](https://github.com/coder/coder/releases/tag/v2.16.1) | +| [2.17](https://coder.com/changelog/coder-2-17) | November 05, 2024 | Not Supported | [v2.17.3](https://github.com/coder/coder/releases/tag/v2.17.3) | +| [2.18](https://coder.com/changelog/coder-2-18) | December 03, 2024 | Not Supported | [v2.18.5](https://github.com/coder/coder/releases/tag/v2.18.5) | | [2.19](https://coder.com/changelog/coder-2-19) | February 04, 2025 | Security Support | [v2.19.1](https://github.com/coder/coder/releases/tag/v2.19.1) | | [2.20](https://coder.com/changelog/coder-2-20) | March 04, 2025 | Stable | [v2.20.2](https://github.com/coder/coder/releases/tag/v2.20.2) | -| [2.21](https://coder.com/changelog/coder-2-21) | April 01, 2025 | Mainline | [v2.21.1](https://github.com/coder/coder/releases/tag/v2.21.1) | -| 2.22 | May 07, 2024 | Not Released | N/A | +| [2.21](https://coder.com/changelog/coder-2-21) | April 01, 2025 | Mainline | [v2.21.0](https://github.com/coder/coder/releases/tag/v2.21.0) | +| 2.22 | May 06, 2025 | Not Released | N/A | > [!TIP] diff --git a/scripts/update-release-calendar.sh b/scripts/update-release-calendar.sh index a9fe6e54d605d..2643e713eac6d 100755 --- a/scripts/update-release-calendar.sh +++ b/scripts/update-release-calendar.sh @@ -8,16 +8,13 @@ set -euo pipefail DOCS_FILE="docs/install/releases/index.md" -# Define unique markdown comments as anchors CALENDAR_START_MARKER="" CALENDAR_END_MARKER="" -# Get current date current_date=$(date +"%Y-%m-%d") current_month=$(date +"%m") current_year=$(date +"%Y") -# Function to get the first Tuesday of a given month and year get_first_tuesday() { local year=$1 local month=$2 @@ -25,24 +22,20 @@ get_first_tuesday() { local days_until_tuesday local first_tuesday - # Find the first day of the month first_day=$(date -d "$year-$month-01" +"%u") - # Calculate days until first Tuesday (if day 1 is Tuesday, first_day=2) days_until_tuesday=$((first_day == 2 ? 0 : (9 - first_day) % 7)) - # Get the date of the first Tuesday first_tuesday=$(date -d "$year-$month-01 +$days_until_tuesday days" +"%Y-%m-%d") echo "$first_tuesday" } -# Function to format date as "Month DD, YYYY" +# Format date as "Month DD, YYYY" format_date() { date -d "$1" +"%B %d, %Y" } -# Function to get the latest patch version for a minor release get_latest_patch() { local version_major=$1 local version_minor=$2 @@ -52,18 +45,33 @@ get_latest_patch() { # Get all tags for this minor version tags=$(cd "$(git rev-parse --show-toplevel)" && git tag | grep "^v$version_major\\.$version_minor\\." | sort -V) - # Get the latest one latest=$(echo "$tags" | tail -1) if [ -z "$latest" ]; then - # If no tags found, return empty echo "" else - # Return without the v prefix echo "${latest#v}" fi } +get_next_release_month() { + local current_month=$1 + local next_month=$((current_month + 1)) + + # Handle December -> February transition (skip January) + if [[ $next_month -eq 13 ]]; then + next_month=2 # Skip to February + return $next_month + fi + + # Skip January for all years starting 2025 + if [[ $next_month -eq 1 ]]; then + next_month=2 + fi + + return $next_month +} + # Generate releases table showing: # - 3 previous unsupported releases # - 1 security support release (n-2) @@ -84,15 +92,31 @@ generate_release_calendar() { # Start with 3 unsupported releases back start_minor=$((version_minor - 5)) - # Initialize the calendar table with an additional column for latest release result="| Release name | Release Date | Status | Latest Release |\n" result+="|--------------|--------------|--------|----------------|\n" + # Find the latest release month and year + local current_release_minor=$((version_minor - 1)) # Current stable release + local tag_date + tag_date=$(cd "$(git rev-parse --show-toplevel)" && git log -1 --format=%ai "v$version_major.$current_release_minor.0" 2>/dev/null || echo "") + + local current_release_month + local current_release_year + + if [ -n "$tag_date" ]; then + # Extract month and year from tag date + current_release_month=$(date -d "$tag_date" +"%m") + current_release_year=$(date -d "$tag_date" +"%Y") + else + # Default to current month/year if tag not found + current_release_month=$current_month + current_release_year=$current_year + fi + # Generate rows for each release (7 total: 3 unsupported, 1 security, 1 stable, 1 mainline, 1 next) for i in {0..6}; do # Calculate release minor version local rel_minor=$((start_minor + i)) - # Format release name without the .x local version_name="$version_major.$rel_minor" local release_date local formatted_date @@ -101,22 +125,41 @@ generate_release_calendar() { local status local formatted_version_name - # Calculate release month and year based on release pattern - # This is a simplified calculation assuming monthly releases - local rel_month=$(((current_month - (5 - i) + 12) % 12)) - [[ $rel_month -eq 0 ]] && rel_month=12 - local rel_year=$current_year - if [[ $rel_month -gt $current_month ]]; then - rel_year=$((rel_year - 1)) - fi - if [[ $rel_month -lt $current_month && $i -gt 5 ]]; then - rel_year=$((rel_year + 1)) - fi - - # Skip January releases starting from 2025 - if [[ $rel_month -eq 1 && $rel_year -ge 2025 ]]; then - rel_month=2 - # No need to reassign rel_year to itself + # Calculate the release month and year based on the current release's date + # For previous releases, go backward in the release_months array + # For future releases, go forward + local month_offset=$((i - 4)) # 4 is the index of the stable release (i=4) + + # Start from the current stable release month + local rel_month=$current_release_month + local rel_year=$current_release_year + + # Apply the offset to get the target release month + if [ $month_offset -lt 0 ]; then + # For previous releases, go backward + for ((j = 0; j > month_offset; j--)); do + rel_month=$((rel_month - 1)) + if [ $rel_month -eq 0 ]; then + rel_month=12 + rel_year=$((rel_year - 1)) + elif [ $rel_month -eq 1 ]; then + # Skip January (go from February to December of previous year) + rel_month=12 + rel_year=$((rel_year - 1)) + fi + done + elif [ $month_offset -gt 0 ]; then + # For future releases, go forward + for ((j = 0; j < month_offset; j++)); do + rel_month=$((rel_month + 1)) + if [ $rel_month -eq 13 ]; then + rel_month=2 # Skip from December to February + rel_year=$((rel_year + 1)) + elif [ $rel_month -eq 1 ]; then + # Skip January + rel_month=2 + fi + done fi # Get release date (first Tuesday of the month) @@ -147,7 +190,6 @@ generate_release_calendar() { fi # Format version name and patch link based on release status - # No links for unreleased versions if [[ "$status" == "Not Released" ]]; then formatted_version_name="$version_name" patch_link="N/A" From e562e3c882234bd7cd78b284b92d8029b97f4956 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 24 Apr 2025 22:14:21 +0100 Subject: [PATCH 024/220] chore: mark parameters as required (#17551) This adds a red asterisk next to a parameter name if it is required and marks passes the parameter required value to input and textarea form controls. The multi-select combobox needs additional work (in a separate PR) so that it can handle the required prop correctly for form submit. Screenshot 2025-04-24 at 00 02 10 --- .../workspaces/DynamicParameter/DynamicParameter.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx index c09317094a33c..d93933228be92 100644 --- a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx +++ b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx @@ -95,8 +95,12 @@ const ParameterLabel: FC = ({ parameter, isPreset }) => {