Skip to content

Commit df0b65c

Browse files
committed
chore: add initial work for basic tf file parsing
1 parent 8e6e188 commit df0b65c

File tree

22 files changed

+3042
-40
lines changed

22 files changed

+3042
-40
lines changed

attr.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package preview
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/aquasecurity/trivy/pkg/iac/terraform"
7+
"github.com/hashicorp/hcl/v2"
8+
"github.com/zclconf/go-cty/cty"
9+
)
10+
11+
type attributeParser struct {
12+
block *terraform.Block
13+
diags hcl.Diagnostics
14+
}
15+
16+
func newAttributeParser(block *terraform.Block) *attributeParser {
17+
return &attributeParser{
18+
block: block,
19+
diags: make(hcl.Diagnostics, 0),
20+
}
21+
}
22+
23+
func (a *attributeParser) attr(key string) *expectedAttribute {
24+
return &expectedAttribute{
25+
Key: key,
26+
p: a,
27+
}
28+
}
29+
30+
type expectedAttribute struct {
31+
Key string
32+
diag hcl.Diagnostics
33+
p *attributeParser
34+
}
35+
36+
func (a *expectedAttribute) error(diag hcl.Diagnostics) *expectedAttribute {
37+
if a.diag != nil {
38+
return a // already have an error, don't overwrite
39+
}
40+
41+
a.p.diags = a.p.diags.Extend(diag)
42+
a.diag = diag
43+
return a
44+
}
45+
46+
func (a *expectedAttribute) required() *expectedAttribute {
47+
attr := a.p.block.GetAttribute(a.Key)
48+
if attr.IsNil() {
49+
r := a.p.block.HCLBlock().Body.MissingItemRange()
50+
a.error(hcl.Diagnostics{
51+
{
52+
Severity: hcl.DiagError,
53+
Summary: fmt.Sprintf("Missing required attribute %q", a.Key),
54+
Subject: &r,
55+
Extra: nil,
56+
},
57+
})
58+
}
59+
60+
return a
61+
}
62+
63+
func (a *expectedAttribute) string() string {
64+
attr := a.p.block.GetAttribute(a.Key)
65+
if attr.IsNil() {
66+
return ""
67+
}
68+
69+
if attr.Type() != cty.String {
70+
a.expectedTypeError(attr, "string")
71+
return ""
72+
}
73+
74+
return attr.Value().AsString()
75+
}
76+
77+
func (a *expectedAttribute) expectedTypeError(attr *terraform.Attribute, expectedType string) {
78+
a.error(hcl.Diagnostics{
79+
{
80+
Severity: hcl.DiagError,
81+
Summary: "Invalid attribute type",
82+
Detail: fmt.Sprintf("The attribute %q must be of type %q, found type %q", attr.Name(), expectedType, attr.Type().FriendlyNameForConstraint()),
83+
Subject: &attr.HCLAttribute().Range,
84+
Context: &a.p.block.HCLBlock().DefRange,
85+
Expression: attr.HCLAttribute().Expr,
86+
87+
EvalContext: a.p.block.Context().Inner(),
88+
},
89+
})
90+
}

cli/clidisplay/diags.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package clidisplay
2+
3+
import (
4+
"io"
5+
"log"
6+
7+
"github.com/hashicorp/hcl/v2"
8+
)
9+
10+
func WriteDiagnostics(out io.Writer, files map[string]*hcl.File, diags hcl.Diagnostics) {
11+
wr := hcl.NewDiagnosticTextWriter(out, files, 80, true)
12+
werr := wr.WriteDiagnostics(diags)
13+
if werr != nil {
14+
log.Printf("diagnostic writer: %s", werr.Error())
15+
}
16+
}

