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) {