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  }