Skip to content

Commit 46e04c6

Browse files
authored
feat(provisioner): add support for presets to coder provisioners (#16574)
This pull request adds support for presets to coder provisioners. If a template defines presets using a compatible version of the provider, then this PR will allow those presets to be persisted to the control plane database for use in workspace creation.
1 parent a845370 commit 46e04c6

File tree

20 files changed

+2252
-760
lines changed

20 files changed

+2252
-760
lines changed

coderd/presets_test.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ import (
1515
)
1616

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

2220
givenPreset := codersdk.Preset{

coderd/provisionerdserver/provisionerdserver.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1340,6 +1340,11 @@ func (s *server) CompleteJob(ctx context.Context, completed *proto.CompletedJob)
13401340
}
13411341
}
13421342

1343+
err = InsertWorkspacePresetsAndParameters(ctx, s.Logger, s.Database, jobID, input.TemplateVersionID, jobType.TemplateImport.Presets, s.timeNow())
1344+
if err != nil {
1345+
return nil, xerrors.Errorf("insert workspace presets and parameters: %w", err)
1346+
}
1347+
13431348
var completedError sql.NullString
13441349

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

1817+
func InsertWorkspacePresetsAndParameters(ctx context.Context, logger slog.Logger, db database.Store, jobID uuid.UUID, templateVersionID uuid.UUID, protoPresets []*sdkproto.Preset, t time.Time) error {
1818+
for _, preset := range protoPresets {
1819+
logger.Info(ctx, "inserting template import job preset",
1820+
slog.F("job_id", jobID.String()),
1821+
slog.F("preset_name", preset.Name),
1822+
)
1823+
if err := InsertWorkspacePresetAndParameters(ctx, db, templateVersionID, preset, t); err != nil {
1824+
return xerrors.Errorf("insert workspace preset: %w", err)
1825+
}
1826+
}
1827+
return nil
1828+
}
1829+
1830+
func InsertWorkspacePresetAndParameters(ctx context.Context, db database.Store, templateVersionID uuid.UUID, protoPreset *sdkproto.Preset, t time.Time) error {
1831+
err := db.InTx(func(tx database.Store) error {
1832+
dbPreset, err := tx.InsertPreset(ctx, database.InsertPresetParams{
1833+
TemplateVersionID: templateVersionID,
1834+
Name: protoPreset.Name,
1835+
CreatedAt: t,
1836+
})
1837+
if err != nil {
1838+
return xerrors.Errorf("insert preset: %w", err)
1839+
}
1840+
1841+
var presetParameterNames []string
1842+
var presetParameterValues []string
1843+
for _, parameter := range protoPreset.Parameters {
1844+
presetParameterNames = append(presetParameterNames, parameter.Name)
1845+
presetParameterValues = append(presetParameterValues, parameter.Value)
1846+
}
1847+
_, err = tx.InsertPresetParameters(ctx, database.InsertPresetParametersParams{
1848+
TemplateVersionPresetID: dbPreset.ID,
1849+
Names: presetParameterNames,
1850+
Values: presetParameterValues,
1851+
})
1852+
if err != nil {
1853+
return xerrors.Errorf("insert preset parameters: %w", err)
1854+
}
1855+
return nil
1856+
}, nil)
1857+
if err != nil {
1858+
return xerrors.Errorf("insert preset and parameters: %w", err)
1859+
}
1860+
return nil
1861+
}
1862+
18121863
func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid.UUID, transition database.WorkspaceTransition, protoResource *sdkproto.Resource, snapshot *telemetry.Snapshot) error {
18131864
resource, err := db.InsertWorkspaceResource(ctx, database.InsertWorkspaceResourceParams{
18141865
ID: uuid.New(),

coderd/provisionerdserver/provisionerdserver_test.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"github.com/coder/coder/v2/coderd/database"
3131
"github.com/coder/coder/v2/coderd/database/dbgen"
3232
"github.com/coder/coder/v2/coderd/database/dbmem"
33+
"github.com/coder/coder/v2/coderd/database/dbtestutil"
3334
"github.com/coder/coder/v2/coderd/database/dbtime"
3435
"github.com/coder/coder/v2/coderd/database/pubsub"
3536
"github.com/coder/coder/v2/coderd/externalauth"
@@ -1708,6 +1709,155 @@ func TestCompleteJob(t *testing.T) {
17081709
})
17091710
}
17101711

1712+
func TestInsertWorkspacePresetsAndParameters(t *testing.T) {
1713+
t.Parallel()
1714+
1715+
type testCase struct {
1716+
name string
1717+
givenPresets []*sdkproto.Preset
1718+
}
1719+
1720+
testCases := []testCase{
1721+
{
1722+
name: "no presets",
1723+
},
1724+
{
1725+
name: "one preset with no parameters",
1726+
givenPresets: []*sdkproto.Preset{
1727+
{
1728+
Name: "preset1",
1729+
},
1730+
},
1731+
},
1732+
{
1733+
name: "one preset with multiple parameters",
1734+
givenPresets: []*sdkproto.Preset{
1735+
{
1736+
Name: "preset1",
1737+
Parameters: []*sdkproto.PresetParameter{
1738+
{
1739+
Name: "param1",
1740+
Value: "value1",
1741+
},
1742+
{
1743+
Name: "param2",
1744+
Value: "value2",
1745+
},
1746+
},
1747+
},
1748+
},
1749+
},
1750+
{
1751+
name: "multiple presets with parameters",
1752+
givenPresets: []*sdkproto.Preset{
1753+
{
1754+
Name: "preset1",
1755+
Parameters: []*sdkproto.PresetParameter{
1756+
{
1757+
Name: "param1",
1758+
Value: "value1",
1759+
},
1760+
{
1761+
Name: "param2",
1762+
Value: "value2",
1763+
},
1764+
},
1765+
},
1766+
{
1767+
Name: "preset2",
1768+
Parameters: []*sdkproto.PresetParameter{
1769+
{
1770+
Name: "param3",
1771+
Value: "value3",
1772+
},
1773+
{
1774+
Name: "param4",
1775+
Value: "value4",
1776+
},
1777+
},
1778+
},
1779+
},
1780+
},
1781+
}
1782+
1783+
for _, c := range testCases {
1784+
c := c
1785+
t.Run(c.name, func(t *testing.T) {
1786+
t.Parallel()
1787+
1788+
ctx := testutil.Context(t, testutil.WaitLong)
1789+
logger := testutil.Logger(t)
1790+
db, ps := dbtestutil.NewDB(t)
1791+
org := dbgen.Organization(t, db, database.Organization{})
1792+
user := dbgen.User(t, db, database.User{})
1793+
job := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{
1794+
Type: database.ProvisionerJobTypeWorkspaceBuild,
1795+
OrganizationID: org.ID,
1796+
})
1797+
templateVersion := dbgen.TemplateVersion(t, db, database.TemplateVersion{
1798+
JobID: job.ID,
1799+
OrganizationID: org.ID,
1800+
CreatedBy: user.ID,
1801+
})
1802+
1803+
err := provisionerdserver.InsertWorkspacePresetsAndParameters(
1804+
ctx,
1805+
logger,
1806+
db,
1807+
job.ID,
1808+
templateVersion.ID,
1809+
c.givenPresets,
1810+
time.Now(),
1811+
)
1812+
require.NoError(t, err)
1813+
1814+
gotPresets, err := db.GetPresetsByTemplateVersionID(ctx, templateVersion.ID)
1815+
require.NoError(t, err)
1816+
require.Len(t, gotPresets, len(c.givenPresets))
1817+
1818+
for _, givenPreset := range c.givenPresets {
1819+
foundMatch := false
1820+
for _, gotPreset := range gotPresets {
1821+
if givenPreset.Name == gotPreset.Name {
1822+
foundMatch = true
1823+
break
1824+
}
1825+
}
1826+
require.True(t, foundMatch, "preset %s not found in parameters", givenPreset.Name)
1827+
}
1828+
1829+
gotPresetParameters, err := db.GetPresetParametersByTemplateVersionID(ctx, templateVersion.ID)
1830+
require.NoError(t, err)
1831+
1832+
for _, givenPreset := range c.givenPresets {
1833+
for _, givenParameter := range givenPreset.Parameters {
1834+
foundMatch := false
1835+
for _, gotParameter := range gotPresetParameters {
1836+
nameMatches := givenParameter.Name == gotParameter.Name
1837+
valueMatches := givenParameter.Value == gotParameter.Value
1838+
1839+
// ensure that preset parameters are matched to the correct preset:
1840+
var gotPreset database.TemplateVersionPreset
1841+
for _, preset := range gotPresets {
1842+
if preset.ID == gotParameter.TemplateVersionPresetID {
1843+
gotPreset = preset
1844+
break
1845+
}
1846+
}
1847+
presetMatches := gotPreset.Name == givenPreset.Name
1848+
1849+
if nameMatches && valueMatches && presetMatches {
1850+
foundMatch = true
1851+
break
1852+
}
1853+
}
1854+
require.True(t, foundMatch, "preset parameter %s not found in presets", givenParameter.Name)
1855+
}
1856+
}
1857+
})
1858+
}
1859+
}
1860+
17111861
func TestInsertWorkspaceResource(t *testing.T) {
17121862
t.Parallel()
17131863
ctx := context.Background()

provisioner/terraform/executor.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ func (e *executor) plan(ctx, killCtx context.Context, env, vars []string, logr l
308308
Resources: state.Resources,
309309
ExternalAuthProviders: state.ExternalAuthProviders,
310310
Timings: append(e.timings.aggregate(), graphTimings.aggregate()...),
311+
Presets: state.Presets,
311312
}, nil
312313
}
313314

