feat: persist prebuild definitions on template import (#16951)

This PR allows provisioners to recognise and report prebuild definitions
to the coder control plane. It also allows the coder control plane to
then persist these to its store.

closes https://github.com/coder/internal/issues/507

---------

Signed-off-by: Danny Kopping <dannykopping@gmail.com>
Co-authored-by: Danny Kopping <dannykopping@gmail.com>
Co-authored-by: evgeniy-scherbina <evgeniy.shcherbina.es@gmail.com>
This commit is contained in:
Sas Swart
2025-04-07 10:35:28 +02:00
committed by GitHub
co-authored by Danny Kopping evgeniy-scherbina
parent 074ec2887d
commit 0b2b643ce2
25 changed files with 1207 additions and 843 deletions
+13 -3
View File
@@ -2187,14 +2187,24 @@ func (q *querier) GetPresetByWorkspaceBuildID(ctx context.Context, workspaceID u
return q.db.GetPresetByWorkspaceBuildID(ctx, workspaceID)
}
func (q *querier) GetPresetParametersByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionPresetParameter, error) {
func (q *querier) GetPresetParametersByPresetID(ctx context.Context, presetID uuid.UUID) ([]database.TemplateVersionPresetParameter, error) {
// An actor can read template version presets if they can read the related template version.
_, err := q.GetTemplateVersionByID(ctx, templateVersionID)
_, err := q.GetPresetByID(ctx, presetID)
if err != nil {
return nil, err
}
return q.db.GetPresetParametersByTemplateVersionID(ctx, templateVersionID)
return q.db.GetPresetParametersByPresetID(ctx, presetID)
}
func (q *querier) GetPresetParametersByTemplateVersionID(ctx context.Context, args uuid.UUID) ([]database.TemplateVersionPresetParameter, error) {
// An actor can read template version presets if they can read the related template version.
_, err := q.GetTemplateVersionByID(ctx, args)
if err != nil {
return nil, err
}
return q.db.GetPresetParametersByTemplateVersionID(ctx, args)
}
func (q *querier) GetPresetsBackoff(ctx context.Context, lookback time.Time) ([]database.GetPresetsBackoffRow, error) {
+124 -96
View File
@@ -182,7 +182,6 @@ func TestDBAuthzRecursive(t *testing.T) {
method.Name == "PGLocks" {
continue
}
// Log the name of the last method, so if there is a panic, it is
// easy to know which method failed.
// t.Log(method.Name)
// Call the function. Any infinite recursion will stack overflow.
@@ -969,8 +968,7 @@ func (s *MethodTestSuite) TestOrganization() {
TemplateVersionID: workspaceBuild.TemplateVersionID,
Name: "test",
}
preset, err := db.InsertPreset(context.Background(), insertPresetParams)
require.NoError(s.T(), err)
preset := dbgen.Preset(s.T(), db, insertPresetParams)
insertPresetParametersParams := database.InsertPresetParametersParams{
TemplateVersionPresetID: preset.ID,
Names: []string{"test"},
@@ -1027,8 +1025,8 @@ func (s *MethodTestSuite) TestOrganization() {
})
check.Args(database.OrganizationMembersParams{
OrganizationID: uuid.UUID{},
UserID: uuid.UUID{},
OrganizationID: o.ID,
UserID: u.ID,
}).Asserts(
mem, policy.ActionRead,
)
@@ -3906,96 +3904,6 @@ func (s *MethodTestSuite) TestSystemFunctions() {
ErrorsWithInMemDB(sql.ErrNoRows).
Returns([]database.ParameterSchema{})
}))
s.Run("GetPresetByWorkspaceBuildID", s.Subtest(func(db database.Store, check *expects) {
org := dbgen.Organization(s.T(), db, database.Organization{})
user := dbgen.User(s.T(), db, database.User{})
template := dbgen.Template(s.T(), db, database.Template{
CreatedBy: user.ID,
OrganizationID: org.ID,
})
templateVersion := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{
TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true},
OrganizationID: org.ID,
CreatedBy: user.ID,
})
preset, err := db.InsertPreset(context.Background(), database.InsertPresetParams{
TemplateVersionID: templateVersion.ID,
Name: "test",
})
require.NoError(s.T(), err)
workspace := dbgen.Workspace(s.T(), db, database.WorkspaceTable{
OrganizationID: org.ID,
OwnerID: user.ID,
TemplateID: template.ID,
})
job := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{
OrganizationID: org.ID,
})
workspaceBuild := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{
WorkspaceID: workspace.ID,
TemplateVersionID: templateVersion.ID,
TemplateVersionPresetID: uuid.NullUUID{UUID: preset.ID, Valid: true},
InitiatorID: user.ID,
JobID: job.ID,
})
_, err = db.GetPresetByWorkspaceBuildID(context.Background(), workspaceBuild.ID)
require.NoError(s.T(), err)
check.Args(workspaceBuild.ID).Asserts(rbac.ResourceTemplate, policy.ActionRead)
}))
s.Run("GetPresetParametersByTemplateVersionID", s.Subtest(func(db database.Store, check *expects) {
ctx := context.Background()
org := dbgen.Organization(s.T(), db, database.Organization{})
user := dbgen.User(s.T(), db, database.User{})
template := dbgen.Template(s.T(), db, database.Template{
CreatedBy: user.ID,
OrganizationID: org.ID,
})
templateVersion := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{
TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true},
OrganizationID: org.ID,
CreatedBy: user.ID,
})
preset, err := db.InsertPreset(ctx, database.InsertPresetParams{
TemplateVersionID: templateVersion.ID,
Name: "test",
})
require.NoError(s.T(), err)
_, err = db.InsertPresetParameters(ctx, database.InsertPresetParametersParams{
TemplateVersionPresetID: preset.ID,
Names: []string{"test"},
Values: []string{"test"},
})
require.NoError(s.T(), err)
presetParameters, err := db.GetPresetParametersByTemplateVersionID(ctx, templateVersion.ID)
require.NoError(s.T(), err)
check.Args(templateVersion.ID).Asserts(template.RBACObject(), policy.ActionRead).Returns(presetParameters)
}))
s.Run("GetPresetsByTemplateVersionID", s.Subtest(func(db database.Store, check *expects) {
ctx := context.Background()
org := dbgen.Organization(s.T(), db, database.Organization{})
user := dbgen.User(s.T(), db, database.User{})
template := dbgen.Template(s.T(), db, database.Template{
CreatedBy: user.ID,
OrganizationID: org.ID,
})
templateVersion := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{
TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true},
OrganizationID: org.ID,
CreatedBy: user.ID,
})
_, err := db.InsertPreset(ctx, database.InsertPresetParams{
TemplateVersionID: templateVersion.ID,
Name: "test",
})
require.NoError(s.T(), err)
presets, err := db.GetPresetsByTemplateVersionID(ctx, templateVersion.ID)
require.NoError(s.T(), err)
check.Args(templateVersion.ID).Asserts(template.RBACObject(), policy.ActionRead).Returns(presets)
}))
s.Run("GetWorkspaceAppsByAgentIDs", s.Subtest(func(db database.Store, check *expects) {
dbtestutil.DisableForeignKeysAndTriggers(s.T(), db)
aWs := dbgen.Workspace(s.T(), db, database.WorkspaceTable{})
@@ -4839,6 +4747,125 @@ func (s *MethodTestSuite) TestNotifications() {
}
func (s *MethodTestSuite) TestPrebuilds() {
s.Run("GetPresetByWorkspaceBuildID", s.Subtest(func(db database.Store, check *expects) {
org := dbgen.Organization(s.T(), db, database.Organization{})
user := dbgen.User(s.T(), db, database.User{})
template := dbgen.Template(s.T(), db, database.Template{
CreatedBy: user.ID,
OrganizationID: org.ID,
})
templateVersion := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{
TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true},
OrganizationID: org.ID,
CreatedBy: user.ID,
})
preset, err := db.InsertPreset(context.Background(), database.InsertPresetParams{
TemplateVersionID: templateVersion.ID,
Name: "test",
})
require.NoError(s.T(), err)
workspace := dbgen.Workspace(s.T(), db, database.WorkspaceTable{
OrganizationID: org.ID,
OwnerID: user.ID,
TemplateID: template.ID,
})
job := dbgen.ProvisionerJob(s.T(), db, nil, database.ProvisionerJob{
OrganizationID: org.ID,
})
workspaceBuild := dbgen.WorkspaceBuild(s.T(), db, database.WorkspaceBuild{
WorkspaceID: workspace.ID,
TemplateVersionID: templateVersion.ID,
TemplateVersionPresetID: uuid.NullUUID{UUID: preset.ID, Valid: true},
InitiatorID: user.ID,
JobID: job.ID,
})
_, err = db.GetPresetByWorkspaceBuildID(context.Background(), workspaceBuild.ID)
require.NoError(s.T(), err)
check.Args(workspaceBuild.ID).Asserts(rbac.ResourceTemplate, policy.ActionRead)
}))
s.Run("GetPresetParametersByTemplateVersionID", s.Subtest(func(db database.Store, check *expects) {
ctx := context.Background()
org := dbgen.Organization(s.T(), db, database.Organization{})
user := dbgen.User(s.T(), db, database.User{})
template := dbgen.Template(s.T(), db, database.Template{
CreatedBy: user.ID,
OrganizationID: org.ID,
})
templateVersion := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{
TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true},
OrganizationID: org.ID,
CreatedBy: user.ID,
})
preset, err := db.InsertPreset(ctx, database.InsertPresetParams{
TemplateVersionID: templateVersion.ID,
Name: "test",
})
require.NoError(s.T(), err)
insertedParameters, err := db.InsertPresetParameters(ctx, database.InsertPresetParametersParams{
TemplateVersionPresetID: preset.ID,
Names: []string{"test"},
Values: []string{"test"},
})
require.NoError(s.T(), err)
check.
Args(templateVersion.ID).
Asserts(template.RBACObject(), policy.ActionRead).
Returns(insertedParameters)
}))
s.Run("GetPresetParametersByPresetID", s.Subtest(func(db database.Store, check *expects) {
ctx := context.Background()
org := dbgen.Organization(s.T(), db, database.Organization{})
user := dbgen.User(s.T(), db, database.User{})
template := dbgen.Template(s.T(), db, database.Template{
CreatedBy: user.ID,
OrganizationID: org.ID,
})
templateVersion := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{
TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true},
OrganizationID: org.ID,
CreatedBy: user.ID,
})
preset, err := db.InsertPreset(ctx, database.InsertPresetParams{
TemplateVersionID: templateVersion.ID,
Name: "test",
})
require.NoError(s.T(), err)
insertedParameters, err := db.InsertPresetParameters(ctx, database.InsertPresetParametersParams{
TemplateVersionPresetID: preset.ID,
Names: []string{"test"},
Values: []string{"test"},
})
require.NoError(s.T(), err)
check.
Args(preset.ID).
Asserts(template.RBACObject(), policy.ActionRead).
Returns(insertedParameters)
}))
s.Run("GetPresetsByTemplateVersionID", s.Subtest(func(db database.Store, check *expects) {
ctx := context.Background()
org := dbgen.Organization(s.T(), db, database.Organization{})
user := dbgen.User(s.T(), db, database.User{})
template := dbgen.Template(s.T(), db, database.Template{
CreatedBy: user.ID,
OrganizationID: org.ID,
})
templateVersion := dbgen.TemplateVersion(s.T(), db, database.TemplateVersion{
TemplateID: uuid.NullUUID{UUID: template.ID, Valid: true},
OrganizationID: org.ID,
CreatedBy: user.ID,
})
_, err := db.InsertPreset(ctx, database.InsertPresetParams{
TemplateVersionID: templateVersion.ID,
Name: "test",
})
require.NoError(s.T(), err)
presets, err := db.GetPresetsByTemplateVersionID(ctx, templateVersion.ID)
require.NoError(s.T(), err)
check.Args(templateVersion.ID).Asserts(template.RBACObject(), policy.ActionRead).Returns(presets)
}))
s.Run("ClaimPrebuiltWorkspace", s.Subtest(func(db database.Store, check *expects) {
org := dbgen.Organization(s.T(), db, database.Organization{})
user := dbgen.User(s.T(), db, database.User{})
@@ -4923,7 +4950,8 @@ func (s *MethodTestSuite) TestPrebuilds() {
UUID: template.ID,
Valid: true,
},
OrganizationID: org.ID,
InvalidateAfterSecs: preset.InvalidateAfterSecs,
OrganizationID: org.ID,
})
}))
}
+20 -1
View File
@@ -4275,6 +4275,21 @@ func (q *FakeQuerier) GetPresetByWorkspaceBuildID(_ context.Context, workspaceBu
return database.TemplateVersionPreset{}, sql.ErrNoRows
}
func (q *FakeQuerier) GetPresetParametersByPresetID(_ context.Context, presetID uuid.UUID) ([]database.TemplateVersionPresetParameter, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
parameters := make([]database.TemplateVersionPresetParameter, 0)
for _, parameter := range q.presetParameters {
if parameter.TemplateVersionPresetID != presetID {
continue
}
parameters = append(parameters, parameter)
}
return parameters, nil
}
func (q *FakeQuerier) GetPresetParametersByTemplateVersionID(_ context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionPresetParameter, error) {
q.mutex.RLock()
defer q.mutex.RUnlock()
@@ -4293,7 +4308,6 @@ func (q *FakeQuerier) GetPresetParametersByTemplateVersionID(_ context.Context,
continue
}
parameters = append(parameters, parameter)
break
}
}
@@ -8854,6 +8868,11 @@ func (q *FakeQuerier) InsertPreset(_ context.Context, arg database.InsertPresetP
TemplateVersionID: arg.TemplateVersionID,
Name: arg.Name,
CreatedAt: arg.CreatedAt,
DesiredInstances: arg.DesiredInstances,
InvalidateAfterSecs: sql.NullInt32{
Int32: 0,
Valid: true,
},
}
q.presets = append(q.presets, preset)
return preset, nil
@@ -1110,6 +1110,13 @@ func (m queryMetricsStore) GetPresetByWorkspaceBuildID(ctx context.Context, work
return r0, r1
}
func (m queryMetricsStore) GetPresetParametersByPresetID(ctx context.Context, presetID uuid.UUID) ([]database.TemplateVersionPresetParameter, error) {
start := time.Now()
r0, r1 := m.s.GetPresetParametersByPresetID(ctx, presetID)
m.queryLatencies.WithLabelValues("GetPresetParametersByPresetID").Observe(time.Since(start).Seconds())
return r0, r1
}
func (m queryMetricsStore) GetPresetParametersByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionPresetParameter, error) {
start := time.Now()
r0, r1 := m.s.GetPresetParametersByTemplateVersionID(ctx, templateVersionID)
+15
View File
@@ -2269,6 +2269,21 @@ func (mr *MockStoreMockRecorder) GetPresetByWorkspaceBuildID(ctx, workspaceBuild
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPresetByWorkspaceBuildID", reflect.TypeOf((*MockStore)(nil).GetPresetByWorkspaceBuildID), ctx, workspaceBuildID)
}
// GetPresetParametersByPresetID mocks base method.
func (m *MockStore) GetPresetParametersByPresetID(ctx context.Context, presetID uuid.UUID) ([]database.TemplateVersionPresetParameter, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetPresetParametersByPresetID", ctx, presetID)
ret0, _ := ret[0].([]database.TemplateVersionPresetParameter)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetPresetParametersByPresetID indicates an expected call of GetPresetParametersByPresetID.
func (mr *MockStoreMockRecorder) GetPresetParametersByPresetID(ctx, presetID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPresetParametersByPresetID", reflect.TypeOf((*MockStore)(nil).GetPresetParametersByPresetID), ctx, presetID)
}
// GetPresetParametersByTemplateVersionID mocks base method.
func (m *MockStore) GetPresetParametersByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]database.TemplateVersionPresetParameter, error) {
m.ctrl.T.Helper()
+1
View File
@@ -237,6 +237,7 @@ type sqlcQuerier interface {
GetPrebuildMetrics(ctx context.Context) ([]GetPrebuildMetricsRow, error)
GetPresetByID(ctx context.Context, presetID uuid.UUID) (GetPresetByIDRow, error)
GetPresetByWorkspaceBuildID(ctx context.Context, workspaceBuildID uuid.UUID) (TemplateVersionPreset, error)
GetPresetParametersByPresetID(ctx context.Context, presetID uuid.UUID) ([]TemplateVersionPresetParameter, error)
GetPresetParametersByTemplateVersionID(ctx context.Context, templateVersionID uuid.UUID) ([]TemplateVersionPresetParameter, error)
// GetPresetsBackoff groups workspace builds by preset ID.
// Each preset is associated with exactly one template version ID.
+37
View File
@@ -6389,6 +6389,43 @@ func (q *sqlQuerier) GetPresetByWorkspaceBuildID(ctx context.Context, workspaceB
return i, err
}
const getPresetParametersByPresetID = `-- name: GetPresetParametersByPresetID :many
SELECT
tvpp.id, tvpp.template_version_preset_id, tvpp.name, tvpp.value
FROM
template_version_preset_parameters tvpp
WHERE
tvpp.template_version_preset_id = $1
`
func (q *sqlQuerier) GetPresetParametersByPresetID(ctx context.Context, presetID uuid.UUID) ([]TemplateVersionPresetParameter, error) {
rows, err := q.db.QueryContext(ctx, getPresetParametersByPresetID, presetID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []TemplateVersionPresetParameter
for rows.Next() {
var i TemplateVersionPresetParameter
if err := rows.Scan(
&i.ID,
&i.TemplateVersionPresetID,
&i.Name,
&i.Value,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getPresetParametersByTemplateVersionID = `-- name: GetPresetParametersByTemplateVersionID :many
SELECT
template_version_preset_parameters.id, template_version_preset_parameters.template_version_preset_id, template_version_preset_parameters.name, template_version_preset_parameters.value
+8
View File
@@ -49,6 +49,14 @@ FROM
WHERE
template_version_presets.template_version_id = @template_version_id;
-- name: GetPresetParametersByPresetID :many
SELECT
tvpp.*
FROM
template_version_preset_parameters tvpp
WHERE
tvpp.template_version_preset_id = @preset_id;
-- name: GetPresetByID :one
SELECT tvp.*, tv.template_id, tv.organization_id FROM
template_version_presets tvp
+3 -7
View File
@@ -8,6 +8,7 @@ import (
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk"
@@ -86,16 +87,12 @@ func TestTemplateVersionPresets(t *testing.T) {
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
// nolint:gocritic // This is a test
provisionerCtx := dbauthz.AsProvisionerd(ctx)
// Insert all presets for this test case
for _, givenPreset := range tc.presets {
dbPreset, err := db.InsertPreset(provisionerCtx, database.InsertPresetParams{
dbPreset := dbgen.Preset(t, db, database.InsertPresetParams{
Name: givenPreset.Name,
TemplateVersionID: version.ID,
})
require.NoError(t, err)
if len(givenPreset.Parameters) > 0 {
var presetParameterNames []string
@@ -104,12 +101,11 @@ func TestTemplateVersionPresets(t *testing.T) {
presetParameterNames = append(presetParameterNames, presetParameter.Name)
presetParameterValues = append(presetParameterValues, presetParameter.Value)
}
_, err = db.InsertPresetParameters(provisionerCtx, database.InsertPresetParametersParams{
dbgen.PresetParameter(t, db, database.InsertPresetParametersParams{
TemplateVersionPresetID: dbPreset.ID,
Names: presetParameterNames,
Values: presetParameterValues,
})
require.NoError(t, err)
}
}
@@ -1855,12 +1855,22 @@ func InsertWorkspacePresetsAndParameters(ctx context.Context, logger slog.Logger
func InsertWorkspacePresetAndParameters(ctx context.Context, db database.Store, templateVersionID uuid.UUID, protoPreset *sdkproto.Preset, t time.Time) error {
err := db.InTx(func(tx database.Store) error {
var desiredInstances sql.NullInt32
if protoPreset != nil && protoPreset.Prebuild != nil {
desiredInstances = sql.NullInt32{
Int32: protoPreset.Prebuild.Instances,
Valid: true,
}
}
dbPreset, err := tx.InsertPreset(ctx, database.InsertPresetParams{
TemplateVersionID: templateVersionID,
Name: protoPreset.Name,
CreatedAt: t,
DesiredInstances: sql.NullInt32{},
InvalidateAfterSecs: sql.NullInt32{},
TemplateVersionID: templateVersionID,
Name: protoPreset.Name,
CreatedAt: t,
DesiredInstances: desiredInstances,
InvalidateAfterSecs: sql.NullInt32{
Int32: 0,
Valid: false,
}, // TODO: implement cache invalidation
})
if err != nil {
return xerrors.Errorf("insert preset: %w", err)
@@ -1880,6 +1890,7 @@ func InsertWorkspacePresetAndParameters(ctx context.Context, db database.Store,
if err != nil {
return xerrors.Errorf("insert preset parameters: %w", err)
}
return nil
}, nil)
if err != nil {
@@ -1733,6 +1733,34 @@ func TestInsertWorkspacePresetsAndParameters(t *testing.T) {
},
},
},
{
name: "one preset, no parameters, requesting prebuilds",
givenPresets: []*sdkproto.Preset{
{
Name: "preset1",
Prebuild: &sdkproto.Prebuild{
Instances: 1,
},
},
},
},
{
name: "one preset with multiple parameters, requesting 0 prebuilds",
givenPresets: []*sdkproto.Preset{
{
Name: "preset1",
Parameters: []*sdkproto.PresetParameter{
{
Name: "param1",
Value: "value1",
},
},
Prebuild: &sdkproto.Prebuild{
Instances: 0,
},
},
},
},
{
name: "one preset with multiple parameters",
givenPresets: []*sdkproto.Preset{
@@ -1751,6 +1779,27 @@ func TestInsertWorkspacePresetsAndParameters(t *testing.T) {
},
},
},
{
name: "one preset, multiple parameters, requesting prebuilds",
givenPresets: []*sdkproto.Preset{
{
Name: "preset1",
Parameters: []*sdkproto.PresetParameter{
{
Name: "param1",
Value: "value1",
},
{
Name: "param2",
Value: "value2",
},
},
Prebuild: &sdkproto.Prebuild{
Instances: 1,
},
},
},
},
{
name: "multiple presets with parameters",
givenPresets: []*sdkproto.Preset{
@@ -1766,6 +1815,9 @@ func TestInsertWorkspacePresetsAndParameters(t *testing.T) {
Value: "value2",
},
},
Prebuild: &sdkproto.Prebuild{
Instances: 1,
},
},
{
Name: "preset2",
@@ -1794,6 +1846,7 @@ func TestInsertWorkspacePresetsAndParameters(t *testing.T) {
db, ps := dbtestutil.NewDB(t)
org := dbgen.Organization(t, db, database.Organization{})
user := dbgen.User(t, db, database.User{})
job := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{
Type: database.ProvisionerJobTypeWorkspaceBuild,
OrganizationID: org.ID,
@@ -1820,42 +1873,37 @@ func TestInsertWorkspacePresetsAndParameters(t *testing.T) {
require.Len(t, gotPresets, len(c.givenPresets))
for _, givenPreset := range c.givenPresets {
foundMatch := false
var foundPreset *database.TemplateVersionPreset
for _, gotPreset := range gotPresets {
if givenPreset.Name == gotPreset.Name {
foundMatch = true
foundPreset = &gotPreset
break
}
}
require.True(t, foundMatch, "preset %s not found in parameters", givenPreset.Name)
}
require.NotNil(t, foundPreset, "preset %s not found in parameters", givenPreset.Name)
gotPresetParameters, err := db.GetPresetParametersByTemplateVersionID(ctx, templateVersion.ID)
require.NoError(t, err)
gotPresetParameters, err := db.GetPresetParametersByPresetID(ctx, foundPreset.ID)
require.NoError(t, err)
require.Len(t, gotPresetParameters, len(givenPreset.Parameters))
for _, givenPreset := range c.givenPresets {
for _, givenParameter := range givenPreset.Parameters {
foundMatch := false
for _, gotParameter := range gotPresetParameters {
nameMatches := givenParameter.Name == gotParameter.Name
valueMatches := givenParameter.Value == gotParameter.Value
// ensure that preset parameters are matched to the correct preset:
var gotPreset database.TemplateVersionPreset
for _, preset := range gotPresets {
if preset.ID == gotParameter.TemplateVersionPresetID {
gotPreset = preset
break
}
}
presetMatches := gotPreset.Name == givenPreset.Name
if nameMatches && valueMatches && presetMatches {
if nameMatches && valueMatches {
foundMatch = true
break
}
}
require.True(t, foundMatch, "preset parameter %s not found in presets", givenParameter.Name)
require.True(t, foundMatch, "preset parameter %s not found in parameters", givenParameter.Name)
}
if givenPreset.Prebuild == nil {
require.False(t, foundPreset.DesiredInstances.Valid)
}
if givenPreset.Prebuild != nil {
require.True(t, foundPreset.DesiredInstances.Valid)
require.Equal(t, givenPreset.Prebuild.Instances, foundPreset.DesiredInstances.Int32)
}
}
})
+6
View File
@@ -6,6 +6,8 @@ import (
"testing"
"time"
"github.com/coder/coder/v2/coderd/prebuilds"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
@@ -830,6 +832,9 @@ func TestGroup(t *testing.T) {
_, user2 := coderdtest.CreateAnotherUser(t, client, user.OrganizationID)
ctx := testutil.Context(t, testutil.WaitLong)
// nolint:gocritic // "This client is operating as the owner user" is fine in this case.
prebuildsUser, err := client.User(ctx, prebuilds.SystemUserID.String())
require.NoError(t, err)
// The 'Everyone' group always has an ID that matches the organization ID.
group, err := userAdminClient.Group(ctx, user.OrganizationID)
require.NoError(t, err)
@@ -838,6 +843,7 @@ func TestGroup(t *testing.T) {
require.Equal(t, user.OrganizationID, group.OrganizationID)
require.Contains(t, group.Members, user1.ReducedUser)
require.Contains(t, group.Members, user2.ReducedUser)
require.NotContains(t, group.Members, prebuildsUser.ReducedUser)
})
}
+1
View File
@@ -0,0 +1 @@
package prebuilds
+2 -2
View File
@@ -94,7 +94,7 @@ require (
github.com/coder/quartz v0.1.2
github.com/coder/retry v1.5.1
github.com/coder/serpent v0.10.0
github.com/coder/terraform-provider-coder/v2 v2.1.3
github.com/coder/terraform-provider-coder/v2 v2.3.1-0.20250407075538-3a2c18dab13e
github.com/coder/websocket v1.8.12
github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0
github.com/coreos/go-oidc/v3 v3.13.0
@@ -341,7 +341,7 @@ require (
github.com/hashicorp/logutils v1.0.0 // indirect
github.com/hashicorp/terraform-plugin-go v0.26.0 // indirect
github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.0 // indirect
github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1 // indirect
github.com/hdevalence/ed25519consensus v0.1.0 // indirect
github.com/illarion/gonotify v1.0.1 // indirect
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 // indirect
+4 -4
View File
@@ -248,8 +248,8 @@ github.com/coder/tailscale v1.1.1-0.20250227024825-c9983534152a h1:18TQ03KlYrkW8
github.com/coder/tailscale v1.1.1-0.20250227024825-c9983534152a/go.mod h1:1ggFFdHTRjPRu9Yc1yA7nVHBYB50w9Ce7VIXNqcW6Ko=
github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e h1:JNLPDi2P73laR1oAclY6jWzAbucf70ASAvf5mh2cME0=
github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI=
github.com/coder/terraform-provider-coder/v2 v2.1.3 h1:zB7ObGsiOGBHcJUUMmcSauEPlTWRIYmMYieF05LxHSc=
github.com/coder/terraform-provider-coder/v2 v2.1.3/go.mod h1:RHGyb+ghiy8UpDAMJM8duRFuzd+1VqA3AtkRLh2P3Ug=
github.com/coder/terraform-provider-coder/v2 v2.3.1-0.20250407075538-3a2c18dab13e h1:coy2k2X/d+bGys9wUqQn/TR/0xBibiOIX6vZzPSVGso=
github.com/coder/terraform-provider-coder/v2 v2.3.1-0.20250407075538-3a2c18dab13e/go.mod h1:X28s3rz+aEM5PkBKvk3xcUrQFO2eNPjzRChUg9wb70U=
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0 h1:C2/eCr+r0a5Auuw3YOiSyLNHkdMtyCZHPFBx7syN4rk=
@@ -565,8 +565,8 @@ github.com/hashicorp/terraform-plugin-go v0.26.0 h1:cuIzCv4qwigug3OS7iKhpGAbZTiy
github.com/hashicorp/terraform-plugin-go v0.26.0/go.mod h1:+CXjuLDiFgqR+GcrM5a2E2Kal5t5q2jb0E3D57tTdNY=
github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow=
github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.0 h1:7/iejAPyCRBhqAg3jOx+4UcAhY0A+Sg8B+0+d/GxSfM=
github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.0/go.mod h1:TiQwXAjFrgBf5tg5rvBRz8/ubPULpU0HjSaVi5UoJf8=
github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1 h1:WNMsTLkZf/3ydlgsuXePa3jvZFwAJhruxTxP/c1Viuw=
github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1/go.mod h1:P6o64QS97plG44iFzSM6rAn6VJIC/Sy9a9IkEtl79K4=
github.com/hashicorp/terraform-registry-address v0.2.4 h1:JXu/zHB2Ymg/TGVCRu10XqNa4Sh2bWcqCNyKWjnCPJA=
github.com/hashicorp/terraform-registry-address v0.2.4/go.mod h1:tUNYTVyCtU4OIGXXMDp7WNcJ+0W1B4nmstVDgHMjfAU=
github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
+15
View File
@@ -3,6 +3,7 @@ package terraform
import (
"context"
"fmt"
"math"
"strings"
"github.com/awalterschulze/gographviz"
@@ -883,10 +884,24 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s
)
}
if len(preset.Prebuilds) != 1 {
logger.Warn(
ctx,
"coder_workspace_preset must have exactly one prebuild block",
)
}
var prebuildInstances int32
if len(preset.Prebuilds) > 0 {
prebuildInstances = int32(math.Min(math.MaxInt32, float64(preset.Prebuilds[0].Instances)))
}
protoPreset := &proto.Preset{
Name: preset.Name,
Parameters: presetParameters,
Prebuild: &proto.Prebuild{
Instances: prebuildInstances,
},
}
if slice.Contains(duplicatedPresetNames, preset.Name) {
duplicatedPresetNames = append(duplicatedPresetNames, preset.Name)
}
+3
View File
@@ -828,6 +828,9 @@ func TestConvertResources(t *testing.T) {
Name: "Sample",
Value: "A1B2C3",
}},
Prebuild: &proto.Prebuild{
Instances: 4,
},
}},
},
"devcontainer": {
@@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "2.1.3"
version = "2.3.0-pre2"
}
docker = {
source = "kreuzwerker/docker"
@@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "2.1.3"
version = "2.3.0-pre2"
}
docker = {
source = "kreuzwerker/docker"
@@ -2,7 +2,7 @@ terraform {
required_providers {
coder = {
source = "coder/coder"
version = "2.1.3"
version = "2.3.0-pre2"
}
}
}
@@ -22,9 +22,9 @@ data "coder_workspace_preset" "MyFirstProject" {
name = "My First Project"
parameters = {
(data.coder_parameter.sample.name) = "A1B2C3"
# TODO (sasswart): Add support for parameters from external modules
# (data.coder_parameter.first_parameter_from_module.name) = "A1B2C3"
# (data.coder_parameter.child_first_parameter_from_module.name) = "A1B2C3"
}
prebuilds {
instances = 4
}
}
+24 -4
View File
@@ -21,6 +21,7 @@
"motd_file": null,
"order": null,
"os": "windows",
"resources_monitoring": [],
"shutdown_script": null,
"startup_script": null,
"startup_script_behavior": "non-blocking",
@@ -29,6 +30,7 @@
"sensitive_values": {
"display_apps": [],
"metadata": [],
"resources_monitoring": [],
"token": true
}
},
@@ -69,6 +71,7 @@
"motd_file": null,
"order": null,
"os": "windows",
"resources_monitoring": [],
"shutdown_script": null,
"startup_script": null,
"startup_script_behavior": "non-blocking",
@@ -79,12 +82,14 @@
"id": true,
"init_script": true,
"metadata": [],
"resources_monitoring": [],
"token": true
},
"before_sensitive": false,
"after_sensitive": {
"display_apps": [],
"metadata": [],
"resources_monitoring": [],
"token": true
}
}
@@ -156,10 +161,18 @@
"name": "My First Project",
"parameters": {
"Sample": "A1B2C3"
}
},
"prebuilds": [
{
"instances": 4
}
]
},
"sensitive_values": {
"parameters": {}
"parameters": {},
"prebuilds": [
{}
]
}
}
],
@@ -293,7 +306,7 @@
"coder": {
"name": "coder",
"full_name": "registry.terraform.io/coder/coder",
"version_constraint": "2.1.3"
"version_constraint": "2.3.0-pre2"
},
"module.this_is_external_module:docker": {
"name": "docker",
@@ -372,7 +385,14 @@
"data.coder_parameter.sample.name",
"data.coder_parameter.sample"
]
}
},
"prebuilds": [
{
"instances": {
"constant_value": 4
}
}
]
},
"schema_version": 0
}
+12 -2
View File
@@ -43,10 +43,18 @@
"name": "My First Project",
"parameters": {
"Sample": "A1B2C3"
}
},
"prebuilds": [
{
"instances": 4
}
]
},
"sensitive_values": {
"parameters": {}
"parameters": {},
"prebuilds": [
{}
]
}
},
{
@@ -77,6 +85,7 @@
"motd_file": null,
"order": null,
"os": "windows",
"resources_monitoring": [],
"shutdown_script": null,
"startup_script": null,
"startup_script_behavior": "non-blocking",
@@ -88,6 +97,7 @@
{}
],
"metadata": [],
"resources_monitoring": [],
"token": true
}
},
+789 -692
View File
File diff suppressed because it is too large Load Diff
+8 -1
View File
@@ -57,10 +57,15 @@ message RichParameterValue {
string value = 2;
}
message Prebuild {
int32 instances = 1;
}
// Preset represents a set of preset parameters for a template version.
message Preset {
string name = 1;
repeated PresetParameter parameters = 2;
Prebuild prebuild = 3;
}
message PresetParameter {
@@ -287,7 +292,9 @@ message Metadata {
string workspace_owner_ssh_private_key = 16;
string workspace_build_id = 17;
string workspace_owner_login_type = 18;
repeated Role workspace_owner_rbac_roles = 19;
repeated Role workspace_owner_rbac_roles = 19;
bool is_prebuild = 20;
string running_workspace_agent_token = 21;
}
// Config represents execution configuration shared by all subsequent requests in the Session
+25
View File
@@ -94,10 +94,15 @@ export interface RichParameterValue {
value: string;
}
export interface Prebuild {
instances: number;
}
/** Preset represents a set of preset parameters for a template version. */
export interface Preset {
name: string;
parameters: PresetParameter[];
prebuild: Prebuild | undefined;
}
export interface PresetParameter {
@@ -302,6 +307,8 @@ export interface Metadata {
workspaceBuildId: string;
workspaceOwnerLoginType: string;
workspaceOwnerRbacRoles: Role[];
isPrebuild: boolean;
runningWorkspaceAgentToken: string;
}
/** Config represents execution configuration shared by all subsequent requests in the Session */
@@ -511,6 +518,15 @@ export const RichParameterValue = {
},
};
export const Prebuild = {
encode(message: Prebuild, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.instances !== 0) {
writer.uint32(8).int32(message.instances);
}
return writer;
},
};
export const Preset = {
encode(message: Preset, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.name !== "") {
@@ -519,6 +535,9 @@ export const Preset = {
for (const v of message.parameters) {
PresetParameter.encode(v!, writer.uint32(18).fork()).ldelim();
}
if (message.prebuild !== undefined) {
Prebuild.encode(message.prebuild, writer.uint32(26).fork()).ldelim();
}
return writer;
},
};
@@ -1008,6 +1027,12 @@ export const Metadata = {
for (const v of message.workspaceOwnerRbacRoles) {
Role.encode(v!, writer.uint32(154).fork()).ldelim();
}
if (message.isPrebuild === true) {
writer.uint32(160).bool(message.isPrebuild);
}
if (message.runningWorkspaceAgentToken !== "") {
writer.uint32(170).string(message.runningWorkspaceAgentToken);
}
return writer;
},
};