github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/command/views/validate.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package views 5 6 import ( 7 "encoding/json" 8 "fmt" 9 10 "github.com/terramate-io/tf/command/arguments" 11 "github.com/terramate-io/tf/command/format" 12 viewsjson "github.com/terramate-io/tf/command/views/json" 13 "github.com/terramate-io/tf/tfdiags" 14 ) 15 16 // The Validate is used for the validate command. 17 type Validate interface { 18 // Results renders the diagnostics returned from a validation walk, and 19 // returns a CLI exit code: 0 if there are no errors, 1 otherwise 20 Results(diags tfdiags.Diagnostics) int 21 22 // Diagnostics renders early diagnostics, resulting from argument parsing. 23 Diagnostics(diags tfdiags.Diagnostics) 24 } 25 26 // NewValidate returns an initialized Validate implementation for the given ViewType. 27 func NewValidate(vt arguments.ViewType, view *View) Validate { 28 switch vt { 29 case arguments.ViewJSON: 30 return &ValidateJSON{view: view} 31 case arguments.ViewHuman: 32 return &ValidateHuman{view: view} 33 default: 34 panic(fmt.Sprintf("unknown view type %v", vt)) 35 } 36 } 37 38 // The ValidateHuman implementation renders diagnostics in a human-readable form, 39 // along with a success/failure message if Terraform is able to execute the 40 // validation walk. 41 type ValidateHuman struct { 42 view *View 43 } 44 45 var _ Validate = (*ValidateHuman)(nil) 46 47 func (v *ValidateHuman) Results(diags tfdiags.Diagnostics) int { 48 columns := v.view.outputColumns() 49 50 if len(diags) == 0 { 51 v.view.streams.Println(format.WordWrap(v.view.colorize.Color(validateSuccess), columns)) 52 } else { 53 v.Diagnostics(diags) 54 55 if !diags.HasErrors() { 56 v.view.streams.Println(format.WordWrap(v.view.colorize.Color(validateWarnings), columns)) 57 } 58 } 59 60 if diags.HasErrors() { 61 return 1 62 } 63 return 0 64 } 65 66 const validateSuccess = "[green][bold]Success![reset] The configuration is valid.\n" 67 68 const validateWarnings = "[green][bold]Success![reset] The configuration is valid, but there were some validation warnings as shown above.\n" 69 70 func (v *ValidateHuman) Diagnostics(diags tfdiags.Diagnostics) { 71 v.view.Diagnostics(diags) 72 } 73 74 // The ValidateJSON implementation renders validation results as a JSON object. 75 // This object includes top-level fields summarizing the result, and an array 76 // of JSON diagnostic objects. 77 type ValidateJSON struct { 78 view *View 79 } 80 81 var _ Validate = (*ValidateJSON)(nil) 82 83 func (v *ValidateJSON) Results(diags tfdiags.Diagnostics) int { 84 // FormatVersion represents the version of the json format and will be 85 // incremented for any change to this format that requires changes to a 86 // consuming parser. 87 const FormatVersion = "1.0" 88 89 type Output struct { 90 FormatVersion string `json:"format_version"` 91 92 // We include some summary information that is actually redundant 93 // with the detailed diagnostics, but avoids the need for callers 94 // to re-implement our logic for deciding these. 95 Valid bool `json:"valid"` 96 ErrorCount int `json:"error_count"` 97 WarningCount int `json:"warning_count"` 98 Diagnostics []*viewsjson.Diagnostic `json:"diagnostics"` 99 } 100 101 output := Output{ 102 FormatVersion: FormatVersion, 103 Valid: true, // until proven otherwise 104 } 105 configSources := v.view.configSources() 106 for _, diag := range diags { 107 output.Diagnostics = append(output.Diagnostics, viewsjson.NewDiagnostic(diag, configSources)) 108 109 switch diag.Severity() { 110 case tfdiags.Error: 111 output.ErrorCount++ 112 output.Valid = false 113 case tfdiags.Warning: 114 output.WarningCount++ 115 } 116 } 117 if output.Diagnostics == nil { 118 // Make sure this always appears as an array in our output, since 119 // this is easier to consume for dynamically-typed languages. 120 output.Diagnostics = []*viewsjson.Diagnostic{} 121 } 122 123 j, err := json.MarshalIndent(&output, "", " ") 124 if err != nil { 125 // Should never happen because we fully-control the input here 126 panic(err) 127 } 128 v.view.streams.Println(string(j)) 129 130 if diags.HasErrors() { 131 return 1 132 } 133 return 0 134 } 135 136 // Diagnostics should only be called if the validation walk cannot be executed. 137 // In this case, we choose to render human-readable diagnostic output, 138 // primarily for backwards compatibility. 139 func (v *ValidateJSON) Diagnostics(diags tfdiags.Diagnostics) { 140 v.view.Diagnostics(diags) 141 }