feat: validate prebuild presets using dynamic parameter validation (#21858)

Prebuilds need to be valid. Before this change, you can push a template
version that's preset will fail when making a prebuild. This PR ensures
all presets that are used for prebuilds are valid
This commit is contained in:
Steven Masley
2026-03-03 16:50:18 +00:00
committed by GitHub
parent 8a095ae722
commit bca638a498
4 changed files with 164 additions and 5 deletions
+11 -2
View File
@@ -1814,13 +1814,22 @@ func (api *API) dynamicTemplateVersionTags(ctx context.Context, rw http.Response
tfVarValues[variable.Name] = cty.StringVal(variable.Value)
}
output, diags := preview.Preview(ctx, preview.Input{
input := preview.Input{
PlanJSON: nil, // Template versions are before `terraform plan`
ParameterValues: nil, // No user-specified parameters
Owner: *ownerData,
Logger: stdslog.New(stdslog.DiscardHandler),
TFVars: tfVarValues,
}, files)
}
output, diags := preview.Preview(ctx, input, files)
if output != nil {
// ValidatePrebuilds iterates through the presets and validate their values. This
// ensures the prebuild can actually succeed in a workspace build. The failure
// diagnostics are added to the existing presets, and checked by
// 'dynamicparameters.CheckPresets'
preview.ValidatePrebuilds(ctx, input, output.Presets, files)
}
tagErr := dynamicparameters.CheckTags(output, diags)
if tagErr != nil {
code, resp := tagErr.Response()
+150
View File
@@ -700,6 +700,39 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
}
`,
},
expectError: "", // Presets are not validated unless they are for a prebuild
},
{
name: "invalid prebuild",
files: map[string]string{
`main.tf`: `
terraform {
required_providers {
coder = {
source = "coder/coder"
version = "2.8.0"
}
}
}
data "coder_parameter" "valid_parameter" {
name = "valid_parameter_name"
default = "valid_option_value"
option {
name = "valid_option_name"
value = "valid_option_value"
}
}
data "coder_workspace_preset" "invalid_parameter_name" {
name = "invalid_parameter_name"
parameters = {
"invalid_parameter_name" = "irrelevant_value"
}
prebuilds {
instances = 2
}
}
`,
},
expectError: "Undefined Parameter",
},
} {
@@ -742,6 +775,123 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) {
})
}
// TestTemplateVersionPresetValidation validates that presets with prebuilds
// are validated dynamically. A preset that enables a conditional parameter
// but doesn't provide the required value for the newly-visible parameter
// should fail validation during template version import.
//
// Scenario:
// - Parameter A (use_custom_image): defaults to false
// - Parameter B (custom_image_url): only exists when A is true, has no default
// - Preset with prebuilds enables A but doesn't provide B
//
// Static validation passes because B doesn't exist when evaluated with default
// values. ValidatePrebuilds catches this by evaluating with the preset's
// parameter values.
func TestTemplateVersionPresetValidation(t *testing.T) {
t.Parallel()
store, ps := dbtestutil.NewDB(t)
client := coderdtest.New(t, &coderdtest.Options{
Database: store,
Pubsub: ps,
})
owner := coderdtest.CreateFirstUser(t, client)
templateAdmin, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleTemplateAdmin())
ctx := testutil.Context(t, testutil.WaitShort)
tf := func(valid bool, prebuildCount int) string {
customImageURL := ""
if valid {
customImageURL = `custom_image_url = "ghcr.io/coder/example:latest"`
}
return fmt.Sprintf(`
terraform {
required_providers {
coder = {
source = "coder/coder"
version = "2.8.0"
}
}
}
data "coder_parameter" "use_custom_image" {
name = "use_custom_image"
type = "bool"
default = "false"
}
data "coder_parameter" "custom_image_url" {
count = data.coder_parameter.use_custom_image.value == "true" ? 1 : 0
name = "custom_image_url"
type = "string"
# No default - required when shown
}
data "coder_workspace_preset" "invalid" {
name = "Invalid Preset"
parameters = {
"use_custom_image" = "true"
%s
}
prebuilds {
instances = %d
}
}
`, customImageURL, prebuildCount)
}
tarFile := testutil.CreateTar(t, map[string]string{
`main.tf`: tf(false, 1),
})
fi, err := templateAdmin.Upload(ctx, "application/x-tar", bytes.NewReader(tarFile))
require.NoError(t, err)
_, err = templateAdmin.CreateTemplateVersion(ctx, owner.OrganizationID, codersdk.CreateTemplateVersionRequest{
Name: testutil.GetRandomNameHyphenated(t),
StorageMethod: codersdk.ProvisionerStorageMethodFile,
Provisioner: codersdk.ProvisionerTypeTerraform,
FileID: fi.ID,
})
require.Error(t, err)
require.ErrorContains(t, err, "Parameter custom_image_url: Required parameter not provided; parameter value is null")
// If the preset is not a prebuild, validation should pass. As presets can
// be partially applied, we test with a prebuild count of 0.
tarFile = testutil.CreateTar(t, map[string]string{
`main.tf`: tf(false, 0),
})
fi, err = templateAdmin.Upload(ctx, "application/x-tar", bytes.NewReader(tarFile))
require.NoError(t, err)
_, err = templateAdmin.CreateTemplateVersion(ctx, owner.OrganizationID, codersdk.CreateTemplateVersionRequest{
Name: testutil.GetRandomNameHyphenated(t),
StorageMethod: codersdk.ProvisionerStorageMethodFile,
Provisioner: codersdk.ProvisionerTypeTerraform,
FileID: fi.ID,
})
require.NoError(t, err)
// The valid preset should pass
tarFile = testutil.CreateTar(t, map[string]string{
`main.tf`: tf(true, 1),
})
fi, err = templateAdmin.Upload(ctx, "application/x-tar", bytes.NewReader(tarFile))
require.NoError(t, err)
_, err = templateAdmin.CreateTemplateVersion(ctx, owner.OrganizationID, codersdk.CreateTemplateVersionRequest{
Name: testutil.GetRandomNameHyphenated(t),
StorageMethod: codersdk.ProvisionerStorageMethodFile,
Provisioner: codersdk.ProvisionerTypeTerraform,
FileID: fi.ID,
})
require.NoError(t, err)
}
func TestPatchCancelTemplateVersion(t *testing.T) {
t.Parallel()
t.Run("AlreadyCompleted", func(t *testing.T) {
+1 -1
View File
@@ -484,7 +484,7 @@ require (
github.com/coder/aibridge v1.0.7
github.com/coder/aisdk-go v0.0.9
github.com/coder/boundary v0.8.3
github.com/coder/preview v1.0.4
github.com/coder/preview v1.0.7
github.com/danieljoos/wincred v1.2.3
github.com/dgraph-io/ristretto/v2 v2.4.0
github.com/elazarl/goproxy v1.8.0
+2 -2
View File
@@ -337,8 +337,8 @@ github.com/coder/pq v1.10.5-0.20250807075151-6ad9b0a25151 h1:YAxwg3lraGNRwoQ18H7
github.com/coder/pq v1.10.5-0.20250807075151-6ad9b0a25151/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs=
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc=
github.com/coder/preview v1.0.4 h1:f506bnyhHtI3ICl/8Eb/gemcKvm/AGzQ91uyxjF+D9k=
github.com/coder/preview v1.0.4/go.mod h1:PpLayC3ngQQ0iUhW2yVRFszOooto4JrGGMomv1rqUvA=
github.com/coder/preview v1.0.7 h1:LF8WRYDcYyBUyfmlAaXD6hZOpBH+qDIxU9mcbmSRKxM=
github.com/coder/preview v1.0.7/go.mod h1:PpLayC3ngQQ0iUhW2yVRFszOooto4JrGGMomv1rqUvA=
github.com/coder/quartz v0.3.0 h1:bUoSEJ77NBfKtUqv6CPSC0AS8dsjqAqqAv7bN02m1mg=
github.com/coder/quartz v0.3.0/go.mod h1:BgE7DOj/8NfvRgvKw0jPLDQH/2Lya2kxcTaNJ8X0rZk=
github.com/coder/retry v1.5.1 h1:iWu8YnD8YqHs3XwqrqsjoBTAVqT9ml6z9ViJ2wlMiqc=