mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
`BuildError` response from `wsbuilder` does not support rich errors from validation. Changed this to use the `Validations` block of codersdk responses to return all errors for invalid parameters.
243 lines
7.7 KiB
Go
243 lines
7.7 KiB
Go
package dynamicparameters
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/hashicorp/hcl/v2"
|
|
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/util/slice"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
)
|
|
|
|
type parameterValueSource int
|
|
|
|
const (
|
|
sourceDefault parameterValueSource = iota
|
|
sourcePrevious
|
|
sourceBuild
|
|
sourcePreset
|
|
)
|
|
|
|
type parameterValue struct {
|
|
Value string
|
|
Source parameterValueSource
|
|
}
|
|
|
|
type ResolverError struct {
|
|
Diagnostics hcl.Diagnostics
|
|
Parameter map[string]hcl.Diagnostics
|
|
}
|
|
|
|
// Error is a pretty bad format for these errors. Try to avoid using this.
|
|
func (e *ResolverError) Error() string {
|
|
var diags hcl.Diagnostics
|
|
diags = diags.Extend(e.Diagnostics)
|
|
for _, d := range e.Parameter {
|
|
diags = diags.Extend(d)
|
|
}
|
|
|
|
return diags.Error()
|
|
}
|
|
|
|
func (e *ResolverError) HasError() bool {
|
|
if e.Diagnostics.HasErrors() {
|
|
return true
|
|
}
|
|
|
|
for _, diags := range e.Parameter {
|
|
if diags.HasErrors() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (e *ResolverError) Extend(parameterName string, diag hcl.Diagnostics) {
|
|
if e.Parameter == nil {
|
|
e.Parameter = make(map[string]hcl.Diagnostics)
|
|
}
|
|
if _, ok := e.Parameter[parameterName]; !ok {
|
|
e.Parameter[parameterName] = hcl.Diagnostics{}
|
|
}
|
|
e.Parameter[parameterName] = e.Parameter[parameterName].Extend(diag)
|
|
}
|
|
|
|
//nolint:revive // firstbuild is a control flag to turn on immutable validation
|
|
func ResolveParameters(
|
|
ctx context.Context,
|
|
ownerID uuid.UUID,
|
|
renderer Renderer,
|
|
firstBuild bool,
|
|
previousValues []database.WorkspaceBuildParameter,
|
|
buildValues []codersdk.WorkspaceBuildParameter,
|
|
presetValues []database.TemplateVersionPresetParameter,
|
|
) (map[string]string, error) {
|
|
previousValuesMap := slice.ToMapFunc(previousValues, func(p database.WorkspaceBuildParameter) (string, string) {
|
|
return p.Name, p.Value
|
|
})
|
|
|
|
// Start with previous
|
|
values := parameterValueMap(slice.ToMapFunc(previousValues, func(p database.WorkspaceBuildParameter) (string, parameterValue) {
|
|
return p.Name, parameterValue{Source: sourcePrevious, Value: p.Value}
|
|
}))
|
|
|
|
// Add build values (overwrite previous values if they exist)
|
|
for _, buildValue := range buildValues {
|
|
values[buildValue.Name] = parameterValue{Source: sourceBuild, Value: buildValue.Value}
|
|
}
|
|
|
|
// Add preset values (overwrite previous and build values if they exist)
|
|
for _, preset := range presetValues {
|
|
values[preset.Name] = parameterValue{Source: sourcePreset, Value: preset.Value}
|
|
}
|
|
|
|
// originalValues is going to be used to detect if a user tried to change
|
|
// an immutable parameter after the first build.
|
|
originalValues := make(map[string]parameterValue, len(values))
|
|
for name, value := range values {
|
|
// Store the original values for later use.
|
|
originalValues[name] = value
|
|
}
|
|
|
|
// Render the parameters using the values that were supplied to the previous build.
|
|
//
|
|
// This is how the form should look to the user on their workspace settings page.
|
|
// This is the original form truth that our validations should initially be based on.
|
|
output, diags := renderer.Render(ctx, ownerID, values.ValuesMap())
|
|
if diags.HasErrors() {
|
|
// Top level diagnostics should break the build. Previous values (and new) should
|
|
// always be valid. If there is a case where this is not true, then this has to
|
|
// be changed to allow the build to continue with a different set of values.
|
|
|
|
return nil, &ResolverError{
|
|
Diagnostics: diags,
|
|
Parameter: nil,
|
|
}
|
|
}
|
|
|
|
// The user's input now needs to be validated against the parameters.
|
|
// Mutability & Ephemeral parameters depend on sequential workspace builds.
|
|
//
|
|
// To enforce these, the user's input values are trimmed based on the
|
|
// mutability and ephemeral parameters defined in the template version.
|
|
for _, parameter := range output.Parameters {
|
|
// Ephemeral parameters should not be taken from the previous build.
|
|
// They must always be explicitly set in every build.
|
|
// So remove their values if they are sourced from the previous build.
|
|
if parameter.Ephemeral {
|
|
v := values[parameter.Name]
|
|
if v.Source == sourcePrevious {
|
|
delete(values, parameter.Name)
|
|
}
|
|
}
|
|
|
|
// Immutable parameters should also not be allowed to be changed from
|
|
// the previous build. Remove any values taken from the preset or
|
|
// new build params. This forces the value to be the same as it was before.
|
|
//
|
|
// We do this so the next form render uses the original immutable value.
|
|
if !firstBuild && !parameter.Mutable {
|
|
delete(values, parameter.Name)
|
|
prev, ok := previousValuesMap[parameter.Name]
|
|
if ok {
|
|
values[parameter.Name] = parameterValue{
|
|
Value: prev,
|
|
Source: sourcePrevious,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// This is the final set of values that will be used. Any errors at this stage
|
|
// are fatal. Additional validation for immutability has to be done manually.
|
|
output, diags = renderer.Render(ctx, ownerID, values.ValuesMap())
|
|
if diags.HasErrors() {
|
|
return nil, &ResolverError{
|
|
Diagnostics: diags,
|
|
Parameter: nil,
|
|
}
|
|
}
|
|
|
|
// parameterNames is going to be used to remove any excess values that were left
|
|
// around without a parameter.
|
|
parameterNames := make(map[string]struct{}, len(output.Parameters))
|
|
parameterError := &ResolverError{}
|
|
for _, parameter := range output.Parameters {
|
|
parameterNames[parameter.Name] = struct{}{}
|
|
|
|
if !firstBuild && !parameter.Mutable {
|
|
// Immutable parameters should not be changed after the first build.
|
|
// They can match the original value though!
|
|
if parameter.Value.AsString() != originalValues[parameter.Name].Value {
|
|
var src *hcl.Range
|
|
if parameter.Source != nil {
|
|
src = ¶meter.Source.HCLBlock().TypeRange
|
|
}
|
|
|
|
// An immutable parameter was changed, which is not allowed.
|
|
// Add a failed diagnostic to the output.
|
|
parameterError.Extend(parameter.Name, hcl.Diagnostics{
|
|
&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Immutable parameter changed",
|
|
Detail: fmt.Sprintf("Parameter %q is not mutable, so it can't be updated after creating a workspace.", parameter.Name),
|
|
Subject: src,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
// TODO: Fix the `hcl.Diagnostics(...)` type casting. It should not be needed.
|
|
if hcl.Diagnostics(parameter.Diagnostics).HasErrors() {
|
|
// All validation errors are raised here for each parameter.
|
|
parameterError.Extend(parameter.Name, hcl.Diagnostics(parameter.Diagnostics))
|
|
}
|
|
|
|
// If the parameter has a value, but it was not set explicitly by the user at any
|
|
// build, then save the default value. An example where this is important is if a
|
|
// template has a default value of 'region = us-west-2', but the user never sets
|
|
// it. If the default value changes to 'region = us-east-1', we want to preserve
|
|
// the original value of 'us-west-2' for the existing workspaces.
|
|
//
|
|
// parameter.Value will be populated from the default at this point. So grab it
|
|
// from there.
|
|
if _, ok := values[parameter.Name]; !ok && parameter.Value.IsKnown() && parameter.Value.Valid() {
|
|
values[parameter.Name] = parameterValue{
|
|
Value: parameter.Value.AsString(),
|
|
Source: sourceDefault,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delete any values that do not belong to a parameter. This is to not save
|
|
// parameter values that have no effect. These leaky parameter values can cause
|
|
// problems in the future, as it makes it challenging to remove values from the
|
|
// database
|
|
for k := range values {
|
|
if _, ok := parameterNames[k]; !ok {
|
|
delete(values, k)
|
|
}
|
|
}
|
|
|
|
if parameterError.HasError() {
|
|
// If there are any errors, return them.
|
|
return nil, parameterError
|
|
}
|
|
|
|
// Return the values to be saved for the build.
|
|
return values.ValuesMap(), nil
|
|
}
|
|
|
|
type parameterValueMap map[string]parameterValue
|
|
|
|
func (p parameterValueMap) ValuesMap() map[string]string {
|
|
values := make(map[string]string, len(p))
|
|
for name, paramValue := range p {
|
|
values[name] = paramValue.Value
|
|
}
|
|
return values
|
|
}
|