Skip to content

feat: Preview can now show presets and validate them #149

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions cli/clidisplay/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,35 @@ func Parameters(writer io.Writer, params []types.Parameter, files map[string]*hc
_, _ = fmt.Fprintln(writer, tableWriter.Render())
}

func Presets(writer io.Writer, presets []types.Preset, files map[string]*hcl.File) {
tableWriter := table.NewWriter()
tableWriter.SetStyle(table.StyleLight)
tableWriter.Style().Options.SeparateColumns = false
row := table.Row{"Preset"}
tableWriter.AppendHeader(row)
for _, p := range presets {
tableWriter.AppendRow(table.Row{
fmt.Sprintf("%s\n%s", p.Name, formatPresetParameters(p.Parameters)),
})
if hcl.Diagnostics(p.Diagnostics).HasErrors() {
var out bytes.Buffer
WriteDiagnostics(&out, files, hcl.Diagnostics(p.Diagnostics))
tableWriter.AppendRow(table.Row{out.String()})
}

tableWriter.AppendSeparator()
}
_, _ = fmt.Fprintln(writer, tableWriter.Render())
}

func formatPresetParameters(presetParameters map[string]string) string {
var str strings.Builder
for presetParamName, PresetParamValue := range presetParameters {
_, _ = str.WriteString(fmt.Sprintf("%s = %s\n", presetParamName, PresetParamValue))
}
return str.String()
}

func formatOptions(selected []string, options []*types.ParameterOption) string {
var str strings.Builder
sep := ""
Expand Down
27 changes: 26 additions & 1 deletion cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"os"
"slices"
"strings"

"github.com/hashicorp/hcl/v2"
Expand All @@ -27,6 +28,7 @@ func (r *RootCmd) Root() *serpent.Command {
vars []string
groups []string
planJSON string
preset string
)
cmd := &serpent.Command{
Use: "codertf",
Expand Down Expand Up @@ -64,10 +66,25 @@ func (r *RootCmd) Root() *serpent.Command {
Default: "",
Value: serpent.StringArrayOf(&groups),
},
{
Name: "preset",
Description: "Name of the preset to define parameters. Run preview without this flag first to see a list of presets.",
Flag: "preset",
FlagShorthand: "s",
Default: "",
Value: serpent.StringOf(&preset),
},
},
Handler: func(i *serpent.Invocation) error {
dfs := os.DirFS(dir)

ctx := i.Context()

presets, _ := preview.PreviewPresets(ctx, dfs)
chosenPresetIndex := slices.IndexFunc(presets, func(p types.Preset) bool {
return p.Name == preset
})

rvars := make(map[string]string)
for _, val := range vars {
parts := strings.Split(val, "=")
Expand All @@ -76,6 +93,11 @@ func (r *RootCmd) Root() *serpent.Command {
}
rvars[parts[0]] = parts[1]
}
if chosenPresetIndex != -1 {
for paramName, paramValue := range presets[chosenPresetIndex].Parameters {
rvars[paramName] = paramValue
}
}

input := preview.Input{
PlanJSONPath: planJSON,
Expand All @@ -85,7 +107,6 @@ func (r *RootCmd) Root() *serpent.Command {
},
}

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

if chosenPresetIndex == -1 {
clidisplay.Presets(os.Stdout, presets, output.Files)
}

clidisplay.Parameters(os.Stdout, output.Parameters, output.Files)

if !output.ModuleOutput.IsNull() && !(output.ModuleOutput.Type().IsObjectType() && output.ModuleOutput.LengthInt() == 0) {
Expand Down
31 changes: 31 additions & 0 deletions extract/preset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package extract

import (
"github.com/aquasecurity/trivy/pkg/iac/terraform"
"github.com/coder/preview/types"
"github.com/hashicorp/hcl/v2"
)

func PresetFromBlock(block *terraform.Block) (*types.Preset, hcl.Diagnostics) {
var diags hcl.Diagnostics

pName, nameDiag := requiredString(block, "name")
if nameDiag != nil {
diags = append(diags, nameDiag)
}

p := types.Preset{
PresetData: types.PresetData{
Name: pName,
Parameters: make(map[string]string),
},
Diagnostics: types.Diagnostics{},
}

params := block.GetAttribute("parameters").AsMapValue()
for presetParamName, presetParamValue := range params.Value() {
p.Parameters[presetParamName] = presetParamValue
}
Comment on lines +25 to +28
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder what happens on unknown or null values 🤔


return &p, diags
}
53 changes: 53 additions & 0 deletions preset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package preview

import (
"fmt"
"slices"

"github.com/aquasecurity/trivy/pkg/iac/terraform"
"github.com/hashicorp/hcl/v2"

"github.com/coder/preview/extract"
"github.com/coder/preview/types"
)

func presets(modules terraform.Modules, parameters []types.Parameter) ([]types.Preset, hcl.Diagnostics) {
diags := make(hcl.Diagnostics, 0)
presets := make([]types.Preset, 0)
Comment on lines +14 to +16
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the function name presets is shadowed by the variable. Would be nice if they were different


for _, mod := range modules {
blocks := mod.GetDatasByType(types.BlockTypePreset)
for _, block := range blocks {
preset, pDiags := extract.PresetFromBlock(block)
if len(pDiags) > 0 {
diags = diags.Extend(pDiags)
}

if preset == nil {
continue
}

for paramName, paramValue := range preset.Parameters {
templateParamIndex := slices.IndexFunc(parameters, func(p types.Parameter) bool {
return p.Name == paramName
})
if templateParamIndex == -1 {
preset.Diagnostics = append(preset.Diagnostics, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Undefined Parameter",
Detail: fmt.Sprintf("Preset %q requires parameter %q, but it is not defined by the template.", preset.Name, paramName),
})
continue
}
templateParam := parameters[templateParamIndex]
for _, diag := range templateParam.Valid(types.StringLiteral(paramValue)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice 👍

preset.Diagnostics = append(preset.Diagnostics, diag)
}
}

presets = append(presets, *preset)
}
}

return presets, diags
}
67 changes: 67 additions & 0 deletions preview.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,70 @@ func tfVarFiles(path string, dir fs.FS) ([]string, error) {
}
return files, nil
}

func PreviewPresets(ctx context.Context, dir fs.FS) ([]types.Preset, hcl.Diagnostics) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we'd just put this in Preview. And if the diagnostics are scoped to Presets, then we can essentially ignore preset diagnostics when they are not relevant.

But I do see it can throw top level diags.
So maybe there is a way we can filter diags by certain categories 🤔

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had it in Preview initially, but then I moved it out as my understanding of the codebase evolved. I think it has evolved further such that we can move it back into Preview, yes.

// The trivy package works with `github.com/zclconf/go-cty`. This package is
// similar to `reflect` in its usage. This package can panic if types are
// misused. To protect the caller, a general `recover` is used to catch any
// mistakes. If this happens, there is a developer bug that needs to be resolved.
var diagnostics hcl.Diagnostics
defer func() {
if r := recover(); r != nil {
diagnostics.Extend(hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "Panic occurred in preview. This should not happen, please report this to Coder.",
Detail: fmt.Sprintf("panic in preview: %+v", r),
},
})
}
}()

