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.
This commit is contained in:
Chris Arcand 2021-08-24 14:28:12 -05:00
parent a5f063625a
commit a4c24e3147
28 changed files with 155 additions and 35 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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() {

View File

@ -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..."))

View File

@ -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()
}
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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")

View File

@ -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()

View File

@ -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

View File

@ -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])

View File

@ -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() {

View File

@ -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() {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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) {

27
internal/configs/cloud.go Normal file
View File

@ -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,
}
}

View File

@ -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 {

View File

@ -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",
},

View File

@ -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 {
}
}

View File

@ -0,0 +1,3 @@
module "child" {
source = "./child"
}

View File

@ -0,0 +1,10 @@
terraform {
cloud {
foo = "bar"
baz {
bar = "foo"
}
}
}