Skip to content

feat(provisioner): add support for presets to coder provisioners #16574

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

Merged
merged 8 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 0 additions & 2 deletions coderd/presets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ import (
)

func TestTemplateVersionPresets(t *testing.T) {
// TODO (sasswart): Test case: what if a user tries to read presets or preset parameters from a different org?

t.Parallel()

givenPreset := codersdk.Preset{
Expand Down
51 changes: 51 additions & 0 deletions coderd/provisionerdserver/provisionerdserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -1340,6 +1340,11 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob)
}
}

err = InsertWorkspacePresetsAndParameters(ctx, s.Logger, s.Database, jobID, input.TemplateVersionID, jobType.TemplateImport.Presets, s.timeNow())
if err != nil {
return nil, xerrors.Errorf("insert workspace presets and parameters: %w", err)
}

var completedError sql.NullString

for _, externalAuthProvider := range jobType.TemplateImport.ExternalAuthProviders {
Expand Down Expand Up @@ -1809,6 +1814,52 @@ func InsertWorkspaceModule(ctx context.Context, db database.Store, jobID uuid.UU
return nil
}

func InsertWorkspacePresetsAndParameters(ctx context.Context, logger slog.Logger, db database.Store, jobID uuid.UUID, templateVersionID uuid.UUID, protoPresets []*sdkproto.Preset, t time.Time) error {
for _, preset := range protoPresets {
logger.Info(ctx, "inserting template import job preset",
slog.F("job_id", jobID.String()),
slog.F("preset_name", preset.Name),
)
if err := InsertWorkspacePresetAndParameters(ctx, db, templateVersionID, preset, t); err != nil {
return xerrors.Errorf("insert workspace preset: %w", err)
}
}
return nil
}

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 {
dbPreset, err := tx.InsertPreset(ctx, database.InsertPresetParams{
TemplateVersionID: templateVersionID,
Name: protoPreset.Name,
CreatedAt: t,
})
if err != nil {
return xerrors.Errorf("insert preset: %w", err)
}

var presetParameterNames []string
var presetParameterValues []string
for _, parameter := range protoPreset.Parameters {
presetParameterNames = append(presetParameterNames, parameter.Name)
presetParameterValues = append(presetParameterValues, parameter.Value)
}
_, err = tx.InsertPresetParameters(ctx, database.InsertPresetParametersParams{
TemplateVersionPresetID: dbPreset.ID,
Names: presetParameterNames,
Values: presetParameterValues,
})
if err != nil {
return xerrors.Errorf("insert preset parameters: %w", err)
}
return nil
}, nil)
if err != nil {
return xerrors.Errorf("insert preset and parameters: %w", err)
}
return nil
}

func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.UUID, transition database.WorkspaceTransition, protoResource *sdkproto.Resource, snapshot *telemetry.Snapshot) error {
resource, err := db.InsertWorkspaceResource(ctx, database.InsertWorkspaceResourceParams{
ID: uuid.New(),
Expand Down
150 changes: 150 additions & 0 deletions coderd/provisionerdserver/provisionerdserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbmem"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/database/pubsub"
"github.com/coder/coder/v2/coderd/externalauth"
Expand Down Expand Up @@ -1708,6 +1709,155 @@ func TestCompleteJob(t *testing.T) {
})
}

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

type testCase struct {
name string
givenPresets []*sdkproto.Preset
}

testCases := []testCase{
{
name: "no presets",
},
{
name: "one preset with no parameters",
givenPresets: []*sdkproto.Preset{
{
Name: "preset1",
},
},
},
{
name: "one preset with multiple parameters",
givenPresets: []*sdkproto.Preset{
{
Name: "preset1",
Parameters: []*sdkproto.PresetParameter{
{
Name: "param1",
Value: "value1",
},
{
Name: "param2",
Value: "value2",
},
},
},
},
},
{
name: "multiple presets with parameters",
givenPresets: []*sdkproto.Preset{
{
Name: "preset1",
Parameters: []*sdkproto.PresetParameter{
{
Name: "param1",
Value: "value1",
},
{
Name: "param2",
Value: "value2",
},
},
},
{
Name: "preset2",
Parameters: []*sdkproto.PresetParameter{
{
Name: "param3",
Value: "value3",
},
{
Name: "param4",
Value: "value4",
},
},
},
},
},
}

for _, c := range testCases {
c := c
t.Run(c.name, func(t *testing.T) {
t.Parallel()

ctx := testutil.Context(t, testutil.WaitLong)
logger := testutil.Logger(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,
})
templateVersion := dbgen.TemplateVersion(t, db, database.TemplateVersion{
JobID: job.ID,
OrganizationID: org.ID,
CreatedBy: user.ID,
})

err := provisionerdserver.InsertWorkspacePresetsAndParameters(
ctx,
logger,
db,
job.ID,
templateVersion.ID,
c.givenPresets,
time.Now(),
)
require.NoError(t, err)

gotPresets, err := db.GetPresetsByTemplateVersionID(ctx, templateVersion.ID)
require.NoError(t, err)
require.Len(t, gotPresets, len(c.givenPresets))

for _, givenPreset := range c.givenPresets {
foundMatch := false
for _, gotPreset := range gotPresets {
if givenPreset.Name == gotPreset.Name {
foundMatch = true
break
}
}
require.True(t, foundMatch, "preset %s not found in parameters", givenPreset.Name)
}

gotPresetParameters, err := db.GetPresetParametersByTemplateVersionID(ctx, templateVersion.ID)
require.NoError(t, err)

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 {
foundMatch = true
break
}
}
require.True(t, foundMatch, "preset parameter %s not found in presets", givenParameter.Name)
}
}
})
}
}

