Skip to content

Commit bbc1114

Browse files
committed
feat: ValidatePresets to validate presets for prebuild use
1 parent f33a070 commit bbc1114

File tree

5 files changed

+242
-4
lines changed

5 files changed

+242
-4
lines changed

extract/preset.go

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,34 @@
11
package extract
22

33
import (
4+
"fmt"
5+
46
"github.com/aquasecurity/trivy/pkg/iac/terraform"
5-
"github.com/coder/preview/types"
67
"github.com/hashicorp/hcl/v2"
8+
9+
"github.com/coder/preview/types"
710
)
811

9-
func PresetFromBlock(block *terraform.Block) types.Preset {
12+
func PresetFromBlock(block *terraform.Block) (tfPreset types.Preset) {
13+
defer func() {
14+
// Extra safety mechanism to ensure that if a panic occurs, we do not break
15+
// everything else.
16+
if r := recover(); r != nil {
17+
tfPreset = types.Preset{
18+
PresetData: types.PresetData{
19+
Name: block.Label(),
20+
},
21+
Diagnostics: types.Diagnostics{
22+
{
23+
Severity: hcl.DiagError,
24+
Summary: "Panic occurred in extracting preset. This should not happen, please report this to Coder.",
25+
Detail: fmt.Sprintf("panic in preset extract: %+v", r),
26+
},
27+
},
28+
}
29+
}
30+
}()
31+
1032
p := types.Preset{
1133
PresetData: types.PresetData{
1234
Parameters: make(map[string]string),
@@ -41,5 +63,13 @@ func PresetFromBlock(block *terraform.Block) types.Preset {
4163
p.Default = defaultAttr.Value().True()
4264
}
4365

66+
prebuildBlock := block.GetBlock("prebuilds")
67+
if prebuildBlock != nil {
68+
p.Prebuild = &types.PrebuildData{
69+
// Invalid values will be set to 0
70+
Instances: int(optionalInteger(prebuildBlock, "instances")),
71+
}
72+
}
73+
4474
return p
4575
}

preview.go

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,42 @@ func (o Output) MarshalJSON() ([]byte, error) {
6868
})
6969
}
7070

71+
// ValidatePresets will iterate over each preset, validate the inputs as a set,
72+
// and attach any diagnostics to the preset if there are issues.
73+
func ValidatePresets(ctx context.Context, input Input, preValid []types.Preset, dir fs.FS) {
74+
for i := range preValid {
75+
pre := &preValid[i]
76+
if pre.Prebuild == nil || pre.Prebuild.Instances <= 0 {
77+
// No prebuilds, so presets do not need to be valid without user input
78+
continue
79+
}
80+
81+
input.ParameterValues = pre.Parameters
82+
83+
output, diagnostics := Preview(ctx, input, dir)
84+
if diagnostics.HasErrors() {
85+
pre.Diagnostics = append(pre.Diagnostics, diagnostics...)
86+
}
87+
88+
if output == nil {
89+
continue
90+
}
91+
92+
for _, param := range output.Parameters {
93+
if hcl.Diagnostics(param.Diagnostics).HasErrors() {
94+
for _, paramDiag := range param.Diagnostics {
95+
if paramDiag.Severity != hcl.DiagError {
96+
continue // Only care about errors here
97+
}
98+
orig := paramDiag.Summary
99+
paramDiag.Summary = fmt.Sprintf("Parameter %s: %s", param.Name, orig)
100+
pre.Diagnostics = append(pre.Diagnostics, paramDiag)
101+
}
102+
}
103+
}
104+
}
105+
}
106+
71107
func Preview(ctx context.Context, input Input, dir fs.FS) (output *Output, diagnostics hcl.Diagnostics) {
72108
// The trivy package works with `github.com/zclconf/go-cty`. This package is
73109
// similar to `reflect` in its usage. This package can panic if types are
@@ -180,7 +216,9 @@ func Preview(ctx context.Context, input Input, dir fs.FS) (output *Output, diagn
180216

181217
diags := make(hcl.Diagnostics, 0)
182218
rp, rpDiags := parameters(modules)
183-
presets := presets(modules, rp)
219+
// preValidPresets are extracted as written in terraform. Each individual
220+
// param value is checked, however the preset as a whole might not valid.
221+
preValidPresets := presets(modules, rp)
184222
tags, tagDiags := workspaceTags(modules, p.Files())
185223
vars := variables(modules)
186224

@@ -191,7 +229,7 @@ func Preview(ctx context.Context, input Input, dir fs.FS) (output *Output, diagn
191229
ModuleOutput: outputs,
192230
Parameters: rp,
193231
WorkspaceTags: tags,
194-
Presets: presets,
232+
Presets: preValidPresets,
195233
Files: p.Files(),
196234
Variables: vars,
197235
}, diags.Extend(rpDiags).Extend(tagDiags)

preview_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,55 @@ func Test_Extract(t *testing.T) {
700700
}
701701
}
702702

703+
func TestPresetValidation(t *testing.T) {
704+
t.Parallel()
705+
706+
for _, tc := range []struct {
707+
name string
708+
dir string
709+
input preview.Input
710+
presetAssert map[string]assertPreset
711+
}{
712+
{
713+
name: "preset failure",
714+
dir: "presetfail",
715+
input: preview.Input{},
716+
presetAssert: map[string]assertPreset{
717+
"invalid_parameters": aPreWithDiags().
718+
errorDiagnostics("Parameter no_default: Required parameter not provided"),
719+
"valid_preset": aPre().
720+
value("has_default", "changed").
721+
value("no_default", "custom value").
722+
noDiagnostics(),
723+
"prebuild_instance_zero": aPre(),
724+
"not_prebuild": aPre(),
725+
},
726+
},
727+
} {
728+
t.Run(tc.name, func(t *testing.T) {
729+
t.Parallel()
730+
731+
dirFs := os.DirFS(filepath.Join("testdata", tc.dir))
732+
output, diags := preview.Preview(context.Background(), tc.input, dirFs)
733+
if diags.HasErrors() {
734+
t.Logf("diags: %s", diags)
735+
}
736+
require.False(t, diags.HasErrors())
737+
require.Len(t, diags, 0)
738+
739+
preview.ValidatePresets(context.Background(), tc.input, output.Presets, dirFs)
740+
for _, preset := range output.Presets {
741+
check, ok := tc.presetAssert[preset.Name]
742+
require.True(t, ok, "unknown preset %s", preset.Name)
743+
check(t, preset)
744+
delete(tc.presetAssert, preset.Name)
745+
}
746+
747+
require.Len(t, tc.presetAssert, 0, "some presets were not found")
748+
})
749+
}
750+
}
751+
703752
type assertVariable func(t *testing.T, variable types.Variable)
704753

705754
func av() assertVariable {
@@ -890,6 +939,71 @@ func (a assertVariable) extend(f assertVariable) assertVariable {
890939
}
891940
}
892941

942+
type assertPreset func(t *testing.T, preset types.Preset)
943+
944+
func aPre() assertPreset {
945+
return func(t *testing.T, parameter types.Preset) {
946+
t.Helper()
947+
assert.Empty(t, parameter.Diagnostics, "parameter should have no diagnostics")
948+
}
949+
}
950+
951+
func aPreWithDiags() assertPreset {
952+
return func(t *testing.T, parameter types.Preset) {}
953+
}
954+
955+
func (a assertPreset) def(def bool) assertPreset {
956+
return a.extend(func(t *testing.T, preset types.Preset) {
957+
require.Equal(t, def, preset.Default)
958+
})
959+
}
960+
961+
func (a assertPreset) value(key, value string) assertPreset {
962+
return a.extend(func(t *testing.T, preset types.Preset) {
963+
v, ok := preset.Parameters[key]
964+
require.Truef(t, ok, "preset parameter %q existence check", key)
965+
assert.Equalf(t, value, v, "preset parameter %q value equality check", key)
966+
})
967+
}
968+
969+
func (a assertPreset) errorDiagnostics(patterns ...string) assertPreset {
970+
return a.diagnostics(hcl.DiagError, patterns...)
971+
}
972+
973+
func (a assertPreset) warnDiagnostics(patterns ...string) assertPreset {
974+
return a.diagnostics(hcl.DiagWarning, patterns...)
975+
}
976+
977+
func (a assertPreset) diagnostics(sev hcl.DiagnosticSeverity, patterns ...string) assertPreset {
978+
shadow := patterns
979+
return a.extend(func(t *testing.T, preset types.Preset) {
980+
t.Helper()
981+
982+
assertDiags(t, sev, preset.Diagnostics, shadow...)
983+
})
984+
}
985+
986+
func (a assertPreset) noDiagnostics() assertPreset {
987+
return a.extend(func(t *testing.T, preset types.Preset) {
988+
t.Helper()
989+
990+
assert.Empty(t, preset.Diagnostics, "parameter should have no diagnostics")
991+
})
992+
}
993+
994+
//nolint:revive
995+
func (a assertPreset) extend(f assertPreset) assertPreset {
996+
if a == nil {
997+
a = func(t *testing.T, v types.Preset) {}
998+
}
999+
1000+
return func(t *testing.T, v types.Preset) {
1001+
t.Helper()
1002+
(a)(t, v)
1003+
f(t, v)
1004+
}
1005+
}
1006+
8931007
func assertDiags(t *testing.T, sev hcl.DiagnosticSeverity, diags types.Diagnostics, patterns ...string) {
8941008
t.Helper()
8951009
checks := make([]string, len(patterns))

testdata/presetfail/main.tf

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
terraform {
2+
required_providers {
3+
coder = {
4+
source = "coder/coder"
5+
version = "2.8.0"
6+
}
7+
}
8+
}
9+
10+
data "coder_parameter" "no_default" {
11+
name = "no_default"
12+
}
13+
14+
data "coder_parameter" "has_default" {
15+
name = "has_default"
16+
default = "hello world"
17+
}
18+
19+
20+
data "coder_workspace_preset" "invalid_parameters" {
21+
name = "invalid_parameters"
22+
23+
prebuilds {
24+
instances = 1
25+
}
26+
}
27+
28+
data "coder_workspace_preset" "valid_preset" {
29+
name = "valid_preset"
30+
31+
parameters = {
32+
"no_default" = "custom value"
33+
"has_default" = "changed"
34+
}
35+
prebuilds {
36+
instances = 1
37+
}
38+
}
39+
40+
data "coder_workspace_preset" "prebuild_instance_zero" {
41+
name = "prebuild_instance_zero"
42+
43+
prebuilds {
44+
// No instances
45+
instances = 0
46+
}
47+
}
48+
49+
data "coder_workspace_preset" "not_prebuild" {
50+
name = "not_prebuild"
51+
}

types/preset.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,13 @@ type Preset struct {
1111
Diagnostics Diagnostics `json:"diagnostics"`
1212
}
1313

14+
type PrebuildData struct {
15+
Instances int `json:"instances"`
16+
}
17+
1418
type PresetData struct {
1519
Name string `json:"name"`
1620
Parameters map[string]string `json:"parameters"`
1721
Default bool `json:"default"`
22+
Prebuild *PrebuildData
1823
}

0 commit comments

Comments
 (0)