-
Notifications
You must be signed in to change notification settings - Fork 9.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #35884 from hashicorp/sebasslash/modules-command
`terraform modules` command
- Loading branch information
Showing
21 changed files
with
906 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
|
||
package arguments | ||
|
||
import "github.com/hashicorp/terraform/internal/tfdiags" | ||
|
||
// Modules represents the command-line arguments for the modules command | ||
type Modules struct { | ||
// ViewType specifies which output format to use: human, JSON, or "raw" | ||
ViewType ViewType | ||
} | ||
|
||
// ParseModules processes CLI arguments, returning a Modules value and error | ||
// diagnostics. If there are any diagnostics present, a Modules value is still | ||
// returned representing the best effort interpretation of the arguments. | ||
func ParseModules(args []string) (*Modules, tfdiags.Diagnostics) { | ||
var diags tfdiags.Diagnostics | ||
var jsonOutput bool | ||
|
||
modules := &Modules{} | ||
cmdFlags := defaultFlagSet("modules") | ||
cmdFlags.BoolVar(&jsonOutput, "json", false, "json") | ||
|
||
if err := cmdFlags.Parse(args); err != nil { | ||
diags = diags.Append(tfdiags.Sourceless( | ||
tfdiags.Error, | ||
"Failed to parse command-line flags", | ||
err.Error(), | ||
)) | ||
} | ||
|
||
args = cmdFlags.Args() | ||
if len(args) > 0 { | ||
diags = diags.Append(tfdiags.Sourceless( | ||
tfdiags.Error, | ||
"Too many command line arguments", | ||
"Expected no positional arguments", | ||
)) | ||
} | ||
|
||
switch { | ||
case jsonOutput: | ||
modules.ViewType = ViewJSON | ||
default: | ||
modules.ViewType = ViewHuman | ||
} | ||
|
||
return modules, diags | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
|
||
package arguments | ||
|
||
import ( | ||
"reflect" | ||
"testing" | ||
|
||
"github.com/davecgh/go-spew/spew" | ||
"github.com/hashicorp/terraform/internal/tfdiags" | ||
) | ||
|
||
func TestParseModules_valid(t *testing.T) { | ||
testCases := map[string]struct { | ||
args []string | ||
want *Modules | ||
}{ | ||
"default": { | ||
nil, | ||
&Modules{ | ||
ViewType: ViewHuman, | ||
}, | ||
}, | ||
"json": { | ||
[]string{"-json"}, | ||
&Modules{ | ||
ViewType: ViewJSON, | ||
}, | ||
}, | ||
} | ||
|
||
for name, tc := range testCases { | ||
t.Run(name, func(t *testing.T) { | ||
got, diags := ParseModules(tc.args) | ||
if len(diags) > 0 { | ||
t.Fatalf("unexpected diags: %v", diags) | ||
} | ||
if *got != *tc.want { | ||
t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestParseModules_invalid(t *testing.T) { | ||
testCases := map[string]struct { | ||
args []string | ||
want *Modules | ||
wantDiags tfdiags.Diagnostics | ||
}{ | ||
"invalid flag": { | ||
[]string{"-sauron"}, | ||
&Modules{ | ||
ViewType: ViewHuman, | ||
}, | ||
tfdiags.Diagnostics{ | ||
tfdiags.Sourceless( | ||
tfdiags.Error, | ||
"Failed to parse command-line flags", | ||
"flag provided but not defined: -sauron", | ||
), | ||
}, | ||
}, | ||
"too many arguments": { | ||
[]string{"-json", "frodo"}, | ||
&Modules{ | ||
ViewType: ViewJSON, | ||
}, | ||
tfdiags.Diagnostics{ | ||
tfdiags.Sourceless( | ||
tfdiags.Error, | ||
"Too many command line arguments", | ||
"Expected no positional arguments", | ||
), | ||
}, | ||
}, | ||
} | ||
|
||
for name, tc := range testCases { | ||
t.Run(name, func(t *testing.T) { | ||
got, gotDiags := ParseModules(tc.args) | ||
if *got != *tc.want { | ||
t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want) | ||
} | ||
if !reflect.DeepEqual(gotDiags, tc.wantDiags) { | ||
t.Fatalf("wrong result\ngot: %s\nwant: %s", spew.Sdump(gotDiags), spew.Sdump(tc.wantDiags)) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: BUSL-1.1 | ||
|
||
package command | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
|
||
"github.com/hashicorp/terraform/internal/command/arguments" | ||
"github.com/hashicorp/terraform/internal/command/views" | ||
"github.com/hashicorp/terraform/internal/modsdir" | ||
"github.com/hashicorp/terraform/internal/moduleref" | ||
"github.com/hashicorp/terraform/internal/terraform" | ||
"github.com/hashicorp/terraform/internal/tfdiags" | ||
) | ||
|
||
// ModulesCommand is a Command implementation that prints out information | ||
// about the modules declared by the current configuration. | ||
type ModulesCommand struct { | ||
Meta | ||
viewType arguments.ViewType | ||
} | ||
|
||
func (c *ModulesCommand) Help() string { | ||
return modulesCommandHelp | ||
} | ||
|
||
func (c *ModulesCommand) Synopsis() string { | ||
return "Show all declared modules in a working directory" | ||
} | ||
|
||
func (c *ModulesCommand) Run(rawArgs []string) int { | ||
// Parse global view arguments | ||
rawArgs = c.Meta.process(rawArgs) | ||
common, rawArgs := arguments.ParseView(rawArgs) | ||
c.View.Configure(common) | ||
|
||
// Parse command specific flags | ||
args, diags := arguments.ParseModules(rawArgs) | ||
if diags.HasErrors() { | ||
c.View.Diagnostics(diags) | ||
c.View.HelpPrompt("modules") | ||
return 1 | ||
} | ||
c.viewType = args.ViewType | ||
|
||
// Set up the command's view | ||
view := views.NewModules(c.viewType, c.View) | ||
|
||
// TODO: Remove this check once a human readable view is supported | ||
// for this command | ||
if c.viewType != arguments.ViewJSON { | ||
c.Ui.Error( | ||
"The `terraform modules` command requires the `-json` flag.\n") | ||
c.Ui.Error(modulesCommandHelp) | ||
return 1 | ||
} | ||
|
||
rootModPath, err := ModulePath([]string{}) | ||
if err != nil { | ||
diags = diags.Append(err) | ||
view.Diagnostics(diags) | ||
return 1 | ||
} | ||
|
||
// Read the root module path so we can then traverse the tree | ||
rootModEarly, earlyConfDiags := c.loadSingleModule(rootModPath) | ||
if rootModEarly == nil { | ||
diags = diags.Append(errors.New("root module not found. Please run terraform init"), earlyConfDiags) | ||
view.Diagnostics(diags) | ||
return 1 | ||
} | ||
|
||
config, confDiags := c.loadConfig(rootModPath) | ||
// Here we check if there are any uninstalled dependencies | ||
versionDiags := terraform.CheckCoreVersionRequirements(config) | ||
if versionDiags.HasErrors() { | ||
view.Diagnostics(versionDiags) | ||
return 1 | ||
} | ||
|
||
diags = diags.Append(earlyConfDiags) | ||
if earlyConfDiags.HasErrors() { | ||
view.Diagnostics(diags) | ||
return 1 | ||
} | ||
|
||
diags = diags.Append(confDiags) | ||
if confDiags.HasErrors() { | ||
view.Diagnostics(diags) | ||
return 1 | ||
} | ||
|
||
// Fetch the module manifest | ||
internalManifest, diags := c.internalManifest() | ||
if diags.HasErrors() { | ||
view.Diagnostics(diags) | ||
return 1 | ||
} | ||
|
||
// Create a module reference resolver | ||
resolver := moduleref.NewResolver(internalManifest) | ||
|
||
// Crawl the Terraform config and find entries with references | ||
manifestWithRef := resolver.Resolve(config) | ||
|
||
// Render the new manifest with references | ||
return view.Display(*manifestWithRef) | ||
} | ||
|
||
// internalManifest will use the configuration loader to refresh and load the | ||
// internal manifest. | ||
func (c *ModulesCommand) internalManifest() (modsdir.Manifest, tfdiags.Diagnostics) { | ||
var diags tfdiags.Diagnostics | ||
|
||
loader, err := c.initConfigLoader() | ||
if err != nil { | ||
diags = diags.Append(fmt.Errorf("Failed to initialize config loader: %w", err)) | ||
return nil, diags | ||
} | ||
|
||
if err = loader.RefreshModules(); err != nil { | ||
diags = diags.Append(fmt.Errorf("Failed to refresh module manifest: %w", err)) | ||
return nil, diags | ||
} | ||
|
||
return loader.ModuleManifest(), diags | ||
} | ||
|
||
const modulesCommandHelp = ` | ||
Usage: terraform [global options] modules -json | ||
Prints out a list of all declared Terraform modules and their resolved versions | ||
in a Terraform working directory. | ||
` |
Oops, something went wrong.