github.com/opentofu/opentofu@v1.7.1/internal/command/providers.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/xlab/treeprint"
    14  
    15  	"github.com/opentofu/opentofu/internal/configs"
    16  	"github.com/opentofu/opentofu/internal/getproviders"
    17  	"github.com/opentofu/opentofu/internal/tfdiags"
    18  )
    19  
    20  // ProvidersCommand is a Command implementation that prints out information
    21  // about the providers used in the current configuration/state.
    22  type ProvidersCommand struct {
    23  	Meta
    24  }
    25  
    26  func (c *ProvidersCommand) Help() string {
    27  	return providersCommandHelp
    28  }
    29  
    30  func (c *ProvidersCommand) Synopsis() string {
    31  	return "Show the providers required for this configuration"
    32  }
    33  
    34  func (c *ProvidersCommand) Run(args []string) int {
    35  	var testsDirectory string
    36  
    37  	args = c.Meta.process(args)
    38  	cmdFlags := c.Meta.defaultFlagSet("providers")
    39  	cmdFlags.StringVar(&testsDirectory, "test-directory", "tests", "test-directory")
    40  	cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
    41  	if err := cmdFlags.Parse(args); err != nil {
    42  		c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
    43  		return 1
    44  	}
    45  
    46  	configPath, err := modulePath(cmdFlags.Args())
    47  	if err != nil {
    48  		c.Ui.Error(err.Error())
    49  		return 1
    50  	}
    51  
    52  	var diags tfdiags.Diagnostics
    53  
    54  	empty, err := configs.IsEmptyDir(configPath)
    55  	if err != nil {
    56  		diags = diags.Append(tfdiags.Sourceless(
    57  			tfdiags.Error,
    58  			"Error validating configuration directory",
    59  			fmt.Sprintf("OpenTofu encountered an unexpected error while verifying that the given configuration directory is valid: %s.", err),
    60  		))
    61  		c.showDiagnostics(diags)
    62  		return 1
    63  	}
    64  	if empty {
    65  		absPath, err := filepath.Abs(configPath)
    66  		if err != nil {
    67  			absPath = configPath
    68  		}
    69  		diags = diags.Append(tfdiags.Sourceless(
    70  			tfdiags.Error,
    71  			"No configuration files",
    72  			fmt.Sprintf("The directory %s contains no OpenTofu configuration files.", absPath),
    73  		))
    74  		c.showDiagnostics(diags)
    75  		return 1
    76  	}
    77  
    78  	config, configDiags := c.loadConfigWithTests(configPath, testsDirectory)
    79  	diags = diags.Append(configDiags)
    80  	if configDiags.HasErrors() {
    81  		c.showDiagnostics(diags)
    82  		return 1
    83  	}
    84  
    85  	// Load the encryption configuration
    86  	enc, encDiags := c.EncryptionFromPath(configPath)
    87  	diags = diags.Append(encDiags)
    88  	if encDiags.HasErrors() {
    89  		c.showDiagnostics(diags)
    90  		return 1
    91  	}
    92  
    93  	// Load the backend
    94  	b, backendDiags := c.Backend(&BackendOpts{
    95  		Config: config.Module.Backend,
    96  	}, enc.State())
    97  	diags = diags.Append(backendDiags)
    98  	if backendDiags.HasErrors() {
    99  		c.showDiagnostics(diags)
   100  		return 1
   101  	}
   102  
   103  	// This is a read-only command
   104  	c.ignoreRemoteVersionConflict(b)
   105  
   106  	// Get the state
   107  	env, err := c.Workspace()
   108  	if err != nil {
   109  		c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err))
   110  		return 1
   111  	}
   112  	s, err := b.StateMgr(env)
   113  	if err != nil {
   114  		c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
   115  		return 1
   116  	}
   117  	if err := s.RefreshState(); err != nil {
   118  		c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
   119  		return 1
   120  	}
   121  
   122  	reqs, reqDiags := config.ProviderRequirementsByModule()
   123  	diags = diags.Append(reqDiags)
   124  	if diags.HasErrors() {
   125  		c.showDiagnostics(diags)
   126  		return 1
   127  	}
   128  
   129  	state := s.State()
   130  	var stateReqs getproviders.Requirements
   131  	if state != nil {
   132  		stateReqs = state.ProviderRequirements()
   133  	}
   134  
   135  	printRoot := treeprint.New()
   136  	c.populateTreeNode(printRoot, reqs)
   137  
   138  	c.Ui.Output("\nProviders required by configuration:")
   139  	c.Ui.Output(printRoot.String())
   140  
   141  	if len(stateReqs) > 0 {
   142  		c.Ui.Output("Providers required by state:\n")
   143  		for fqn := range stateReqs {
   144  			c.Ui.Output(fmt.Sprintf("    provider[%s]\n", fqn.String()))
   145  		}
   146  	}
   147  
   148  	c.showDiagnostics(diags)
   149  	if diags.HasErrors() {
   150  		return 1
   151  	}
   152  	return 0
   153  }
   154  
   155  func (c *ProvidersCommand) populateTreeNode(tree treeprint.Tree, node *configs.ModuleRequirements) {
   156  	for fqn, dep := range node.Requirements {
   157  		versionsStr := getproviders.VersionConstraintsString(dep)
   158  		if versionsStr != "" {
   159  			versionsStr = " " + versionsStr
   160  		}
   161  		tree.AddNode(fmt.Sprintf("provider[%s]%s", fqn.String(), versionsStr))
   162  	}
   163  	for name, testNode := range node.Tests {
   164  		name = strings.TrimSuffix(name, ".tftest.hcl")
   165  		name = strings.ReplaceAll(name, "/", ".")
   166  		branch := tree.AddBranch(fmt.Sprintf("test.%s", name))
   167  
   168  		for fqn, dep := range testNode.Requirements {
   169  			versionsStr := getproviders.VersionConstraintsString(dep)
   170  			if versionsStr != "" {
   171  				versionsStr = " " + versionsStr
   172  			}
   173  			branch.AddNode(fmt.Sprintf("provider[%s]%s", fqn.String(), versionsStr))
   174  		}
   175  
   176  		for _, run := range testNode.Runs {
   177  			branch := branch.AddBranch(fmt.Sprintf("run.%s", run.Name))
   178  			c.populateTreeNode(branch, run)
   179  		}
   180  	}
   181  	for name, childNode := range node.Children {
   182  		branch := tree.AddBranch(fmt.Sprintf("module.%s", name))
   183  		c.populateTreeNode(branch, childNode)
   184  	}
   185  }
   186  
   187  const providersCommandHelp = `
   188  Usage: tofu [global options] providers [options] [DIR]
   189  
   190    Prints out a tree of modules in the referenced configuration annotated with
   191    their provider requirements.
   192  
   193    This provides an overview of all of the provider requirements across all
   194    referenced modules, as an aid to understanding why particular provider
   195    plugins are needed and why particular versions are selected.
   196  
   197  Options:
   198  
   199    -test-directory=path  Set the OpenTofu test directory, defaults to "tests". When set, the
   200                          test command will search for test files in the current directory and
   201                          in the one specified by the flag.
   202  `