mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
Reference in New Issue
Block a user