Skip to content

Commit 625c327

Browse files
committed
feat: Preview can now show presets and validate them
1 parent 9e7d207 commit 625c327

File tree

7 files changed

+227
-1
lines changed

7 files changed

+227
-1
lines changed

cli/clidisplay/resources.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,35 @@ func Parameters(writer io.Writer, params []types.Parameter, files map[string]*hc
7474
_, _ = fmt.Fprintln(writer, tableWriter.Render())
7575
}
7676

77+
func Presets(writer io.Writer, presets []types.Preset, files map[string]*hcl.File) {
78+
tableWriter := table.NewWriter()
79+
tableWriter.SetStyle(table.StyleLight)
80+
tableWriter.Style().Options.SeparateColumns = false
81+
row := table.Row{"Preset"}
82+
tableWriter.AppendHeader(row)
83+
for _, p := range presets {
84+
tableWriter.AppendRow(table.Row{
85+
fmt.Sprintf("%s\n%s", p.Name, formatPresetParameters(p.Parameters)),
86+
})
87+
if hcl.Diagnostics(p.Diagnostics).HasErrors() {
88+
var out bytes.Buffer
89+
WriteDiagnostics(&out, files, hcl.Diagnostics(p.Diagnostics))
90+
tableWriter.AppendRow(table.Row{out.String()})
91+
}
92+
93+
tableWriter.AppendSeparator()
94+
}
95+
_, _ = fmt.Fprintln(writer, tableWriter.Render())
96+
}
97+
98+
func formatPresetParameters(presetParameters map[string]string) string {
99+
var str strings.Builder
100+
for presetParamName, PresetParamValue := range presetParameters {
101+
_, _ = str.WriteString(fmt.Sprintf("%s = %s\n", presetParamName, PresetParamValue))
102+
}
103+
return str.String()
104+
}
105+
77106
func formatOptions(selected []string, options []*types.ParameterOption) string {
78107
var str strings.Builder
79108
sep := ""

cli/root.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"fmt"
77
"os"
8+
"slices"
89
"strings"
910

1011
"github.com/hashicorp/hcl/v2"
@@ -27,6 +28,7 @@ func (r *RootCmd) Root() *serpent.Command {
2728
vars []string
2829
groups []string
2930
planJSON string
31+
preset string
3032
)
3133
cmd := &serpent.Command{
3234
Use: "codertf",
@@ -64,10 +66,25 @@ func (r *RootCmd) Root() *serpent.Command {
6466
Default: "",
6567
Value: serpent.StringArrayOf(&groups),
6668
},
69+
{
70+
Name: "preset",
71+
Description: "Name of the preset to define parameters. Run preview without this flag first to see a list of presets.",
72+
Flag: "preset",
73+
FlagShorthand: "s",
74+
Default: "",
75+
Value: serpent.StringOf(&preset),
76+
},
6777
},
6878
Handler: func(i *serpent.Invocation) error {
6979
dfs := os.DirFS(dir)
7080

81+
ctx := i.Context()
82+
83+
presets, _ := preview.PreviewPresets(ctx, dfs)
84+
chosenPresetIndex := slices.IndexFunc(presets, func(p types.Preset) bool {
85+
return p.Name == preset
86+
})
87+
7188
rvars := make(map[string]string)
7289
for _, val := range vars {
7390
parts := strings.Split(val, "=")
@@ -76,6 +93,11 @@ func (r *RootCmd) Root() *serpent.Command {
7693
}
7794
rvars[parts[0]] = parts[1]
7895
}
96+
if chosenPresetIndex != -1 {
97+
for paramName, paramValue := range presets[chosenPresetIndex].Parameters {
98+
rvars[paramName] = paramValue
99+
}
100+
}
79101

80102
input := preview.Input{
81103
PlanJSONPath: planJSON,
@@ -85,7 +107,6 @@ func (r *RootCmd) Root() *serpent.Command {
85107
},
86108
}
87109

88-
ctx := i.Context()
89110
output, diags := preview.Preview(ctx, input, dfs)
90111
if output == nil {
91112
return diags
@@ -103,6 +124,10 @@ func (r *RootCmd) Root() *serpent.Command {
103124
clidisplay.WriteDiagnostics(os.Stderr, output.Files, diags)
104125
}
105126

127+
if chosenPresetIndex == -1 {
128+
clidisplay.Presets(os.Stdout, presets, output.Files)
129+
}
130+
106131
clidisplay.Parameters(os.Stdout, output.Parameters, output.Files)
107132

108133
if !output.ModuleOutput.IsNull() && !(output.ModuleOutput.Type().IsObjectType() && output.ModuleOutput.LengthInt() == 0) {

extract/preset.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package extract
2+
3+
import (
4+
"github.com/aquasecurity/trivy/pkg/iac/terraform"
5+
"github.com/coder/preview/types"
6+
"github.com/hashicorp/hcl/v2"
7+
)
8+
9+
func PresetFromBlock(block *terraform.Block) (*types.Preset, hcl.Diagnostics) {
10+
var diags hcl.Diagnostics
11+
12+
pName, nameDiag := requiredString(block, "name")
13+
if nameDiag != nil {
14+
diags = append(diags, nameDiag)
15+
}
16+
17+
p := types.Preset{
18+
PresetData: types.PresetData{
19+
Name: pName,
20+
Parameters: make(map[string]string),
21+
},
22+
Diagnostics: types.Diagnostics{},
23+
}
24+
25+
params := block.GetAttribute("parameters").AsMapValue()
26+
for presetParamName, presetParamValue := range params.Value() {
27+
p.Parameters[presetParamName] = presetParamValue
28+
}
29+
30+
return &p, diags
31+
}

preset.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package preview
2+
3+
import (
4+
"fmt"
5+
"slices"
6+
7+
"github.com/aquasecurity/trivy/pkg/iac/terraform"
8+
"github.com/hashicorp/hcl/v2"
9+
10+
"github.com/coder/preview/extract"
11+
"github.com/coder/preview/types"
12+
)
13+
14+
func presets(modules terraform.Modules, parameters []types.Parameter) ([]types.Preset, hcl.Diagnostics) {
15+
diags := make(hcl.Diagnostics, 0)
16+
presets := make([]types.Preset, 0)
17+
18+
for _, mod := range modules {
19+
blocks := mod.GetDatasByType(types.BlockTypePreset)
20+
for _, block := range blocks {
21+
preset, pDiags := extract.PresetFromBlock(block)
22+
if len(pDiags) > 0 {
23+
diags = diags.Extend(pDiags)
24+
}
25+
26+
if preset == nil {
27+
continue
28+
}
29+
30+
for paramName, paramValue := range preset.Parameters {
31+
templateParamIndex := slices.IndexFunc(parameters, func(p types.Parameter) bool {
32+
return p.Name == paramName
33+
})
34+
if templateParamIndex == -1 {
35+
preset.Diagnostics = append(preset.Diagnostics, &hcl.Diagnostic{
36+
Severity: hcl.DiagError,
37+
Summary: "Undefined Parameter",
38+
Detail: fmt.Sprintf("Preset %q requires parameter %q, but it is not defined by the template.", preset.Name, paramName),
39+
})
40+
continue
41+
}
42+
templateParam := parameters[templateParamIndex]
43+
for _, diag := range templateParam.Valid(types.StringLiteral(paramValue)) {
44+
preset.Diagnostics = append(preset.Diagnostics, diag)
45+
}
46+
}
47+
48+
presets = append(presets, *preset)
49+
}
50+
}
51+
52+
return presets, diags
53+
}

preview.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,3 +209,70 @@ func tfVarFiles(path string, dir fs.FS) ([]string, error) {
209209
}
210210
return files, nil
211211
}
212+
213+
func PreviewPresets(ctx context.Context, dir fs.FS) ([]types.Preset, hcl.Diagnostics) {
214+
// The trivy package works with `github.com/zclconf/go-cty`. This package is
215+
// similar to `reflect` in its usage. This package can panic if types are
216+
// misused. To protect the caller, a general `recover` is used to catch any
217+
// mistakes. If this happens, there is a developer bug that needs to be resolved.
218+
var diagnostics hcl.Diagnostics
219+
defer func() {
220+
if r := recover(); r != nil {
221+
diagnostics.Extend(hcl.Diagnostics{
222+
{
223+
Severity: hcl.DiagError,
224+
Summary: "Panic occurred in preview. This should not happen, please report this to Coder.",
225+
Detail: fmt.Sprintf("panic in preview: %+v", r),
226+
},
227+
})
228+
}
229+
}()
230+
231+
logger := slog.New(slog.DiscardHandler)
232+
233+
varFiles, err := tfVarFiles("", dir)
234+
if err != nil {
235+
return nil, hcl.Diagnostics{
236+
{
237+
Severity: hcl.DiagError,
238+
Summary: "Files not found",
239+
Detail: err.Error(),
240+
},
241+
}
242+
}
243+
244+
// moduleSource is "" for a local module
245+
p := parser.New(dir, "",
246+
parser.OptionWithLogger(logger),
247+
parser.OptionStopOnHCLError(false),
248+
parser.OptionWithDownloads(false),
249+
parser.OptionWithSkipCachedModules(true),
250+
parser.OptionWithTFVarsPaths(varFiles...),
251+
)
252+
253+
err = p.ParseFS(ctx, ".")
254+
if err != nil {
255+
return nil, hcl.Diagnostics{
256+
{
257+
Severity: hcl.DiagError,
258+
Summary: "Parse terraform files",
259+
Detail: err.Error(),
260+
},
261+
}
262+
}
263+
264+
modules, err := p.EvaluateAll(ctx)
265+
if err != nil {
266+
return nil, hcl.Diagnostics{
267+
{
268+
Severity: hcl.DiagError,
269+
Summary: "Evaluate terraform files",
270+
Detail: err.Error(),
271+
},
272+
}
273+
}
274+
275+
rp, rpDiags := parameters(modules)
276+
presets, presetDiags := presets(modules, rp)
277+
return presets, diagnostics.Extend(rpDiags).Extend(presetDiags)
278+
}

preview_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ func Test_Extract(t *testing.T) {
4242
expTags map[string]string
4343
unknownTags []string
4444
params map[string]assertParam
45+
presets []types.Preset
4546
warnings []*regexp.Regexp
4647
}{
4748
{
@@ -543,6 +544,9 @@ func Test_Extract(t *testing.T) {
543544
require.True(t, ok, "unknown parameter %s", param.Name)
544545
check(t, param)
545546
}
547+
548+
presets, diags := preview.PreviewPresets(context.Background(), dirFs)
549+
assert.ElementsMatch(t, tc.presets, presets)
546550
})
547551
}
548552
}

types/preset.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package types
2+
3+
const (
4+
BlockTypePreset = "coder_workspace_preset"
5+
)
6+
7+
type Preset struct {
8+
PresetData
9+
// Diagnostics is used to store any errors that occur during parsing
10+
// of the parameter.
11+
Diagnostics Diagnostics `json:"diagnostics"`
12+
}
13+
14+
type PresetData struct {
15+
Name string `json:"name"`
16+
Parameters map[string]string `json:"parameters"`
17+
}

0 commit comments

Comments
 (0)