logger := slog.New(slog.DiscardHandler)

varFiles, err := tfVarFiles("", dir)
if err != nil {
return nil, hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "Files not found",
Detail: err.Error(),
},
}
}

// moduleSource is "" for a local module
p := parser.New(dir, "",
parser.OptionWithLogger(logger),
parser.OptionStopOnHCLError(false),
parser.OptionWithDownloads(false),
parser.OptionWithSkipCachedModules(true),
parser.OptionWithTFVarsPaths(varFiles...),
)

err = p.ParseFS(ctx, ".")
if err != nil {
return nil, hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "Parse terraform files",
Detail: err.Error(),
},
}
}

modules, err := p.EvaluateAll(ctx)
if err != nil {
return nil, hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "Evaluate terraform files",
Detail: err.Error(),
},
}
}

rp, rpDiags := parameters(modules)
presets, presetDiags := presets(modules, rp)
return presets, diagnostics.Extend(rpDiags).Extend(presetDiags)
}
4 changes: 4 additions & 0 deletions preview_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func Test_Extract(t *testing.T) {
expTags map[string]string
unknownTags []string
params map[string]assertParam
presets []types.Preset
warnings []*regexp.Regexp
}{
{
Expand Down Expand Up @@ -520,6 +521,9 @@ func Test_Extract(t *testing.T) {
require.True(t, ok, "unknown parameter %s", param.Name)
check(t, param)
}

presets, diags := preview.PreviewPresets(context.Background(), dirFs)
assert.ElementsMatch(t, tc.presets, presets)
})
}
}
Expand Down
17 changes: 17 additions & 0 deletions types/preset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package types

const (
BlockTypePreset = "coder_workspace_preset"
)

type Preset struct {
PresetData
// Diagnostics is used to store any errors that occur during parsing
// of the parameter.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// of the parameter.
// of the preset.

Diagnostics Diagnostics `json:"diagnostics"`
}

type PresetData struct {
Name string `json:"name"`
Parameters map[string]string `json:"parameters"`
}
Loading