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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy