github.com/opentofu/opentofu@v1.7.1/internal/command/validate.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package command
     7  
     8  import (
     9  	"fmt"
    10  	"path/filepath"
    11  	"strings"
    12  
    13  	"github.com/opentofu/opentofu/internal/addrs"
    14  	"github.com/opentofu/opentofu/internal/command/arguments"
    15  	"github.com/opentofu/opentofu/internal/command/views"
    16  	"github.com/opentofu/opentofu/internal/configs"
    17  	"github.com/opentofu/opentofu/internal/tfdiags"
    18  	"github.com/opentofu/opentofu/internal/tofu"
    19  )
    20  
    21  // ValidateCommand is a Command implementation that validates the tofu files
    22  type ValidateCommand struct {
    23  	Meta
    24  }
    25  
    26  func (c *ValidateCommand) Run(rawArgs []string) int {
    27  	// Parse and apply global view arguments
    28  	common, rawArgs := arguments.ParseView(rawArgs)
    29  	c.View.Configure(common)
    30  
    31  	// Parse and validate flags
    32  	args, diags := arguments.ParseValidate(rawArgs)
    33  	if diags.HasErrors() {
    34  		c.View.Diagnostics(diags)
    35  		c.View.HelpPrompt("validate")
    36  		return 1
    37  	}
    38  
    39  	view := views.NewValidate(args.ViewType, c.View)
    40  
    41  	// After this point, we must only produce JSON output if JSON mode is
    42  	// enabled, so all errors should be accumulated into diags and we'll
    43  	// print out a suitable result at the end, depending on the format
    44  	// selection. All returns from this point on must be tail-calls into
    45  	// view.Results in order to produce the expected output.
    46  
    47  	dir, err := filepath.Abs(args.Path)
    48  	if err != nil {
    49  		diags = diags.Append(fmt.Errorf("unable to locate module: %w", err))
    50  		return view.Results(diags)
    51  	}
    52  
    53  	// Check for user-supplied plugin path
    54  	if c.pluginPath, err = c.loadPluginPath(); err != nil {
    55  		diags = diags.Append(fmt.Errorf("error loading plugin path: %w", err))
    56  		return view.Results(diags)
    57  	}
    58  
    59  	validateDiags := c.validate(dir, args.TestDirectory, args.NoTests)
    60  	diags = diags.Append(validateDiags)
    61  
    62  	// Validating with dev overrides in effect means that the result might
    63  	// not be valid for a stable release, so we'll warn about that in case
    64  	// the user is trying to use "tofu validate" as a sort of pre-flight
    65  	// check before submitting a change.
    66  	diags = diags.Append(c.providerDevOverrideRuntimeWarnings())
    67  
    68  	return view.Results(diags)
    69  }
    70  
    71  func (c *ValidateCommand) validate(dir, testDir string, noTests bool) tfdiags.Diagnostics {
    72  	var diags tfdiags.Diagnostics
    73  	var cfg *configs.Config
    74  
    75  	if noTests {
    76  		cfg, diags = c.loadConfig(dir)
    77  	} else {
    78  		cfg, diags = c.loadConfigWithTests(dir, testDir)
    79  	}
    80  	if diags.HasErrors() {
    81  		return diags
    82  	}
    83  
    84  	validate := func(cfg *configs.Config) tfdiags.Diagnostics {
    85  		var diags tfdiags.Diagnostics
    86  
    87  		opts, err := c.contextOpts()
    88  		if err != nil {
    89  			diags = diags.Append(err)
    90  			return diags
    91  		}
    92  
    93  		tfCtx, ctxDiags := tofu.NewContext(opts)
    94  		diags = diags.Append(ctxDiags)
    95  		if ctxDiags.HasErrors() {
    96  			return diags
    97  		}
    98  
    99  		return diags.Append(tfCtx.Validate(cfg))
   100  	}
   101  
   102  	diags = diags.Append(validate(cfg))
   103  
   104  	if noTests {
   105  		return diags
   106  	}
   107  
   108  	validatedModules := make(map[string]bool)
   109  
   110  	// We'll also do a quick validation of the OpenTofu test files. These live
   111  	// outside the OpenTofu graph so we have to do this separately.
   112  	for _, file := range cfg.Module.Tests {
   113  		for _, run := range file.Runs {
   114  
   115  			if run.Module != nil {
   116  				// Then we can also validate the referenced modules, but we are
   117  				// only going to do this is if they are local modules.
   118  				//
   119  				// Basically, local testing modules are something the user can
   120  				// reasonably go and fix. If it's a module being downloaded from
   121  				// the registry, the expectation is that the author of the
   122  				// module should have ran `tofu validate` themselves.
   123  				if _, ok := run.Module.Source.(addrs.ModuleSourceLocal); ok {
   124  
   125  					if validated := validatedModules[run.Module.Source.String()]; !validated {
   126  
   127  						// Since we can reference the same module twice, let's
   128  						// not validate the same thing multiple times.
   129  
   130  						validatedModules[run.Module.Source.String()] = true
   131  						diags = diags.Append(validate(run.ConfigUnderTest))
   132  					}
   133  
   134  				}
   135  			}
   136  
   137  			diags = diags.Append(run.Validate())
   138  		}
   139  	}
   140  
   141  	return diags
   142  }
   143  
   144  func (c *ValidateCommand) Synopsis() string {
   145  	return "Check whether the configuration is valid"
   146  }
   147  
   148  func (c *ValidateCommand) Help() string {
   149  	helpText := `
   150  Usage: tofu [global options] validate [options]
   151  
   152    Validate the configuration files in a directory, referring only to the
   153    configuration and not accessing any remote services such as remote state,
   154    provider APIs, etc.
   155  
   156    Validate runs checks that verify whether a configuration is syntactically
   157    valid and internally consistent, regardless of any provided variables or
   158    existing state. It is thus primarily useful for general verification of
   159    reusable modules, including correctness of attribute names and value types.
   160  
   161    It is safe to run this command automatically, for example as a post-save
   162    check in a text editor or as a test step for a re-usable module in a CI
   163    system.
   164  
   165    Validation requires an initialized working directory with any referenced
   166    plugins and modules installed. To initialize a working directory for
   167    validation without accessing any configured remote backend, use:
   168        tofu init -backend=false
   169  
   170    To verify configuration in the context of a particular run (a particular
   171    target workspace, input variable values, etc), use the 'tofu plan'
   172    command instead, which includes an implied validation check.
   173  
   174  Options:
   175  
   176    -json                 Produce output in a machine-readable JSON format, 
   177                          suitable for use in text editor integrations and other 
   178                          automated systems. Always disables color.
   179  
   180    -no-color             If specified, output won't contain any color.
   181  
   182    -no-tests             If specified, OpenTofu will not validate test files.
   183  
   184    -test-directory=path  Set the OpenTofu test directory, defaults to "tests". When set, the
   185                          test command will search for test files in the current directory and
   186                          in the one specified by the flag.
   187  `
   188  	return strings.TrimSpace(helpText)
   189  }