Skip to content

feat: claim prebuilds based on workspace parameters instead of preset id #19279

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Merge remote-tracking branch 'origin/main' into jjs/18356
  • Loading branch information
SasSwart committed Aug 11, 2025
commit 268db8a06bb999c95ef019e798646ad21e1d0e2f
345 changes: 345 additions & 0 deletions coderd/database/querier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6286,3 +6286,348 @@ func TestSetWorkspaceBuildPresetByParameterHash(t *testing.T) {
require.Equal(t, tmplV1.preset.ID, updatedBuild.TemplateVersionPresetID.UUID)
})
}

func TestUserSecretsCRUDOperations(t *testing.T) {
t.Parallel()

// Use raw database without dbauthz wrapper for this test
db, _ := dbtestutil.NewDB(t)
ctx := testutil.Context(t, testutil.WaitMedium)

t.Run("FullCRUDWorkflow", func(t *testing.T) {
t.Parallel()

// Create a new user for this test
testUser := dbgen.User(t, db, database.User{})

// 1. CREATE
secretID := uuid.New()
createParams := database.CreateUserSecretParams{
ID: secretID,
UserID: testUser.ID,
Name: "workflow-secret",
Description: "Secret for full CRUD workflow",
Value: "workflow-value",
EnvName: "WORKFLOW_ENV",
FilePath: "/workflow/path",
}

createdSecret, err := db.CreateUserSecret(ctx, createParams)
require.NoError(t, err)
assert.Equal(t, secretID, createdSecret.ID)

// 2. READ by ID
readSecret, err := db.GetUserSecret(ctx, createdSecret.ID)
require.NoError(t, err)
assert.Equal(t, createdSecret.ID, readSecret.ID)
assert.Equal(t, "workflow-secret", readSecret.Name)

// 3. READ by UserID and Name
readByNameParams := database.GetUserSecretByUserIDAndNameParams{
UserID: testUser.ID,
Name: "workflow-secret",
}
readByNameSecret, err := db.GetUserSecretByUserIDAndName(ctx, readByNameParams)
require.NoError(t, err)
assert.Equal(t, createdSecret.ID, readByNameSecret.ID)

// 4. LIST
secrets, err := db.ListUserSecrets(ctx, testUser.ID)
require.NoError(t, err)
require.Len(t, secrets, 1)
assert.Equal(t, createdSecret.ID, secrets[0].ID)

// 5. UPDATE
updateParams := database.UpdateUserSecretParams{
ID: createdSecret.ID,
Description: "Updated workflow description",
Value: "updated-workflow-value",
EnvName: "UPDATED_WORKFLOW_ENV",
FilePath: "/updated/workflow/path",
}

updatedSecret, err := db.UpdateUserSecret(ctx, updateParams)
require.NoError(t, err)
assert.Equal(t, "Updated workflow description", updatedSecret.Description)
assert.Equal(t, "updated-workflow-value", updatedSecret.Value)

// 6. DELETE
err = db.DeleteUserSecret(ctx, createdSecret.ID)
require.NoError(t, err)

// Verify deletion
_, err = db.GetUserSecret(ctx, createdSecret.ID)
require.Error(t, err)
assert.Contains(t, err.Error(), "no rows in result set")

// Verify list is empty
secrets, err = db.ListUserSecrets(ctx, testUser.ID)
require.NoError(t, err)
assert.Len(t, secrets, 0)
})

t.Run("UniqueConstraints", func(t *testing.T) {
t.Parallel()

// Create a new user for this test
testUser := dbgen.User(t, db, database.User{})

// Create first secret
secret1 := dbgen.UserSecret(t, db, database.UserSecret{
UserID: testUser.ID,
Name: "unique-test",
Description: "First secret",
Value: "value1",
EnvName: "UNIQUE_ENV",
FilePath: "/unique/path",
})

// Try to create another secret with the same name (should fail)
_, err := db.CreateUserSecret(ctx, database.CreateUserSecretParams{
UserID: testUser.ID,
Name: "unique-test", // Same name
Description: "Second secret",
Value: "value2",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "duplicate key value")

// Try to create another secret with the same env_name (should fail)
_, err = db.CreateUserSecret(ctx, database.CreateUserSecretParams{
UserID: testUser.ID,
Name: "unique-test-2",
Description: "Second secret",
Value: "value2",
EnvName: "UNIQUE_ENV", // Same env_name
})
require.Error(t, err)
assert.Contains(t, err.Error(), "duplicate key value")

// Try to create another secret with the same file_path (should fail)
_, err = db.CreateUserSecret(ctx, database.CreateUserSecretParams{
UserID: testUser.ID,
Name: "unique-test-3",
Description: "Second secret",
Value: "value2",
FilePath: "/unique/path", // Same file_path
})
require.Error(t, err)
assert.Contains(t, err.Error(), "duplicate key value")

// Create secret with empty env_name and file_path (should succeed)
secret2 := dbgen.UserSecret(t, db, database.UserSecret{
UserID: testUser.ID,
Name: "unique-test-4",
Description: "Second secret",
Value: "value2",
EnvName: "", // Empty env_name
FilePath: "", // Empty file_path
})

// Verify both secrets exist
_, err = db.GetUserSecret(ctx, secret1.ID)
require.NoError(t, err)
_, err = db.GetUserSecret(ctx, secret2.ID)
require.NoError(t, err)
})
}

func TestUserSecretsAuthorization(t *testing.T) {
t.Parallel()

// Use raw database and wrap with dbauthz for authorization testing
db, _ := dbtestutil.NewDB(t)
authorizer := rbac.NewStrictCachingAuthorizer(prometheus.NewRegistry())
authDB := dbauthz.New(db, authorizer, slogtest.Make(t, &slogtest.Options{}), coderdtest.AccessControlStorePointer())
ctx := testutil.Context(t, testutil.WaitMedium)

// Create test users
user1 := dbgen.User(t, db, database.User{})
user2 := dbgen.User(t, db, database.User{})
owner := dbgen.User(t, db, database.User{})
orgAdmin := dbgen.User(t, db, database.User{})

// Create organization for org-scoped roles
org := dbgen.Organization(t, db, database.Organization{})

// Create secrets for users
user1Secret := dbgen.UserSecret(t, db, database.UserSecret{
UserID: user1.ID,
Name: "user1-secret",
Description: "User 1's secret",
Value: "user1-value",
})

user2Secret := dbgen.UserSecret(t, db, database.UserSecret{
UserID: user2.ID,
Name: "user2-secret",
Description: "User 2's secret",
Value: "user2-value",
})

testCases := []struct {
name string
subject rbac.Subject
secretID uuid.UUID
expectedAccess bool
}{
{
name: "UserCanAccessOwnSecrets",
subject: rbac.Subject{
ID: user1.ID.String(),
Roles: rbac.RoleIdentifiers{rbac.RoleMember()},
Scope: rbac.ScopeAll,
},
secretID: user1Secret.ID,
expectedAccess: true,
},
{
name: "UserCannotAccessOtherUserSecrets",
subject: rbac.Subject{
ID: user1.ID.String(),
Roles: rbac.RoleIdentifiers{rbac.RoleMember()},
Scope: rbac.ScopeAll,
},
secretID: user2Secret.ID,
expectedAccess: false,
},
{
name: "OwnerCannotAccessUserSecrets",
subject: rbac.Subject{
ID: owner.ID.String(),
Roles: rbac.RoleIdentifiers{rbac.RoleOwner()},
Scope: rbac.ScopeAll,
},
secretID: user1Secret.ID,
expectedAccess: false,
},
{
name: "OrgAdminCannotAccessUserSecrets",
subject: rbac.Subject{
ID: orgAdmin.ID.String(),
Roles: rbac.RoleIdentifiers{rbac.ScopedRoleOrgAdmin(org.ID)},
Scope: rbac.ScopeAll,
},
secretID: user1Secret.ID,
expectedAccess: false,
},
}

for _, tc := range testCases {
tc := tc // capture range variable
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

authCtx := dbauthz.As(ctx, tc.subject)

// Test GetUserSecret
_, err := authDB.GetUserSecret(authCtx, tc.secretID)

if tc.expectedAccess {
require.NoError(t, err, "expected access to be granted")
} else {
require.Error(t, err, "expected access to be denied")
assert.True(t, dbauthz.IsNotAuthorizedError(err), "expected authorization error")
}
})
}
}