provisioner/terraform/resources.go

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ type resourceMetadataItem struct {
149149
type State struct {
150150
Resources []*proto.Resource
151151
Parameters []*proto.RichParameter
152+
Presets []*proto.Preset
152153
ExternalAuthProviders []*proto.ExternalAuthProviderResource
153154
}
154155

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

177178
// Extra array to preserve the order of rich parameters.
178179
tfResourcesRichParameters := make([]*tfjson.StateResource, 0)
179-
180+
tfResourcesPresets := make([]*tfjson.StateResource, 0)
180181
var findTerraformResources func(mod *tfjson.StateModule)
181182
findTerraformResources = func(mod *tfjson.StateModule) {
182183
for _, module := range mod.ChildModules {
@@ -186,6 +187,9 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s
186187
if resource.Type == "coder_parameter" {
187188
tfResourcesRichParameters = append(tfResourcesRichParameters, resource)
188189
}
190+
if resource.Type == "coder_workspace_preset" {
191+
tfResourcesPresets = append(tfResourcesPresets, resource)
192+
}
189193

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

782+
var duplicatedPresetNames []string
783+
presets := make([]*proto.Preset, 0)
784+
for _, resource := range tfResourcesPresets {
785+
var preset provider.WorkspacePreset
786+
err = mapstructure.Decode(resource.AttributeValues, &preset)
787+
if err != nil {
788+
return nil, xerrors.Errorf("decode preset attributes: %w", err)
789+
}
790+
791+
var duplicatedPresetParameterNames []string
792+
var nonExistentParameters []string
793+
var presetParameters []*proto.PresetParameter
794+
for name, value := range preset.Parameters {
795+
presetParameter := &proto.PresetParameter{
796+
Name: name,
797+
Value: value,
798+
}
799+
800+
formattedName := fmt.Sprintf("%q", name)
801+
if !slice.Contains(duplicatedPresetParameterNames, formattedName) &&
802+
slice.ContainsCompare(presetParameters, presetParameter, func(a, b *proto.PresetParameter) bool {
803+
return a.Name == b.Name
804+
}) {
805+
duplicatedPresetParameterNames = append(duplicatedPresetParameterNames, formattedName)
806+
}
807+
if !slice.ContainsCompare(parameters, &proto.RichParameter{Name: name}, func(a, b *proto.RichParameter) bool {
808+
return a.Name == b.Name
809+
}) {
810+
nonExistentParameters = append(nonExistentParameters, name)
811+
}
812+
813+
presetParameters = append(presetParameters, presetParameter)
814+
}
815+
816+
if len(duplicatedPresetParameterNames) > 0 {
817+
s := ""
818+
if len(duplicatedPresetParameterNames) == 1 {
819+
s = "s"
820+
}
821+
return nil, xerrors.Errorf(
822+
"coder_workspace_preset parameters must be unique but %s appear%s multiple times", stringutil.JoinWithConjunction(duplicatedPresetParameterNames), s,
823+
)
824+
}
825+
826+
if len(nonExistentParameters) > 0 {
827+
logger.Warn(
828+
ctx,
829+
"coder_workspace_preset defines preset values for at least one parameter that is not defined by the template",
830+
slog.F("parameters", stringutil.JoinWithConjunction(nonExistentParameters)),
831+
)
832+
}
833+
834+
protoPreset := &proto.Preset{
835+
Name: preset.Name,
836+
Parameters: presetParameters,
837+
}
838+
if slice.Contains(duplicatedPresetNames, preset.Name) {
839+
duplicatedPresetNames = append(duplicatedPresetNames, preset.Name)
840+
}
841+
presets = append(presets, protoPreset)
842+
}
843+
if len(duplicatedPresetNames) > 0 {
844+
s := ""
845+
if len(duplicatedPresetNames) == 1 {
846+
s = "s"
847+
}
848+
return nil, xerrors.Errorf(
849+
"coder_workspace_preset names must be unique but %s appear%s multiple times",
850+
stringutil.JoinWithConjunction(duplicatedPresetNames), s,
851+
)
852+
}
853+
778854
// A map is used to ensure we don't have duplicates!
779855
externalAuthProvidersMap := map[string]*proto.ExternalAuthProviderResource{}
780856
for _, tfResources := range tfResourcesByLabel {
@@ -808,6 +884,7 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s
808884
return &State{
809885
Resources: resources,
810886
Parameters: parameters,
887+
Presets: presets,
811888
ExternalAuthProviders: externalAuthProviders,
812889
}, nil
813890
}

0 commit comments

Comments
 (0)