github.com/cycloidio/terraform@v1.1.10-0.20220513142504-76d5c768dc63/command/show.go (about) 1 package command 2 3 import ( 4 "fmt" 5 "os" 6 "strings" 7 8 "github.com/cycloidio/terraform/backend" 9 "github.com/cycloidio/terraform/command/arguments" 10 "github.com/cycloidio/terraform/command/format" 11 "github.com/cycloidio/terraform/command/jsonplan" 12 "github.com/cycloidio/terraform/command/jsonstate" 13 "github.com/cycloidio/terraform/command/views" 14 "github.com/cycloidio/terraform/plans" 15 "github.com/cycloidio/terraform/plans/planfile" 16 "github.com/cycloidio/terraform/states/statefile" 17 "github.com/cycloidio/terraform/states/statemgr" 18 "github.com/cycloidio/terraform/tfdiags" 19 ) 20 21 // ShowCommand is a Command implementation that reads and outputs the 22 // contents of a Terraform plan or state file. 23 type ShowCommand struct { 24 Meta 25 } 26 27 func (c *ShowCommand) Run(args []string) int { 28 args = c.Meta.process(args) 29 cmdFlags := c.Meta.defaultFlagSet("show") 30 var jsonOutput bool 31 cmdFlags.BoolVar(&jsonOutput, "json", false, "produce JSON output") 32 cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } 33 if err := cmdFlags.Parse(args); err != nil { 34 c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) 35 return 1 36 } 37 38 args = cmdFlags.Args() 39 if len(args) > 2 { 40 c.Ui.Error( 41 "The show command expects at most two arguments.\n The path to a " + 42 "Terraform state or plan file, and optionally -json for json output.\n") 43 cmdFlags.Usage() 44 return 1 45 } 46 47 // Check for user-supplied plugin path 48 var err error 49 if c.pluginPath, err = c.loadPluginPath(); err != nil { 50 c.Ui.Error(fmt.Sprintf("Error loading plugin path: %s", err)) 51 return 1 52 } 53 54 var diags tfdiags.Diagnostics 55 56 // Load the backend 57 b, backendDiags := c.Backend(nil) 58 diags = diags.Append(backendDiags) 59 if backendDiags.HasErrors() { 60 c.showDiagnostics(diags) 61 return 1 62 } 63 64 // We require a local backend 65 local, ok := b.(backend.Local) 66 if !ok { 67 c.showDiagnostics(diags) // in case of any warnings in here 68 c.Ui.Error(ErrUnsupportedLocalOp) 69 return 1 70 } 71 72 // This is a read-only command 73 c.ignoreRemoteVersionConflict(b) 74 75 // the show command expects the config dir to always be the cwd 76 cwd, err := os.Getwd() 77 if err != nil { 78 c.Ui.Error(fmt.Sprintf("Error getting cwd: %s", err)) 79 return 1 80 } 81 82 // Determine if a planfile was passed to the command 83 var planFile *planfile.Reader 84 if len(args) > 0 { 85 // We will handle error checking later on - this is just required to 86 // load the local context if the given path is successfully read as 87 // a planfile. 88 planFile, _ = c.PlanFile(args[0]) 89 } 90 91 // Build the operation 92 opReq := c.Operation(b) 93 opReq.ConfigDir = cwd 94 opReq.PlanFile = planFile 95 opReq.ConfigLoader, err = c.initConfigLoader() 96 opReq.AllowUnsetVariables = true 97 opReq.DisablePlanFileStateLineageChecks = true 98 if err != nil { 99 diags = diags.Append(err) 100 c.showDiagnostics(diags) 101 return 1 102 } 103 104 // Get the context 105 lr, _, ctxDiags := local.LocalRun(opReq) 106 diags = diags.Append(ctxDiags) 107 if ctxDiags.HasErrors() { 108 c.showDiagnostics(diags) 109 return 1 110 } 111 112 // Get the schemas from the context 113 schemas, moreDiags := lr.Core.Schemas(lr.Config, lr.InputState) 114 diags = diags.Append(moreDiags) 115 if moreDiags.HasErrors() { 116 c.showDiagnostics(diags) 117 return 1 118 } 119 120 var planErr, stateErr error 121 var plan *plans.Plan 122 var stateFile *statefile.File 123 124 // if a path was provided, try to read it as a path to a planfile 125 // if that fails, try to read the cli argument as a path to a statefile 126 if len(args) > 0 { 127 path := args[0] 128 plan, stateFile, planErr = getPlanFromPath(path) 129 if planErr != nil { 130 stateFile, stateErr = getStateFromPath(path) 131 if stateErr != nil { 132 c.Ui.Error(fmt.Sprintf( 133 "Terraform couldn't read the given file as a state or plan file.\n"+ 134 "The errors while attempting to read the file as each format are\n"+ 135 "shown below.\n\n"+ 136 "State read error: %s\n\nPlan read error: %s", 137 stateErr, 138 planErr)) 139 return 1 140 } 141 } 142 } else { 143 env, err := c.Workspace() 144 if err != nil { 145 c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err)) 146 return 1 147 } 148 stateFile, stateErr = getStateFromEnv(b, env) 149 if stateErr != nil { 150 c.Ui.Error(stateErr.Error()) 151 return 1 152 } 153 } 154 155 if plan != nil { 156 if jsonOutput { 157 config := lr.Config 158 jsonPlan, err := jsonplan.Marshal(config, plan, stateFile, schemas) 159 160 if err != nil { 161 c.Ui.Error(fmt.Sprintf("Failed to marshal plan to json: %s", err)) 162 return 1 163 } 164 c.Ui.Output(string(jsonPlan)) 165 return 0 166 } 167 168 view := views.NewShow(arguments.ViewHuman, c.View) 169 view.Plan(plan, schemas) 170 return 0 171 } 172 173 if jsonOutput { 174 // At this point, it is possible that there is neither state nor a plan. 175 // That's ok, we'll just return an empty object. 176 jsonState, err := jsonstate.Marshal(stateFile, schemas) 177 if err != nil { 178 c.Ui.Error(fmt.Sprintf("Failed to marshal state to json: %s", err)) 179 return 1 180 } 181 c.Ui.Output(string(jsonState)) 182 } else { 183 if stateFile == nil { 184 c.Ui.Output("No state.") 185 return 0 186 } 187 c.Ui.Output(format.State(&format.StateOpts{ 188 State: stateFile.State, 189 Color: c.Colorize(), 190 Schemas: schemas, 191 })) 192 } 193 194 return 0 195 } 196 197 func (c *ShowCommand) Help() string { 198 helpText := ` 199 Usage: terraform [global options] show [options] [path] 200 201 Reads and outputs a Terraform state or plan file in a human-readable 202 form. If no path is specified, the current state will be shown. 203 204 Options: 205 206 -no-color If specified, output won't contain any color. 207 -json If specified, output the Terraform plan or state in 208 a machine-readable form. 209 210 ` 211 return strings.TrimSpace(helpText) 212 } 213 214 func (c *ShowCommand) Synopsis() string { 215 return "Show the current state or a saved plan" 216 } 217 218 // getPlanFromPath returns a plan and statefile if the user-supplied path points 219 // to a planfile. If both plan and error are nil, the path is likely a 220 // directory. An error could suggest that the given path points to a statefile. 221 func getPlanFromPath(path string) (*plans.Plan, *statefile.File, error) { 222 pr, err := planfile.Open(path) 223 if err != nil { 224 return nil, nil, err 225 } 226 plan, err := pr.ReadPlan() 227 if err != nil { 228 return nil, nil, err 229 } 230 231 stateFile, err := pr.ReadStateFile() 232 return plan, stateFile, err 233 } 234 235 // getStateFromPath returns a statefile if the user-supplied path points to a statefile. 236 func getStateFromPath(path string) (*statefile.File, error) { 237 f, err := os.Open(path) 238 if err != nil { 239 return nil, fmt.Errorf("Error loading statefile: %s", err) 240 } 241 defer f.Close() 242 243 var stateFile *statefile.File 244 stateFile, err = statefile.Read(f) 245 if err != nil { 246 return nil, fmt.Errorf("Error reading %s as a statefile: %s", path, err) 247 } 248 return stateFile, nil 249 } 250 251 // getStateFromEnv returns the State for the current workspace, if available. 252 func getStateFromEnv(b backend.Backend, env string) (*statefile.File, error) { 253 // Get the state 254 stateStore, err := b.StateMgr(env) 255 if err != nil { 256 return nil, fmt.Errorf("Failed to load state manager: %s", err) 257 } 258 259 if err := stateStore.RefreshState(); err != nil { 260 return nil, fmt.Errorf("Failed to load state: %s", err) 261 } 262 263 sf := statemgr.Export(stateStore) 264 265 return sf, nil 266 }