Skip to content

Commit 414bacf

Browse files
committed
add support for presets to coder provisioners
1 parent 39a33ec commit 414bacf

File tree

56 files changed

+2415
-1098
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+2415
-1098
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 := context.Background()
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()

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ require (
9494
github.com/coder/quartz v0.1.2
9595
github.com/coder/retry v1.5.1
9696
github.com/coder/serpent v0.10.0
97-
github.com/coder/terraform-provider-coder v1.0.5-0.20250131073245-5b9a30ca496b
97+
github.com/coder/terraform-provider-coder v1.0.4
9898
github.com/coder/websocket v1.8.12
9999
github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0
100100
github.com/coreos/go-oidc/v3 v3.12.0
@@ -467,3 +467,5 @@ require (
467467
kernel.org/pub/linux/libs/security/libcap/psx v1.2.73 // indirect
468468
sigs.k8s.io/yaml v1.4.0 // indirect
469469
)
470+
471+
require github.com/coder/terraform-provider-coder/v2 v2.1.3

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,8 +240,10 @@ github.com/coder/tailscale v1.1.1-0.20250129014916-8086c871eae6 h1:prDIwUcsSEKbs
240240
github.com/coder/tailscale v1.1.1-0.20250129014916-8086c871eae6/go.mod h1:1ggFFdHTRjPRu9Yc1yA7nVHBYB50w9Ce7VIXNqcW6Ko=
241241
github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e h1:JNLPDi2P73laR1oAclY6jWzAbucf70ASAvf5mh2cME0=
242242
github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI=
243-
github.com/coder/terraform-provider-coder v1.0.5-0.20250131073245-5b9a30ca496b h1:Z9ssmlGrbf+mRIiyRzQj1P6vH8drKOqgzeTG6D0Ldjg=
244-
github.com/coder/terraform-provider-coder v1.0.5-0.20250131073245-5b9a30ca496b/go.mod h1:dQ1e/IccUxnmh/1bXTA3PopSoBkHMyWT6EkdBw8Lx6Y=
243+
github.com/coder/terraform-provider-coder v1.0.4 h1:MJldCvykIQzzqBVUDjCJpPyqvKelAAHrtJKfIIx4Qxo=
244+
github.com/coder/terraform-provider-coder v1.0.4/go.mod h1:dQ1e/IccUxnmh/1bXTA3PopSoBkHMyWT6EkdBw8Lx6Y=
245+
github.com/coder/terraform-provider-coder/v2 v2.1.3 h1:zB7ObGsiOGBHcJUUMmcSauEPlTWRIYmMYieF05LxHSc=
246+
github.com/coder/terraform-provider-coder/v2 v2.1.3/go.mod h1:RHGyb+ghiy8UpDAMJM8duRFuzd+1VqA3AtkRLh2P3Ug=
245247
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
246248
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
247249
github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0 h1:C2/eCr+r0a5Auuw3YOiSyLNHkdMtyCZHPFBx7syN4rk=

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: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212

1313
"cdr.dev/slog"
1414

15-
"github.com/coder/terraform-provider-coder/provider"
15+
"github.com/coder/terraform-provider-coder/v2/provider"
1616

1717
tfaddr "github.com/hashicorp/go-terraform-address"
1818

@@ -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,79 @@ 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+
// TODO (sasswart): should this be an error? Or should we just log it?
828+
logger.Warn(
829+
ctx,
830+
"coder_workspace_preset defines preset values for at least one parameter that is not defined by the template",
831+
slog.F("parameters", stringutil.JoinWithConjunction(nonExistentParameters)),
832+
)
833+
}
834+
835+
protoPreset := &proto.Preset{
836+
Name: preset.Name,
837+
Parameters: presetParameters,
838+
}
839+
if slice.Contains(duplicatedPresetNames, preset.Name) {
840+
duplicatedPresetNames = append(duplicatedPresetNames, preset.Name)
841+
}
842+
presets = append(presets, protoPreset)
843+
}
844+
if len(duplicatedPresetNames) > 0 {
845+
s := ""
846+
if len(duplicatedPresetNames) == 1 {
847+
s = "s"
848+
}
849+
return nil, xerrors.Errorf(
850+
"coder_workspace_preset names must be unique but %s appear%s multiple times",
851+
stringutil.JoinWithConjunction(duplicatedPresetNames), s,
852+
)
853+
}
854+
778855
// A map is used to ensure we don't have duplicates!
779856
externalAuthProvidersMap := map[string]*proto.ExternalAuthProviderResource{}
780857
for _, tfResources := range tfResourcesByLabel {
@@ -808,6 +885,7 @@ func ConvertState(ctx context.Context, modules []*tfjson.StateModule, rawGraph s
808885
return &State{
809886
Resources: resources,
810887
Parameters: parameters,
888+
Presets: presets,
811889
ExternalAuthProviders: externalAuthProviders,
812890
}, nil
813891
}

0 commit comments

Comments
 (0)