Skip to content

Commit d0d0e66

Browse files
authored
Merge pull request #5 from coder/tf_test
test: e2e testing framework to compare against `terraform apply`
2 parents 6fed6a0 + b250c6d commit d0d0e66

File tree

21 files changed

+1004
-74
lines changed

21 files changed

+1004
-74
lines changed

attrs/values.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package attrs
2+
3+
import (
4+
"github.com/aquasecurity/trivy/pkg/iac/terraform"
5+
"github.com/zclconf/go-cty/cty"
6+
"github.com/zclconf/go-cty/cty/gocty"
7+
)
8+
9+
type Values interface {
10+
// Attr returns the value of the attribute with the given key.
11+
// If the attribute does not exist, it returns cty.NilVal.
12+
Attr(key string) cty.Value
13+
}
14+
15+
func NewValues[T *terraform.Block | map[string]any](block T) Values {
16+
switch v := any(block).(type) {
17+
case *terraform.Block:
18+
return &blockValues{Values: v}
19+
case map[string]any:
20+
return &mapValues{Values: v}
21+
default:
22+
panic("unsupported type")
23+
}
24+
}
25+
26+
type blockValues struct {
27+
Values *terraform.Block
28+
}
29+
30+
func (b *blockValues) Attr(key string) cty.Value {
31+
attr := b.Values.GetAttribute(key)
32+
if attr.IsNil() {
33+
return cty.NilVal
34+
}
35+
return attr.Value()
36+
}
37+
38+
type mapValues struct {
39+
Values map[string]any
40+
}
41+
42+
func (m *mapValues) Attr(key string) cty.Value {
43+
val, ok := m.Values[key]
44+
if !ok {
45+
return cty.NilVal
46+
}
47+
48+
gt, err := gocty.ImpliedType(val)
49+
if err != nil {
50+
return cty.NilVal
51+
}
52+
v, err := gocty.ToCtyValue(val, gt)
53+
if err != nil {
54+
return cty.NilVal
55+
}
56+
return v
57+
}

cli/clidisplay/resources.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func Parameters(writer io.Writer, params []types.Parameter) {
7979
_, _ = fmt.Fprintln(writer, tableWriter.Render())
8080
}
8181

