mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 09:48:32 -05:00
OpenTelemetry has various Go packages split across several Go modules that often need to be carefully upgraded together. And in particular, we are using the "semconv" package in conjunction with the OpenTelemetry SDK's "resource" package in a way that requires that they both agree on which version of the OpenTelemetry Semantic Conventions are being followed. To help avoid "dependency hell" situations when upgrading, this centralizes all of our direct calls into the OpenTelemetry SDK and tracing API into packages under internal/tracing, by exposing a few thin wrapper functions that other packages can use to access the same functionality indirectly. We only use a relatively small subset of the OpenTelemetry library surface area, so we don't need too many of these reexports and they should not represent a significant additional maintenance burden. For the semconv and resource interaction in particular this also factors that out into a separate helper function with a unit test, so we should notice quickly whenever they become misaligned. This complements the end-to-end test previously added in opentofu/opentofu#3447 to give us faster feedback about this particular problem, while the end-to-end test has the broader scope of making sure there aren't any errors at all when initializing OpenTelemetry tracing. Finally, this also replaces the constants we previously had in package traceaddrs with functions that return attribute.KeyValue values directly. This matches the API style used by the OpenTelemetry semconv packages, and makes the calls to these helpers from elsewhere in the system a little more concise. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
588 lines
19 KiB
Go
588 lines
19 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package command
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/opentofu/opentofu/internal/backend"
|
|
"github.com/opentofu/opentofu/internal/cloud"
|
|
"github.com/opentofu/opentofu/internal/cloud/cloudplan"
|
|
"github.com/opentofu/opentofu/internal/command/arguments"
|
|
"github.com/opentofu/opentofu/internal/command/views"
|
|
"github.com/opentofu/opentofu/internal/configs"
|
|
"github.com/opentofu/opentofu/internal/encryption"
|
|
"github.com/opentofu/opentofu/internal/plans"
|
|
"github.com/opentofu/opentofu/internal/plans/planfile"
|
|
"github.com/opentofu/opentofu/internal/states/statefile"
|
|
"github.com/opentofu/opentofu/internal/states/statemgr"
|
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
|
"github.com/opentofu/opentofu/internal/tofu"
|
|
"github.com/opentofu/opentofu/internal/tracing"
|
|
"github.com/opentofu/opentofu/internal/tracing/traceattrs"
|
|
)
|
|
|
|
// Many of the methods we get data from can emit special error types if they're
|
|
// pretty sure about the file type but still can't use it. But they can't all do
|
|
// that! So, we have to do a couple ourselves if we want to preserve that data.
|
|
type errUnusableDataMisc struct {
|
|
inner error
|
|
kind string
|
|
}
|
|
|
|
func errUnusable(err error, kind string) *errUnusableDataMisc {
|
|
return &errUnusableDataMisc{inner: err, kind: kind}
|
|
}
|
|
func (e *errUnusableDataMisc) Error() string {
|
|
return e.inner.Error()
|
|
}
|
|
func (e *errUnusableDataMisc) Unwrap() error {
|
|
return e.inner
|
|
}
|
|
|
|
// ShowCommand is a Command implementation that reads and outputs the
|
|
// contents of a OpenTofu plan or state file.
|
|
// write about config here
|
|
type ShowCommand struct {
|
|
Meta
|
|
viewType arguments.ViewType
|
|
}
|
|
|
|
func (c *ShowCommand) Run(rawArgs []string) int {
|
|
ctx := c.CommandContext()
|
|
|
|
// Parse and apply global view arguments
|
|
common, rawArgs := arguments.ParseView(rawArgs)
|
|
c.View.Configure(common)
|
|
|
|
// Parse and validate flags
|
|
args, diags := arguments.ParseShow(rawArgs)
|
|
if diags.HasErrors() {
|
|
c.View.Diagnostics(diags)
|
|
c.View.HelpPrompt("show")
|
|
return 1
|
|
}
|
|
c.viewType = args.ViewType
|
|
c.View.SetShowSensitive(args.ShowSensitive)
|
|
|
|
//nolint:ineffassign - As this is a high-level call, we want to ensure that we are correctly using the right ctx later on when
|
|
ctx, span := tracing.Tracer().Start(ctx, "Show",
|
|
tracing.SpanAttributes(
|
|
traceattrs.String("opentofu.show.view", args.ViewType.String()),
|
|
traceattrs.String("opentofu.show.target", args.TargetType.String()),
|
|
traceattrs.String("opentofu.show.target_arg", args.TargetArg),
|
|
traceattrs.Bool("opentofu.show.show_sensitive", args.ShowSensitive),
|
|
),
|
|
)
|
|
defer span.End()
|
|
|
|
// Set up view
|
|
view := views.NewShow(args.ViewType, c.View)
|
|
|
|
// Check for user-supplied plugin path
|
|
var err error
|
|
if c.pluginPath, err = c.loadPluginPath(); err != nil {
|
|
diags = diags.Append(fmt.Errorf("error loading plugin path: %w", err))
|
|
view.Diagnostics(diags)
|
|
return 1
|
|
}
|
|
|
|
// Inject variables from args into meta for static evaluation
|
|
c.GatherVariables(args.Vars)
|
|
|
|
// Load the encryption configuration
|
|
enc, encDiags := c.Encryption(ctx)
|
|
diags = diags.Append(encDiags)
|
|
if encDiags.HasErrors() {
|
|
c.showDiagnostics(diags)
|
|
return 1
|
|
}
|
|
|
|
renderResult, showDiags := c.show(ctx, args.TargetType, args.TargetArg, enc)
|
|
diags = diags.Append(showDiags)
|
|
if showDiags.HasErrors() {
|
|
// "tofu show" intentionally ignores warnings unless there is at
|
|
// least one error, because view.Diagnostics produces human output
|
|
// even in the JSON view and so would cause the JSON output to
|
|
// be invalid if only warnings were returned.
|
|
view.Diagnostics(diags)
|
|
tracing.SetSpanError(span, showDiags)
|
|
return 1
|
|
}
|
|
return renderResult(view)
|
|
}
|
|
|
|
func (c *ShowCommand) Help() string {
|
|
helpText := `
|
|
Usage: tofu [global options] show [target-selection-option] [other-options]
|
|
|
|
Reads and outputs a OpenTofu state or plan file in a human-readable
|
|
form. If no path is specified, the current state will be shown.
|
|
|
|
Target selection options:
|
|
|
|
Use one of the following options to specify what to show.
|
|
|
|
-state The latest state snapshot, if any.
|
|
-plan=FILENAME The plan from a saved plan file.
|
|
-config Show the current configuration (requires -json).
|
|
|
|
If no target selection options are provided, -state is the default.
|
|
|
|
Other options:
|
|
|
|
-no-color Disable terminal escape sequences.
|
|
|
|
-json Show the information in a machine-readable form.
|
|
|
|
-show-sensitive If specified, sensitive values will be displayed.
|
|
|
|
-var 'foo=bar' Set a value for one of the input variables in the root
|
|
module of the configuration. Use this option more than
|
|
once to set more than one variable.
|
|
|
|
-var-file=filename Load variable values from the given file, in addition
|
|
to the default files terraform.tfvars and *.auto.tfvars.
|
|
Use this option more than once to include more than one
|
|
variables file.
|
|
|
|
`
|
|
return strings.TrimSpace(helpText)
|
|
}
|
|
|
|
func (c *ShowCommand) Synopsis() string {
|
|
return "Show the current state or a saved plan"
|
|
}
|
|
|
|
func (c *ShowCommand) GatherVariables(args *arguments.Vars) {
|
|
// FIXME the arguments package currently trivially gathers variable related
|
|
// arguments in a heterogeneous slice, in order to minimize the number of
|
|
// code paths gathering variables during the transition to this structure.
|
|
// Once all commands that gather variables have been converted to this
|
|
// structure, we could move the variable gathering code to the arguments
|
|
// package directly, removing this shim layer.
|
|
|
|
varArgs := args.All()
|
|
items := make([]rawFlag, len(varArgs))
|
|
for i := range varArgs {
|
|
items[i].Name = varArgs[i].Name
|
|
items[i].Value = varArgs[i].Value
|
|
}
|
|
c.Meta.variableArgs = rawFlags{items: &items}
|
|
}
|
|
|
|
type showRenderFunc func(view views.Show) int
|
|
|
|
func (c *ShowCommand) show(ctx context.Context, targetType arguments.ShowTargetType, targetArg string, enc encryption.Encryption) (showRenderFunc, tfdiags.Diagnostics) {
|
|
switch targetType {
|
|
case arguments.ShowState:
|
|
return c.showFromLatestStateSnapshot(ctx, enc)
|
|
case arguments.ShowPlan:
|
|
return c.showFromSavedPlanFile(ctx, targetArg, enc)
|
|
case arguments.ShowConfig:
|
|
return c.showConfiguration(ctx)
|
|
case arguments.ShowModule:
|
|
return c.showModule(ctx, targetArg)
|
|
case arguments.ShowUnknownType:
|
|
// This is a legacy case where we just have a filename and need to
|
|
// try treating it as either a saved plan file or a local state
|
|
// snapshot file.
|
|
return c.legacyShowFromPath(ctx, targetArg, enc)
|
|
default:
|
|
// Should not get here because the above cases should cover all
|
|
// possible values of [arguments.ShowTargetType].
|
|
panic(fmt.Sprintf("unsupported show target type %s", targetType))
|
|
}
|
|
}
|
|
|
|
func (c *ShowCommand) showFromLatestStateSnapshot(ctx context.Context, enc encryption.Encryption) (showRenderFunc, tfdiags.Diagnostics) {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
ctx, span := tracing.Tracer().Start(ctx, "Show State")
|
|
defer span.End()
|
|
|
|
// Load the backend
|
|
b, backendDiags := c.Backend(ctx, nil, enc.State())
|
|
diags = diags.Append(backendDiags)
|
|
if backendDiags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
c.ignoreRemoteVersionConflict(b)
|
|
|
|
// Load the workspace
|
|
workspace, err := c.Workspace(ctx)
|
|
if err != nil {
|
|
diags = diags.Append(fmt.Errorf("error selecting workspace: %w", err))
|
|
return nil, diags
|
|
}
|
|
|
|
// Get the latest state snapshot from the backend for the current workspace
|
|
stateFile, stateErr := getStateFromBackend(ctx, b, workspace)
|
|
if stateErr != nil {
|
|
diags = diags.Append(stateErr)
|
|
return nil, diags
|
|
}
|
|
|
|
schemas, schemaDiags := c.maybeGetSchemas(ctx, stateFile, nil)
|
|
diags = diags.Append(schemaDiags)
|
|
if schemaDiags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
return func(view views.Show) int {
|
|
return view.DisplayState(ctx, stateFile, schemas)
|
|
}, diags
|
|
}
|
|
|
|
func (c *ShowCommand) showFromSavedPlanFile(ctx context.Context, filename string, enc encryption.Encryption) (showRenderFunc, tfdiags.Diagnostics) {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
ctx, span := tracing.Tracer().Start(ctx, "Show Plan")
|
|
defer span.End()
|
|
|
|
rootCall, callDiags := c.rootModuleCall(ctx, ".")
|
|
diags = diags.Append(callDiags)
|
|
if diags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
|
|
plan, jsonPlan, stateFile, config, err := c.getPlanFromPath(ctx, filename, enc, rootCall)
|
|
if err != nil {
|
|
diags = diags.Append(err)
|
|
return nil, diags
|
|
}
|
|
|
|
schemas, schemaDiags := c.maybeGetSchemas(ctx, stateFile, config)
|
|
diags = diags.Append(schemaDiags)
|
|
if schemaDiags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
|
|
return func(view views.Show) int {
|
|
return view.DisplayPlan(ctx, plan, jsonPlan, config, stateFile, schemas)
|
|
}, diags
|
|
}
|
|
|
|
func (c *ShowCommand) legacyShowFromPath(ctx context.Context, path string, enc encryption.Encryption) (showRenderFunc, tfdiags.Diagnostics) {
|
|
var diags tfdiags.Diagnostics
|
|
var planErr, stateErr error
|
|
var plan *plans.Plan
|
|
var jsonPlan *cloudplan.RemotePlanJSON
|
|
var stateFile *statefile.File
|
|
var config *configs.Config
|
|
|
|
ctx, span := tracing.Tracer().Start(ctx, "Show")
|
|
defer span.End()
|
|
|
|
rootCall, callDiags := c.rootModuleCall(ctx, ".")
|
|
diags = diags.Append(callDiags)
|
|
if diags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
|
|
// Path might be a local plan file, a bookmark to a saved cloud plan, or a
|
|
// state file. First, try to get a plan and associated data from a local
|
|
// plan file. If that fails, try to get a json plan from the path argument.
|
|
// If that fails, try to get the statefile from the path argument.
|
|
plan, jsonPlan, stateFile, config, planErr = c.getPlanFromPath(ctx, path, enc, rootCall)
|
|
if planErr != nil {
|
|
stateFile, stateErr = getStateFromPath(path, enc)
|
|
if stateErr != nil {
|
|
// To avoid spamming the user with irrelevant errors, first check to
|
|
// see if one of our errors happens to know for a fact what file
|
|
// type we were dealing with. If so, then we can ignore the other
|
|
// ones (which are likely to be something unhelpful like "not a
|
|
// valid zip file"). If not, we can fall back to dumping whatever
|
|
// we've got.
|
|
var unLocal *planfile.ErrUnusableLocalPlan
|
|
var unState *statefile.ErrUnusableState
|
|
var unMisc *errUnusableDataMisc
|
|
if errors.As(planErr, &unLocal) {
|
|
diags = diags.Append(
|
|
tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Couldn't show local plan",
|
|
fmt.Sprintf("Plan read error: %s", unLocal),
|
|
),
|
|
)
|
|
} else if errors.As(planErr, &unMisc) {
|
|
diags = diags.Append(
|
|
tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
fmt.Sprintf("Couldn't show %s", unMisc.kind),
|
|
fmt.Sprintf("Plan read error: %s", unMisc),
|
|
),
|
|
)
|
|
} else if errors.As(stateErr, &unState) {
|
|
diags = diags.Append(
|
|
tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Couldn't show state file",
|
|
fmt.Sprintf("Plan read error: %s", unState),
|
|
),
|
|
)
|
|
} else if errors.As(stateErr, &unMisc) {
|
|
diags = diags.Append(
|
|
tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
fmt.Sprintf("Couldn't show %s", unMisc.kind),
|
|
fmt.Sprintf("Plan read error: %s", unMisc),
|
|
),
|
|
)
|
|
} else {
|
|
// Ok, give up and show the really big error
|
|
diags = diags.Append(
|
|
tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Failed to read the given file as a state or plan file",
|
|
fmt.Sprintf("State read error: %s\n\nPlan read error: %s", stateErr, planErr),
|
|
),
|
|
)
|
|
}
|
|
tracing.SetSpanError(span, diags)
|
|
return nil, diags
|
|
}
|
|
}
|
|
|
|
schemas, schemaDiags := c.maybeGetSchemas(ctx, stateFile, config)
|
|
diags = diags.Append(schemaDiags)
|
|
if schemaDiags.HasErrors() {
|
|
tracing.SetSpanError(span, diags)
|
|
return nil, diags
|
|
}
|
|
|
|
// If we successfully loaded some things then the show mode we
|
|
// choose depends on what we loaded.
|
|
switch {
|
|
case plan != nil || jsonPlan != nil:
|
|
return func(view views.Show) int {
|
|
return view.DisplayPlan(ctx, plan, jsonPlan, config, stateFile, schemas)
|
|
}, diags
|
|
default:
|
|
// We treat all other cases as a state, and DisplayState
|
|
// tolerates stateFile being nil.
|
|
return func(view views.Show) int {
|
|
return view.DisplayState(ctx, stateFile, schemas)
|
|
}, diags
|
|
}
|
|
}
|
|
|
|
// getPlanFromPath returns a plan, json plan, statefile, and config if the
|
|
// user-supplied path points to either a local or cloud plan file. Note that
|
|
// some of the return values will be nil no matter what; local plan files do not
|
|
// yield a json plan, and cloud plans do not yield real plan/state/config
|
|
// structs. An error generally suggests that the given path is either a
|
|
// directory or a statefile.
|
|
func (c *ShowCommand) getPlanFromPath(ctx context.Context, path string, enc encryption.Encryption, rootCall configs.StaticModuleCall) (*plans.Plan, *cloudplan.RemotePlanJSON, *statefile.File, *configs.Config, error) {
|
|
var err error
|
|
var plan *plans.Plan
|
|
var jsonPlan *cloudplan.RemotePlanJSON
|
|
var stateFile *statefile.File
|
|
var config *configs.Config
|
|
|
|
pf, err := planfile.OpenWrapped(path, enc.Plan())
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
|
|
if lp, ok := pf.Local(); ok {
|
|
plan, stateFile, config, err = getDataFromPlanfileReader(ctx, lp, rootCall)
|
|
} else if cp, ok := pf.Cloud(); ok {
|
|
redacted := c.viewType != arguments.ViewJSON
|
|
jsonPlan, err = c.getDataFromCloudPlan(ctx, cp, redacted, enc)
|
|
}
|
|
|
|
return plan, jsonPlan, stateFile, config, err
|
|
}
|
|
|
|
func (c *ShowCommand) getDataFromCloudPlan(ctx context.Context, plan *cloudplan.SavedPlanBookmark, redacted bool, enc encryption.Encryption) (*cloudplan.RemotePlanJSON, error) {
|
|
// Set up the backend
|
|
b, backendDiags := c.Backend(ctx, nil, enc.State())
|
|
if backendDiags.HasErrors() {
|
|
return nil, errUnusable(backendDiags.Err(), "cloud plan")
|
|
}
|
|
// Cloud plans only work if we're cloud.
|
|
cl, ok := b.(*cloud.Cloud)
|
|
if !ok {
|
|
return nil, errUnusable(fmt.Errorf("can't show a saved cloud plan unless the current root module is connected to Terraform Cloud"), "cloud plan")
|
|
}
|
|
|
|
result, err := cl.ShowPlanForRun(context.Background(), plan.RunID, plan.Hostname, redacted)
|
|
if err != nil {
|
|
err = errUnusable(err, "cloud plan")
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
// maybeGetSchemas is a thin wrapper around [Meta.MaybeGetSchemas] that
|
|
// takes a [*statefile.File] instead of a [*states.State] and tolerates
|
|
// the state file being nil, since that's more convenient for the
|
|
// "tofu show" methods that may or may not have a state file to use.
|
|
func (c *ShowCommand) maybeGetSchemas(ctx context.Context, stateFile *statefile.File, config *configs.Config) (*tofu.Schemas, tfdiags.Diagnostics) {
|
|
ctx, span := tracing.Tracer().Start(ctx, "Get Schemas")
|
|
defer span.End()
|
|
|
|
if stateFile == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
schemas, diags := c.MaybeGetSchemas(ctx, stateFile.State, config)
|
|
if diags.HasErrors() {
|
|
tracing.SetSpanError(span, diags.Err())
|
|
return nil, diags
|
|
}
|
|
|
|
return schemas, nil
|
|
|
|
}
|
|
|
|
// getDataFromPlanfileReader returns a plan, statefile, and config, extracted from a local plan file.
|
|
func getDataFromPlanfileReader(ctx context.Context, planReader *planfile.Reader, rootCall configs.StaticModuleCall) (*plans.Plan, *statefile.File, *configs.Config, error) {
|
|
// Get plan
|
|
plan, err := planReader.ReadPlan()
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
// Get statefile
|
|
stateFile, err := planReader.ReadStateFile()
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
subCall := rootCall.WithVariables(plan.VariableMapper())
|
|
// Get config
|
|
config, diags := planReader.ReadConfig(ctx, subCall)
|
|
if diags.HasErrors() {
|
|
return nil, nil, nil, errUnusable(diags.Err(), "local plan")
|
|
}
|
|
|
|
return plan, stateFile, config, err
|
|
}
|
|
|
|
// getStateFromPath returns a statefile if the user-supplied path points to a statefile.
|
|
func getStateFromPath(path string, enc encryption.Encryption) (*statefile.File, error) {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error loading statefile: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
var stateFile *statefile.File
|
|
stateFile, err = statefile.Read(file, enc.State())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error reading %s as a statefile: %w", path, err)
|
|
}
|
|
return stateFile, nil
|
|
}
|
|
|
|
// getStateFromBackend returns the State for the current workspace, if available.
|
|
func getStateFromBackend(ctx context.Context, b backend.Backend, workspace string) (*statefile.File, error) {
|
|
ctx, span := tracing.Tracer().Start(ctx, "Get State from Backend")
|
|
defer span.End()
|
|
// Get the state store for the given workspace
|
|
stateStore, err := b.StateMgr(ctx, workspace)
|
|
if err != nil {
|
|
tracing.SetSpanError(span, err)
|
|
return nil, fmt.Errorf("failed to load state manager: %w", err)
|
|
}
|
|
|
|
// Refresh the state store with the latest state snapshot from persistent storage
|
|
if err := stateStore.RefreshState(context.TODO()); err != nil {
|
|
tracing.SetSpanError(span, err)
|
|
return nil, fmt.Errorf("failed to load state: %w", err)
|
|
}
|
|
|
|
// Get the latest state snapshot and return it
|
|
stateFile := statemgr.Export(stateStore)
|
|
return stateFile, nil
|
|
}
|
|
|
|
// showConfiguration returns a function that will display the current configuration
|
|
// in JSON format. This is a new feature that requires -json to be specified.
|
|
func (c *ShowCommand) showConfiguration(ctx context.Context) (showRenderFunc, tfdiags.Diagnostics) {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
// Check if the directory is empty
|
|
empty, err := configs.IsEmptyDir(".")
|
|
if err != nil {
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Error validating configuration directory",
|
|
fmt.Sprintf("OpenTofu encountered an unexpected error while verifying that the given configuration directory is valid: %s.", err),
|
|
))
|
|
return nil, diags
|
|
}
|
|
if empty {
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"No configuration files",
|
|
"This directory contains no OpenTofu configuration files.",
|
|
))
|
|
return nil, diags
|
|
}
|
|
|
|
// Load the configuration
|
|
config, configDiags := c.loadConfig(ctx, ".")
|
|
diags = diags.Append(configDiags)
|
|
if configDiags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
|
|
// Load provider schemas (without state)
|
|
schemas, schemaDiags := c.MaybeGetSchemas(ctx, nil, config)
|
|
diags = diags.Append(schemaDiags)
|
|
if schemaDiags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
if schemas == nil {
|
|
diags = diags.Append(tfdiags.Sourceless(
|
|
tfdiags.Error,
|
|
"Failed to load provider schemas",
|
|
"The configuration cannot be shown without provider schema information.",
|
|
))
|
|
return nil, diags
|
|
}
|
|
|
|
// Return a function that will render the configuration as JSON
|
|
return func(view views.Show) int {
|
|
// Display the configuration using the view
|
|
return view.DisplayConfig(config, schemas)
|
|
}, diags
|
|
}
|
|
|
|
// showModule returns a function that will display metadata about the module
|
|
// in the given directory, in JSON format.
|
|
//
|
|
// The module representation is a subset of the configuration representation
|
|
// produced by [ShowCommand.showConfiguration], including only what can be
|
|
// generated without access to dependencies of the module. In particular, it
|
|
// does not include information about resource configuration arguments (which
|
|
// would require access to provider schemas) or child modules.
|
|
//
|
|
// This target type requires requires -json to be specified; it has no
|
|
// human-oriented rendering.
|
|
func (c *ShowCommand) showModule(ctx context.Context, dir string) (showRenderFunc, tfdiags.Diagnostics) {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
ctx, span := tracing.Tracer().Start(ctx, "Show Module")
|
|
defer span.End()
|
|
|
|
mod, moreDiags := c.loadSingleModule(ctx, dir, configs.SelectiveLoadAll)
|
|
diags = diags.Append(moreDiags)
|
|
if diags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
|
|
return func(view views.Show) int {
|
|
// Display the configuration using the view
|
|
return view.DisplaySingleModule(mod)
|
|
}, diags
|
|
}
|