From 8d0db6357105e48a929f24bca349dd74e55ee372 Mon Sep 17 00:00:00 2001 From: Mark DeCrane Date: Mon, 18 Nov 2024 13:41:53 -0500 Subject: [PATCH 1/3] Add human view for modules cmd --- internal/command/modules.go | 16 +++----- internal/command/views/modules.go | 66 ++++++++++++++++++++++++++++++- internal/moduleref/record.go | 29 ++++++++------ internal/moduleref/resolver.go | 58 +++++++++++++++++---------- 4 files changed, 125 insertions(+), 44 deletions(-) diff --git a/internal/command/modules.go b/internal/command/modules.go index c8bfed5e0744..19eab321fa18 100644 --- a/internal/command/modules.go +++ b/internal/command/modules.go @@ -48,15 +48,6 @@ func (c *ModulesCommand) Run(rawArgs []string) int { // 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) @@ -129,8 +120,13 @@ func (c *ModulesCommand) internalManifest() (modsdir.Manifest, tfdiags.Diagnosti } const modulesCommandHelp = ` -Usage: terraform [global options] modules -json +Usage: terraform [global options] modules [options] Prints out a list of all declared Terraform modules and their resolved versions in a Terraform working directory. + +Options: + + -json If specified, output declared Terraform modules and + their resolved versions in a machine-readable format. ` diff --git a/internal/command/views/modules.go b/internal/command/views/modules.go index 3b1c8120be8c..82ab0ce46ac7 100644 --- a/internal/command/views/modules.go +++ b/internal/command/views/modules.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/moduleref" "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/xlab/treeprint" ) type Modules interface { @@ -38,9 +39,34 @@ type ModulesHuman struct { var _ Modules = (*ModulesHuman)(nil) func (v *ModulesHuman) Display(manifest moduleref.Manifest) int { + if len(manifest.Records) == 0 { + v.view.streams.Println("No modules found in configuration.") + return 0 + } + printRoot := treeprint.New() + populateTreeNode(printRoot, &moduleref.Record{ + Children: manifest.Records, + }) + + v.view.streams.Println(fmt.Sprintf("Modules declared by configuration:\n\n%s", printRoot.String())) return 0 } +func populateTreeNode(tree treeprint.Tree, node *moduleref.Record) { + for _, childNode := range node.Children { + item := fmt.Sprintf("\"%s\"[%s]", childNode.Key, childNode.Source.String()) + if childNode.Version != nil { + item += fmt.Sprintf(" %s", childNode.Version) + // Avoid rendering the version constraint if an exact version is given i.e. 'version = "1.2.3"' + if childNode.VersionConstraints != nil && childNode.VersionConstraints.String() != childNode.Version.String() { + item += fmt.Sprintf(" (%s)", childNode.VersionConstraints.String()) + } + } + branch := tree.AddBranch(item) + populateTreeNode(branch, childNode) + } +} + func (v *ModulesHuman) Diagnostics(diags tfdiags.Diagnostics) { v.view.Diagnostics(diags) } @@ -54,7 +80,9 @@ var _ Modules = (*ModulesHuman)(nil) func (v *ModulesJSON) Display(manifest moduleref.Manifest) int { var bytes []byte var err error - if bytes, err = encJson.Marshal(manifest); err != nil { + + flattenedManifest := flattenManifest(manifest) + if bytes, err = encJson.Marshal(flattenedManifest); err != nil { v.view.streams.Eprintf("error marshalling manifest: %v", err) return 1 } @@ -63,6 +91,42 @@ func (v *ModulesJSON) Display(manifest moduleref.Manifest) int { return 0 } +// FlattenManifest returns the nested contents of a moduleref.Manifest in +// a flattened format with the VersionConstraints and Children attributes +// ommited for the purposes of the json format of the modules command +func flattenManifest(m moduleref.Manifest) map[string]interface{} { + var flatten func(records []*moduleref.Record) + var recordList []map[string]string + flatten = func(records []*moduleref.Record) { + for _, record := range records { + if record.Version != nil { + recordList = append(recordList, map[string]string{ + "key": record.Key, + "source": record.Source.String(), + "version": record.Version.String(), + }) + } else { + recordList = append(recordList, map[string]string{ + "key": record.Key, + "source": record.Source.String(), + "version": "", + }) + } + + if len(record.Children) > 0 { + flatten(record.Children) + } + } + } + + flatten(m.Records) + ret := map[string]interface{}{ + "format_version": m.FormatVersion, + "modules": recordList, + } + return ret +} + func (v *ModulesJSON) Diagnostics(diags tfdiags.Diagnostics) { v.view.Diagnostics(diags) } diff --git a/internal/moduleref/record.go b/internal/moduleref/record.go index ad6b757afc26..fb4f918226c4 100644 --- a/internal/moduleref/record.go +++ b/internal/moduleref/record.go @@ -3,29 +3,34 @@ package moduleref -import "github.com/hashicorp/terraform/internal/modsdir" +import ( + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform/internal/addrs" +) const FormatVersion = "1.0" // ModuleRecord is the implementation of a module entry defined in the module // manifest that is declared by configuration. type Record struct { - Key string `json:"key"` - Source string `json:"source"` - Version string `json:"version"` + Key string + Source addrs.ModuleSource + Version *version.Version + VersionConstraints version.Constraints + Children []*Record } // ModuleRecordManifest is the view implementation of module entries declared // in configuration type Manifest struct { - FormatVersion string `json:"format_version"` - Records []Record `json:"modules"` + FormatVersion string + Records []*Record } -func (m *Manifest) addModuleEntry(entry modsdir.Record) { - m.Records = append(m.Records, Record{ - Key: entry.Key, - Source: entry.SourceAddr, - Version: entry.VersionStr, - }) +func (m *Manifest) addModuleEntry(entry *Record) { + m.Records = append(m.Records, entry) +} + +func (r *Record) addChild(child *Record) { + r.Children = append(r.Children, child) } diff --git a/internal/moduleref/resolver.go b/internal/moduleref/resolver.go index 71f232338ca6..48e6da87a802 100644 --- a/internal/moduleref/resolver.go +++ b/internal/moduleref/resolver.go @@ -6,6 +6,7 @@ package moduleref import ( "strings" + "github.com/hashicorp/go-version" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/modsdir" ) @@ -35,7 +36,7 @@ func NewResolver(internalManifest modsdir.Manifest) *Resolver { internalManifest: internalManifestCopy, manifest: &Manifest{ FormatVersion: FormatVersion, - Records: []Record{}, + Records: []*Record{}, }, } } @@ -44,7 +45,7 @@ func NewResolver(internalManifest modsdir.Manifest) *Resolver { // and return a new manifest encapsulating this information. func (r *Resolver) Resolve(cfg *configs.Config) *Manifest { // First find all the referenced modules. - r.findAndTrimReferencedEntries(cfg) + r.findAndTrimReferencedEntries(cfg, nil) return r.manifest } @@ -52,32 +53,47 @@ func (r *Resolver) Resolve(cfg *configs.Config) *Manifest { // findAndTrimReferencedEntries will traverse a given Terraform configuration // and attempt find a caller for every entry in the internal module manifest. // If an entry is found, it will be removed from the internal manifest and -// appended to the manifest that records this new information. -func (r *Resolver) findAndTrimReferencedEntries(cfg *configs.Config) { - for entryKey, entry := range r.internalManifest { - for callerKey := range cfg.Module.ModuleCalls { - // Construct the module path with the caller key to get - // the full module entry key. If it's a root module caller - // do nothing since the path will be empty. - path := strings.Join(cfg.Path, ".") - if path != "" { - callerKey = path + "." + callerKey +// appended to the manifest that records this new information in a nested heirarchy. +func (r *Resolver) findAndTrimReferencedEntries(cfg *configs.Config, parentRecord *Record) { + var name string + var versionConstraints version.Constraints + if cfg.Parent != nil { + for key, config := range cfg.Parent.Children { + if config.SourceAddr.String() == cfg.SourceAddr.String() { + name = key + if cfg.Parent.Module.ModuleCalls[key] != nil { + versionConstraints = cfg.Parent.Module.ModuleCalls[key].Version.Required + } + break } + } + } - // This is a sufficient check as caller keys are unique per module - // entry. - if callerKey == entryKey { - r.manifest.addModuleEntry(entry) - // "Trim" the entry from the internal manifest, saving us cycles - // as we descend into the module tree. - delete(r.internalManifest, entryKey) - break + childRecord := &Record{ + Key: name, + Source: cfg.SourceAddr, + VersionConstraints: versionConstraints, + } + key := strings.Join(cfg.Path, ".") + + for entryKey, entry := range r.internalManifest { + if entryKey == key { + // Use resolved version from manifest + childRecord.Version = entry.Version + if parentRecord.Source != nil { + parentRecord.addChild(childRecord) + } else { + r.manifest.addModuleEntry(childRecord) } + // "Trim" the entry from the internal manifest, saving us cycles + // as we descend into the module tree. + delete(r.internalManifest, entryKey) + break } } // Traverse the child configurations for _, childCfg := range cfg.Children { - r.findAndTrimReferencedEntries(childCfg) + r.findAndTrimReferencedEntries(childCfg, childRecord) } } From b2a90cf5878541d6b8a8e47e0abce6a3881e662e Mon Sep 17 00:00:00 2001 From: Mark DeCrane Date: Mon, 18 Nov 2024 16:21:03 -0500 Subject: [PATCH 2/3] Testing updates --- internal/command/modules_test.go | 56 +++++++-- .../.terraform/modules/modules.json | 29 +++++ .../modules-nested-dependencies/main.tf | 7 ++ .../mods/other/main.tf | 5 + .../mods/test/main.tf | 3 + .../mods/test/test2/main.tf | 3 + .../mods/test/test2/test3/main.tf | 5 + .../terraform.tfstate | 0 internal/moduleref/resolver_test.go | 111 +++++++++++++++--- 9 files changed, 189 insertions(+), 30 deletions(-) create mode 100644 internal/command/testdata/modules-nested-dependencies/.terraform/modules/modules.json create mode 100644 internal/command/testdata/modules-nested-dependencies/main.tf create mode 100644 internal/command/testdata/modules-nested-dependencies/mods/other/main.tf create mode 100644 internal/command/testdata/modules-nested-dependencies/mods/test/main.tf create mode 100644 internal/command/testdata/modules-nested-dependencies/mods/test/test2/main.tf create mode 100644 internal/command/testdata/modules-nested-dependencies/mods/test/test2/test3/main.tf create mode 100644 internal/command/testdata/modules-nested-dependencies/terraform.tfstate diff --git a/internal/command/modules_test.go b/internal/command/modules_test.go index 702cf613092b..ec30972d1688 100644 --- a/internal/command/modules_test.go +++ b/internal/command/modules_test.go @@ -11,6 +11,8 @@ import ( "strings" "testing" + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/moduleref" ) @@ -18,8 +20,9 @@ import ( func TestModules_noJsonFlag(t *testing.T) { dir := t.TempDir() os.MkdirAll(dir, 0755) + testCopyDir(t, testFixturePath("modules-nested-dependencies"), dir) ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) defer testChdir(t, dir)() cmd := &ModulesCommand{ @@ -32,24 +35,51 @@ func TestModules_noJsonFlag(t *testing.T) { args := []string{} code := cmd.Run(args) - if code == 0 { - t.Fatal("expected an non zero exit status\n") + if code != 0 { + t.Fatalf("Got a non-zero exit code: %d\n", code) } - output := ui.ErrorWriter.String() - if !strings.Contains(output, "The `terraform modules` command requires the `-json` flag.\n") { - t.Fatal("expected an error message about requiring -json flag.\n") + actual := done(t).All() + + for _, part := range expectedOutputHuman { + if !strings.Contains(actual, part) { + t.Fatalf("unexpected output: %s\n", part) + } } +} - if !strings.Contains(output, modulesCommandHelp) { - t.Fatal("expected the modules command help to be displayed\n") +func TestModules_noJsonFlag_noModules(t *testing.T) { + dir := t.TempDir() + os.MkdirAll(dir, 0755) + ui := new(cli.MockUi) + view, done := testView(t) + defer testChdir(t, dir)() + + cmd := &ModulesCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + View: view, + }, + } + + args := []string{} + code := cmd.Run(args) + if code != 0 { + t.Fatalf("Got a non-zero exit code: %d\n", code) + } + + actual := done(t).All() + + if diff := cmp.Diff("No modules found in configuration.\n", actual); diff != "" { + t.Fatalf("unexpected output (-want +got):\n%s", diff) } } func TestModules_fullCmd(t *testing.T) { dir := t.TempDir() os.MkdirAll(dir, 0755) - testCopyDir(t, testFixturePath("modules"), dir) + testCopyDir(t, testFixturePath("modules-nested-dependencies"), dir) ui := new(cli.MockUi) view, done := testView(t) @@ -70,7 +100,7 @@ func TestModules_fullCmd(t *testing.T) { } output := done(t).All() - compareJSONOutput(t, output, expectedOutput) + compareJSONOutput(t, output, expectedOutputJSON) } func TestModules_fullCmd_unreferencedEntries(t *testing.T) { @@ -96,7 +126,7 @@ func TestModules_fullCmd_unreferencedEntries(t *testing.T) { t.Fatalf("Got a non-zero exit code: %d\n", code) } output := done(t).All() - compareJSONOutput(t, output, expectedOutput) + compareJSONOutput(t, output, expectedOutputJSON) } func TestModules_uninstalledModules(t *testing.T) { @@ -154,4 +184,6 @@ func compareJSONOutput(t *testing.T, got string, want string) { } } -var expectedOutput = `{"format_version":"1.0","modules":[{"key":"child","source":"./child","version":""},{"key":"count_child","source":"./child","version":""}]}` +var expectedOutputJSON = `{"format_version":"1.0","modules":[{"key":"test","source":"./mods/test","version":""},{"key":"test2","source":"./test2","version":""},{"key":"test3","source":"./test3","version":""},{"key":"other","source":"./mods/other","version":""}]}` + +var expectedOutputHuman = []string{"── \"other\"[./mods/other]", "── \"test\"[./mods/test]\n └── \"test2\"[./test2]\n └── \"test3\"[./test3]"} diff --git a/internal/command/testdata/modules-nested-dependencies/.terraform/modules/modules.json b/internal/command/testdata/modules-nested-dependencies/.terraform/modules/modules.json new file mode 100644 index 000000000000..a4f71c7b2ba8 --- /dev/null +++ b/internal/command/testdata/modules-nested-dependencies/.terraform/modules/modules.json @@ -0,0 +1,29 @@ +{ + "Modules": [ + { + "Key": "", + "Source": "", + "Dir": "." + }, + { + "Key": "other", + "Source": "./mods/other", + "Dir": "mods/other" + }, + { + "Key": "test", + "Source": "./mods/test", + "Dir": "mods/test" + }, + { + "Key": "test.test2", + "Source": "./test2", + "Dir": "mods/test/test2" + }, + { + "Key": "test.test2.test3", + "Source": "./test3", + "Dir": "mods/test/test2/test3" + } + ] +} \ No newline at end of file diff --git a/internal/command/testdata/modules-nested-dependencies/main.tf b/internal/command/testdata/modules-nested-dependencies/main.tf new file mode 100644 index 000000000000..77bf76351987 --- /dev/null +++ b/internal/command/testdata/modules-nested-dependencies/main.tf @@ -0,0 +1,7 @@ +module "test" { + source = "./mods/test" +} + +module "other" { + source = "./mods/other" +} diff --git a/internal/command/testdata/modules-nested-dependencies/mods/other/main.tf b/internal/command/testdata/modules-nested-dependencies/mods/other/main.tf new file mode 100644 index 000000000000..f059e25f9e9a --- /dev/null +++ b/internal/command/testdata/modules-nested-dependencies/mods/other/main.tf @@ -0,0 +1,5 @@ +resource "test_instance" "test" { +} +output "myoutput" { + value = "bar" +} diff --git a/internal/command/testdata/modules-nested-dependencies/mods/test/main.tf b/internal/command/testdata/modules-nested-dependencies/mods/test/main.tf new file mode 100644 index 000000000000..9e17562a6190 --- /dev/null +++ b/internal/command/testdata/modules-nested-dependencies/mods/test/main.tf @@ -0,0 +1,3 @@ +module "test2" { + source = "./test2" +} diff --git a/internal/command/testdata/modules-nested-dependencies/mods/test/test2/main.tf b/internal/command/testdata/modules-nested-dependencies/mods/test/test2/main.tf new file mode 100644 index 000000000000..ecbfa4be19dc --- /dev/null +++ b/internal/command/testdata/modules-nested-dependencies/mods/test/test2/main.tf @@ -0,0 +1,3 @@ +module "test3" { + source = "./test3" +} diff --git a/internal/command/testdata/modules-nested-dependencies/mods/test/test2/test3/main.tf b/internal/command/testdata/modules-nested-dependencies/mods/test/test2/test3/main.tf new file mode 100644 index 000000000000..f059e25f9e9a --- /dev/null +++ b/internal/command/testdata/modules-nested-dependencies/mods/test/test2/test3/main.tf @@ -0,0 +1,5 @@ +resource "test_instance" "test" { +} +output "myoutput" { + value = "bar" +} diff --git a/internal/command/testdata/modules-nested-dependencies/terraform.tfstate b/internal/command/testdata/modules-nested-dependencies/terraform.tfstate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/internal/moduleref/resolver_test.go b/internal/moduleref/resolver_test.go index 93ec55119249..eaa9f0a92574 100644 --- a/internal/moduleref/resolver_test.go +++ b/internal/moduleref/resolver_test.go @@ -19,6 +19,18 @@ func TestResolver_Resolve(t *testing.T) { }, } + cfg.Children = map[string]*configs.Config{ + "foo": &configs.Config{ + Path: addrs.Module{"foo"}, + Parent: cfg, + Children: make(map[string]*configs.Config), + SourceAddr: addrs.ModuleSourceLocal("./foo"), + Module: &configs.Module{ + ModuleCalls: map[string]*configs.ModuleCall{}, + }, + }, + } + manifest := modsdir.Manifest{ "foo": modsdir.Record{ Key: "foo", @@ -48,12 +60,27 @@ func TestResolver_ResolveNestedChildren(t *testing.T) { cfg.Children = make(map[string]*configs.Config) cfg.Module = &configs.Module{ ModuleCalls: map[string]*configs.ModuleCall{ - "foo": {Name: "foo"}, + "foo": {Name: "foo"}, + "fellowship": {Name: "fellowship"}, + }, + } + + cfg.Children["foo"] = &configs.Config{ + Path: addrs.Module{"foo"}, + Parent: cfg, + SourceAddr: addrs.ModuleSourceLocal("./foo"), + Children: make(map[string]*configs.Config), + Module: &configs.Module{ + ModuleCalls: map[string]*configs.ModuleCall{}, }, } - childCfg := &configs.Config{ - Path: addrs.Module{"fellowship"}, + childCfgFellowship := &configs.Config{ + Path: addrs.Module{"fellowship"}, + Parent: cfg, + SourceAddr: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("fellowship"), + }, Children: make(map[string]*configs.Config), Module: &configs.Module{ ModuleCalls: map[string]*configs.ModuleCall{ @@ -61,9 +88,26 @@ func TestResolver_ResolveNestedChildren(t *testing.T) { }, }, } + cfg.Children["fellowship"] = childCfgFellowship + + childCfgFellowship.Children["frodo"] = &configs.Config{ + Path: addrs.Module{"fellowship", "frodo"}, + Parent: childCfgFellowship, + SourceAddr: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("fellowship/frodo"), + }, + Children: make(map[string]*configs.Config), + Module: &configs.Module{ + ModuleCalls: map[string]*configs.ModuleCall{}, + }, + } - childCfg2 := &configs.Config{ - Path: addrs.Module{"fellowship", "weapons"}, + childCfgWeapons := &configs.Config{ + Path: addrs.Module{"fellowship", "weapons"}, + Parent: childCfgFellowship, + SourceAddr: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("fellowship/weapons"), + }, Children: make(map[string]*configs.Config), Module: &configs.Module{ ModuleCalls: map[string]*configs.ModuleCall{ @@ -71,9 +115,19 @@ func TestResolver_ResolveNestedChildren(t *testing.T) { }, }, } + childCfgFellowship.Children["weapons"] = childCfgWeapons - cfg.Children["fellowship"] = childCfg - childCfg.Children["weapons"] = childCfg2 + childCfgWeapons.Children["sting"] = &configs.Config{ + Path: addrs.Module{"fellowship", "weapons", "sting"}, + Parent: childCfgWeapons, + SourceAddr: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("fellowship/weapons/sting"), + }, + Children: make(map[string]*configs.Config), + Module: &configs.Module{ + ModuleCalls: map[string]*configs.ModuleCall{}, + }, + } manifest := modsdir.Manifest{ "foo": modsdir.Record{ @@ -84,10 +138,18 @@ func TestResolver_ResolveNestedChildren(t *testing.T) { Key: "bar", SourceAddr: "./bar", }, + "fellowship": modsdir.Record{ + Key: "fellowship", + SourceAddr: "fellowship", + }, "fellowship.frodo": modsdir.Record{ Key: "fellowship.frodo", SourceAddr: "fellowship/frodo", }, + "fellowship.weapons": modsdir.Record{ + Key: "fellowship.weapons", + SourceAddr: "fellowship/weapons", + }, "fellowship.weapons.sting": modsdir.Record{ Key: "fellowship.weapons.sting", SourceAddr: "fellowship/weapons/sting", @@ -100,23 +162,36 @@ func TestResolver_ResolveNestedChildren(t *testing.T) { resolver := NewResolver(manifest) result := resolver.Resolve(cfg) - - if len(result.Records) != 3 { - t.Fatalf("expected the resolved number of entries to equal 3, got: %d", len(result.Records)) + recordsCount, sources := countAndListSources(result.Records) + if recordsCount != 5 { + t.Fatalf("expected the resolved number of entries to equal 5, got: %d", recordsCount) } assertions := map[string]bool{ - "foo": true, - "bar": false, - "fellowship.frodo": true, - "fellowship.weapons.sting": true, - "fellowship.weapons.anduril": false, + "./foo": true, + "./bar": false, + "fellowship": true, + "fellowship/frodo": true, + "fellowship/weapons": true, + "fellowship/weapons/sting": true, + "fellowship/weapons/anduril": false, } - for _, record := range result.Records { - referenced, ok := assertions[record.Key] + for _, source := range sources { + referenced, ok := assertions[source] if !ok || !referenced { - t.Fatalf("expected to find referenced entry with key: %s", record.Key) + t.Fatalf("expected to find referenced entry with key: %s", source) } } } + +func countAndListSources(records []*Record) (count int, sources []string) { + for _, record := range records { + sources = append(sources, record.Source.String()) + count++ + childCount, childSources := countAndListSources(record.Children) + count += childCount + sources = append(sources, childSources...) + } + return +} From e4b4aa83e49c0d88126b1f541795bf4841f12388 Mon Sep 17 00:00:00 2001 From: Mark DeCrane Date: Mon, 18 Nov 2024 16:21:33 -0500 Subject: [PATCH 3/3] Docs update for modules cmd human view --- website/docs/cli/commands/modules.mdx | 45 ++++++++++++++++++--------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/website/docs/cli/commands/modules.mdx b/website/docs/cli/commands/modules.mdx index 00db42dce736..5d8c2acaeb8e 100644 --- a/website/docs/cli/commands/modules.mdx +++ b/website/docs/cli/commands/modules.mdx @@ -17,37 +17,44 @@ key, source, and version. ## Usage -Usage: `terraform modules -json` +Usage: `terraform modules [options]` -The following flags are available: +The following optional flags are available: - `-json` - Displays the module declarations in a machine-readable, JSON format. -The `-json` flag is _required_ to run the `terraform modules` command. In future releases, we will extend this command to allow for additional options. - -The output of `terraform modules` includes a `format_version` key, which is set to the value of `"1.0"` in Terraform 1.10.0. The semantics of this version are: +The output of `terraform modules -json` includes a `format_version` key, which is set to the value of `"1.0"` in Terraform 1.10.0. The semantics of this version are: - For minor versions, such as `"1.1"`, changes or additions will be backward-compatible. Ignore object properties that are unrecognized to remain forward-compatible with future minor versions. -- For major versions, e.g. `"2.0"`, changes will not be backward-compatible. Reject any input which reports an unsupported major version. +- For major versions, such as `"2.0"`, changes will not be backward-compatible. Reject any input which reports an unsupported major version. We will introduce new major versions only within the bounds of [the Terraform 1.0 Compatibility Promises](/terraform/language/v1-compatibility-promises). ## Output Format +The following example is a representation of the human readable output that `terraform modules` returns. The nested structure represents the +module composition/heirarchy within your configuration. + +``` +Modules declared by configuration: + +. +├── "my_private_registry_module"[app.terraform.io/hashicorp/label/null] 1.0.0 (>=1.0.0, < 2.0.1) +├── "my_public_registry_module"[terraform-aws-modules/iam/aws] 5.47.1 (>5.0.1) +└── "my_local_module_a"[./path/to/local/module_a] + └── "my_local_module_b"[./path/to/local/module_a/module_b] + └── "my_local_module_c"[./path/to/local/module/module_a/module_b/module_c] +``` + The following example is a representation of the JSON output format that `terraform modules -json` returns. ```javascript { "format_version": "1.0", "modules": [ - { - "key": "my_local_module", - "source": "./path/to/local/module", - "version": "" - }, { "key": "my_private_registry_module", "source": "app.terraform.io/hashicorp/label/null", @@ -59,10 +66,20 @@ The following example is a representation of the JSON output format that `terraf "version": "5.47.1" }, { - "key": "my_remote_module", - "source": "https://example.com/vpc-module.zip", + "key": "my_local_module_a", + "source": "./path/to/local/module_a", "version": "" - } + }, + { + "key": "my_local_module_b", + "source": "./path/to/local/module_a/module_b", + "version": "" + }, + { + "key": "my_local_module_c", + "source": "./path/to/local/module/module_a/module_b/module_c", + "version": "" + }, ] } ```