diff --git a/coderd/dynamicparameters/error.go b/coderd/dynamicparameters/error.go index 4c27905bfa..ae2217936b 100644 --- a/coderd/dynamicparameters/error.go +++ b/coderd/dynamicparameters/error.go @@ -26,6 +26,14 @@ func tagValidationError(diags hcl.Diagnostics) *DiagnosticError { } } +func presetValidationError(diags hcl.Diagnostics) *DiagnosticError { + return &DiagnosticError{ + Message: "Unable to validate presets", + Diagnostics: diags, + KeyedDiagnostics: make(map[string]hcl.Diagnostics), + } +} + type DiagnosticError struct { // Message is the human-readable message that will be returned to the user. Message string diff --git a/coderd/dynamicparameters/presets.go b/coderd/dynamicparameters/presets.go new file mode 100644 index 0000000000..24974962e0 --- /dev/null +++ b/coderd/dynamicparameters/presets.go @@ -0,0 +1,28 @@ +package dynamicparameters + +import ( + "github.com/hashicorp/hcl/v2" + + "github.com/coder/preview" +) + +// CheckPresets extracts the preset related diagnostics from a template version preset +func CheckPresets(output *preview.Output, diags hcl.Diagnostics) *DiagnosticError { + de := presetValidationError(diags) + if output == nil { + return de + } + + presets := output.Presets + for _, preset := range presets { + if hcl.Diagnostics(preset.Diagnostics).HasErrors() { + de.Extend(preset.Name, hcl.Diagnostics(preset.Diagnostics)) + } + } + + if de.HasError() { + return de + } + + return nil +} diff --git a/coderd/dynamicparameters/tags.go b/coderd/dynamicparameters/tags.go index 38a9bf4691..d9037db5dd 100644 --- a/coderd/dynamicparameters/tags.go +++ b/coderd/dynamicparameters/tags.go @@ -11,6 +11,10 @@ import ( func CheckTags(output *preview.Output, diags hcl.Diagnostics) *DiagnosticError { de := tagValidationError(diags) + if output == nil { + return de + } + failedTags := output.WorkspaceTags.UnusableTags() if len(failedTags) == 0 && !de.HasError() { return nil // No errors, all is good! diff --git a/coderd/templateversions.go b/coderd/templateversions.go index cc106b390f..2a6e09d949 100644 --- a/coderd/templateversions.go +++ b/coderd/templateversions.go @@ -1822,6 +1822,14 @@ func (api *API) dynamicTemplateVersionTags(ctx context.Context, rw http.Response return nil, false } + // Fails early if presets are invalid to prevent downstream workspace creation errors + presetErr := dynamicparameters.CheckPresets(output, nil) + if presetErr != nil { + code, resp := presetErr.Response() + httpapi.Write(ctx, rw, code, resp) + return nil, false + } + return output.WorkspaceTags.Tags(), true } diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index 912bca1c5f..0b5bf6fcf2 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -620,6 +620,119 @@ func TestPostTemplateVersionsByOrganization(t *testing.T) { }) } }) + + t.Run("Presets", func(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()) + + for _, tt := range []struct { + name string + files map[string]string + expectError string + }{ + { + name: "valid preset", + 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" "valid_preset" { + name = "valid_preset" + parameters = { + "valid_parameter_name" = "valid_option_value" + } + } + `, + }, + }, + { + name: "invalid preset", + 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" + } + } + `, + }, + expectError: "Undefined Parameter", + }, + } { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + + // Create an archive from the files provided in the test case. + tarFile := testutil.CreateTar(t, tt.files) + + // Post the archive file + fi, err := templateAdmin.Upload(ctx, "application/x-tar", bytes.NewReader(tarFile)) + require.NoError(t, err) + + // Create a template version from the archive + tvName := testutil.GetRandomNameHyphenated(t) + tv, err := templateAdmin.CreateTemplateVersion(ctx, owner.OrganizationID, codersdk.CreateTemplateVersionRequest{ + Name: tvName, + StorageMethod: codersdk.ProvisionerStorageMethodFile, + Provisioner: codersdk.ProvisionerTypeTerraform, + FileID: fi.ID, + }) + + if tt.expectError == "" { + require.NoError(t, err) + // Assert the expected provisioner job is created from the template version import + pj, err := store.GetProvisionerJobByID(ctx, tv.Job.ID) + require.NoError(t, err) + require.NotNil(t, pj) + // Also assert that we get the expected information back from the API endpoint + require.Zero(t, tv.MatchedProvisioners.Count) + require.Zero(t, tv.MatchedProvisioners.Available) + require.Zero(t, tv.MatchedProvisioners.MostRecentlySeen.Time) + } else { + require.ErrorContains(t, err, tt.expectError) + require.Equal(t, tv.Job.ID, uuid.Nil) + } + }) + } + }) } func TestPatchCancelTemplateVersion(t *testing.T) {