package arguments import ( "flag" "fmt" "time" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/plans" "github.com/hashicorp/terraform/tfdiags" ) // DefaultParallelism is the limit Terraform places on total parallel // operations as it walks the dependency graph. const DefaultParallelism = 10 // State describes arguments which are used to define how Terraform interacts // with state. type State struct { // Lock controls whether or not the state manager is used to lock state // during operations. Lock bool // LockTimeout allows setting a time limit on acquiring the state lock. // The default is 0, meaning no limit. LockTimeout time.Duration // StatePath specifies a non-default location for the state file. The // default value is blank, which is interpeted as "terraform.tfstate". StatePath string // StateOutPath specifies a different path to write the final state file. // The default value is blank, which results in state being written back to // StatePath. StateOutPath string // BackupPath specifies the path where a backup copy of the state file will // be stored before the new state is written. The default value is blank, // which is interpreted as StateOutPath + // ".backup". BackupPath string } // Operation describes arguments which are used to configure how a Terraform // operation such as a plan or apply executes. type Operation struct { // PlanMode selects one of the mutually-exclusive planning modes that // decides the overall goal of a plan operation. This field is relevant // only for an operation that produces a plan. PlanMode plans.Mode // Parallelism is the limit Terraform places on total parallel operations // as it walks the dependency graph. Parallelism int // Refresh controls whether or not the operation should refresh existing // state before proceeding. Default is true. Refresh bool // Targets allow limiting an operation to a set of resource addresses and // their dependencies. Targets []addrs.Targetable // ForceReplace addresses cause Terraform to force a particular set of // resource instances to generate "replace" actions in any plan where they // would normally have generated "no-op" or "update" actions. // // This is currently limited to specific instances because typical uses // of replace are associated with only specific remote objects that the // user has somehow learned to be malfunctioning, in which case it // would be unusual and potentially dangerous to replace everything under // a module all at once. We could potentially loosen this later if we // learn a use-case for broader matching. ForceReplace []addrs.AbsResourceInstance // These private fields are used only temporarily during decoding. Use // method Parse to populate the exported fields from these, validating // the raw values in the process. targetsRaw []string forceReplaceRaw []string destroyRaw bool refreshOnlyRaw bool } // Parse must be called on Operation after initial flag parse. This processes // the raw target flags into addrs.Targetable values, returning diagnostics if // invalid. func (o *Operation) Parse() tfdiags.Diagnostics { var diags tfdiags.Diagnostics o.Targets = nil for _, tr := range o.targetsRaw { traversal, syntaxDiags := hclsyntax.ParseTraversalAbs([]byte(tr), "", hcl.Pos{Line: 1, Column: 1}) if syntaxDiags.HasErrors() { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, fmt.Sprintf("Invalid target %q", tr), syntaxDiags[0].Detail, )) continue } target, targetDiags := addrs.ParseTarget(traversal) if targetDiags.HasErrors() { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, fmt.Sprintf("Invalid target %q", tr), targetDiags[0].Description().Detail, )) continue } o.Targets = append(o.Targets, target.Subject) } for _, raw := range o.forceReplaceRaw { traversal, syntaxDiags := hclsyntax.ParseTraversalAbs([]byte(raw), "", hcl.Pos{Line: 1, Column: 1}) if syntaxDiags.HasErrors() { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, fmt.Sprintf("Invalid force-replace address %q", raw), syntaxDiags[0].Detail, )) continue } addr, addrDiags := addrs.ParseAbsResourceInstance(traversal) if addrDiags.HasErrors() { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, fmt.Sprintf("Invalid force-replace address %q", raw), addrDiags[0].Description().Detail, )) continue } if addr.Resource.Resource.Mode != addrs.ManagedResourceMode { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, fmt.Sprintf("Invalid force-replace address %q", raw), "Only managed resources can be used with the -replace=... option.", )) continue } o.ForceReplace = append(o.ForceReplace, addr) } // If you add a new possible value for o.PlanMode here, consider also // adding a specialized error message for it in ParseApplyDestroy. switch { case o.destroyRaw && o.refreshOnlyRaw: diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Incompatible plan mode options", "The -destroy and -refresh-only options are mutually-exclusive.", )) case o.destroyRaw: o.PlanMode = plans.DestroyMode case o.refreshOnlyRaw: o.PlanMode = plans.RefreshOnlyMode if !o.Refresh { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Incompatible refresh options", "It doesn't make sense to use -refresh-only at the same time as -refresh=false, because Terraform would have nothing to do.", )) } default: o.PlanMode = plans.NormalMode } return diags } // Vars describes arguments which specify non-default variable values. This // interfce is unfortunately obscure, because the order of the CLI arguments // determines the final value of the gathered variables. In future it might be // desirable for the arguments package to handle the gathering of variables // directly, returning a map of variable values. type Vars struct { vars *flagNameValueSlice varFiles *flagNameValueSlice } func (v *Vars) All() []FlagNameValue { if v.vars == nil { return nil } return v.vars.AllItems() } func (v *Vars) Empty() bool { if v.vars == nil { return true } return v.vars.Empty() } // extendedFlagSet creates a FlagSet with common backend, operation, and vars // flags used in many commands. Target structs for each subset of flags must be // provided in order to support those flags. func extendedFlagSet(name string, state *State, operation *Operation, vars *Vars) *flag.FlagSet { f := defaultFlagSet(name) if state == nil && operation == nil && vars == nil { panic("use defaultFlagSet") } if state != nil { f.BoolVar(&state.Lock, "lock", true, "lock") f.DurationVar(&state.LockTimeout, "lock-timeout", 0, "lock-timeout") f.StringVar(&state.StatePath, "state", "", "state-path") f.StringVar(&state.StateOutPath, "state-out", "", "state-path") f.StringVar(&state.BackupPath, "backup", "", "backup-path") } if operation != nil { f.IntVar(&operation.Parallelism, "parallelism", DefaultParallelism, "parallelism") f.BoolVar(&operation.Refresh, "refresh", true, "refresh") f.BoolVar(&operation.destroyRaw, "destroy", false, "destroy") f.BoolVar(&operation.refreshOnlyRaw, "refresh-only", false, "refresh-only") f.Var((*flagStringSlice)(&operation.targetsRaw), "target", "target") f.Var((*flagStringSlice)(&operation.forceReplaceRaw), "replace", "replace") } // Gather all -var and -var-file arguments into one heterogenous structure // to preserve the overall order. if vars != nil { varsFlags := newFlagNameValueSlice("-var") varFilesFlags := varsFlags.Alias("-var-file") vars.vars = &varsFlags vars.varFiles = &varFilesFlags f.Var(vars.vars, "var", "var") f.Var(vars.varFiles, "var-file", "var-file") } return f }