github.com/pulumi/terraform@v1.4.0/pkg/command/show.go (about) 1 package command 2 3 import ( 4 "fmt" 5 "os" 6 "strings" 7 8 "github.com/pulumi/terraform/pkg/backend" 9 "github.com/pulumi/terraform/pkg/command/arguments" 10 "github.com/pulumi/terraform/pkg/command/views" 11 "github.com/pulumi/terraform/pkg/configs" 12 "github.com/pulumi/terraform/pkg/plans" 13 "github.com/pulumi/terraform/pkg/plans/planfile" 14 "github.com/pulumi/terraform/pkg/states/statefile" 15 "github.com/pulumi/terraform/pkg/states/statemgr" 16 "github.com/pulumi/terraform/pkg/terraform" 17 "github.com/pulumi/terraform/pkg/tfdiags" 18 ) 19 20 // ShowCommand is a Command implementation that reads and outputs the 21 // contents of a Terraform plan or state file. 22 type ShowCommand struct { 23 Meta 24 } 25 26 func (c *ShowCommand) Run(rawArgs []string) int { 27 // Parse and apply global view arguments 28 common, rawArgs := arguments.ParseView(rawArgs) 29 c.View.Configure(common) 30 31 // Parse and validate flags 32 args, diags := arguments.ParseShow(rawArgs) 33 if diags.HasErrors() { 34 c.View.Diagnostics(diags) 35 c.View.HelpPrompt("show") 36 return 1 37 } 38 39 // Set up view 40 view := views.NewShow(args.ViewType, c.View) 41 42 // Check for user-supplied plugin path 43 var err error 44 if c.pluginPath, err = c.loadPluginPath(); err != nil { 45 diags = diags.Append(fmt.Errorf("error loading plugin path: %s", err)) 46 view.Diagnostics(diags) 47 return 1 48 } 49 50 // Get the data we need to display 51 plan, stateFile, config, schemas, showDiags := c.show(args.Path) 52 diags = diags.Append(showDiags) 53 if showDiags.HasErrors() { 54 view.Diagnostics(diags) 55 return 1 56 } 57 58 // Display the data 59 return view.Display(config, plan, stateFile, schemas) 60 } 61 62 func (c *ShowCommand) Help() string { 63 helpText := ` 64 Usage: terraform [global options] show [options] [path] 65 66 Reads and outputs a Terraform state or plan file in a human-readable 67 form. If no path is specified, the current state will be shown. 68 69 Options: 70 71 -no-color If specified, output won't contain any color. 72 -json If specified, output the Terraform plan or state in 73 a machine-readable form. 74 75 ` 76 return strings.TrimSpace(helpText) 77 } 78 79 func (c *ShowCommand) Synopsis() string { 80 return "Show the current state or a saved plan" 81 } 82 83 func (c *ShowCommand) show(path string) (*plans.Plan, *statefile.File, *configs.Config, *terraform.Schemas, tfdiags.Diagnostics) { 84 var diags, showDiags tfdiags.Diagnostics 85 var plan *plans.Plan 86 var stateFile *statefile.File 87 var config *configs.Config 88 var schemas *terraform.Schemas 89 90 // No plan file or state file argument provided, 91 // so get the latest state snapshot 92 if path == "" { 93 stateFile, showDiags = c.showFromLatestStateSnapshot() 94 diags = diags.Append(showDiags) 95 if showDiags.HasErrors() { 96 return plan, stateFile, config, schemas, diags 97 } 98 } 99 100 // Plan file or state file argument provided, 101 // so try to load the argument as a plan file first. 102 // If that fails, try to load it as a statefile. 103 if path != "" { 104 plan, stateFile, config, showDiags = c.showFromPath(path) 105 diags = diags.Append(showDiags) 106 if showDiags.HasErrors() { 107 return plan, stateFile, config, schemas, diags 108 } 109 } 110 111 // Get schemas, if possible 112 if config != nil || stateFile != nil { 113 schemas, diags = c.MaybeGetSchemas(stateFile.State, config) 114 if diags.HasErrors() { 115 return plan, stateFile, config, schemas, diags 116 } 117 } 118 119 return plan, stateFile, config, schemas, diags 120 } 121 func (c *ShowCommand) showFromLatestStateSnapshot() (*statefile.File, tfdiags.Diagnostics) { 122 var diags tfdiags.Diagnostics 123 124 // Load the backend 125 b, backendDiags := c.Backend(nil) 126 diags = diags.Append(backendDiags) 127 if backendDiags.HasErrors() { 128 return nil, diags 129 } 130 c.ignoreRemoteVersionConflict(b) 131 132 // Load the workspace 133 workspace, err := c.Workspace() 134 if err != nil { 135 diags = diags.Append(fmt.Errorf("error selecting workspace: %s", err)) 136 return nil, diags 137 } 138 139 // Get the latest state snapshot from the backend for the current workspace 140 stateFile, stateErr := getStateFromBackend(b, workspace) 141 if stateErr != nil { 142 diags = diags.Append(stateErr) 143 return nil, diags 144 } 145 146 return stateFile, diags 147 } 148 149 func (c *ShowCommand) showFromPath(path string) (*plans.Plan, *statefile.File, *configs.Config, tfdiags.Diagnostics) { 150 var diags tfdiags.Diagnostics 151 var planErr, stateErr error 152 var plan *plans.Plan 153 var stateFile *statefile.File 154 var config *configs.Config 155 156 // Try to get the plan file and associated data from 157 // the path argument. If that fails, try to get the 158 // statefile from the path argument. 159 plan, stateFile, config, planErr = getPlanFromPath(path) 160 if planErr != nil { 161 stateFile, stateErr = getStateFromPath(path) 162 if stateErr != nil { 163 diags = diags.Append( 164 tfdiags.Sourceless( 165 tfdiags.Error, 166 "Failed to read the given file as a state or plan file", 167 fmt.Sprintf("State read error: %s\n\nPlan read error: %s", stateErr, planErr), 168 ), 169 ) 170 return nil, nil, nil, diags 171 } 172 } 173 return plan, stateFile, config, diags 174 } 175 176 // getPlanFromPath returns a plan, statefile, and config if the user-supplied 177 // path points to a plan file. If both plan and error are nil, the path is likely 178 // a directory. An error could suggest that the given path points to a statefile. 179 func getPlanFromPath(path string) (*plans.Plan, *statefile.File, *configs.Config, error) { 180 planReader, err := planfile.Open(path) 181 if err != nil { 182 return nil, nil, nil, err 183 } 184 185 // Get plan 186 plan, err := planReader.ReadPlan() 187 if err != nil { 188 return nil, nil, nil, err 189 } 190 191 // Get statefile 192 stateFile, err := planReader.ReadStateFile() 193 if err != nil { 194 return nil, nil, nil, err 195 } 196 197 // Get config 198 config, diags := planReader.ReadConfig() 199 if diags.HasErrors() { 200 return nil, nil, nil, diags.Err() 201 } 202 203 return plan, stateFile, config, err 204 } 205 206 // getStateFromPath returns a statefile if the user-supplied path points to a statefile. 207 func getStateFromPath(path string) (*statefile.File, error) { 208 file, err := os.Open(path) 209 if err != nil { 210 return nil, fmt.Errorf("Error loading statefile: %s", err) 211 } 212 defer file.Close() 213 214 var stateFile *statefile.File 215 stateFile, err = statefile.Read(file) 216 if err != nil { 217 return nil, fmt.Errorf("Error reading %s as a statefile: %s", path, err) 218 } 219 return stateFile, nil 220 } 221 222 // getStateFromBackend returns the State for the current workspace, if available. 223 func getStateFromBackend(b backend.Backend, workspace string) (*statefile.File, error) { 224 // Get the state store for the given workspace 225 stateStore, err := b.StateMgr(workspace) 226 if err != nil { 227 return nil, fmt.Errorf("Failed to load state manager: %s", err) 228 } 229 230 // Refresh the state store with the latest state snapshot from persistent storage 231 if err := stateStore.RefreshState(); err != nil { 232 return nil, fmt.Errorf("Failed to load state: %s", err) 233 } 234 235 // Get the latest state snapshot and return it 236 stateFile := statemgr.Export(stateStore) 237 return stateFile, nil 238 }