cli/clidisplay/resources.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package clidisplay
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"strings"
7+
8+
"github.com/coder/preview/types"
9+
"github.com/hashicorp/hcl/v2"
10+
"github.com/jedib0t/go-pretty/v6/table"
11+
)
12+
13+
func WorkspaceTags(writer io.Writer, tags types.TagBlocks) hcl.Diagnostics {
14+
var diags hcl.Diagnostics
15+
16+
tableWriter := table.NewWriter()
17+
tableWriter.SetTitle("Provisioner Tags")
18+
tableWriter.SetStyle(table.StyleLight)
19+
tableWriter.Style().Options.SeparateColumns = false
20+
row := table.Row{"Key", "Value", "Refs"}
21+
tableWriter.AppendHeader(row)
22+
for _, tb := range tags {
23+
for _, tag := range tb.Tags {
24+
if tag.IsKnown() {
25+
k, v := tag.AsStrings()
26+
tableWriter.AppendRow(table.Row{k, v, ""})
27+
continue
28+
//diags = diags.Extend(tDiags)
29+
//if !diags.HasErrors() {
30+
// tableWriter.AppendRow(table.Row{k, v, ""})
31+
// continue
32+
//}
33+
}
34+
35+
k := tag.SafeKeyString()
36+
refs := tag.References()
37+
tableWriter.AppendRow(table.Row{k, "??", strings.Join(refs, "\n")})
38+
39+
//refs := tb.AllReferences()
40+
//refsStr := make([]string, 0, len(refs))
41+
//for _, ref := range refs {
42+
// refsStr = append(refsStr, ref.String())
43+
//}
44+
//tableWriter.AppendRow(table.Row{unknown, "???", strings.Join(refsStr, "\n")})
45+
}
46+
}
47+
_, _ = fmt.Fprintln(writer, tableWriter.Render())
48+
return diags
49+
}
50+
51+
func Parameters(writer io.Writer, params []types.Parameter) {
52+
tableWriter := table.NewWriter()
53+
//tableWriter.SetTitle("Parameters")
54+
tableWriter.SetStyle(table.StyleLight)
55+
tableWriter.Style().Options.SeparateColumns = false
56+
row := table.Row{"Parameter"}
57+
tableWriter.AppendHeader(row)
58+
for _, p := range params {
59+
strVal := ""
60+
value := p.Value.Value
61+
62+
if value.IsNull() {
63+
strVal = "null"
64+
} else if !p.Value.Value.IsKnown() {
65+
strVal = "unknown"
66+
} else {
67+
strVal = value.AsString()
68+
}
69+
70+
tableWriter.AppendRow(table.Row{
71+
fmt.Sprintf("%s: %s\n%s", p.Name, p.Description, formatOptions(strVal, p.Options)),
72+
})
73+
tableWriter.AppendSeparator()
74+
}
75+
_, _ = fmt.Fprintln(writer, tableWriter.Render())
76+
}
77+
78+
func formatOptions(selected string, options []*types.RichParameterOption) string {
79+
var str strings.Builder
80+
sep := ""
81+
found := false
82+
for _, opt := range options {
83+
str.WriteString(sep)
84+
prefix := "[ ]"
85+
if opt.Value == selected {
86+
prefix = "[X]"
87+
found = true
88+
}
89+
str.WriteString(fmt.Sprintf("%s %s (%s)", prefix, opt.Name, opt.Value))
90+
if opt.Description != "" {
91+
str.WriteString(fmt.Sprintf(": %s", maxLength(opt.Description, 20)))
92+
}
93+
sep = "\n"
94+
}
95+
if !found {
96+
str.WriteString(sep)
97+
str.WriteString(fmt.Sprintf("= %s", selected))
98+
}
99+
return str.String()
100+
}
101+
102+
func maxLength(s string, max int) string {
103+
if len(s) > max {
104+
return s[:max] + "..."
105+
}
106+
return s
107+
}

