Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ab677c2

Browse files
committed
feat: implement dynamic parameter validation
1 parent 46d2751 commit ab677c2

File tree

4 files changed

+332
-72
lines changed

4 files changed

+332
-72
lines changed

coderd/dynamicparameters/render.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package dynamicparameters
22

33
import (
44
"context"
5+
"database/sql"
56
"io/fs"
67
"log/slog"
78
"sync"
9+
"time"
810

911
"github.com/google/uuid"
1012
"golang.org/x/sync/errgroup"
@@ -48,7 +50,7 @@ type loader struct {
4850
// Prepare is the entrypoint for this package. It loads the necessary objects &
4951
// files from the database and returns a Renderer that can be used to render the
5052
// template version's parameters.
51-
func Prepare(ctx context.Context, db database.Store, cache *files.Cache, versionID uuid.UUID, options ...func(r *loader)) (Renderer, error) {
53+
func Prepare(ctx context.Context, db database.Store, cache files.FileAcquirer, versionID uuid.UUID, options ...func(r *loader)) (Renderer, error) {
5254
l := &loader{
5355
templateVersionID: versionID,
5456
}
@@ -105,9 +107,24 @@ func (r *loader) loadData(ctx context.Context, db database.Store) error {
105107

106108
if r.terraformValues == nil {
107109
values, err := db.GetTemplateVersionTerraformValues(ctx, r.templateVersion.ID)
108-
if err != nil {
110+
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
109111
return xerrors.Errorf("template version terraform values: %w", err)
110112
}
113+
114+
if xerrors.Is(err, sql.ErrNoRows) {
115+
// If the row does not exist, return zero values.
116+
//
117+
// Older template versions (prior to dynamic parameters) will be missing
118+
// this row, and we can assume the 'ProvisionerdVersion' "" (unknown).
119+
values = database.TemplateVersionTerraformValue{
120+
TemplateVersionID: r.templateVersionID,
121+
UpdatedAt: time.Time{},
122+
CachedPlan: nil,
123+
CachedModuleFiles: uuid.NullUUID{},
124+
ProvisionerdVersion: "",
125+
}
126+
}
127+
111128
r.terraformValues = &values
112129
}
113130

@@ -121,7 +138,7 @@ func (r *loader) loadData(ctx context.Context, db database.Store) error {
121138
// Static parameter rendering is required to support older template versions that
122139
// do not have the database state to support dynamic parameters. A constant
123140
// warning will be displayed for these template versions.
124-
func (r *loader) Renderer(ctx context.Context, db database.Store, cache *files.Cache) (Renderer, error) {
141+
func (r *loader) Renderer(ctx context.Context, db database.Store, cache files.FileAcquirer) (Renderer, error) {
125142
err := r.loadData(ctx, db)
126143
if err != nil {
127144
return nil, xerrors.Errorf("load data: %w", err)

coderd/dynamicparameters/resolver.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package dynamicparameters
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/google/uuid"
8+
"github.com/hashicorp/hcl/v2"
9+
10+
"github.com/coder/coder/v2/coderd/database"
11+
"github.com/coder/coder/v2/coderd/util/slice"
12+
"github.com/coder/coder/v2/codersdk"
13+
)
14+
15+
type ParameterResolver struct {
16+
renderer Renderer
17+
firstBuild bool
18+
presetValues []database.TemplateVersionPresetParameter
19+
previousValues []database.WorkspaceBuildParameter
20+
buildValues []database.WorkspaceBuildParameter
21+
}
22+
23+
type parameterValueSource int
24+
25+
const (
26+
sourcePrevious parameterValueSource = iota
27+
sourceBuild
28+
sourcePreset
29+
)
30+
31+
type parameterValue struct {
32+
Value string
33+
Source parameterValueSource
34+
}
35+
36+
func ResolveParameters(
37+
ctx context.Context,
38+
ownerID uuid.UUID,
39+
renderer Renderer,
40+
firstBuild bool,
41+
previousValues []database.WorkspaceBuildParameter,
42+
buildValues []codersdk.WorkspaceBuildParameter,
43+
presetValues []database.TemplateVersionPresetParameter,
44+
) (map[string]string, hcl.Diagnostics) {
45+
previousValuesMap := slice.ToMap(previousValues, func(p database.WorkspaceBuildParameter) (string, string) {
46+
return p.Value, p.Value
47+
})
48+
49+
// Start with previous
50+
values := parameterValueMap(slice.ToMap(previousValues, func(p database.WorkspaceBuildParameter) (string, parameterValue) {
51+
return p.Name, parameterValue{Source: sourcePrevious, Value: p.Value}
52+
}))
53+
54+
// Add build values
55+
for _, buildValue := range buildValues {
56+
if _, ok := values[buildValue.Name]; !ok {
57+
values[buildValue.Name] = parameterValue{Source: sourceBuild, Value: buildValue.Value}
58+
}
59+
}
60+
61+
// Add preset values
62+
for _, preset := range presetValues {
63+
if _, ok := values[preset.Name]; !ok {
64+
values[preset.Name] = parameterValue{Source: sourcePreset, Value: preset.Value}
65+
}
66+
}
67+
68+
originalValues := make(map[string]parameterValue, len(values))
69+
for name, value := range values {
70+
// Store the original values for later use.
71+
originalValues[name] = value
72+
}
73+
74+
// Render the parameters using the values that were supplied to the previous build.
75+
//
76+
// This is how the form should look to the user on their workspace settings page.
77+
// This is the original form truth that our validations should be based on going forward.
78+
output, diags := renderer.Render(ctx, ownerID, values.ValuesMap())
79+
if diags.HasErrors() {
80+
// Top level diagnostics should break the build. Previous values (and new) should
81+
// always be valid. If there is a case where this is not true, then this has to
82+
// be changed to allow the build to continue with a different set of values.
83+
84+
return nil, diags
85+
}
86+
87+
// The user's input now needs to be validated against the parameters.
88+
// Mutability & Ephemeral parameters depend on sequential workspace builds.
89+
//
90+
// To enforce these, the user's input values are trimmed based on the
91+
// mutability and ephemeral parameters defined in the template version.
92+
// The output parameters
93+
for _, parameter := range output.Parameters {
94+
// Ephemeral parameters should not be taken from the previous build.
95+
// Remove their values from the input if they are sourced from the previous build.
96+
if parameter.Ephemeral {
97+
v := values[parameter.Name]
98+
if v.Source == sourcePrevious {
99+
delete(values, parameter.Name)
100+
}
101+
}
102+
103+
// Immutable parameters should also not be allowed to be changed from
104+
// the previous build. Remove any values taken from the preset or
105+
// new build params. This forces the value to be the same as it was before.
106+
if !firstBuild && !parameter.Mutable {
107+
delete(values, parameter.Name)
108+
prev, ok := previousValuesMap[parameter.Name]
109+
if ok {
110+
values[parameter.Name] = parameterValue{
111+
Value: prev,
112+
Source: sourcePrevious,
113+
}
114+
}
115+
}
116+
}
117+
118+
// This is the final set of values that will be used. Any errors at this stage
119+
// are fatal. Additional validation for immutability has to be done manually.
120+
output, diags = renderer.Render(ctx, ownerID, values.ValuesMap())
121+
if diags.HasErrors() {
122+
return nil, diags
123+
}
124+
125+
for _, parameter := range output.Parameters {
126+
if !firstBuild && !parameter.Mutable {
127+
if parameter.Value.AsString() != originalValues[parameter.Name].Value {
128+
var src *hcl.Range
129+
if parameter.Source != nil {
130+
src = &parameter.Source.HCLBlock().TypeRange
131+
}
132+
133+
// An immutable parameter was changed, which is not allowed.
134+
// Add the failed diagnostic to the output.
135+
diags = diags.Append(&hcl.Diagnostic{
136+
Severity: 0,
137+
Summary: "Immutable parameter changed",
138+
Detail: fmt.Sprintf("Parameter %q is not mutable, so it can't be updated after creating a workspace.", parameter.Name),
139+
Subject: src,
140+
})
141+
}
142+
}
143+
}
144+
145+
// TODO: Validate all parameter values.
146+
147+
// Return the values to be saved for the build.
148+
return values.ValuesMap(), diags
149+
}
150+
151+
type parameterValueMap map[string]parameterValue
152+
153+
func (p parameterValueMap) ValuesMap() map[string]string {
154+
values := make(map[string]string, len(p))
155+
for name, paramValue := range p {
156+
values[name] = paramValue.Value
157+
}
158+
return values
159+
}

coderd/util/slice/slice.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,13 @@ func Convert[F any, T any](a []F, f func(F) T) []T {
230230
}
231231
return tmp
232232
}
233+
234+
func ToMap[T any, K comparable, V any](a []T, cnv func(t T) (K, V)) map[K]V {
235+
m := make(map[K]V, len(a))
236+
237+
for i := range a {
238+
k, v := cnv(a[i])
239+
m[k] = v
240+
}
241+
return m
242+
}

0 commit comments

Comments
 (0)