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:
parent
a5f063625a
commit
a4c24e3147
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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..."))
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
|
|
|
@ -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 {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module "child" {
|
||||
source = "./child"
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
terraform {
|
||||
cloud {
|
||||
foo = "bar"
|
||||
|
||||
baz {
|
||||
bar = "foo"
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue