github.com/opentofu/opentofu@v1.7.1/internal/command/show.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package command 7 8 import ( 9 "context" 10 "errors" 11 "fmt" 12 "os" 13 "strings" 14 15 "github.com/opentofu/opentofu/internal/backend" 16 "github.com/opentofu/opentofu/internal/cloud" 17 "github.com/opentofu/opentofu/internal/cloud/cloudplan" 18 "github.com/opentofu/opentofu/internal/command/arguments" 19 "github.com/opentofu/opentofu/internal/command/views" 20 "github.com/opentofu/opentofu/internal/configs" 21 "github.com/opentofu/opentofu/internal/encryption" 22 "github.com/opentofu/opentofu/internal/plans" 23 "github.com/opentofu/opentofu/internal/plans/planfile" 24 "github.com/opentofu/opentofu/internal/states/statefile" 25 "github.com/opentofu/opentofu/internal/states/statemgr" 26 "github.com/opentofu/opentofu/internal/tfdiags" 27 "github.com/opentofu/opentofu/internal/tofu" 28 "github.com/opentofu/opentofu/internal/tofumigrate" 29 ) 30 31 // Many of the methods we get data from can emit special error types if they're 32 // pretty sure about the file type but still can't use it. But they can't all do 33 // that! So, we have to do a couple ourselves if we want to preserve that data. 34 type errUnusableDataMisc struct { 35 inner error 36 kind string 37 } 38 39 func errUnusable(err error, kind string) *errUnusableDataMisc { 40 return &errUnusableDataMisc{inner: err, kind: kind} 41 } 42 func (e *errUnusableDataMisc) Error() string { 43 return e.inner.Error() 44 } 45 func (e *errUnusableDataMisc) Unwrap() error { 46 return e.inner 47 } 48 49 // ShowCommand is a Command implementation that reads and outputs the 50 // contents of a OpenTofu plan or state file. 51 type ShowCommand struct { 52 Meta 53 viewType arguments.ViewType 54 } 55 56 func (c *ShowCommand) Run(rawArgs []string) int { 57 // Parse and apply global view arguments 58 common, rawArgs := arguments.ParseView(rawArgs) 59 c.View.Configure(common) 60 61 // Parse and validate flags 62 args, diags := arguments.ParseShow(rawArgs) 63 if diags.HasErrors() { 64 c.View.Diagnostics(diags) 65 c.View.HelpPrompt("show") 66 return 1 67 } 68 c.viewType = args.ViewType 69 70 // Set up view 71 view := views.NewShow(args.ViewType, c.View) 72 73 // Check for user-supplied plugin path 74 var err error 75 if c.pluginPath, err = c.loadPluginPath(); err != nil { 76 diags = diags.Append(fmt.Errorf("error loading plugin path: %w", err)) 77 view.Diagnostics(diags) 78 return 1 79 } 80 81 // Load the encryption configuration 82 enc, encDiags := c.Encryption() 83 diags = diags.Append(encDiags) 84 if encDiags.HasErrors() { 85 c.showDiagnostics(diags) 86 return 1 87 } 88 89 // Get the data we need to display 90 plan, jsonPlan, stateFile, config, schemas, showDiags := c.show(args.Path, enc) 91 diags = diags.Append(showDiags) 92 if showDiags.HasErrors() { 93 view.Diagnostics(diags) 94 return 1 95 } 96 97 // Display the data 98 return view.Display(config, plan, jsonPlan, stateFile, schemas) 99 } 100 101 func (c *ShowCommand) Help() string { 102 helpText := ` 103 Usage: tofu [global options] show [options] [path] 104 105 Reads and outputs a OpenTofu state or plan file in a human-readable 106 form. If no path is specified, the current state will be shown. 107 108 Options: 109 110 -no-color If specified, output won't contain any color. 111 -json If specified, output the OpenTofu plan or state in 112 a machine-readable form. 113 114 ` 115 return strings.TrimSpace(helpText) 116 } 117 118 func (c *ShowCommand) Synopsis() string { 119 return "Show the current state or a saved plan" 120 } 121 122 func (c *ShowCommand) show(path string, enc encryption.Encryption) (*plans.Plan, *cloudplan.RemotePlanJSON, *statefile.File, *configs.Config, *tofu.Schemas, tfdiags.Diagnostics) { 123 var diags, showDiags, migrateDiags tfdiags.Diagnostics 124 var plan *plans.Plan 125 var jsonPlan *cloudplan.RemotePlanJSON 126 var stateFile *statefile.File 127 var config *configs.Config 128 var schemas *tofu.Schemas 129 130 // No plan file or state file argument provided, 131 // so get the latest state snapshot 132 if path == "" { 133 stateFile, showDiags = c.showFromLatestStateSnapshot(enc) 134 diags = diags.Append(showDiags) 135 if showDiags.HasErrors() { 136 return plan, jsonPlan, stateFile, config, schemas, diags 137 } 138 } 139 140 // Plan file or state file argument provided, 141 // so try to load the argument as a plan file first. 142 // If that fails, try to load it as a statefile. 143 if path != "" { 144 plan, jsonPlan, stateFile, config, showDiags = c.showFromPath(path, enc) 145 diags = diags.Append(showDiags) 146 if showDiags.HasErrors() { 147 return plan, jsonPlan, stateFile, config, schemas, diags 148 } 149 } 150 151 if stateFile != nil { 152 stateFile.State, migrateDiags = tofumigrate.MigrateStateProviderAddresses(config, stateFile.State) 153 diags = diags.Append(migrateDiags) 154 if migrateDiags.HasErrors() { 155 return plan, jsonPlan, stateFile, config, schemas, diags 156 } 157 } 158 159 // Get schemas, if possible 160 if config != nil || stateFile != nil { 161 schemas, diags = c.MaybeGetSchemas(stateFile.State, config) 162 if diags.HasErrors() { 163 return plan, jsonPlan, stateFile, config, schemas, diags 164 } 165 } 166 167 return plan, jsonPlan, stateFile, config, schemas, diags 168 } 169 func (c *ShowCommand) showFromLatestStateSnapshot(enc encryption.Encryption) (*statefile.File, tfdiags.Diagnostics) { 170 var diags tfdiags.Diagnostics 171 172 // Load the backend 173 b, backendDiags := c.Backend(nil, enc.State()) 174 diags = diags.Append(backendDiags) 175 if backendDiags.HasErrors() { 176 return nil, diags 177 } 178 c.ignoreRemoteVersionConflict(b) 179 180 // Load the workspace 181 workspace, err := c.Workspace() 182 if err != nil { 183 diags = diags.Append(fmt.Errorf("error selecting workspace: %w", err)) 184 return nil, diags 185 } 186 187 // Get the latest state snapshot from the backend for the current workspace 188 stateFile, stateErr := getStateFromBackend(b, workspace) 189 if stateErr != nil { 190 diags = diags.Append(stateErr) 191 return nil, diags 192 } 193 194 return stateFile, diags 195 } 196 197 func (c *ShowCommand) showFromPath(path string, enc encryption.Encryption) (*plans.Plan, *cloudplan.RemotePlanJSON, *statefile.File, *configs.Config, tfdiags.Diagnostics) { 198 var diags tfdiags.Diagnostics 199 var planErr, stateErr error 200 var plan *plans.Plan 201 var jsonPlan *cloudplan.RemotePlanJSON 202 var stateFile *statefile.File 203 var config *configs.Config 204 205 // Path might be a local plan file, a bookmark to a saved cloud plan, or a 206 // state file. First, try to get a plan and associated data from a local 207 // plan file. If that fails, try to get a json plan from the path argument. 208 // If that fails, try to get the statefile from the path argument. 209 plan, jsonPlan, stateFile, config, planErr = c.getPlanFromPath(path, enc) 210 if planErr != nil { 211 stateFile, stateErr = getStateFromPath(path, enc) 212 if stateErr != nil { 213 // To avoid spamming the user with irrelevant errors, first check to 214 // see if one of our errors happens to know for a fact what file 215 // type we were dealing with. If so, then we can ignore the other 216 // ones (which are likely to be something unhelpful like "not a 217 // valid zip file"). If not, we can fall back to dumping whatever 218 // we've got. 219 var unLocal *planfile.ErrUnusableLocalPlan 220 var unState *statefile.ErrUnusableState 221 var unMisc *errUnusableDataMisc 222 if errors.As(planErr, &unLocal) { 223 diags = diags.Append( 224 tfdiags.Sourceless( 225 tfdiags.Error, 226 "Couldn't show local plan", 227 fmt.Sprintf("Plan read error: %s", unLocal), 228 ), 229 ) 230 } else if errors.As(planErr, &unMisc) { 231 diags = diags.Append( 232 tfdiags.Sourceless( 233 tfdiags.Error, 234 fmt.Sprintf("Couldn't show %s", unMisc.kind), 235 fmt.Sprintf("Plan read error: %s", unMisc), 236 ), 237 ) 238 } else if errors.As(stateErr, &unState) { 239 diags = diags.Append( 240 tfdiags.Sourceless( 241 tfdiags.Error, 242 "Couldn't show state file", 243 fmt.Sprintf("Plan read error: %s", unState), 244 ), 245 ) 246 } else if errors.As(stateErr, &unMisc) { 247 diags = diags.Append( 248 tfdiags.Sourceless( 249 tfdiags.Error, 250 fmt.Sprintf("Couldn't show %s", unMisc.kind), 251 fmt.Sprintf("Plan read error: %s", unMisc), 252 ), 253 ) 254 } else { 255 // Ok, give up and show the really big error 256 diags = diags.Append( 257 tfdiags.Sourceless( 258 tfdiags.Error, 259 "Failed to read the given file as a state or plan file", 260 fmt.Sprintf("State read error: %s\n\nPlan read error: %s", stateErr, planErr), 261 ), 262 ) 263 } 264 265 return nil, nil, nil, nil, diags 266 } 267 } 268 return plan, jsonPlan, stateFile, config, diags 269 } 270 271 // getPlanFromPath returns a plan, json plan, statefile, and config if the 272 // user-supplied path points to either a local or cloud plan file. Note that 273 // some of the return values will be nil no matter what; local plan files do not 274 // yield a json plan, and cloud plans do not yield real plan/state/config 275 // structs. An error generally suggests that the given path is either a 276 // directory or a statefile. 277 func (c *ShowCommand) getPlanFromPath(path string, enc encryption.Encryption) (*plans.Plan, *cloudplan.RemotePlanJSON, *statefile.File, *configs.Config, error) { 278 var err error 279 var plan *plans.Plan 280 var jsonPlan *cloudplan.RemotePlanJSON 281 var stateFile *statefile.File 282 var config *configs.Config 283 284 pf, err := planfile.OpenWrapped(path, enc.Plan()) 285 if err != nil { 286 return nil, nil, nil, nil, err 287 } 288 289 if lp, ok := pf.Local(); ok { 290 plan, stateFile, config, err = getDataFromPlanfileReader(lp) 291 } else if cp, ok := pf.Cloud(); ok { 292 redacted := c.viewType != arguments.ViewJSON 293 jsonPlan, err = c.getDataFromCloudPlan(cp, redacted, enc) 294 } 295 296 return plan, jsonPlan, stateFile, config, err 297 } 298 299 func (c *ShowCommand) getDataFromCloudPlan(plan *cloudplan.SavedPlanBookmark, redacted bool, enc encryption.Encryption) (*cloudplan.RemotePlanJSON, error) { 300 // Set up the backend 301 b, backendDiags := c.Backend(nil, enc.State()) 302 if backendDiags.HasErrors() { 303 return nil, errUnusable(backendDiags.Err(), "cloud plan") 304 } 305 // Cloud plans only work if we're cloud. 306 cl, ok := b.(*cloud.Cloud) 307 if !ok { 308 return nil, errUnusable(fmt.Errorf("can't show a saved cloud plan unless the current root module is connected to Terraform Cloud"), "cloud plan") 309 } 310 311 result, err := cl.ShowPlanForRun(context.Background(), plan.RunID, plan.Hostname, redacted) 312 if err != nil { 313 err = errUnusable(err, "cloud plan") 314 } 315 return result, err 316 } 317 318 // getDataFromPlanfileReader returns a plan, statefile, and config, extracted from a local plan file. 319 func getDataFromPlanfileReader(planReader *planfile.Reader) (*plans.Plan, *statefile.File, *configs.Config, error) { 320 // Get plan 321 plan, err := planReader.ReadPlan() 322 if err != nil { 323 return nil, nil, nil, err 324 } 325 326 // Get statefile 327 stateFile, err := planReader.ReadStateFile() 328 if err != nil { 329 return nil, nil, nil, err 330 } 331 332 // Get config 333 config, diags := planReader.ReadConfig() 334 if diags.HasErrors() { 335 return nil, nil, nil, errUnusable(diags.Err(), "local plan") 336 } 337 338 return plan, stateFile, config, err 339 } 340 341 // getStateFromPath returns a statefile if the user-supplied path points to a statefile. 342 func getStateFromPath(path string, enc encryption.Encryption) (*statefile.File, error) { 343 file, err := os.Open(path) 344 if err != nil { 345 return nil, fmt.Errorf("Error loading statefile: %w", err) 346 } 347 defer file.Close() 348 349 var stateFile *statefile.File 350 stateFile, err = statefile.Read(file, enc.State()) 351 if err != nil { 352 return nil, fmt.Errorf("Error reading %s as a statefile: %w", path, err) 353 } 354 return stateFile, nil 355 } 356 357 // getStateFromBackend returns the State for the current workspace, if available. 358 func getStateFromBackend(b backend.Backend, workspace string) (*statefile.File, error) { 359 // Get the state store for the given workspace 360 stateStore, err := b.StateMgr(workspace) 361 if err != nil { 362 return nil, fmt.Errorf("Failed to load state manager: %w", err) 363 } 364 365 // Refresh the state store with the latest state snapshot from persistent storage 366 if err := stateStore.RefreshState(); err != nil { 367 return nil, fmt.Errorf("Failed to load state: %w", err) 368 } 369 370 // Get the latest state snapshot and return it 371 stateFile := statemgr.Export(stateStore) 372 return stateFile, nil 373 }