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 `