From b59aadb6fff8e06d927edd3dca42eada64a3b85e Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 11 Apr 2025 15:50:02 +0100 Subject: [PATCH 1/2] feat(codersdk/toolsdk): add template_version_id parameter to coder_create_workspace_build tool --- codersdk/toolsdk/toolsdk.go | 23 +++++++++++++-- codersdk/toolsdk/toolsdk_test.go | 49 ++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/codersdk/toolsdk/toolsdk.go b/codersdk/toolsdk/toolsdk.go index 835c37a65180e..4f79e75e1ac68 100644 --- a/codersdk/toolsdk/toolsdk.go +++ b/codersdk/toolsdk/toolsdk.go @@ -340,6 +340,11 @@ is provisioned correctly and the agent can connect to the control plane. "transition": map[string]any{ "type": "string", "description": "The transition to perform. Must be one of: start, stop, delete", + "enum": []string{"start", "stop", "delete"}, + }, + "template_version_id": map[string]any{ + "type": "string", + "description": "(Optional) The template version ID to use for the workspace build. If not provided, the previously built version will be used.", }, }, Required: []string{"workspace_id", "transition"}, @@ -358,9 +363,17 @@ is provisioned correctly and the agent can connect to the control plane. if !ok { return codersdk.WorkspaceBuild{}, xerrors.New("transition must be a string") } - return client.CreateWorkspaceBuild(ctx, workspaceID, codersdk.CreateWorkspaceBuildRequest{ + templateVersionID, err := uuidFromArgs(args, "template_version_id") + if err != nil { + return codersdk.WorkspaceBuild{}, err + } + cbr := codersdk.CreateWorkspaceBuildRequest{ Transition: codersdk.WorkspaceTransition(rawTransition), - }) + } + if templateVersionID != uuid.Nil { + cbr.TemplateVersionID = templateVersionID + } + return client.CreateWorkspaceBuild(ctx, workspaceID, cbr) }, } @@ -1232,7 +1245,11 @@ func workspaceAppStatusSlugFromContext(ctx context.Context) (string, bool) { } func uuidFromArgs(args map[string]any, key string) (uuid.UUID, error) { - raw, ok := args[key].(string) + argKey, ok := args[key] + if !ok { + return uuid.Nil, nil // No error if key is not present + } + raw, ok := argKey.(string) if !ok { return uuid.Nil, xerrors.Errorf("%s must be a string", key) } diff --git a/codersdk/toolsdk/toolsdk_test.go b/codersdk/toolsdk/toolsdk_test.go index ee48a6dd8c780..fcd1ae7d1d9f2 100644 --- a/codersdk/toolsdk/toolsdk_test.go +++ b/codersdk/toolsdk/toolsdk_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/google/uuid" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/coderdtest" @@ -154,6 +155,8 @@ func TestTools(t *testing.T) { require.NoError(t, err) require.Equal(t, codersdk.WorkspaceTransitionStop, result.Transition) require.Equal(t, r.Workspace.ID, result.WorkspaceID) + require.Equal(t, r.TemplateVersion.ID, result.TemplateVersionID) + require.Equal(t, codersdk.WorkspaceTransitionStop, result.Transition) // Important: cancel the build. We don't run any provisioners, so this // will remain in the 'pending' state indefinitely. @@ -172,11 +175,57 @@ func TestTools(t *testing.T) { require.NoError(t, err) require.Equal(t, codersdk.WorkspaceTransitionStart, result.Transition) require.Equal(t, r.Workspace.ID, result.WorkspaceID) + require.Equal(t, r.TemplateVersion.ID, result.TemplateVersionID) + require.Equal(t, codersdk.WorkspaceTransitionStart, result.Transition) // Important: cancel the build. We don't run any provisioners, so this // will remain in the 'pending' state indefinitely. require.NoError(t, client.CancelWorkspaceBuild(ctx, result.ID)) }) + + t.Run("TemplateVersionChange", func(t *testing.T) { + ctx := testutil.Context(t, testutil.WaitShort) + ctx = toolsdk.WithClient(ctx, memberClient) + + // Get the current template version ID before updating + workspace, err := memberClient.Workspace(ctx, r.Workspace.ID) + require.NoError(t, err) + originalVersionID := workspace.LatestBuild.TemplateVersionID + + // Create a new template version to update to + newVersion := dbfake.TemplateVersion(t, store). + Seed(database.TemplateVersion{ + OrganizationID: owner.OrganizationID, + CreatedBy: owner.UserID, + TemplateID: uuid.NullUUID{UUID: r.Template.ID, Valid: true}, + }).Do() + + // Update to new version + updateBuild, err := testTool(ctx, t, toolsdk.CreateWorkspaceBuild, map[string]any{ + "workspace_id": r.Workspace.ID.String(), + "transition": "start", + "template_version_id": newVersion.TemplateVersion.ID.String(), + }) + require.NoError(t, err) + require.Equal(t, codersdk.WorkspaceTransitionStart, updateBuild.Transition) + require.Equal(t, r.Workspace.ID.String(), updateBuild.WorkspaceID.String()) + require.Equal(t, newVersion.TemplateVersion.ID.String(), updateBuild.TemplateVersionID.String()) + // Cancel the build so it doesn't remain in the 'pending' state indefinitely. + require.NoError(t, client.CancelWorkspaceBuild(ctx, updateBuild.ID)) + + // Roll back to the original version + rollbackBuild, err := testTool(ctx, t, toolsdk.CreateWorkspaceBuild, map[string]any{ + "workspace_id": r.Workspace.ID.String(), + "transition": "start", + "template_version_id": originalVersionID.String(), + }) + require.NoError(t, err) + require.Equal(t, codersdk.WorkspaceTransitionStart, rollbackBuild.Transition) + require.Equal(t, r.Workspace.ID.String(), rollbackBuild.WorkspaceID.String()) + require.Equal(t, originalVersionID.String(), rollbackBuild.TemplateVersionID.String()) + // Cancel the build so it doesn't remain in the 'pending' state indefinitely. + require.NoError(t, client.CancelWorkspaceBuild(ctx, rollbackBuild.ID)) + }) }) t.Run("ListTemplateVersionParameters", func(t *testing.T) { From 637384d9939d3bb0b05939c8c4e159ca9b939186 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Fri, 11 Apr 2025 19:02:24 +0100 Subject: [PATCH 2/2] lint --- codersdk/toolsdk/toolsdk_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/codersdk/toolsdk/toolsdk_test.go b/codersdk/toolsdk/toolsdk_test.go index fcd1ae7d1d9f2..aca4045f36e8e 100644 --- a/codersdk/toolsdk/toolsdk_test.go +++ b/codersdk/toolsdk/toolsdk_test.go @@ -194,6 +194,7 @@ func TestTools(t *testing.T) { // Create a new template version to update to newVersion := dbfake.TemplateVersion(t, store). + // nolint:gocritic // This is in a test package and does not end up in the build Seed(database.TemplateVersion{ OrganizationID: owner.OrganizationID, CreatedBy: owner.UserID,