Add human view for modules cmd

pull/36062/head
Mark DeCrane 2 years ago
parent 9d03e9dc1c
commit 8d0db63571

@ -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.
`

@ -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)
}

@ -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)
}

@ -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)
}
}

Loading…
Cancel
Save