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

Skip to content

Commit 898bdf3

Browse files
committed
feat: implement dynamic parameter validation
1 parent d7398d9 commit 898bdf3

File tree

10 files changed

+397
-116
lines changed

10 files changed

+397
-116
lines changed

coderd/autobuild/lifecycle_executor.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"golang.org/x/xerrors"
1818

1919
"cdr.dev/slog"
20+
"github.com/coder/coder/v2/coderd/files"
2021

2122
"github.com/coder/coder/v2/coderd/audit"
2223
"github.com/coder/coder/v2/coderd/database"
@@ -32,9 +33,10 @@ import (
3233

3334
// Executor automatically starts or stops workspaces.
3435
type Executor struct {
35-
ctx context.Context
36-
db database.Store
37-
ps pubsub.Pubsub
36+
ctx context.Context
37+
db database.Store
38+
ps pubsub.Pubsub
39+
//fileCache *files.Cache
3840
templateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore]
3941
accessControlStore *atomic.Pointer[dbauthz.AccessControlStore]
4042
auditor *atomic.Pointer[audit.Auditor]
@@ -61,13 +63,14 @@ type Stats struct {
6163
}
6264

6365
// New returns a new wsactions executor.
64-
func NewExecutor(ctx context.Context, db database.Store, ps pubsub.Pubsub, reg prometheus.Registerer, tss *atomic.Pointer[schedule.TemplateScheduleStore], auditor *atomic.Pointer[audit.Auditor], acs *atomic.Pointer[dbauthz.AccessControlStore], log slog.Logger, tick <-chan time.Time, enqueuer notifications.Enqueuer, exp codersdk.Experiments) *Executor {
66+
func NewExecutor(ctx context.Context, db database.Store, ps pubsub.Pubsub, fc *files.Cache, reg prometheus.Registerer, tss *atomic.Pointer[schedule.TemplateScheduleStore], auditor *atomic.Pointer[audit.Auditor], acs *atomic.Pointer[dbauthz.AccessControlStore], log slog.Logger, tick <-chan time.Time, enqueuer notifications.Enqueuer, exp codersdk.Experiments) *Executor {
6567
factory := promauto.With(reg)
6668
le := &Executor{
6769
//nolint:gocritic // Autostart has a limited set of permissions.
6870
ctx: dbauthz.AsAutostart(ctx),
6971
db: db,
7072
ps: ps,
73+
fileCache: fc,
7174
templateScheduleStore: tss,
7275
tick: tick,
7376
log: log.Named("autobuild"),
@@ -276,7 +279,7 @@ func (e *Executor) runOnce(t time.Time) Stats {
276279
}
277280
}
278281

279-
nextBuild, job, _, err = builder.Build(e.ctx, tx, nil, audit.WorkspaceBuildBaggage{IP: "127.0.0.1"})
282+
nextBuild, job, _, err = builder.Build(e.ctx, tx, e.fileCache, nil, audit.WorkspaceBuildBaggage{IP: "127.0.0.1"})
280283
if err != nil {
281284
return xerrors.Errorf("build workspace with transition %q: %w", nextTransition, err)
282285
}

coderd/dynamicparameters/render.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ type loader struct {
5050
// Prepare is the entrypoint for this package. It loads the necessary objects &
5151
// files from the database and returns a Renderer that can be used to render the
5252
// template version's parameters.
53-
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) {
5454
l := &loader{
5555
templateVersionID: versionID,
5656
}
@@ -138,7 +138,7 @@ func (r *loader) loadData(ctx context.Context, db database.Store) error {
138138
// Static parameter rendering is required to support older template versions that
139139
// do not have the database state to support dynamic parameters. A constant
140140
// warning will be displayed for these template versions.
141-
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) {
142142
err := r.loadData(ctx, db)
143143
if err != nil {
144144
return nil, xerrors.Errorf("load data: %w", err)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package dynamicparameters
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestProvisionerVersionSupportsDynamicParameters(t *testing.T) {
10+
t.Parallel()
11+
12+
for v, dyn := range map[string]bool{
13+
"": false,
14+
"na": false,
15+
"0.0": false,
16+
"0.10": false,
17+
"1.4": false,
18+
"1.5": false,
19+
"1.6": true,
20+
"1.7": true,
21+
"1.8": true,
22+
"2.0": true,
23+
"2.17": true,
24+
"4.0": true,
25+
} {
26+
t.Run(v, func(t *testing.T) {
27+
t.Parallel()
28+
29+
does := ProvisionerVersionSupportsDynamicParameters(v)
30+
require.Equal(t, dyn, does)
31+
})
32+
}
33+
}

coderd/dynamicparameters/resolver.go

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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+
// TODO: The previous code always returned parameter names and values, even if they were not set
149+
// by the user. So this should loop over the parameters and return all of them.
150+
// This catches things like if a default value changes, we keep the old value.
151+
return values.ValuesMap(), diags
152+
}
153+
154+
type parameterValueMap map[string]parameterValue
155+
156+
func (p parameterValueMap) ValuesMap() map[string]string {
157+
values := make(map[string]string, len(p))
158+
for name, paramValue := range p {
159+
values[name] = paramValue.Value
160+
}
161+
return values
162+
}

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+
}

coderd/workspacebuilds.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,7 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
391391
workspaceBuild, provisionerJob, provisionerDaemons, err = builder.Build(
392392
ctx,
393393
tx,
394+
api.FileCache,
394395
func(action policy.Action, object rbac.Objecter) bool {
395396
// Special handling for prebuilt workspace deletion
396397
if object.RBACObject().Type == rbac.ResourceWorkspace.Type && action == policy.ActionDelete {

coderd/workspaces.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,7 @@ func createWorkspace(
724724
workspaceBuild, provisionerJob, provisionerDaemons, err = builder.Build(
725725
ctx,
726726
db,
727+
api.FileCache,
727728
func(action policy.Action, object rbac.Objecter) bool {
728729
return api.Authorize(r, action, object)
729730
},

0 commit comments

Comments
 (0)