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