Skip to content

Commit 01b32d1

Browse files
committed
chore: add parameter min/max/regex validation
1 parent ae74722 commit 01b32d1

File tree

5 files changed

+106
-3
lines changed

5 files changed

+106
-3
lines changed

cli/clidisplay/resources.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package clidisplay
22

33
import (
4+
"bytes"
45
"fmt"
56
"io"
67
"strings"
@@ -49,7 +50,7 @@ func WorkspaceTags(writer io.Writer, tags types.TagBlocks) hcl.Diagnostics {
4950
return diags
5051
}
5152

52-
func Parameters(writer io.Writer, params []types.Parameter) {
53+
func Parameters(writer io.Writer, params []types.Parameter, files map[string]*hcl.File) {
5354
tableWriter := table.NewWriter()
5455
//tableWriter.SetTitle("Parameters")
5556
tableWriter.SetStyle(table.StyleLight)
@@ -73,6 +74,14 @@ func Parameters(writer io.Writer, params []types.Parameter) {
7374
tableWriter.AppendRow(table.Row{
7475
fmt.Sprintf("%s: %s\n%s", p.Name, p.Description, formatOptions(strVal, p.Options)),
7576
})
77+
78+
if hcl.Diagnostics(p.Diagnostics).HasErrors() {
79+
var out bytes.Buffer
80+
WriteDiagnostics(&out, files, hcl.Diagnostics(p.Diagnostics))
81+
tableWriter.AppendRow(table.Row{out.String()})
82+
83+
}
84+
7685
tableWriter.AppendSeparator()
7786
}
7887
_, _ = fmt.Fprintln(writer, tableWriter.Render())

cli/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ func (r *RootCmd) Root() *serpent.Command {
8787
clidisplay.WriteDiagnostics(os.Stderr, output.Files, diags)
8888
}
8989

90-
clidisplay.Parameters(os.Stdout, output.Parameters)
90+
clidisplay.Parameters(os.Stdout, output.Parameters, output.Files)
9191

9292
return nil
9393
},

extract/parameter.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,40 @@ func ParameterFromBlock(block *terraform.Block) (*types.Parameter, hcl.Diagnosti
7777
p.Validations = append(p.Validations, &valid)
7878
}
7979

80+
ctyType, err := p.CtyType()
81+
if err != nil {
82+
paramTypeDiag := &hcl.Diagnostic{
83+
Severity: hcl.DiagError,
84+
Summary: fmt.Sprintf("Invalid parameter type %q", p.Type),
85+
Detail: err.Error(),
86+
Context: &block.HCLBlock().DefRange,
87+
}
88+
89+
if attr := block.GetAttribute("type"); attr != nil && !attr.IsNil() {
90+
paramTypeDiag.Subject = &attr.HCLAttribute().Range
91+
paramTypeDiag.Expression = attr.HCLAttribute().Expr
92+
paramTypeDiag.EvalContext = block.Context().Inner()
93+
}
94+
diags = diags.Append(paramTypeDiag)
95+
}
96+
97+
if ctyType != cty.NilType && pVal.Value.Type().Equals(cty.String) {
98+
// TODO: Wish we could support more types, but only string types are
99+
// allowed.
100+
valStr := pVal.Value.AsString()
101+
// Apply validations to the parameter value
102+
for _, v := range p.Validations {
103+
if err := v.Valid(valStr); err != nil {
104+
diags = diags.Append(&hcl.Diagnostic{
105+
Severity: hcl.DiagError,
106+
Summary: fmt.Sprintf("Paramater validation failed for value %q", valStr),
107+
Detail: err.Error(),
108+
Expression: pVal.ValueExpr,
109+
})
110+
}
111+
}
112+
}
113+
80114
// Diagnostics are scoped to the parameter
81115
p.Diagnostics = types.Diagnostics(diags)
82116

testdata/conditional/main.tf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ data "coder_parameter" "project" {
2222
value = "small"
2323
description = "The small project with minimal resource requirements to work on."
2424
}
25+
26+
validation {
27+
regex = "^massive|small$"
28+
error = "You must select either massive or small."
29+
}
2530
}
2631

2732
locals {

types/parameter.go

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ package types
22

33
import (
44
"fmt"
5+
"regexp"
56
"slices"
7+
"strconv"
68
"strings"
79

810
"github.com/zclconf/go-cty/cty"
11+
"golang.org/x/xerrors"
912
)
1013

1114
// @typescript-ignore BlockTypeParameter
@@ -63,6 +66,50 @@ type ParameterValidation struct {
6366
Monotonic *string `json:"validation_monotonic"`
6467
}
6568

69+
// TODO: Match implementation from https://github.com/coder/terraform-provider-coder/blob/main/provider/parameter.go#L404-L462
70+
// TODO: Does the value have to be an option from the set of options?
71+
func (v ParameterValidation) Valid(p string) error {
72+
validErr := xerrors.New(v.errorRendered(p))
73+
if v.Regex != nil {
74+
exp, err := regexp.Compile(*v.Regex)
75+
if err != nil {
76+
return fmt.Errorf("invalid regex %q: %w", *v.Regex, err)
77+
}
78+
79+
if !exp.MatchString(p) {
80+
return validErr
81+
}
82+
}
83+
84+
if v.Min != nil || v.Max != nil {
85+
vd, err := strconv.ParseInt(p, 10, 64)
86+
if err != nil {
87+
return fmt.Errorf("invalid number value %q: %w", p, err)
88+
}
89+
90+
if v.Min != nil && vd < *v.Min {
91+
return validErr
92+
}
93+
94+
if v.Max != nil && vd > *v.Max {
95+
return validErr
96+
}
97+
}
98+
99+
// Monotonic?
100+
101+
return nil
102+
}
103+
104+
func (v ParameterValidation) errorRendered(value string) string {
105+
r := strings.NewReplacer(
106+
"{min}", fmt.Sprintf("%d", safeDeref(v.Min)),
107+
"{max}", fmt.Sprintf("%d", safeDeref(v.Max)),
108+
"{value}", value,
109+
)
110+
return r.Replace(v.Error)
111+
}
112+
66113
type ParameterOption struct {
67114
Name string `json:"name"`
68115
Description string `json:"description"`
@@ -83,6 +130,14 @@ func (r *RichParameter) CtyType() (cty.Type, error) {
83130
case "list(string)":
84131
return cty.List(cty.String), nil
85132
default:
86-
return cty.Type{}, fmt.Errorf("unsupported type: %q", r.Type)
133+
return cty.NilType, fmt.Errorf("unsupported type: %q", r.Type)
134+
}
135+
}
136+
137+
func safeDeref[T any](v *T) T {
138+
if v == nil {
139+
var zero T
140+
return zero
87141
}
142+
return *v
88143
}

0 commit comments

Comments
 (0)