Files
coder/coderd/dynamicparameters/error.go
T
Sas Swart f256a23a77 feat: validate presets on template import (#18844)
Typos and other errors often result in invalid presets in a template.
Coder would import these broken templates and present them to users when
they create workspaces. An unsuspecting user who chooses a broken preset
would then experience a failed workspace build with no obvious error
message.

This PR adds additional validation beyond what is possible in the
Terraform provider schema. Coder will now present a more helpful error
message to template authors when they upload a new template version:

<img width="1316" height="286" alt="Screenshot 2025-07-14 at 12 22 49"
src="https://github.com/user-attachments/assets/7f5f778f-d9ae-487a-95e2-f6f1ca604a9c"
/>

The frontend warning is less helpful right now, but I'd like to address
that in a follow-up since I need frontend help:

<img width="1102" height="616" alt="image"
src="https://github.com/user-attachments/assets/e838ffc8-ef4f-428d-9280-74fa0c491666"
/>

closes https://github.com/coder/coder/issues/17333


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Improved validation and error reporting for template presets,
providing clearer feedback when presets cannot be parsed or reference
undefined parameters.

* **Bug Fixes**
* Enhanced error handling during template version creation to better
detect and report issues with presets.

* **Tests**
* Added new tests to verify validation of both valid and invalid
Terraform presets during template version creation.
* Improved test reliability by enabling dynamic control over error
injection in database-related tests.

* **Chores**
* Updated a dependency to the latest version for improved stability and
features.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-30 15:28:56 +02:00

138 lines
3.6 KiB
Go

package dynamicparameters
import (
"fmt"
"net/http"
"sort"
"github.com/hashicorp/hcl/v2"
"github.com/coder/coder/v2/codersdk"
)
func parameterValidationError(diags hcl.Diagnostics) *DiagnosticError {
return &DiagnosticError{
Message: "Unable to validate parameters",
Diagnostics: diags,
KeyedDiagnostics: make(map[string]hcl.Diagnostics),
}
}
func tagValidationError(diags hcl.Diagnostics) *DiagnosticError {
return &DiagnosticError{
Message: "Unable to parse workspace tags",
Diagnostics: diags,
KeyedDiagnostics: make(map[string]hcl.Diagnostics),
}
}
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
// Diagnostics are top level diagnostics that will be returned as "Detail" in the response.
Diagnostics hcl.Diagnostics
// KeyedDiagnostics translate to Validation errors in the response. A key could
// be a parameter name, or a tag name. This allows diagnostics to be more closely
// associated with a specific index/parameter/tag.
KeyedDiagnostics map[string]hcl.Diagnostics
}
// Error is a pretty bad format for these errors. Try to avoid using this.
func (e *DiagnosticError) Error() string {
var diags hcl.Diagnostics
diags = diags.Extend(e.Diagnostics)
for _, d := range e.KeyedDiagnostics {
diags = diags.Extend(d)
}
return diags.Error()
}
func (e *DiagnosticError) HasError() bool {
if e.Diagnostics.HasErrors() {
return true
}
for _, diags := range e.KeyedDiagnostics {
if diags.HasErrors() {
return true
}
}
return false
}
func (e *DiagnosticError) Append(key string, diag *hcl.Diagnostic) {
e.Extend(key, hcl.Diagnostics{diag})
}
func (e *DiagnosticError) Extend(key string, diag hcl.Diagnostics) {
if e.KeyedDiagnostics == nil {
e.KeyedDiagnostics = make(map[string]hcl.Diagnostics)
}
if _, ok := e.KeyedDiagnostics[key]; !ok {
e.KeyedDiagnostics[key] = hcl.Diagnostics{}
}
e.KeyedDiagnostics[key] = e.KeyedDiagnostics[key].Extend(diag)
}
func (e *DiagnosticError) Response() (int, codersdk.Response) {
resp := codersdk.Response{
Message: e.Message,
Validations: nil,
}
// Sort the parameter names so that the order is consistent.
sortedNames := make([]string, 0, len(e.KeyedDiagnostics))
for name := range e.KeyedDiagnostics {
sortedNames = append(sortedNames, name)
}
sort.Strings(sortedNames)
for _, name := range sortedNames {
diag := e.KeyedDiagnostics[name]
resp.Validations = append(resp.Validations, codersdk.ValidationError{
Field: name,
Detail: DiagnosticsErrorString(diag),
})
}
if e.Diagnostics.HasErrors() {
resp.Detail = DiagnosticsErrorString(e.Diagnostics)
}
return http.StatusBadRequest, resp
}
func DiagnosticErrorString(d *hcl.Diagnostic) string {
return fmt.Sprintf("%s; %s", d.Summary, d.Detail)
}
func DiagnosticsErrorString(d hcl.Diagnostics) string {
count := len(d)
switch {
case count == 0:
return "no diagnostics"
case count == 1:
return DiagnosticErrorString(d[0])
default:
for _, d := range d {
// Render the first error diag.
// If there are warnings, do not priority them over errors.
if d.Severity == hcl.DiagError {
return fmt.Sprintf("%s, and %d other diagnostic(s)", DiagnosticErrorString(d), count-1)
}
}
// All warnings? ok...
return fmt.Sprintf("%s, and %d other diagnostic(s)", DiagnosticErrorString(d[0]), count-1)
}
}