11package command
22
33import (
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
5768func (c * ValidateCommand ) Synopsis () string {
@@ -89,20 +100,22 @@ Usage: terraform validate [options] [dir]
89100
90101Options:
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