func TestWorkspaceBuildDeadlineConstraint(t *testing.T) {
t.Parallel()

ctx := testutil.Context(t, testutil.WaitLong)

db, _ := dbtestutil.NewDB(t)
org := dbgen.Organization(t, db, database.Organization{})
user := dbgen.User(t, db, database.User{})
template := dbgen.Template(t, db, database.Template{
CreatedBy: user.ID,
OrganizationID: org.ID,
})
templateVersion := dbgen.TemplateVersion(t, db, database.TemplateVersion{
TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true},
OrganizationID: org.ID,
CreatedBy: user.ID,
})
workspace := dbgen.Workspace(t, db, database.WorkspaceTable{
OwnerID: user.ID,
TemplateID: template.ID,
Name: "test-workspace",
Deleted: false,
})
job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
OrganizationID: org.ID,
InitiatorID: database.PrebuildsSystemUserID,
Provisioner: database.ProvisionerTypeEcho,
Type: database.ProvisionerJobTypeWorkspaceBuild,
StartedAt: sql.NullTime{Time: time.Now().Add(-time.Minute), Valid: true},
CompletedAt: sql.NullTime{Time: time.Now(), Valid: true},
})
workspaceBuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
WorkspaceID: workspace.ID,
TemplateVersionID: templateVersion.ID,
JobID: job.ID,
BuildNumber: 1,
})

cases := []struct {
name string
deadline time.Time
maxDeadline time.Time
expectOK bool
}{
{
name: "no deadline or max_deadline",
deadline: time.Time{},
maxDeadline: time.Time{},
expectOK: true,
},
{
name: "deadline set when max_deadline is not set",
deadline: time.Now().Add(time.Hour),
maxDeadline: time.Time{},
expectOK: true,
},
{
name: "deadline before max_deadline",
deadline: time.Now().Add(-time.Hour),
maxDeadline: time.Now().Add(time.Hour),
expectOK: true,
},
{
name: "deadline is max_deadline",
deadline: time.Now().Add(time.Hour),
maxDeadline: time.Now().Add(time.Hour),
expectOK: true,
},

{
name: "deadline after max_deadline",
deadline: time.Now().Add(time.Hour),
maxDeadline: time.Now().Add(-time.Hour),
expectOK: false,
},
{
name: "deadline is not set when max_deadline is set",
deadline: time.Time{},
maxDeadline: time.Now().Add(time.Hour),
expectOK: false,
},
}

for _, c := range cases {
err := db.UpdateWorkspaceBuildDeadlineByID(ctx, database.UpdateWorkspaceBuildDeadlineByIDParams{
ID: workspaceBuild.ID,
Deadline: c.deadline,
MaxDeadline: c.maxDeadline,
UpdatedAt: time.Now(),
})
if c.expectOK {
require.NoError(t, err)
} else {
require.Error(t, err)
require.True(t, database.IsCheckViolation(err, database.CheckWorkspaceBuildsDeadlineBelowMaxDeadline))
}
}
}
Loading
You are viewing a condensed version of this merge commit. You can view the full changes here.