Skip to content

Commit e2d2fb5

Browse files
command: "terraform validate" JSON support
In the long run we'd like to offer machine-readable output for more commands, but for now we'll just start with a tactical feature in "terraform validate" since this is useful for automated testing scenarios, editor integrations, etc and doesn't include any representations of types that are expected to have breaking changes in the near future.
1 parent 0f76716 commit e2d2fb5

File tree

1 file changed

+116
-12
lines changed

1 file changed

+116
-12
lines changed

command/validate.go

Lines changed: 116 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package command
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"path/filepath"
67
"strings"
@@ -21,14 +22,23 @@ func (c *ValidateCommand) Run(args []string) int {
2122
return 1
2223
}
2324

25+
var jsonOutput bool
26+
2427
cmdFlags := c.Meta.flagSet("validate")
28+
cmdFlags.BoolVar(&jsonOutput, "json", false, "produce JSON output")
2529
cmdFlags.Usage = func() {
2630
c.Ui.Error(c.Help())
2731
}
2832
if err := cmdFlags.Parse(args); err != nil {
2933
return 1
3034
}
3135

36+
// After this point, we must only produce JSON output if JSON mode is
37+
// enabled, so all errors should be accumulated into diags and we'll
38+
// print out a suitable result at the end, depending on the format
39+
// selection. All returns from this point on must be tail-calls into
40+
// c.showResults in order to produce the expected output.
41+
var diags tfdiags.Diagnostics
3242
args = cmdFlags.Args()
3343

3444
var dirPath string
@@ -39,19 +49,20 @@ func (c *ValidateCommand) Run(args []string) int {
3949
}
4050
dir, err := filepath.Abs(dirPath)
4151
if err != nil {
42-
c.Ui.Error(fmt.Sprintf(
43-
"Unable to locate directory %v\n", err.Error()))
52+
diags = diags.Append(fmt.Errorf("unable to locate module: %s", err))
53+
return c.showResults(diags, jsonOutput)
4454
}
4555

4656
// Check for user-supplied plugin path
4757
if c.pluginPath, err = c.loadPluginPath(); err != nil {
48-
c.Ui.Error(fmt.Sprintf("Error loading plugin path: %s", err))
49-
return 1
58+
diags = diags.Append(fmt.Errorf("error loading plugin path: %s", err))
59+
return c.showResults(diags, jsonOutput)
5060
}
5161

52-
rtnCode := c.validate(dir)
62+
validateDiags := c.validate(dir)
63+
diags = diags.Append(validateDiags)
5364

54-
return rtnCode
65+
return c.showResults(diags, jsonOutput)
5566
}
5667

5768
func (c *ValidateCommand) Synopsis() string {
@@ -89,20 +100,22 @@ Usage: terraform validate [options] [dir]
89100
90101
Options:
91102
103+
-json Produce output in a machine-readable JSON format, suitable for
104+
use in e.g. text editor integrations.
105+
92106
-no-color If specified, output won't contain any color.
93107
`
94108
return strings.TrimSpace(helpText)
95109
}
96110

97-
func (c *ValidateCommand) validate(dir string) int {
111+
func (c *ValidateCommand) validate(dir string) tfdiags.Diagnostics {
98112
var diags tfdiags.Diagnostics
99113

100114
_, cfgDiags := c.loadConfig(dir)
101115
diags = diags.Append(cfgDiags)
102116

103117
if diags.HasErrors() {
104-
c.showDiagnostics(diags)
105-
return 1
118+
return diags
106119
}
107120

108121
// TODO: run a validation walk once terraform.NewContext is updated
@@ -128,10 +141,101 @@ func (c *ValidateCommand) validate(dir string) int {
128141
diags = diags.Append(tfCtx.Validate())
129142
*/
130143

131-
c.showDiagnostics(diags)
144+
return diags
145+
}
146+
147+
func (c *ValidateCommand) showResults(diags tfdiags.Diagnostics, jsonOutput bool) int {
148+
switch {
149+
case jsonOutput:
150+
// FIXME: Eventually we'll probably want to factor this out somewhere
151+
// to support machine-readable outputs for other commands too, but for
152+
// now it's simplest to do this inline here.
153+
type Pos struct {
154+
Line int `json:"line"`
155+
Column int `json:"column"`
156+
Byte int `json:"byte"`
157+
}
158+
type Range struct {
159+
Filename string `json:"filename"`
160+
Start Pos `json:"start"`
161+
End Pos `json:"end"`
162+
}
163+
type Diagnostic struct {
164+
Severity string `json:"severity,omitempty"`
165+
Summary string `json:"summary,omitempty"`
166+
Detail string `json:"detail,omitempty"`
167+
Range *Range `json:"range,omitempty"`
168+
}
169+
type Output struct {
170+
// We include some summary information that is actually redundant
171+
// with the detailed diagnostics, but avoids the need for callers
172+
// to re-implement our logic for deciding these.
173+
Valid bool `json:"valid"`
174+
ErrorCount int `json:"error_count"`
175+
WarningCount int `json:"warning_count"`
176+
Diagnostics []Diagnostic `json:"diagnostics"`
177+
}
178+
179+
var output Output
180+
output.Valid = true // until proven otherwise
181+
for _, diag := range diags {
182+
var jsonDiag Diagnostic
183+
switch diag.Severity() {
184+
case tfdiags.Error:
185+
jsonDiag.Severity = "error"
186+
output.ErrorCount++
187+
output.Valid = false
188+
case tfdiags.Warning:
189+
jsonDiag.Severity = "warning"
190+
output.WarningCount++
191+
}
192+
193+
desc := diag.Description()
194+
jsonDiag.Summary = desc.Summary
195+
jsonDiag.Detail = desc.Detail
196+
197+
ranges := diag.Source()
198+
if ranges.Subject != nil {
199+
subj := ranges.Subject
200+
jsonDiag.Range = &Range{
201+
Filename: subj.Filename,
202+
Start: Pos{
203+
Line: subj.Start.Line,
204+
Column: subj.Start.Column,
205+
Byte: subj.Start.Byte,
206+
},
207+
End: Pos{
208+
Line: subj.End.Line,
209+
Column: subj.End.Column,
210+
Byte: subj.End.Byte,
211+
},
212+
}
213+
}
214+
215+
output.Diagnostics = append(output.Diagnostics, jsonDiag)
216+
}
217+
218+
j, err := json.MarshalIndent(&output, "", " ")
219+
if err != nil {
220+
// Should never happen because we fully-control the input here
221+
panic(err)
222+
}
223+
c.Ui.Output(string(j))
224+
225+
default:
226+
if len(diags) == 0 {
227+
c.Ui.Output(c.Colorize().Color("[green][bold]Success![reset] The configuration is valid.\n"))
228+
} else {
229+
c.showDiagnostics(diags)
230+
231+
if !diags.HasErrors() {
232+
c.Ui.Output(c.Colorize().Color("[green][bold]Success![reset] The configuration is valid, but there were some validation warnings as shown above.\n"))
233+
}
234+
}
235+
}
236+
132237
if diags.HasErrors() {
133238
return 1
134239
}
135-
136-
return 0
240+
return 1
137241
}

0 commit comments

Comments
 (0)