kubeform.dev/terraform-backend-sdk@v0.0.0-20220310143633-45f07fe731c5/command/show.go (about) 1 package command 2 3 import ( 4 "fmt" 5 "os" 6 "strings" 7 8 "kubeform.dev/terraform-backend-sdk/backend" 9 "kubeform.dev/terraform-backend-sdk/command/arguments" 10 "kubeform.dev/terraform-backend-sdk/command/format" 11 "kubeform.dev/terraform-backend-sdk/command/jsonplan" 12 "kubeform.dev/terraform-backend-sdk/command/jsonstate" 13 "kubeform.dev/terraform-backend-sdk/command/views" 14 "kubeform.dev/terraform-backend-sdk/plans" 15 "kubeform.dev/terraform-backend-sdk/plans/planfile" 16 "kubeform.dev/terraform-backend-sdk/states/statefile" 17 "kubeform.dev/terraform-backend-sdk/states/statemgr" 18 "kubeform.dev/terraform-backend-sdk/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.ignoreRemoteBackendVersionConflict(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 if err != nil { 98 diags = diags.Append(err) 99 c.showDiagnostics(diags) 100 return 1 101 } 102 103 // Get the context 104 lr, _, ctxDiags := local.LocalRun(opReq) 105 diags = diags.Append(ctxDiags) 106 if ctxDiags.HasErrors() { 107 c.showDiagnostics(diags) 108 return 1 109 } 110 111 // Get the schemas from the context 112 schemas, moreDiags := lr.Core.Schemas(lr.Config, lr.InputState) 113 diags = diags.Append(moreDiags) 114 if moreDiags.HasErrors() { 115 c.showDiagnostics(diags) 116 return 1 117 } 118 119 var planErr, stateErr error 120 var plan *plans.Plan 121 var stateFile *statefile.File 122 123 // if a path was provided, try to read it as a path to a planfile 124 // if that fails, try to read the cli argument as a path to a statefile 125 if len(args) > 0 { 126 path := args[0] 127 plan, stateFile, planErr = getPlanFromPath(path) 128 if planErr != nil { 129 stateFile, stateErr = getStateFromPath(path) 130 if stateErr != nil { 131 c.Ui.Error(fmt.Sprintf( 132 "Terraform couldn't read the given file as a state or plan file.\n"+ 133 "The errors while attempting to read the file as each format are\n"+ 134 "shown below.\n\n"+ 135 "State read error: %s\n\nPlan read error: %s", 136 stateErr, 137 planErr)) 138 return 1 139 } 140 } 141 } else { 142 env, err := c.Workspace() 143 if err != nil { 144 c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err)) 145 return 1 146 } 147 stateFile, stateErr = getStateFromEnv(b, env) 148 if stateErr != nil { 149 c.Ui.Error(stateErr.Error()) 150 return 1 151 } 152 } 153 154 if plan != nil { 155 if jsonOutput { 156 config := lr.Config 157 jsonPlan, err := jsonplan.Marshal(config, plan, stateFile, schemas) 158 159 if err != nil { 160 c.Ui.Error(fmt.Sprintf("Failed to marshal plan to json: %s", err)) 161 return 1 162 } 163 c.Ui.Output(string(jsonPlan)) 164 return 0 165 } 166 167 view := views.NewShow(arguments.ViewHuman, c.View) 168 view.Plan(plan, schemas) 169 return 0 170 } 171 172 if jsonOutput { 173 // At this point, it is possible that there is neither state nor a plan. 174 // That's ok, we'll just return an empty object. 175 jsonState, err := jsonstate.Marshal(stateFile, schemas) 176 if err != nil { 177 c.Ui.Error(fmt.Sprintf("Failed to marshal state to json: %s", err)) 178 return 1 179 } 180 c.Ui.Output(string(jsonState)) 181 } else { 182 if stateFile == nil { 183 c.Ui.Output("No state.") 184 return 0 185 } 186 c.Ui.Output(format.State(&format.StateOpts{ 187 State: stateFile.State, 188 Color: c.Colorize(), 189 Schemas: schemas, 190 })) 191 } 192 193 return 0 194 } 195 196 func (c *ShowCommand) Help() string { 197 helpText := ` 198 Usage: terraform [global options] show [options] [path] 199 200 Reads and outputs a Terraform state or plan file in a human-readable 201 form. If no path is specified, the current state will be shown. 202 203 Options: 204 205 -no-color If specified, output won't contain any color. 206 -json If specified, output the Terraform plan or state in 207 a machine-readable form. 208 209 ` 210 return strings.TrimSpace(helpText) 211 } 212 213 func (c *ShowCommand) Synopsis() string { 214 return "Show the current state or a saved plan" 215 } 216 217 // getPlanFromPath returns a plan and statefile if the user-supplied path points 218 // to a planfile. If both plan and error are nil, the path is likely a 219 // directory. An error could suggest that the given path points to a statefile. 220 func getPlanFromPath(path string) (*plans.Plan, *statefile.File, error) { 221 pr, err := planfile.Open(path) 222 if err != nil { 223 return nil, nil, err 224 } 225 plan, err := pr.ReadPlan() 226 if err != nil { 227 return nil, nil, err 228 } 229 230 stateFile, err := pr.ReadStateFile() 231 return plan, stateFile, err 232 } 233 234 // getStateFromPath returns a statefile if the user-supplied path points to a statefile. 235 func getStateFromPath(path string) (*statefile.File, error) { 236 f, err := os.Open(path) 237 if err != nil { 238 return nil, fmt.Errorf("Error loading statefile: %s", err) 239 } 240 defer f.Close() 241 242 var stateFile *statefile.File 243 stateFile, err = statefile.Read(f) 244 if err != nil { 245 return nil, fmt.Errorf("Error reading %s as a statefile: %s", path, err) 246 } 247 return stateFile, nil 248 } 249 250 // getStateFromEnv returns the State for the current workspace, if available. 251 func getStateFromEnv(b backend.Backend, env string) (*statefile.File, error) { 252 // Get the state 253 stateStore, err := b.StateMgr(env) 254 if err != nil { 255 return nil, fmt.Errorf("Failed to load state manager: %s", err) 256 } 257 258 if err := stateStore.RefreshState(); err != nil { 259 return nil, fmt.Errorf("Failed to load state: %s", err) 260 } 261 262 sf := statemgr.Export(stateStore) 263 264 return sf, nil 265 }