func TestInsertWorkspaceResource(t *testing.T) {
t.Parallel()
ctx := context.Background()
Expand Down
1 change: 1 addition & 0 deletions provisioner/terraform/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr l
Resources: state.Resources,
ExternalAuthProviders: state.ExternalAuthProviders,
Timings: append(e.timings.aggregate(), graphTimings.aggregate()...),
Presets: state.Presets,
}, nil
}

Expand Down
79 changes: 78 additions & 1 deletion provisioner/terraform/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ type resourceMetadataItem struct {
type State struct {
Resources []*proto.Resource
Parameters []*proto.RichParameter
Presets []*proto.Preset
ExternalAuthProviders []*proto.ExternalAuthProviderResource
}

Expand Down Expand Up @@ -176,7 +177,7 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s

// Extra array to preserve the order of rich parameters.
tfResourcesRichParameters := make([]*tfjson.StateResource, 0)

tfResourcesPresets := make([]*tfjson.StateResource, 0)
var findTerraformResources func(mod *tfjson.StateModule)
findTerraformResources = func(mod *tfjson.StateModule) {
for _, module := range mod.ChildModules {
Expand All @@ -186,6 +187,9 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s
if resource.Type == "coder_parameter" {
tfResourcesRichParameters = append(tfResourcesRichParameters, resource)
}
if resource.Type == "coder_workspace_preset" {
tfResourcesPresets = append(tfResourcesPresets, resource)
}

label := convertAddressToLabel(resource.Address)
if tfResourcesByLabel[label] == nil {
Expand Down Expand Up @@ -775,6 +779,78 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s
)
}

var duplicatedPresetNames []string
presets := make([]*proto.Preset, 0)
for _, resource := range tfResourcesPresets {
var preset provider.WorkspacePreset
err = mapstructure.Decode(resource.AttributeValues, &preset)
if err != nil {
return nil, xerrors.Errorf("decode preset attributes: %w", err)
}

var duplicatedPresetParameterNames []string
var nonExistentParameters []string
var presetParameters []*proto.PresetParameter
for name, value := range preset.Parameters {
presetParameter := &proto.PresetParameter{
Name: name,
Value: value,
}

formattedName := fmt.Sprintf("%q", name)
if !slice.Contains(duplicatedPresetParameterNames, formattedName) &&
slice.ContainsCompare(presetParameters, presetParameter, func(a, b *proto.PresetParameter) bool {
return a.Name == b.Name
}) {
duplicatedPresetParameterNames = append(duplicatedPresetParameterNames, formattedName)
}
if !slice.ContainsCompare(parameters, &proto.RichParameter{Name: name}, func(a, b *proto.RichParameter) bool {
return a.Name == b.Name
}) {
nonExistentParameters = append(nonExistentParameters, name)
}

presetParameters = append(presetParameters, presetParameter)
}

if len(duplicatedPresetParameterNames) > 0 {
s := ""
if len(duplicatedPresetParameterNames) == 1 {
s = "s"
}
return nil, xerrors.Errorf(
"coder_workspace_preset parameters must be unique but %s appear%s multiple times", stringutil.JoinWithConjunction(duplicatedPresetParameterNames), s,
)
}

if len(nonExistentParameters) > 0 {
logger.Warn(
ctx,
"coder_workspace_preset defines preset values for at least one parameter that is not defined by the template",
slog.F("parameters", stringutil.JoinWithConjunction(nonExistentParameters)),
)
}

protoPreset := &proto.Preset{
Name: preset.Name,
Parameters: presetParameters,
}
if slice.Contains(duplicatedPresetNames, preset.Name) {
duplicatedPresetNames = append(duplicatedPresetNames, preset.Name)
}
presets = append(presets, protoPreset)
}
if len(duplicatedPresetNames) > 0 {
s := ""
if len(duplicatedPresetNames) == 1 {
s = "s"
}
return nil, xerrors.Errorf(
"coder_workspace_preset names must be unique but %s appear%s multiple times",
stringutil.JoinWithConjunction(duplicatedPresetNames), s,
)
}

// A map is used to ensure we don't have duplicates!
externalAuthProvidersMap := map[string]*proto.ExternalAuthProviderResource{}
for _, tfResources := range tfResourcesByLabel {
Expand Down Expand Up @@ -808,6 +884,7 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s
return &State{
Resources: resources,
Parameters: parameters,
Presets: presets,
ExternalAuthProviders: externalAuthProviders,
}, nil
}
Expand Down
Loading
Loading