feat(coderd): implement task to app linking (#20237)

This change adds workspace build/agent/app linking to tasks and wires it
into `wsbuilder` and `provisionerdserver`.

Closes coder/internal#948
Closes coder/coder#20212
Closes coder/coder#19773
This commit is contained in:
Mathias Fredriksson
2025-10-13 12:57:06 +03:00
committed by GitHub
parent 5dc57da6b4
commit a8f87c2625
4 changed files with 225 additions and 3 deletions
+16
View File
@@ -6,6 +6,7 @@ import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
@@ -488,6 +489,21 @@ func (b *Builder) buildTx(authFunc func(action policy.Action, object rbac.Object
return BuildError{code, "insert workspace build", err}
}
// If this is a task workspace, link it to the latest workspace build.
if task, err := store.GetTaskByWorkspaceID(b.ctx, b.workspace.ID); err == nil {
_, err = store.UpsertTaskWorkspaceApp(b.ctx, database.UpsertTaskWorkspaceAppParams{
TaskID: task.ID,
WorkspaceBuildNumber: buildNum,
WorkspaceAgentID: uuid.NullUUID{}, // Updated by the provisioner upon job completion.
WorkspaceAppID: uuid.NullUUID{}, // Updated by the provisioner upon job completion.
})
if err != nil {
return BuildError{http.StatusInternalServerError, "upsert task workspace app", err}
}
} else if !errors.Is(err, sql.ErrNoRows) {
return BuildError{http.StatusInternalServerError, "get task by workspace id", err}
}
err = store.InsertWorkspaceBuildParameters(b.ctx, database.InsertWorkspaceBuildParametersParams{
WorkspaceBuildID: workspaceBuildID,
Name: names,
+105
View File
@@ -47,6 +47,7 @@ var (
lastBuildJobID = uuid.MustParse("12341234-0000-0000-000c-000000000000")
otherUserID = uuid.MustParse("12341234-0000-0000-000d-000000000000")
presetID = uuid.MustParse("12341234-0000-0000-000e-000000000000")
taskID = uuid.MustParse("12341234-0000-0000-000f-000000000000")
)
func TestBuilder_NoOptions(t *testing.T) {
@@ -94,6 +95,7 @@ func TestBuilder_NoOptions(t *testing.T) {
asrt.Equal(buildID, bld.ID)
}),
withBuild,
withNoTask,
expectBuildParameters(func(params database.InsertWorkspaceBuildParametersParams) {
asrt.Equal(buildID, params.WorkspaceBuildID)
asrt.Empty(params.Name)
@@ -140,6 +142,7 @@ func TestBuilder_Initiator(t *testing.T) {
expectBuildParameters(func(params database.InsertWorkspaceBuildParametersParams) {
}),
withBuild,
withNoTask,
)
fc := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
@@ -188,6 +191,7 @@ func TestBuilder_Baggage(t *testing.T) {
expectBuildParameters(func(params database.InsertWorkspaceBuildParametersParams) {
}),
withBuild,
withNoTask,
)
fc := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
@@ -229,6 +233,7 @@ func TestBuilder_Reason(t *testing.T) {
expectBuildParameters(func(params database.InsertWorkspaceBuildParametersParams) {
}),
withBuild,
withNoTask,
)
fc := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
@@ -275,6 +280,7 @@ func TestBuilder_ActiveVersion(t *testing.T) {
expectBuildParameters(func(params database.InsertWorkspaceBuildParametersParams) {
}),
withBuild,
withNoTask,
)
fc := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
@@ -391,6 +397,7 @@ func TestWorkspaceBuildWithTags(t *testing.T) {
expectBuildParameters(func(_ database.InsertWorkspaceBuildParametersParams) {
}),
withBuild,
withNoTask,
expectFindMatchingPresetID(uuid.Nil, sql.ErrNoRows),
)
fc := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
@@ -476,6 +483,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
}
}),
withBuild,
withNoTask,
expectFindMatchingPresetID(uuid.Nil, sql.ErrNoRows),
)
fc := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
@@ -526,6 +534,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
}
}),
withBuild,
withNoTask,
expectFindMatchingPresetID(uuid.Nil, sql.ErrNoRows),
)
fc := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
@@ -669,6 +678,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
}
}),
withBuild,
withNoTask,
expectFindMatchingPresetID(uuid.Nil, sql.ErrNoRows),
)
fc := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
@@ -735,6 +745,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
}
}),
withBuild,
withNoTask,
)
fc := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
@@ -798,6 +809,7 @@ func TestWorkspaceBuildWithRichParameters(t *testing.T) {
}
}),
withBuild,
withNoTask,
)
fc := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
@@ -860,6 +872,7 @@ func TestWorkspaceBuildWithPreset(t *testing.T) {
asrt.Equal(presetID, bld.TemplateVersionPresetID.UUID)
}),
withBuild,
withNoTask,
expectBuildParameters(func(params database.InsertWorkspaceBuildParametersParams) {
asrt.Equal(buildID, params.WorkspaceBuildID)
asrt.Empty(params.Name)
@@ -929,6 +942,7 @@ func TestWorkspaceBuildDeleteOrphan(t *testing.T) {
asrt.Equal(buildID, bld.ID)
}),
withBuild,
withNoTask,
expectBuildParameters(func(params database.InsertWorkspaceBuildParametersParams) {
asrt.Equal(buildID, params.WorkspaceBuildID)
asrt.Empty(params.Name)
@@ -992,6 +1006,7 @@ func TestWorkspaceBuildDeleteOrphan(t *testing.T) {
asrt.Equal(buildID, bld.ID)
}),
withBuild,
withNoTask,
expectBuildParameters(func(params database.InsertWorkspaceBuildParametersParams) {
asrt.Equal(buildID, params.WorkspaceBuildID)
asrt.Empty(params.Name)
@@ -1057,6 +1072,7 @@ func TestWorkspaceBuildUsageChecker(t *testing.T) {
expectFindMatchingPresetID(uuid.Nil, sql.ErrNoRows),
expectBuild(func(bld database.InsertWorkspaceBuildParams) {}),
withBuild,
withNoTask,
expectBuildParameters(func(params database.InsertWorkspaceBuildParametersParams) {}),
)
fc := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
@@ -1133,6 +1149,59 @@ func TestWorkspaceBuildUsageChecker(t *testing.T) {
}
}
func TestWorkspaceBuildWithTask(t *testing.T) {
t.Parallel()
req := require.New(t)
asrt := assert.New(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
testTask := database.Task{
ID: taskID,
OrganizationID: orgID,
OwnerID: userID,
Name: "test-task",
WorkspaceID: uuid.NullUUID{UUID: workspaceID, Valid: true},
TemplateVersionID: activeVersionID,
CreatedAt: dbtime.Now(),
}
mDB := expectDB(t,
// Inputs
withTemplate,
withInactiveVersion(nil),
withLastBuildFound,
withTemplateVersionVariables(inactiveVersionID, nil),
withRichParameters(nil),
withParameterSchemas(inactiveJobID, nil),
withWorkspaceTags(inactiveVersionID, nil),
withProvisionerDaemons([]database.GetEligibleProvisionerDaemonsByProvisionerJobIDsRow{}),
// Outputs
expectProvisionerJob(func(job database.InsertProvisionerJobParams) {}),
withInTx,
expectFindMatchingPresetID(uuid.Nil, sql.ErrNoRows),
expectBuild(func(bld database.InsertWorkspaceBuildParams) {}),
withBuild,
withTask(testTask),
expectUpsertTaskWorkspaceApp(func(params database.UpsertTaskWorkspaceAppParams) {
asrt.Equal(taskID, params.TaskID)
asrt.Equal(int32(2), params.WorkspaceBuildNumber)
asrt.False(params.WorkspaceAgentID.Valid, "workspace_agent_id should be NULL initially")
asrt.False(params.WorkspaceAppID.Valid, "workspace_app_id should be NULL initially")
}),
expectBuildParameters(func(params database.InsertWorkspaceBuildParametersParams) {}),
)
fc := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
ws := database.Workspace{ID: workspaceID, TemplateID: templateID, OwnerID: userID}
uut := wsbuilder.New(ws, database.WorkspaceTransitionStart, wsbuilder.NoopUsageChecker{})
// nolint: dogsled
_, _, _, err := uut.Build(ctx, mDB, fc, nil, audit.WorkspaceBuildBaggage{})
req.NoError(err)
}
func TestWsbuildError(t *testing.T) {
t.Parallel()
@@ -1514,3 +1583,39 @@ type fakeUsageChecker struct {
func (f *fakeUsageChecker) CheckBuildUsage(ctx context.Context, store database.Store, templateVersion *database.TemplateVersion) (wsbuilder.UsageCheckResponse, error) {
return f.checkBuildUsageFunc(ctx, store, templateVersion)
}
func withNoTask(mTx *dbmock.MockStore) {
mTx.EXPECT().GetTaskByWorkspaceID(gomock.Any(), gomock.Any()).Times(1).
DoAndReturn(func(ctx context.Context, id uuid.UUID) (database.Task, error) {
return database.Task{}, sql.ErrNoRows
})
}
func withTask(task database.Task) func(mTx *dbmock.MockStore) {
return func(mTx *dbmock.MockStore) {
mTx.EXPECT().GetTaskByWorkspaceID(gomock.Any(), gomock.Any()).Times(1).
DoAndReturn(func(ctx context.Context, id uuid.UUID) (database.Task, error) {
return task, nil
})
}
}
func expectUpsertTaskWorkspaceApp(
assertions func(database.UpsertTaskWorkspaceAppParams),
) func(mTx *dbmock.MockStore) {
return func(mTx *dbmock.MockStore) {
mTx.EXPECT().UpsertTaskWorkspaceApp(gomock.Any(), gomock.Any()).
Times(1).
DoAndReturn(
func(ctx context.Context, params database.UpsertTaskWorkspaceAppParams) (database.TaskWorkspaceApp, error) {
assertions(params)
return database.TaskWorkspaceApp{
TaskID: params.TaskID,
WorkspaceBuildNumber: params.WorkspaceBuildNumber,
WorkspaceAgentID: params.WorkspaceAgentID,
WorkspaceAppID: params.WorkspaceAppID,
}, nil
},
)
}
}