82-
func formatOptions(selected string, options []*types.RichParameterOption) string {
82+
func formatOptions(selected string, options []*types.ParameterOption) string {
8383
var str strings.Builder
8484
sep := ""
8585
found := false

extract/json.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package extract
2+
3+
import (
4+
"fmt"
5+
)
6+
7+
type stateParse struct {
8+
errors []error
9+
values map[string]any
10+
}
11+
12+
func newStateParse(v map[string]any) *stateParse {
13+
return &stateParse{
14+
errors: make([]error, 0),
15+
values: v,
16+
}
17+
}
18+
19+
func (p *stateParse) optionalString(key string) string {
20+
return optional[string](p.values, key)
21+
}
22+
23+
func (p *stateParse) optionalInteger(key string) int64 {
24+
return optional[int64](p.values, key)
25+
}
26+
27+
func (p *stateParse) optionalBool(key string) bool {
28+
return optional[bool](p.values, key)
29+
}
30+
31+
func (p *stateParse) nullableInteger(key string) *int64 {
32+
if p.values[key] == nil {
33+
return nil
34+
}
35+
v := optional[int64](p.values, key)
36+
return &v
37+
}
38+
39+
func (p *stateParse) string(key string) string {
40+
v, err := expected[string](p.values, key)
41+
if err != nil {
42+
p.errors = append(p.errors, err)
43+
return ""
44+
}
45+
return v
46+
}
47+
48+
func optional[T any](vals map[string]any, key string) T {
49+
v, _ := expected[T](vals, key)
50+
return v
51+
}
52+
53+
func expected[T any](vals map[string]any, key string) (T, error) {
54+
v, ok := vals[key]
55+
if !ok {
56+
return *new(T), fmt.Errorf("missing required key %q", key)
57+
}
58+
59+
val, ok := v.(T)
60+
if !ok {
61+
return *new(T), fmt.Errorf("key %q is not of type %T", key, v)
62+
}
63+
return val, nil
64+
}

extract/parameter.go

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
package extract
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/aquasecurity/trivy/pkg/iac/terraform"
8+
"github.com/hashicorp/hcl/v2"
9+
"github.com/zclconf/go-cty/cty"
10+
11+
"github.com/coder/preview/hclext"
12+
"github.com/coder/preview/types"
13+
)
14+
15+
func ParameterFromBlock(block *terraform.Block) (types.Parameter, hcl.Diagnostics) {
16+
diags := required(block, "name", "type")
17+
if diags.HasErrors() {
18+
return types.Parameter{}, diags
19+
}
20+
21+
pType, typDiag := requiredString("type", block)
22+
if typDiag != nil {
23+
diags = diags.Append(typDiag)
24+
}
25+
26+
pName, nameDiag := requiredString("name", block)
27+
if nameDiag != nil {
28+
diags = diags.Append(nameDiag)
29+
}
30+
31+
pVal, valDiags := richParameterValue(block)
32+
diags = diags.Extend(valDiags)
33+
34+
if diags.HasErrors() {
35+
return types.Parameter{}, diags
36+
}
37+
38+
p := types.Parameter{
39+
Value: types.ParameterValue{
40+
Value: pVal,
41+
},
42+
RichParameter: types.RichParameter{
43+
Name: pName,
44+
Description: optionalString(block, "description"),
45+
Type: pType,
46+
Mutable: optionalBoolean(block, "mutable"),
47+
// Default value is always written as a string, then converted
48+
// to the correct type.
49+
DefaultValue: optionalString(block, "default"),
50+
Icon: optionalString(block, "icon"),
51+
Options: make([]*types.ParameterOption, 0),
52+
Validations: make([]*types.ParameterValidation, 0),
53+
Required: optionalBoolean(block, "required"),
54+
DisplayName: optionalString(block, "display_name"),
55+
Order: optionalInteger(block, "order"),
56+
Ephemeral: optionalBoolean(block, "ephemeral"),
57+
BlockName: block.NameLabel(),
58+
},
59+
}
60+
61+
for _, b := range block.GetBlocks("option") {
62+
opt, optDiags := ParameterOptionFromBlock(b)
63+
diags = diags.Extend(optDiags)
64+
65+
if optDiags.HasErrors() {
66+
continue
67+
}
68+
69+
p.Options = append(p.Options, &opt)
70+
}
71+
72+
for _, b := range block.GetBlocks("validation") {
73+
valid, validDiags := ParameterValidationFromBlock(b)
74+
diags = diags.Extend(validDiags)
75+
76+
if validDiags.HasErrors() {
77+
continue
78+
}
79+
80+
p.Validations = append(p.Validations, &valid)
81+
}
82+
83+
return p, diags
84+
}
85+
86+
func ParameterValidationFromBlock(block *terraform.Block) (types.ParameterValidation, hcl.Diagnostics) {
87+
diags := required(block, "error")
88+
if diags.HasErrors() {
89+
return types.ParameterValidation{}, diags
90+
}
91+
92+
pErr, errDiag := requiredString("error", block)
93+
if errDiag != nil {
94+
diags = diags.Append(errDiag)
95+
}
96+
97+
if diags.HasErrors() {
98+
return types.ParameterValidation{}, diags
99+
}
100+
101+
p := types.ParameterValidation{
102+
Regex: optionalString(block, "regex"),
103+
Error: pErr,
104+
Min: nullableInteger(block, "min"),
105+
Max: nullableInteger(block, "max"),
106+
Monotonic: optionalString(block, "monotonic"),
107+
}
108+
109+
return p, diags
110+
}
111+
112+
func ParameterOptionFromBlock(block *terraform.Block) (types.ParameterOption, hcl.Diagnostics) {
113+
diags := required(block, "name", "value")
114+
if diags.HasErrors() {
115+
return types.ParameterOption{}, diags
116+
}
117+
118+
pName, nameDiag := requiredString("name", block)
119+
if nameDiag != nil {
120+
diags = diags.Append(nameDiag)
121+
}
122+
123+
pVal, valDiag := requiredString("value", block)
124+
if valDiag != nil {
125+
diags = diags.Append(valDiag)
126+
}
127+
128+
if diags.HasErrors() {
129+
return types.ParameterOption{}, diags
130+
}
131+
132+
p := types.ParameterOption{
133+
Name: pName,
134+
Description: optionalString(block, "description"),
135+
Value: pVal,
136+
Icon: optionalString(block, "icon"),
137+
}
138+
139+
return p, diags
140+
}
141+
142+
func requiredString(key string, block *terraform.Block) (string, *hcl.Diagnostic) {
143+
tyAttr := block.GetAttribute(key)
144+
tyVal := tyAttr.Value()
145+
if tyVal.Type() != cty.String {
146+
diag := &hcl.Diagnostic{
147+
Severity: hcl.DiagError,
148+
Summary: fmt.Sprintf("Invalid %q attribute", key),
149+
Detail: fmt.Sprintf("Expected a string, got %q", tyVal.Type().FriendlyName()),
150+
Subject: &(tyAttr.HCLAttribute().Range),
151+
//Context: &(block.HCLBlock().DefRange),
152+
Expression: tyAttr.HCLAttribute().Expr,
153+
EvalContext: block.Context().Inner(),
154+
}
155+
156+
if !tyVal.IsWhollyKnown() {
157+
refs := hclext.ReferenceNames(tyAttr.HCLAttribute().Expr)
158+
if len(refs) > 0 {
159+
diag.Detail = fmt.Sprintf("Value is not known, check the references [%s] are resolvable",
160+
strings.Join(refs, ", "))
161+
}
162+
}
163+
164+
return "", diag
165+
}
166+
167+
return tyVal.AsString(), nil
168+
}
169+
170+
func optionalBoolean(block *terraform.Block, key string) bool {
171+
attr := block.GetAttribute(key)
172+
if attr == nil || attr.IsNil() {
173+
return false
174+
}
175+
val := attr.Value()
176+
if val.Type() != cty.Bool {
177+
return false
178+
}
179+
180+
return val.True()
181+
}
182+
183+
func nullableInteger(block *terraform.Block, key string) *int64 {
184+
attr := block.GetAttribute(key)
185+
if attr == nil || attr.IsNil() {
186+
return nil
187+
}
188+
val := attr.Value()
189+
if val.Type() != cty.Number {
190+
return nil
191+
}
192+
193+
i, acc := val.AsBigFloat().Int64()
194+
var _ = acc // acc should be 0
195+
196+
return &i
197+
}
198+
199+
func optionalInteger(block *terraform.Block, key string) int64 {
200+
attr := block.GetAttribute(key)
201+
if attr == nil || attr.IsNil() {
202+
return 0
203+
}
204+
val := attr.Value()
205+
if val.Type() != cty.Number {
206+
return 0
207+
}
208+
209+
i, acc := val.AsBigFloat().Int64()
210+
var _ = acc // acc should be 0
211+
212+
return i
213+
}
214+
215+
func optionalString(block *terraform.Block, key string) string {
216+
attr := block.GetAttribute(key)
217+
if attr == nil || attr.IsNil() {
218+
return ""
219+
}
220+
val := attr.Value()
221+
if val.Type() != cty.String {
222+
return ""
223+
}
224+
225+
return val.AsString()
226+
}
227+
228+
func required(block *terraform.Block, keys ...string) hcl.Diagnostics {
229+
var diags hcl.Diagnostics
230+
for _, key := range keys {
231+
attr := block.GetAttribute(key)
232+
if attr == nil || attr.IsNil() || attr.Value() == cty.NilVal {
233+
r := block.HCLBlock().Body.MissingItemRange()
234+
diags = diags.Append(&hcl.Diagnostic{
235+
Severity: hcl.DiagError,
236+
Summary: fmt.Sprintf("Missing required attribute %q", key),
237+
Detail: fmt.Sprintf("The %s attribute is required", key),
238+
Subject: &r,
239+
Extra: nil,
240+
})
241+
}
242+
}
243+
return diags
244+
}
245+
246+
func richParameterValue(block *terraform.Block) (cty.Value, hcl.Diagnostics) {
247+
// Find the value of the parameter from the context.
248+
paramPath := append([]string{"data"}, block.Labels()...)
249+
valueRef := hclext.ScopeTraversalExpr(append(paramPath, "value")...)
250+
return valueRef.Value(block.Context().Inner())
251+
}
252+
253+
func ParameterCtyType(typ string) (cty.Type, error) {
254+
switch typ {
255+
case "string":
256+
return cty.String, nil
257+
case "number":
258+
return cty.Number, nil
259+
case "bool":
260+
return cty.Bool, nil
261+
case "list(string)":
262+
return cty.List(cty.String), nil
263+
default:
264+
return cty.Type{}, fmt.Errorf("unsupported type: %q", typ)
265+
}
266+
}

0 commit comments

Comments
 (0)