github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/command/plan.go (about) 1 package command 2 3 import ( 4 "fmt" 5 "strings" 6 7 "github.com/hashicorp/terraform/backend" 8 "github.com/hashicorp/terraform/configs" 9 "github.com/hashicorp/terraform/plans" 10 "github.com/hashicorp/terraform/tfdiags" 11 ) 12 13 // PlanCommand is a Command implementation that compares a Terraform 14 // configuration to an actual infrastructure and shows the differences. 15 type PlanCommand struct { 16 Meta 17 } 18 19 func (c *PlanCommand) Run(args []string) int { 20 var destroy, refresh, detailed bool 21 var outPath string 22 23 args, err := c.Meta.process(args, true) 24 if err != nil { 25 return 1 26 } 27 28 cmdFlags := c.Meta.extendedFlagSet("plan") 29 cmdFlags.BoolVar(&destroy, "destroy", false, "destroy") 30 cmdFlags.BoolVar(&refresh, "refresh", true, "refresh") 31 cmdFlags.StringVar(&outPath, "out", "", "path") 32 cmdFlags.IntVar(&c.Meta.parallelism, "parallelism", DefaultParallelism, "parallelism") 33 cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path") 34 cmdFlags.BoolVar(&detailed, "detailed-exitcode", false, "detailed-exitcode") 35 cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state") 36 cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout") 37 cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } 38 if err := cmdFlags.Parse(args); err != nil { 39 return 1 40 } 41 42 configPath, err := ModulePath(cmdFlags.Args()) 43 if err != nil { 44 c.Ui.Error(err.Error()) 45 return 1 46 } 47 48 // Check for user-supplied plugin path 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 // Check if the path is a plan, which is not permitted 55 planFileReader, err := c.PlanFile(configPath) 56 if err != nil { 57 c.Ui.Error(err.Error()) 58 return 1 59 } 60 if planFileReader != nil { 61 c.showDiagnostics(tfdiags.Sourceless( 62 tfdiags.Error, 63 "Invalid configuration directory", 64 fmt.Sprintf("Cannot pass a saved plan file to the 'terraform plan' command. To apply a saved plan, use: terraform apply %s", configPath), 65 )) 66 return 1 67 } 68 69 var diags tfdiags.Diagnostics 70 71 var backendConfig *configs.Backend 72 var configDiags tfdiags.Diagnostics 73 backendConfig, configDiags = c.loadBackendConfig(configPath) 74 diags = diags.Append(configDiags) 75 if configDiags.HasErrors() { 76 c.showDiagnostics(diags) 77 return 1 78 } 79 80 // Load the backend 81 b, backendDiags := c.Backend(&BackendOpts{ 82 Config: backendConfig, 83 }) 84 diags = diags.Append(backendDiags) 85 if backendDiags.HasErrors() { 86 c.showDiagnostics(diags) 87 return 1 88 } 89 90 // Emit any diagnostics we've accumulated before we delegate to the 91 // backend, since the backend will handle its own diagnostics internally. 92 c.showDiagnostics(diags) 93 diags = nil 94 95 // Build the operation 96 opReq := c.Operation(b) 97 opReq.ConfigDir = configPath 98 opReq.Destroy = destroy 99 opReq.PlanOutPath = outPath 100 opReq.PlanRefresh = refresh 101 opReq.Type = backend.OperationTypePlan 102 103 opReq.ConfigLoader, err = c.initConfigLoader() 104 if err != nil { 105 c.showDiagnostics(err) 106 return 1 107 } 108 109 { 110 var moreDiags tfdiags.Diagnostics 111 opReq.Variables, moreDiags = c.collectVariableValues() 112 diags = diags.Append(moreDiags) 113 if moreDiags.HasErrors() { 114 c.showDiagnostics(diags) 115 return 1 116 } 117 } 118 119 // c.Backend above has a non-obvious side-effect of also populating 120 // c.backendState, which is the state-shaped formulation of the effective 121 // backend configuration after evaluation of the backend configuration. 122 // We will in turn adapt that to a plans.Backend to include in a plan file 123 // if opReq.PlanOutPath was set to a non-empty value above. 124 // 125 // FIXME: It's ugly to be doing this inline here, but it's also not really 126 // clear where would be better to do it. In future we should find a better 127 // home for this logic, and ideally also stop depending on the side-effect 128 // of c.Backend setting c.backendState. 129 { 130 // This is not actually a state in the usual sense, but rather a 131 // representation of part of the current working directory's 132 // "configuration state". 133 backendPseudoState := c.backendState 134 if backendPseudoState == nil { 135 // Should never happen if c.Backend is behaving properly. 136 diags = diags.Append(fmt.Errorf("Backend initialization didn't produce resolved configuration (This is a bug in Terraform)")) 137 c.showDiagnostics(diags) 138 return 1 139 } 140 var backendForPlan plans.Backend 141 backendForPlan.Type = backendPseudoState.Type 142 backendForPlan.Workspace = c.Workspace() 143 144 // Configuration is a little more awkward to handle here because it's 145 // stored in state as raw JSON but we need it as a plans.DynamicValue 146 // to save it in the state. To do that conversion we need to know the 147 // configuration schema of the backend. 148 configSchema := b.ConfigSchema() 149 config, err := backendPseudoState.Config(configSchema) 150 if err != nil { 151 // This means that the stored settings don't conform to the current 152 // schema, which could either be because we're reading something 153 // created by an older version that is no longer compatible, or 154 // because the user manually tampered with the stored config. 155 diags = diags.Append(tfdiags.Sourceless( 156 tfdiags.Error, 157 "Invalid backend initialization", 158 fmt.Sprintf("The backend configuration for this working directory is not valid: %s.\n\nIf you have recently upgraded Terraform, you may need to re-run \"terraform init\" to re-initialize this working directory.", err), 159 )) 160 c.showDiagnostics(diags) 161 return 1 162 } 163 configForPlan, err := plans.NewDynamicValue(config, configSchema.ImpliedType()) 164 if err != nil { 165 // This should never happen, since we've just decoded this value 166 // using the same schema. 167 diags = diags.Append(fmt.Errorf("Failed to encode backend configuration to store in plan: %s", err)) 168 c.showDiagnostics(diags) 169 return 1 170 } 171 backendForPlan.Config = configForPlan 172 } 173 174 // Perform the operation 175 op, err := c.RunOperation(b, opReq) 176 if err != nil { 177 c.showDiagnostics(err) 178 return 1 179 } 180 181 if op.Result != backend.OperationSuccess { 182 return op.Result.ExitStatus() 183 } 184 if detailed && !op.PlanEmpty { 185 return 2 186 } 187 188 return op.Result.ExitStatus() 189 } 190 191 func (c *PlanCommand) Help() string { 192 helpText := ` 193 Usage: terraform plan [options] [DIR] 194 195 Generates an execution plan for Terraform. 196 197 This execution plan can be reviewed prior to running apply to get a 198 sense for what Terraform will do. Optionally, the plan can be saved to 199 a Terraform plan file, and apply can take this plan file to execute 200 this plan exactly. 201 202 Options: 203 204 -compact-warnings If Terraform produces any warnings that are not 205 accompanied by errors, show them in a more compact form 206 that includes only the summary messages. 207 208 -destroy If set, a plan will be generated to destroy all resources 209 managed by the given configuration and state. 210 211 -detailed-exitcode Return detailed exit codes when the command exits. This 212 will change the meaning of exit codes to: 213 0 - Succeeded, diff is empty (no changes) 214 1 - Errored 215 2 - Succeeded, there is a diff 216 217 -input=true Ask for input for variables if not directly set. 218 219 -lock=true Lock the state file when locking is supported. 220 221 -lock-timeout=0s Duration to retry a state lock. 222 223 -no-color If specified, output won't contain any color. 224 225 -out=path Write a plan file to the given path. This can be used as 226 input to the "apply" command. 227 228 -parallelism=n Limit the number of concurrent operations. Defaults to 10. 229 230 -refresh=true Update state prior to checking for differences. 231 232 -state=statefile Path to a Terraform state file to use to look 233 up Terraform-managed resources. By default it will 234 use the state "terraform.tfstate" if it exists. 235 236 -target=resource Resource to target. Operation will be limited to this 237 resource and its dependencies. This flag can be used 238 multiple times. 239 240 -var 'foo=bar' Set a variable in the Terraform configuration. This 241 flag can be set multiple times. 242 243 -var-file=foo Set variables in the Terraform configuration from 244 a file. If "terraform.tfvars" or any ".auto.tfvars" 245 files are present, they will be automatically loaded. 246 ` 247 return strings.TrimSpace(helpText) 248 } 249 250 func (c *PlanCommand) Synopsis() string { 251 return "Generate and show an execution plan" 252 }