| // Copyright IBM Corp. 2014, 2026 |
| // SPDX-License-Identifier: BUSL-1.1 |
| |
| package views |
| |
| import ( |
| encJson "encoding/json" |
| "fmt" |
| "sort" |
| |
| "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 { |
| // Display renders the list of module entries. |
| Display(manifest moduleref.Manifest) int |
| |
| // Diagnostics renders early diagnostics, resulting from argument parsing. |
| Diagnostics(diags tfdiags.Diagnostics) |
| } |
| |
| func NewModules(vt arguments.ViewType, view *View) Modules { |
| switch vt { |
| case arguments.ViewJSON: |
| return &ModulesJSON{view: view} |
| case arguments.ViewHuman: |
| return &ModulesHuman{view: view} |
| default: |
| panic(fmt.Sprintf("unknown view type %v", vt)) |
| } |
| } |
| |
| type ModulesHuman struct { |
| view *View |
| } |
| |
| 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() |
| |
| // ensure output is deterministic |
| sort.Sort(manifest.Records) |
| |
| populateTreeNode(printRoot, &moduleref.Record{ |
| Children: manifest.Records, |
| }) |
| |
| v.view.streams.Println(fmt.Sprintf("\nModules declared by configuration:\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) |
| } |
| |
| type ModulesJSON struct { |
| view *View |
| } |
| |
| var _ Modules = (*ModulesHuman)(nil) |
| |
| func (v *ModulesJSON) Display(manifest moduleref.Manifest) int { |
| var bytes []byte |
| var err error |
| |
| flattenedManifest := flattenManifest(manifest) |
| if bytes, err = encJson.Marshal(flattenedManifest); err != nil { |
| v.view.streams.Eprintf("error marshalling manifest: %v", err) |
| return 1 |
| } |
| |
| v.view.streams.Println(string(bytes)) |
| 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) |
| } |