From 8d0db6357105e48a929f24bca349dd74e55ee372 Mon Sep 17 00:00:00 2001 From: Mark DeCrane Date: Mon, 18 Nov 2024 13:41:53 -0500 Subject: [PATCH] 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 c8bfed5e07..19eab321fa 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 3b1c8120be..82ab0ce46a 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 ad6b757afc..fb4f918226 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 71f232338c..48e6da87a8 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) } }