cli/root.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
8+
"github.com/hashicorp/hcl/v2"
9+
"github.com/hashicorp/hcl/v2/hclsyntax"
10+
"github.com/zclconf/go-cty/cty"
11+
12+
"github.com/coder/preview"
13+
"github.com/coder/preview/cli/clidisplay"
14+
"github.com/coder/preview/types"
15+
"github.com/coder/serpent"
16+
)
17+
18+
type RootCmd struct {
19+
Files map[string]*hcl.File
20+
}
21+
22+
func (r *RootCmd) Root() *serpent.Command {
23+
var (
24+
dir string
25+
vars []string
26+
)
27+
cmd := &serpent.Command{
28+
Use: "codertf",
29+
Short: "codertf is a command line tool for previewing terraform template outputs.",
30+
Options: serpent.OptionSet{
31+
{
32+
Name: "dir",
33+
Description: "Directory with terraform files.",
34+
Flag: "dir",
35+
FlagShorthand: "d",
36+
Default: ".",
37+
Value: serpent.StringOf(&dir),
38+
},
39+
{
40+
Name: "vars",
41+
Description: "Variables.",
42+
Flag: "vars",
43+
FlagShorthand: "v",
44+
Default: ".",
45+
Value: serpent.StringArrayOf(&vars),
46+
},
47+
},
48+
Handler: func(i *serpent.Invocation) error {
49+
dfs := os.DirFS(dir)
50+
51+
var rvars map[string]types.ParameterValue
52+
for _, val := range vars {
53+
parts := strings.Split(val, "=")
54+
if len(parts) != 2 {
55+
continue
56+
}
57+
rvars[parts[0]] = types.ParameterValue{
58+
Value: cty.StringVal(parts[1]),
59+
}
60+
}
61+
62+
input := preview.Input{
63+
ParameterValues: rvars,
64+
}
65+
66+
ctx := i.Context()
67+
output, diags := preview.Preview(ctx, input, dfs)
68+
if output == nil {
69+
return diags
70+
}
71+
r.Files = output.Files
72+
73+
if len(diags) > 0 {
74+
_, _ = fmt.Fprintf(os.Stderr, "Parsing Diagnostics:\n")
75+
clidisplay.WriteDiagnostics(os.Stderr, output.Files, diags)
76+
}
77+
78+
diags = clidisplay.WorkspaceTags(os.Stdout, output.WorkspaceTags)
79+
if len(diags) > 0 {
80+
_, _ = fmt.Fprintf(os.Stderr, "Workspace Tags Diagnostics:\n")
81+
clidisplay.WriteDiagnostics(os.Stderr, output.Files, diags)
82+
}
83+
84+
clidisplay.Parameters(os.Stdout, output.Parameters)
85+
86+
return nil
87+
},
88+
}
89+
return cmd
90+
}
91+
92+
func hclExpr(expr string) hcl.Expression {
93+
file, diags := hclsyntax.ParseConfig([]byte(fmt.Sprintf(`expr = %s`, expr)), "test.tf", hcl.InitialPos)
94+
if diags.HasErrors() {
95+
panic(diags)
96+
}
97+
attributes, diags := file.Body.JustAttributes()
98+
if diags.HasErrors() {
99+
panic(diags)
100+
}
101+
return attributes["expr"].Expr
102+
}

cmd/preview/main.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"log"
6+
"os"
7+
8+
"github.com/coder/preview/cli"
9+
"github.com/hashicorp/hcl/v2"
10+
)
11+
12+
func main() {
13+
log.SetOutput(os.Stderr)
14+
root := &cli.RootCmd{}
15+
cmd := root.Root()
16+
17+
err := cmd.Invoke().WithOS().Run()
18+
if err != nil {
19+
var diags hcl.Diagnostics
20+
if errors.As(err, &diags) {
21+
var files map[string]*hcl.File
22+
if root.Files != nil {
23+
files = root.Files
24+
}
25+
wr := hcl.NewDiagnosticTextWriter(os.Stderr, files, 80, true)
26+
werr := wr.WriteDiagnostics(diags)
27+
if werr != nil {
28+
log.Printf("diagnostic writer: %s", werr.Error())
29+
}
30+
}
31+
log.Fatal(err.Error())
32+
os.Exit(1)
33+
}
34+
}

0 commit comments

Comments
 (0)