From a4c24e314719a08f859d309f37ea35c7f95c075f Mon Sep 17 00:00:00 2001 From: Chris Arcand Date: Tue, 24 Aug 2021 14:28:12 -0500 Subject: [PATCH] Add cloud {} configuration block for Terraform Cloud This is a replacement declaration for using Terraform Cloud as a remote backend, leaving the literal backend as an implementation detail and not a user-level concept. --- internal/command/console.go | 2 +- internal/command/graph.go | 2 +- internal/command/import.go | 2 +- internal/command/init.go | 33 +++++++++++++++++-- internal/command/meta_backend.go | 26 +++++++++------ internal/command/meta_backend_migrate.go | 8 ++--- internal/command/meta_config.go | 6 ++++ internal/command/output.go | 2 +- internal/command/providers.go | 2 +- internal/command/providers_schema.go | 2 +- internal/command/show.go | 2 +- internal/command/state_list.go | 2 +- internal/command/state_meta.go | 2 +- internal/command/state_pull.go | 2 +- internal/command/state_push.go | 2 +- internal/command/state_show.go | 2 +- internal/command/taint.go | 2 +- internal/command/untaint.go | 2 +- internal/command/workspace_delete.go | 2 +- internal/command/workspace_list.go | 2 +- internal/command/workspace_new.go | 2 +- internal/command/workspace_select.go | 2 +- internal/configs/cloud.go | 27 +++++++++++++++ internal/configs/module.go | 25 ++++++++++++++ internal/configs/parser_config.go | 10 ++++++ .../nested-cloud-warning/child/child.tf | 6 ++++ .../testdata/nested-cloud-warning/root.tf | 3 ++ .../configs/testdata/valid-files/cloud.tf | 10 ++++++ 28 files changed, 155 insertions(+), 35 deletions(-) create mode 100644 internal/configs/cloud.go create mode 100644 internal/configs/testdata/nested-cloud-warning/child/child.tf create mode 100644 internal/configs/testdata/nested-cloud-warning/root.tf create mode 100644 internal/configs/testdata/valid-files/cloud.tf diff --git a/internal/command/console.go b/internal/command/console.go index 319598868..a29007128 100644 --- a/internal/command/console.go +++ b/internal/command/console.go @@ -72,7 +72,7 @@ func (c *ConsoleCommand) Run(args []string) int { } // This is a read-only command - c.ignoreRemoteBackendVersionConflict(b) + c.ignoreRemoteVersionConflict(b) // Build the operation opReq := c.Operation(b) diff --git a/internal/command/graph.go b/internal/command/graph.go index 87880a855..4fe742804 100644 --- a/internal/command/graph.go +++ b/internal/command/graph.go @@ -88,7 +88,7 @@ func (c *GraphCommand) Run(args []string) int { } // This is a read-only command - c.ignoreRemoteBackendVersionConflict(b) + c.ignoreRemoteVersionConflict(b) // Build the operation opReq := c.Operation(b) diff --git a/internal/command/import.go b/internal/command/import.go index 7fc61a2f0..a576a29e4 100644 --- a/internal/command/import.go +++ b/internal/command/import.go @@ -204,7 +204,7 @@ func (c *ImportCommand) Run(args []string) int { opReq.View = views.NewOperation(arguments.ViewHuman, c.RunningInAutomation, c.View) // Check remote Terraform version is compatible - remoteVersionDiags := c.remoteBackendVersionCheck(b, opReq.Workspace) + remoteVersionDiags := c.remoteVersionCheck(b, opReq.Workspace) diags = diags.Append(remoteVersionDiags) c.showDiagnostics(diags) if diags.HasErrors() { diff --git a/internal/command/init.go b/internal/command/init.go index 8f8c5b829..8591f0c53 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -209,8 +209,20 @@ func (c *InitCommand) Run(args []string) int { } var back backend.Backend - if flagBackend { + switch { + case config.Module.CloudConfig != nil: + be, backendOutput, backendDiags := c.initCloud(config.Module) + diags = diags.Append(backendDiags) + if backendDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + if backendOutput { + header = true + } + back = be + case flagBackend: be, backendOutput, backendDiags := c.initBackend(config.Module, flagConfigExtra) diags = diags.Append(backendDiags) if backendDiags.HasErrors() { @@ -221,7 +233,7 @@ func (c *InitCommand) Run(args []string) int { header = true } back = be - } else { + default: // load the previously-stored backend config be, backendDiags := c.Meta.backendFromState() diags = diags.Append(backendDiags) @@ -251,7 +263,7 @@ func (c *InitCommand) Run(args []string) int { // on a previous run) we'll use the current state as a potential source // of provider dependencies. if back != nil { - c.ignoreRemoteBackendVersionConflict(back) + c.ignoreRemoteVersionConflict(back) workspace, err := c.Workspace() if err != nil { c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err)) @@ -337,6 +349,21 @@ func (c *InitCommand) getModules(path string, earlyRoot *tfconfig.Module, upgrad return true, diags } +func (c *InitCommand) initCloud(root *configs.Module) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { + c.Ui.Output(c.Colorize().Color("\n[reset][bold]Initializing Terraform Cloud...")) + + backendConfig := root.CloudConfig.ToBackendConfig() + + opts := &BackendOpts{ + Config: &backendConfig, + Init: true, + } + + back, backDiags := c.Backend(opts) + diags = diags.Append(backDiags) + return back, true, diags +} + func (c *InitCommand) initBackend(root *configs.Module, extraConfig rawFlags) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { c.Ui.Output(c.Colorize().Color("\n[reset][bold]Initializing the backend...")) diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index 042e7cfff..58cafdd4f 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -17,7 +17,6 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hcldec" "github.com/hashicorp/terraform/internal/backend" - remoteBackend "github.com/hashicorp/terraform/internal/backend/remote" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" @@ -55,6 +54,13 @@ type BackendOpts struct { ForceLocal bool } +// BackendWithRemoteTerraformVersion is a shared interface between the 'remote' and 'cloud' backends +// for simplified type checking when calling functions common to those particular backends. +type BackendWithRemoteTerraformVersion interface { + IgnoreVersionConflict() + VerifyWorkspaceTerraformVersion(workspace string) tfdiags.Diagnostics +} + // Backend initializes and returns the backend for this CLI session. // // The backend is used to perform the actual Terraform operations. This @@ -1168,32 +1174,32 @@ func (m *Meta) backendInitFromConfig(c *configs.Backend) (backend.Backend, cty.V return b, configVal, diags } -// Helper method to ignore remote backend version conflicts. Only call this +// Helper method to ignore remote/cloud backend version conflicts. Only call this // for commands which cannot accidentally upgrade remote state files. -func (m *Meta) ignoreRemoteBackendVersionConflict(b backend.Backend) { - if rb, ok := b.(*remoteBackend.Remote); ok { - rb.IgnoreVersionConflict() +func (m *Meta) ignoreRemoteVersionConflict(b backend.Backend) { + if back, ok := b.(BackendWithRemoteTerraformVersion); ok { + back.IgnoreVersionConflict() } } // Helper method to check the local Terraform version against the configured // version in the remote workspace, returning diagnostics if they conflict. -func (m *Meta) remoteBackendVersionCheck(b backend.Backend, workspace string) tfdiags.Diagnostics { +func (m *Meta) remoteVersionCheck(b backend.Backend, workspace string) tfdiags.Diagnostics { var diags tfdiags.Diagnostics - if rb, ok := b.(*remoteBackend.Remote); ok { + if back, ok := b.(BackendWithRemoteTerraformVersion); ok { // Allow user override based on command-line flag if m.ignoreRemoteVersion { - rb.IgnoreVersionConflict() + back.IgnoreVersionConflict() } // If the override is set, this check will return a warning instead of // an error - versionDiags := rb.VerifyWorkspaceTerraformVersion(workspace) + versionDiags := back.VerifyWorkspaceTerraformVersion(workspace) diags = diags.Append(versionDiags) // If there are no errors resulting from this check, we do not need to // check again if !diags.HasErrors() { - rb.IgnoreVersionConflict() + back.IgnoreVersionConflict() } } diff --git a/internal/command/meta_backend_migrate.go b/internal/command/meta_backend_migrate.go index 15206bf8a..e998b0ffd 100644 --- a/internal/command/meta_backend_migrate.go +++ b/internal/command/meta_backend_migrate.go @@ -75,17 +75,17 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error { // Disregard remote Terraform version for the state source backend. If it's a // Terraform Cloud remote backend, we don't care about the remote version, // as we are migrating away and will not break a remote workspace. - m.ignoreRemoteBackendVersionConflict(opts.Source) + m.ignoreRemoteVersionConflict(opts.Source) // Disregard remote Terraform version if instructed to do so via CLI flag. if m.ignoreRemoteVersion { - m.ignoreRemoteBackendVersionConflict(opts.Destination) + m.ignoreRemoteVersionConflict(opts.Destination) } else { // Check the remote Terraform version for the state destination backend. If // it's a Terraform Cloud remote backend, we want to ensure that we don't // break the workspace by uploading an incompatible state file. for _, workspace := range destinationWorkspaces { - diags := m.remoteBackendVersionCheck(opts.Destination, workspace) + diags := m.remoteVersionCheck(opts.Destination, workspace) if diags.HasErrors() { return diags.Err() } @@ -93,7 +93,7 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error { // If there are no specified destination workspaces, perform a remote // backend version check with the default workspace. if len(destinationWorkspaces) == 0 { - diags := m.remoteBackendVersionCheck(opts.Destination, backend.DefaultStateName) + diags := m.remoteVersionCheck(opts.Destination, backend.DefaultStateName) if diags.HasErrors() { return diags.Err() } diff --git a/internal/command/meta_config.go b/internal/command/meta_config.go index 439df6b91..6913db594 100644 --- a/internal/command/meta_config.go +++ b/internal/command/meta_config.go @@ -136,6 +136,12 @@ func (m *Meta) loadBackendConfig(rootDir string) (*configs.Backend, tfdiags.Diag if diags.HasErrors() { return nil, diags } + + if mod.CloudConfig != nil { + backendConfig := mod.CloudConfig.ToBackendConfig() + return &backendConfig, nil + } + return mod.Backend, nil } diff --git a/internal/command/output.go b/internal/command/output.go index 3594fe33d..0f23a6109 100644 --- a/internal/command/output.go +++ b/internal/command/output.go @@ -67,7 +67,7 @@ func (c *OutputCommand) Outputs(statePath string) (map[string]*states.OutputValu } // This is a read-only command - c.ignoreRemoteBackendVersionConflict(b) + c.ignoreRemoteVersionConflict(b) env, err := c.Workspace() if err != nil { diff --git a/internal/command/providers.go b/internal/command/providers.go index 31fd79594..5bc0d4e6c 100644 --- a/internal/command/providers.go +++ b/internal/command/providers.go @@ -83,7 +83,7 @@ func (c *ProvidersCommand) Run(args []string) int { } // This is a read-only command - c.ignoreRemoteBackendVersionConflict(b) + c.ignoreRemoteVersionConflict(b) // Get the state env, err := c.Workspace() diff --git a/internal/command/providers_schema.go b/internal/command/providers_schema.go index 372564f12..b4d61ec76 100644 --- a/internal/command/providers_schema.go +++ b/internal/command/providers_schema.go @@ -68,7 +68,7 @@ func (c *ProvidersSchemaCommand) Run(args []string) int { } // This is a read-only command - c.ignoreRemoteBackendVersionConflict(b) + c.ignoreRemoteVersionConflict(b) // we expect that the config dir is the cwd cwd, err := os.Getwd() diff --git a/internal/command/show.go b/internal/command/show.go index 9886768ca..6ae66beeb 100644 --- a/internal/command/show.go +++ b/internal/command/show.go @@ -70,7 +70,7 @@ func (c *ShowCommand) Run(args []string) int { } // This is a read-only command - c.ignoreRemoteBackendVersionConflict(b) + c.ignoreRemoteVersionConflict(b) // the show command expects the config dir to always be the cwd cwd, err := os.Getwd() diff --git a/internal/command/state_list.go b/internal/command/state_list.go index ebd318bc8..54358b28d 100644 --- a/internal/command/state_list.go +++ b/internal/command/state_list.go @@ -41,7 +41,7 @@ func (c *StateListCommand) Run(args []string) int { } // This is a read-only command - c.ignoreRemoteBackendVersionConflict(b) + c.ignoreRemoteVersionConflict(b) // Get the state env, err := c.Workspace() diff --git a/internal/command/state_meta.go b/internal/command/state_meta.go index fa04245a6..17959f5ff 100644 --- a/internal/command/state_meta.go +++ b/internal/command/state_meta.go @@ -43,7 +43,7 @@ func (c *StateMeta) State() (statemgr.Full, error) { } // Check remote Terraform version is compatible - remoteVersionDiags := c.remoteBackendVersionCheck(b, workspace) + remoteVersionDiags := c.remoteVersionCheck(b, workspace) c.showDiagnostics(remoteVersionDiags) if remoteVersionDiags.HasErrors() { return nil, fmt.Errorf("Error checking remote Terraform version") diff --git a/internal/command/state_pull.go b/internal/command/state_pull.go index 0616df2d4..8ce16ff57 100644 --- a/internal/command/state_pull.go +++ b/internal/command/state_pull.go @@ -31,7 +31,7 @@ func (c *StatePullCommand) Run(args []string) int { } // This is a read-only command - c.ignoreRemoteBackendVersionConflict(b) + c.ignoreRemoteVersionConflict(b) // Get the state manager for the current workspace env, err := c.Workspace() diff --git a/internal/command/state_push.go b/internal/command/state_push.go index 117611e94..eb0ea1679 100644 --- a/internal/command/state_push.go +++ b/internal/command/state_push.go @@ -80,7 +80,7 @@ func (c *StatePushCommand) Run(args []string) int { } // Check remote Terraform version is compatible - remoteVersionDiags := c.remoteBackendVersionCheck(b, workspace) + remoteVersionDiags := c.remoteVersionCheck(b, workspace) c.showDiagnostics(remoteVersionDiags) if remoteVersionDiags.HasErrors() { return 1 diff --git a/internal/command/state_show.go b/internal/command/state_show.go index e95eca70f..7ee86624d 100644 --- a/internal/command/state_show.go +++ b/internal/command/state_show.go @@ -54,7 +54,7 @@ func (c *StateShowCommand) Run(args []string) int { } // This is a read-only command - c.ignoreRemoteBackendVersionConflict(b) + c.ignoreRemoteVersionConflict(b) // Check if the address can be parsed addr, addrDiags := addrs.ParseAbsResourceInstanceStr(args[0]) diff --git a/internal/command/taint.go b/internal/command/taint.go index 46e92d6d6..f2fbbb1ef 100644 --- a/internal/command/taint.go +++ b/internal/command/taint.go @@ -102,7 +102,7 @@ func (c *TaintCommand) Run(args []string) int { } // Check remote Terraform version is compatible - remoteVersionDiags := c.remoteBackendVersionCheck(b, workspace) + remoteVersionDiags := c.remoteVersionCheck(b, workspace) diags = diags.Append(remoteVersionDiags) c.showDiagnostics(diags) if diags.HasErrors() { diff --git a/internal/command/untaint.go b/internal/command/untaint.go index 98f203560..ba290a8a4 100644 --- a/internal/command/untaint.go +++ b/internal/command/untaint.go @@ -67,7 +67,7 @@ func (c *UntaintCommand) Run(args []string) int { } // Check remote Terraform version is compatible - remoteVersionDiags := c.remoteBackendVersionCheck(b, workspace) + remoteVersionDiags := c.remoteVersionCheck(b, workspace) diags = diags.Append(remoteVersionDiags) c.showDiagnostics(diags) if diags.HasErrors() { diff --git a/internal/command/workspace_delete.go b/internal/command/workspace_delete.go index 5c826d908..654aac581 100644 --- a/internal/command/workspace_delete.go +++ b/internal/command/workspace_delete.go @@ -68,7 +68,7 @@ func (c *WorkspaceDeleteCommand) Run(args []string) int { } // This command will not write state - c.ignoreRemoteBackendVersionConflict(b) + c.ignoreRemoteVersionConflict(b) workspaces, err := b.Workspaces() if err != nil { diff --git a/internal/command/workspace_list.go b/internal/command/workspace_list.go index aac6bc97d..7b43bc346 100644 --- a/internal/command/workspace_list.go +++ b/internal/command/workspace_list.go @@ -52,7 +52,7 @@ func (c *WorkspaceListCommand) Run(args []string) int { } // This command will not write state - c.ignoreRemoteBackendVersionConflict(b) + c.ignoreRemoteVersionConflict(b) states, err := b.Workspaces() if err != nil { diff --git a/internal/command/workspace_new.go b/internal/command/workspace_new.go index 1d7b2898c..41e657bef 100644 --- a/internal/command/workspace_new.go +++ b/internal/command/workspace_new.go @@ -83,7 +83,7 @@ func (c *WorkspaceNewCommand) Run(args []string) int { } // This command will not write state - c.ignoreRemoteBackendVersionConflict(b) + c.ignoreRemoteVersionConflict(b) workspaces, err := b.Workspaces() if err != nil { diff --git a/internal/command/workspace_select.go b/internal/command/workspace_select.go index 645a9c2bc..1f98ec55e 100644 --- a/internal/command/workspace_select.go +++ b/internal/command/workspace_select.go @@ -68,7 +68,7 @@ func (c *WorkspaceSelectCommand) Run(args []string) int { } // This command will not write state - c.ignoreRemoteBackendVersionConflict(b) + c.ignoreRemoteVersionConflict(b) name := args[0] if !validWorkspaceName(name) { diff --git a/internal/configs/cloud.go b/internal/configs/cloud.go new file mode 100644 index 000000000..1ed6482e1 --- /dev/null +++ b/internal/configs/cloud.go @@ -0,0 +1,27 @@ +package configs + +import ( + "github.com/hashicorp/hcl/v2" +) + +// Cloud represents a "cloud" block inside a "terraform" block in a module +// or file. +type CloudConfig struct { + Config hcl.Body + + DeclRange hcl.Range +} + +func decodeCloudBlock(block *hcl.Block) (*CloudConfig, hcl.Diagnostics) { + return &CloudConfig{ + Config: block.Body, + DeclRange: block.DefRange, + }, nil +} + +func (c *CloudConfig) ToBackendConfig() Backend { + return Backend{ + Type: "cloud", + Config: c.Config, + } +} diff --git a/internal/configs/module.go b/internal/configs/module.go index 18676de9d..ff9867630 100644 --- a/internal/configs/module.go +++ b/internal/configs/module.go @@ -29,6 +29,7 @@ type Module struct { ActiveExperiments experiments.Set Backend *Backend + CloudConfig *CloudConfig ProviderConfigs map[string]*Provider ProviderRequirements *RequiredProviders ProviderLocalNames map[addrs.Provider]string @@ -63,6 +64,7 @@ type File struct { ActiveExperiments experiments.Set Backends []*Backend + CloudConfigs []*CloudConfig ProviderConfigs []*Provider ProviderMetas []*ProviderMeta RequiredProviders []*RequiredProviders @@ -190,6 +192,29 @@ func (m *Module) appendFile(file *File) hcl.Diagnostics { m.Backend = b } + for _, c := range file.CloudConfigs { + if m.CloudConfig != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate Terraform Cloud configurations", + Detail: fmt.Sprintf("A module may have only one 'cloud' block configuring Terraform Cloud. Terraform Cloud was previously configured at %s.", m.CloudConfig.DeclRange), + Subject: &c.DeclRange, + }) + continue + } + + m.CloudConfig = c + } + + if m.Backend != nil && m.CloudConfig != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Both a backend and Terraform Cloud configuration are present", + Detail: fmt.Sprintf("A module may declare either one 'cloud' block configuring Terraform Cloud OR one 'backend' block configuring a state backend. Terraform Cloud is configured at %s; a backend is configured at %s. Remove the backend block to configure Terraform Cloud.", m.CloudConfig.DeclRange, m.Backend.DeclRange), + Subject: &m.Backend.DeclRange, + }) + } + for _, pc := range file.ProviderConfigs { key := pc.moduleUniqueKey() if existing, exists := m.ProviderConfigs[key]; exists { diff --git a/internal/configs/parser_config.go b/internal/configs/parser_config.go index 4be14501d..caebb8911 100644 --- a/internal/configs/parser_config.go +++ b/internal/configs/parser_config.go @@ -72,6 +72,13 @@ func (p *Parser) loadConfigFile(path string, override bool) (*File, hcl.Diagnost file.Backends = append(file.Backends, backendCfg) } + case "cloud": + cloudCfg, cfgDiags := decodeCloudBlock(innerBlock) + diags = append(diags, cfgDiags...) + if cloudCfg != nil { + file.CloudConfigs = append(file.CloudConfigs, cloudCfg) + } + case "required_providers": reqs, reqsDiags := decodeRequiredProvidersBlock(innerBlock) diags = append(diags, reqsDiags...) @@ -261,6 +268,9 @@ var terraformBlockSchema = &hcl.BodySchema{ Type: "backend", LabelNames: []string{"type"}, }, + { + Type: "cloud", + }, { Type: "required_providers", }, diff --git a/internal/configs/testdata/nested-cloud-warning/child/child.tf b/internal/configs/testdata/nested-cloud-warning/child/child.tf new file mode 100644 index 000000000..540b92170 --- /dev/null +++ b/internal/configs/testdata/nested-cloud-warning/child/child.tf @@ -0,0 +1,6 @@ +terraform { + # Only the root module can declare a Cloud configuration. Terraform should emit a warning + # about this child module Cloud declaration. + cloud { + } +} diff --git a/internal/configs/testdata/nested-cloud-warning/root.tf b/internal/configs/testdata/nested-cloud-warning/root.tf new file mode 100644 index 000000000..1f95749fa --- /dev/null +++ b/internal/configs/testdata/nested-cloud-warning/root.tf @@ -0,0 +1,3 @@ +module "child" { + source = "./child" +} diff --git a/internal/configs/testdata/valid-files/cloud.tf b/internal/configs/testdata/valid-files/cloud.tf new file mode 100644 index 000000000..91985fcad --- /dev/null +++ b/internal/configs/testdata/valid-files/cloud.tf @@ -0,0 +1,10 @@ + +terraform { + cloud { + foo = "bar" + + baz { + bar = "foo" + } + } +}