2024-02-08 03:48:59 -06:00
|
|
|
// Copyright (c) The OpenTofu Authors
|
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
2023-05-02 10:33:06 -05:00
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
|
2014-07-12 21:47:31 -05:00
|
|
|
package command
|
|
|
|
|
|
|
|
import (
|
2023-06-26 19:54:49 -05:00
|
|
|
"context"
|
2023-07-06 19:03:40 -05:00
|
|
|
"errors"
|
2014-07-12 21:47:31 -05:00
|
|
|
"fmt"
|
|
|
|
"os"
|
|
|
|
"strings"
|
|
|
|
|
2024-07-09 06:17:45 -05:00
|
|
|
"github.com/hashicorp/hcl/v2"
|
2024-07-17 10:46:24 -05:00
|
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
|
2023-09-20 06:35:35 -05:00
|
|
|
"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"
|
2024-03-04 08:00:29 -06:00
|
|
|
"github.com/opentofu/opentofu/internal/encryption"
|
2023-09-20 06:35:35 -05:00
|
|
|
"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"
|
2023-09-20 07:16:53 -05:00
|
|
|
"github.com/opentofu/opentofu/internal/tofu"
|
2024-01-18 06:58:33 -06:00
|
|
|
"github.com/opentofu/opentofu/internal/tofumigrate"
|
2014-07-12 21:47:31 -05:00
|
|
|
)
|
|
|
|
|
2023-07-06 19:03:40 -05:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2014-07-12 21:47:31 -05:00
|
|
|
// ShowCommand is a Command implementation that reads and outputs the
|
2023-09-26 12:09:27 -05:00
|
|
|
// contents of a OpenTofu plan or state file.
|
2014-07-12 21:47:31 -05:00
|
|
|
type ShowCommand struct {
|
2014-07-12 22:21:46 -05:00
|
|
|
Meta
|
2023-06-26 19:54:49 -05:00
|
|
|
viewType arguments.ViewType
|
2014-07-12 21:47:31 -05:00
|
|
|
}
|
|
|
|
|
2022-01-10 17:16:12 -06:00
|
|
|
func (c *ShowCommand) Run(rawArgs []string) int {
|
|
|
|
// Parse and apply global view arguments
|
|
|
|
common, rawArgs := arguments.ParseView(rawArgs)
|
|
|
|
c.View.Configure(common)
|
2014-07-12 21:47:31 -05:00
|
|
|
|
2022-01-10 17:16:12 -06:00
|
|
|
// Parse and validate flags
|
|
|
|
args, diags := arguments.ParseShow(rawArgs)
|
|
|
|
if diags.HasErrors() {
|
|
|
|
c.View.Diagnostics(diags)
|
|
|
|
c.View.HelpPrompt("show")
|
2014-07-12 21:47:31 -05:00
|
|
|
return 1
|
|
|
|
}
|
2023-06-26 19:54:49 -05:00
|
|
|
c.viewType = args.ViewType
|
2024-07-22 04:58:57 -05:00
|
|
|
c.View.SetShowSensitive(args.ShowSensitive)
|
2014-10-11 14:56:55 -05:00
|
|
|
|
2022-01-10 17:16:12 -06:00
|
|
|
// Set up view
|
|
|
|
view := views.NewShow(args.ViewType, c.View)
|
|
|
|
|
2019-03-05 10:32:11 -06:00
|
|
|
// Check for user-supplied plugin path
|
2020-04-01 14:01:08 -05:00
|
|
|
var err error
|
2019-03-05 10:32:11 -06:00
|
|
|
if c.pluginPath, err = c.loadPluginPath(); err != nil {
|
2023-09-21 07:45:28 -05:00
|
|
|
diags = diags.Append(fmt.Errorf("error loading plugin path: %w", err))
|
2022-01-10 17:16:12 -06:00
|
|
|
view.Diagnostics(diags)
|
2019-03-05 10:32:11 -06:00
|
|
|
return 1
|
|
|
|
}
|
|
|
|
|
2024-06-24 08:13:07 -05:00
|
|
|
// Inject variables from args into meta for static evaluation
|
|
|
|
c.GatherVariables(args.Vars)
|
|
|
|
|
2024-03-07 07:55:57 -06:00
|
|
|
// Load the encryption configuration
|
|
|
|
enc, encDiags := c.Encryption()
|
|
|
|
diags = diags.Append(encDiags)
|
|
|
|
if encDiags.HasErrors() {
|
|
|
|
c.showDiagnostics(diags)
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
|
2022-01-10 17:16:12 -06:00
|
|
|
// Get the data we need to display
|
2024-03-07 07:55:57 -06:00
|
|
|
plan, jsonPlan, stateFile, config, schemas, showDiags := c.show(args.Path, enc)
|
2022-01-10 17:16:12 -06:00
|
|
|
diags = diags.Append(showDiags)
|
|
|
|
if showDiags.HasErrors() {
|
|
|
|
view.Diagnostics(diags)
|
|
|
|
return 1
|
|
|
|
}
|
2018-09-24 17:00:07 -05:00
|
|
|
|
2022-01-10 17:16:12 -06:00
|
|
|
// Display the data
|
2023-06-26 19:54:49 -05:00
|
|
|
return view.Display(config, plan, jsonPlan, stateFile, schemas)
|
2022-01-10 17:16:12 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *ShowCommand) Help() string {
|
|
|
|
helpText := `
|
2023-09-21 07:38:46 -05:00
|
|
|
Usage: tofu [global options] show [options] [path]
|
2022-01-10 17:16:12 -06:00
|
|
|
|
2023-09-21 07:38:46 -05:00
|
|
|
Reads and outputs a OpenTofu state or plan file in a human-readable
|
2022-01-10 17:16:12 -06:00
|
|
|
form. If no path is specified, the current state will be shown.
|
|
|
|
|
|
|
|
Options:
|
|
|
|
|
|
|
|
-no-color If specified, output won't contain any color.
|
2024-07-22 04:58:57 -05:00
|
|
|
|
2023-09-21 07:38:46 -05:00
|
|
|
-json If specified, output the OpenTofu plan or state in
|
2022-01-10 17:16:12 -06:00
|
|
|
a machine-readable form.
|
|
|
|
|
2024-07-22 04:58:57 -05:00
|
|
|
-show-sensitive If specified, sensitive values will be displayed.
|
|
|
|
|
2024-07-11 10:00:18 -05:00
|
|
|
-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.
|
|
|
|
|
2022-01-10 17:16:12 -06:00
|
|
|
`
|
|
|
|
return strings.TrimSpace(helpText)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *ShowCommand) Synopsis() string {
|
|
|
|
return "Show the current state or a saved plan"
|
|
|
|
}
|
|
|
|
|
2024-06-24 08:13:07 -05:00
|
|
|
func (c *ShowCommand) GatherVariables(args *arguments.Vars) {
|
|
|
|
// FIXME the arguments package currently trivially gathers variable related
|
2024-09-09 06:51:39 -05:00
|
|
|
// arguments in a heterogeneous slice, in order to minimize the number of
|
2024-06-24 08:13:07 -05:00
|
|
|
// 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}
|
|
|
|
}
|
|
|
|
|
2024-03-07 07:55:57 -06:00
|
|
|
func (c *ShowCommand) show(path string, enc encryption.Encryption) (*plans.Plan, *cloudplan.RemotePlanJSON, *statefile.File, *configs.Config, *tofu.Schemas, tfdiags.Diagnostics) {
|
2024-01-18 06:58:33 -06:00
|
|
|
var diags, showDiags, migrateDiags tfdiags.Diagnostics
|
terraform: Ugly huge change to weave in new State and Plan types
Due to how often the state and plan types are referenced throughout
Terraform, there isn't a great way to switch them out gradually. As a
consequence, this huge commit gets us from the old world to a _compilable_
new world, but still has a large number of known test failures due to
key functionality being stubbed out.
The stubs here are for anything that interacts with providers, since we
now need to do the follow-up work to similarly replace the old
terraform.ResourceProvider interface with its replacement in the new
"providers" package. That work, along with work to fix the remaining
failing tests, will follow in subsequent commits.
The aim here was to replace all references to terraform.State and its
downstream types with states.State, terraform.Plan with plans.Plan,
state.State with statemgr.State, and switch to the new implementations of
the state and plan file formats. However, due to the number of times those
types are used, this also ended up affecting numerous other parts of core
such as terraform.Hook, the backend.Backend interface, and most of the CLI
commands.
Just as with 5861dbf3fc49b19587a31816eb06f511ab861bb4 before, I apologize
in advance to the person who inevitably just found this huge commit while
spelunking through the commit history.
2018-08-14 16:24:45 -05:00
|
|
|
var plan *plans.Plan
|
2023-06-26 19:54:49 -05:00
|
|
|
var jsonPlan *cloudplan.RemotePlanJSON
|
2019-01-24 17:28:53 -06:00
|
|
|
var stateFile *statefile.File
|
2022-01-07 14:05:56 -06:00
|
|
|
var config *configs.Config
|
2023-09-20 07:16:53 -05:00
|
|
|
var schemas *tofu.Schemas
|
2018-12-19 13:08:25 -06:00
|
|
|
|
2022-01-10 17:16:12 -06:00
|
|
|
// No plan file or state file argument provided,
|
|
|
|
// so get the latest state snapshot
|
|
|
|
if path == "" {
|
2024-03-07 07:55:57 -06:00
|
|
|
stateFile, showDiags = c.showFromLatestStateSnapshot(enc)
|
2022-01-10 17:16:12 -06:00
|
|
|
diags = diags.Append(showDiags)
|
|
|
|
if showDiags.HasErrors() {
|
2023-06-26 19:54:49 -05:00
|
|
|
return plan, jsonPlan, stateFile, config, schemas, diags
|
2014-07-12 21:47:31 -05:00
|
|
|
}
|
2022-01-10 17:16:12 -06:00
|
|
|
}
|
2022-01-07 14:05:56 -06:00
|
|
|
|
2022-01-10 17:16:12 -06:00
|
|
|
// Plan file or state file argument provided,
|
|
|
|
// so try to load the argument as a plan file first.
|
|
|
|
// If that fails, try to load it as a statefile.
|
|
|
|
if path != "" {
|
2024-03-07 07:55:57 -06:00
|
|
|
plan, jsonPlan, stateFile, config, showDiags = c.showFromPath(path, enc)
|
2022-01-10 17:16:12 -06:00
|
|
|
diags = diags.Append(showDiags)
|
|
|
|
if showDiags.HasErrors() {
|
2023-06-26 19:54:49 -05:00
|
|
|
return plan, jsonPlan, stateFile, config, schemas, diags
|
2017-01-18 22:50:45 -06:00
|
|
|
}
|
2014-07-12 21:47:31 -05:00
|
|
|
}
|
2014-12-05 17:38:41 -06:00
|
|
|
|
2024-01-18 06:58:33 -06:00
|
|
|
if stateFile != nil {
|
|
|
|
stateFile.State, migrateDiags = tofumigrate.MigrateStateProviderAddresses(config, stateFile.State)
|
|
|
|
diags = diags.Append(migrateDiags)
|
|
|
|
if migrateDiags.HasErrors() {
|
|
|
|
return plan, jsonPlan, stateFile, config, schemas, diags
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-10 17:16:12 -06:00
|
|
|
// Get schemas, if possible
|
2022-01-07 14:05:56 -06:00
|
|
|
if config != nil || stateFile != nil {
|
2022-08-30 17:01:44 -05:00
|
|
|
schemas, diags = c.MaybeGetSchemas(stateFile.State, config)
|
2022-08-25 14:57:40 -05:00
|
|
|
if diags.HasErrors() {
|
2023-06-26 19:54:49 -05:00
|
|
|
return plan, jsonPlan, stateFile, config, schemas, diags
|
2022-01-07 14:05:56 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-26 19:54:49 -05:00
|
|
|
return plan, jsonPlan, stateFile, config, schemas, diags
|
2022-01-10 17:16:12 -06:00
|
|
|
}
|
2024-03-07 07:55:57 -06:00
|
|
|
func (c *ShowCommand) showFromLatestStateSnapshot(enc encryption.Encryption) (*statefile.File, tfdiags.Diagnostics) {
|
2022-01-10 17:16:12 -06:00
|
|
|
var diags tfdiags.Diagnostics
|
2019-03-01 15:59:57 -06:00
|
|
|
|
2022-01-10 17:16:12 -06:00
|
|
|
// Load the backend
|
2024-03-13 09:58:52 -05:00
|
|
|
b, backendDiags := c.Backend(nil, enc.State())
|
2022-01-10 17:16:12 -06:00
|
|
|
diags = diags.Append(backendDiags)
|
|
|
|
if backendDiags.HasErrors() {
|
|
|
|
return nil, diags
|
|
|
|
}
|
|
|
|
c.ignoreRemoteVersionConflict(b)
|
2019-11-05 19:20:26 -06:00
|
|
|
|
2022-01-10 17:16:12 -06:00
|
|
|
// Load the workspace
|
|
|
|
workspace, err := c.Workspace()
|
|
|
|
if err != nil {
|
2023-09-21 07:45:28 -05:00
|
|
|
diags = diags.Append(fmt.Errorf("error selecting workspace: %w", err))
|
2022-01-10 17:16:12 -06:00
|
|
|
return nil, diags
|
2014-07-12 21:47:31 -05:00
|
|
|
}
|
|
|
|
|
2022-01-10 17:16:12 -06:00
|
|
|
// Get the latest state snapshot from the backend for the current workspace
|
|
|
|
stateFile, stateErr := getStateFromBackend(b, workspace)
|
|
|
|
if stateErr != nil {
|
2022-07-15 11:31:56 -05:00
|
|
|
diags = diags.Append(stateErr)
|
2022-01-10 17:16:12 -06:00
|
|
|
return nil, diags
|
2019-01-28 17:53:53 -06:00
|
|
|
}
|
|
|
|
|
2022-01-10 17:16:12 -06:00
|
|
|
return stateFile, diags
|
2014-07-12 21:47:31 -05:00
|
|
|
}
|
|
|
|
|
2024-03-07 07:55:57 -06:00
|
|
|
func (c *ShowCommand) showFromPath(path string, enc encryption.Encryption) (*plans.Plan, *cloudplan.RemotePlanJSON, *statefile.File, *configs.Config, tfdiags.Diagnostics) {
|
2022-01-10 17:16:12 -06:00
|
|
|
var diags tfdiags.Diagnostics
|
|
|
|
var planErr, stateErr error
|
|
|
|
var plan *plans.Plan
|
2023-06-26 19:54:49 -05:00
|
|
|
var jsonPlan *cloudplan.RemotePlanJSON
|
2022-01-10 17:16:12 -06:00
|
|
|
var stateFile *statefile.File
|
|
|
|
var config *configs.Config
|
2014-07-12 21:47:31 -05:00
|
|
|
|
2024-06-24 08:13:07 -05:00
|
|
|
rootCall, callDiags := c.rootModuleCall(".")
|
|
|
|
diags = diags.Append(callDiags)
|
|
|
|
if diags.HasErrors() {
|
|
|
|
return nil, nil, nil, nil, diags
|
|
|
|
}
|
|
|
|
|
2023-06-26 19:54:49 -05:00
|
|
|
// 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.
|
2024-06-24 08:13:07 -05:00
|
|
|
plan, jsonPlan, stateFile, config, planErr = c.getPlanFromPath(path, enc, rootCall)
|
2022-01-10 17:16:12 -06:00
|
|
|
if planErr != nil {
|
2024-03-07 07:55:57 -06:00
|
|
|
stateFile, stateErr = getStateFromPath(path, enc)
|
2022-01-10 17:16:12 -06:00
|
|
|
if stateErr != nil {
|
2023-07-06 19:03:40 -05:00
|
|
|
// 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),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-06-26 19:54:49 -05:00
|
|
|
return nil, nil, nil, nil, diags
|
2022-01-10 17:16:12 -06:00
|
|
|
}
|
|
|
|
}
|
2023-06-26 19:54:49 -05:00
|
|
|
return plan, jsonPlan, stateFile, config, diags
|
2014-07-12 21:47:31 -05:00
|
|
|
}
|
2018-12-19 13:08:25 -06:00
|
|
|
|
2023-06-26 19:54:49 -05:00
|
|
|
// 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.
|
2024-06-24 08:13:07 -05:00
|
|
|
func (c *ShowCommand) getPlanFromPath(path string, enc encryption.Encryption, rootCall configs.StaticModuleCall) (*plans.Plan, *cloudplan.RemotePlanJSON, *statefile.File, *configs.Config, error) {
|
2023-06-26 19:54:49 -05:00
|
|
|
var err error
|
|
|
|
var plan *plans.Plan
|
|
|
|
var jsonPlan *cloudplan.RemotePlanJSON
|
|
|
|
var stateFile *statefile.File
|
|
|
|
var config *configs.Config
|
|
|
|
|
2024-03-13 09:58:52 -05:00
|
|
|
pf, err := planfile.OpenWrapped(path, enc.Plan())
|
2018-12-19 13:08:25 -06:00
|
|
|
if err != nil {
|
2023-06-26 19:54:49 -05:00
|
|
|
return nil, nil, nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if lp, ok := pf.Local(); ok {
|
2024-06-24 08:13:07 -05:00
|
|
|
plan, stateFile, config, err = getDataFromPlanfileReader(lp, rootCall)
|
2023-06-26 19:54:49 -05:00
|
|
|
} else if cp, ok := pf.Cloud(); ok {
|
|
|
|
redacted := c.viewType != arguments.ViewJSON
|
2024-03-07 07:55:57 -06:00
|
|
|
jsonPlan, err = c.getDataFromCloudPlan(cp, redacted, enc)
|
2018-12-19 13:08:25 -06:00
|
|
|
}
|
2022-01-07 14:05:56 -06:00
|
|
|
|
2023-06-26 19:54:49 -05:00
|
|
|
return plan, jsonPlan, stateFile, config, err
|
|
|
|
}
|
|
|
|
|
2024-03-07 07:55:57 -06:00
|
|
|
func (c *ShowCommand) getDataFromCloudPlan(plan *cloudplan.SavedPlanBookmark, redacted bool, enc encryption.Encryption) (*cloudplan.RemotePlanJSON, error) {
|
2023-06-26 19:54:49 -05:00
|
|
|
// Set up the backend
|
2024-03-13 09:58:52 -05:00
|
|
|
b, backendDiags := c.Backend(nil, enc.State())
|
2023-06-26 19:54:49 -05:00
|
|
|
if backendDiags.HasErrors() {
|
2023-07-06 19:03:40 -05:00
|
|
|
return nil, errUnusable(backendDiags.Err(), "cloud plan")
|
2023-06-26 19:54:49 -05:00
|
|
|
}
|
|
|
|
// Cloud plans only work if we're cloud.
|
|
|
|
cl, ok := b.(*cloud.Cloud)
|
|
|
|
if !ok {
|
2023-07-06 19:03:40 -05:00
|
|
|
return nil, errUnusable(fmt.Errorf("can't show a saved cloud plan unless the current root module is connected to Terraform Cloud"), "cloud plan")
|
2023-06-26 19:54:49 -05:00
|
|
|
}
|
|
|
|
|
2023-07-06 19:03:40 -05:00
|
|
|
result, err := cl.ShowPlanForRun(context.Background(), plan.RunID, plan.Hostname, redacted)
|
|
|
|
if err != nil {
|
|
|
|
err = errUnusable(err, "cloud plan")
|
|
|
|
}
|
|
|
|
return result, err
|
2023-06-26 19:54:49 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// getDataFromPlanfileReader returns a plan, statefile, and config, extracted from a local plan file.
|
2024-06-24 08:13:07 -05:00
|
|
|
func getDataFromPlanfileReader(planReader *planfile.Reader, rootCall configs.StaticModuleCall) (*plans.Plan, *statefile.File, *configs.Config, error) {
|
2022-01-07 14:05:56 -06:00
|
|
|
// Get plan
|
|
|
|
plan, err := planReader.ReadPlan()
|
2018-12-19 13:08:25 -06:00
|
|
|
if err != nil {
|
2022-01-07 14:05:56 -06:00
|
|
|
return nil, nil, nil, err
|
2018-12-19 13:08:25 -06:00
|
|
|
}
|
2019-06-05 06:29:02 -05:00
|
|
|
|
2022-01-07 14:05:56 -06:00
|
|
|
// Get statefile
|
|
|
|
stateFile, err := planReader.ReadStateFile()
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, nil, err
|
|
|
|
}
|
|
|
|
|
2024-07-09 06:17:45 -05:00
|
|
|
subCall := rootCall.WithVariables(func(variable *configs.Variable) (cty.Value, hcl.Diagnostics) {
|
|
|
|
var diags hcl.Diagnostics
|
|
|
|
|
|
|
|
name := variable.Name
|
|
|
|
v, ok := plan.VariableValues[name]
|
|
|
|
if !ok {
|
|
|
|
if variable.Required() {
|
|
|
|
// This should not happen...
|
|
|
|
return cty.DynamicVal, diags.Append(&hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
Summary: "Missing plan variable " + variable.Name,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return variable.Default, nil
|
|
|
|
}
|
|
|
|
|
2024-07-17 10:46:24 -05:00
|
|
|
parsed, parsedErr := v.Decode(cty.DynamicPseudoType)
|
2024-07-09 06:17:45 -05:00
|
|
|
if parsedErr != nil {
|
|
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
Summary: parsedErr.Error(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return parsed, diags
|
|
|
|
})
|
|
|
|
|
2022-01-07 14:05:56 -06:00
|
|
|
// Get config
|
2024-07-09 06:17:45 -05:00
|
|
|
config, diags := planReader.ReadConfig(subCall)
|
2022-01-07 14:05:56 -06:00
|
|
|
if diags.HasErrors() {
|
2023-07-06 19:03:40 -05:00
|
|
|
return nil, nil, nil, errUnusable(diags.Err(), "local plan")
|
2022-01-07 14:05:56 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
return plan, stateFile, config, err
|
2018-12-19 13:08:25 -06:00
|
|
|
}
|
|
|
|
|
2019-01-24 17:28:53 -06:00
|
|
|
// getStateFromPath returns a statefile if the user-supplied path points to a statefile.
|
2024-03-07 07:55:57 -06:00
|
|
|
func getStateFromPath(path string, enc encryption.Encryption) (*statefile.File, error) {
|
2022-01-07 14:05:56 -06:00
|
|
|
file, err := os.Open(path)
|
2018-12-19 13:08:25 -06:00
|
|
|
if err != nil {
|
2023-07-06 19:03:40 -05:00
|
|
|
return nil, fmt.Errorf("Error loading statefile: %w", err)
|
2018-12-19 13:08:25 -06:00
|
|
|
}
|
2022-01-07 14:05:56 -06:00
|
|
|
defer file.Close()
|
2018-12-19 13:08:25 -06:00
|
|
|
|
|
|
|
var stateFile *statefile.File
|
2024-03-13 09:58:52 -05:00
|
|
|
stateFile, err = statefile.Read(file, enc.State())
|
2018-12-19 13:08:25 -06:00
|
|
|
if err != nil {
|
2023-07-06 19:03:40 -05:00
|
|
|
return nil, fmt.Errorf("Error reading %s as a statefile: %w", path, err)
|
2018-12-19 13:08:25 -06:00
|
|
|
}
|
2019-01-24 17:28:53 -06:00
|
|
|
return stateFile, nil
|
2018-12-19 13:08:25 -06:00
|
|
|
}
|
|
|
|
|
2022-01-07 14:05:56 -06:00
|
|
|
// getStateFromBackend returns the State for the current workspace, if available.
|
|
|
|
func getStateFromBackend(b backend.Backend, workspace string) (*statefile.File, error) {
|
|
|
|
// Get the state store for the given workspace
|
2023-11-08 15:09:14 -06:00
|
|
|
stateStore, err := b.StateMgr(workspace)
|
2018-12-19 13:08:25 -06:00
|
|
|
if err != nil {
|
2023-07-06 19:03:40 -05:00
|
|
|
return nil, fmt.Errorf("Failed to load state manager: %w", err)
|
2018-12-19 13:08:25 -06:00
|
|
|
}
|
|
|
|
|
2022-01-07 14:05:56 -06:00
|
|
|
// Refresh the state store with the latest state snapshot from persistent storage
|
2023-11-08 15:09:14 -06:00
|
|
|
if err := stateStore.RefreshState(); err != nil {
|
2023-07-06 19:03:40 -05:00
|
|
|
return nil, fmt.Errorf("Failed to load state: %w", err)
|
2019-03-25 15:28:35 -05:00
|
|
|
}
|
|
|
|
|
2022-01-07 14:05:56 -06:00
|
|
|
// Get the latest state snapshot and return it
|
|
|
|
stateFile := statemgr.Export(stateStore)
|
|
|
|
return stateFile, nil
|
2018-12-19 13:08:25 -06:00
|
|
|
}
|