opentofu/backend/atlas/backend.go
James Bardin c814f2da37 Change backend.ValidateConfig to PrepareConfig
This mirrors the change made for providers, so that default values can
be inserted into the config by the backend implementation. This is only
the interface and method name changes, it does not yet add any default
values.
2019-02-25 18:37:20 -05:00

195 lines
5.7 KiB
Go

package atlas
import (
"fmt"
"net/url"
"os"
"strings"
"sync"
"github.com/hashicorp/terraform/tfdiags"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
"github.com/mitchellh/colorstring"
)
const EnvVarToken = "ATLAS_TOKEN"
const EnvVarAddress = "ATLAS_ADDRESS"
// Backend is an implementation of EnhancedBackend that performs all operations
// in Atlas. State must currently also be stored in Atlas, although it is worth
// investigating in the future if state storage can be external as well.
type Backend struct {
// CLI and Colorize control the CLI output. If CLI is nil then no CLI
// output will be done. If CLIColor is nil then no coloring will be done.
CLI cli.Ui
CLIColor *colorstring.Colorize
// ContextOpts are the base context options to set when initializing a
// Terraform context. Many of these will be overridden or merged by
// Operation. See Operation for more details.
ContextOpts *terraform.ContextOpts
//---------------------------------------------------------------
// Internal fields, do not set
//---------------------------------------------------------------
// stateClient is the legacy state client, setup in Configure
stateClient *stateClient
// opLock locks operations
opLock sync.Mutex
}
var _ backend.Backend = (*Backend)(nil)
// New returns a new initialized Atlas backend.
func New() *Backend {
return &Backend{}
}
func (b *Backend) ConfigSchema() *configschema.Block {
return &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"name": {
Type: cty.String,
Required: true,
Description: "Full name of the environment in Terraform Enterprise, such as 'myorg/myenv'",
},
"access_token": {
Type: cty.String,
Optional: true,
Description: "Access token to use to access Terraform Enterprise; the ATLAS_TOKEN environment variable is used if this argument is not set",
},
"address": {
Type: cty.String,
Optional: true,
Description: "Base URL for your Terraform Enterprise installation; the ATLAS_ADDRESS environment variable is used if this argument is not set, finally falling back to a default of 'https://atlas.hashicorp.com/' if neither are set.",
},
},
}
}
func (b *Backend) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
name := obj.GetAttr("name").AsString()
if ct := strings.Count(name, "/"); ct != 1 {
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid workspace selector",
`The "name" argument must be an organization name and a workspace name separated by a slash, such as "acme/network-production".`,
cty.Path{cty.GetAttrStep{Name: "name"}},
))
}
if v := obj.GetAttr("address"); !v.IsNull() {
addr := v.AsString()
_, err := url.Parse(addr)
if err != nil {
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid Terraform Enterprise URL",
fmt.Sprintf(`The "address" argument must be a valid URL: %s.`, err),
cty.Path{cty.GetAttrStep{Name: "address"}},
))
}
}
return obj, diags
}
func (b *Backend) Configure(obj cty.Value) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
client := &stateClient{
// This is optionally set during Atlas Terraform runs.
RunId: os.Getenv("ATLAS_RUN_ID"),
}
name := obj.GetAttr("name").AsString() // assumed valid due to PrepareConfig method
slashIdx := strings.Index(name, "/")
client.User = name[:slashIdx]
client.Name = name[slashIdx+1:]
if v := obj.GetAttr("access_token"); !v.IsNull() {
client.AccessToken = v.AsString()
} else {
client.AccessToken = os.Getenv(EnvVarToken)
if client.AccessToken == "" {
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Missing Terraform Enterprise access token",
`The "access_token" argument must be set unless the ATLAS_TOKEN environment variable is set to provide the authentication token for Terraform Enterprise.`,
cty.Path{cty.GetAttrStep{Name: "access_token"}},
))
}
}
if v := obj.GetAttr("address"); !v.IsNull() {
addr := v.AsString()
addrURL, err := url.Parse(addr)
if err != nil {
// We already validated the URL in PrepareConfig, so this shouldn't happen
panic(err)
}
client.Server = addr
client.ServerURL = addrURL
} else {
addr := os.Getenv(EnvVarAddress)
if addr == "" {
addr = defaultAtlasServer
}
addrURL, err := url.Parse(addr)
if err != nil {
diags = diags.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid Terraform Enterprise URL",
fmt.Sprintf(`The ATLAS_ADDRESS environment variable must contain a valid URL: %s.`, err),
cty.Path{cty.GetAttrStep{Name: "address"}},
))
}
client.Server = addr
client.ServerURL = addrURL
}
b.stateClient = client
return diags
}
func (b *Backend) Workspaces() ([]string, error) {
return nil, backend.ErrWorkspacesNotSupported
}
func (b *Backend) DeleteWorkspace(name string) error {
return backend.ErrWorkspacesNotSupported
}
func (b *Backend) StateMgr(name string) (state.State, error) {
if name != backend.DefaultStateName {
return nil, backend.ErrWorkspacesNotSupported
}
return &remote.State{Client: b.stateClient}, nil
}
// Colorize returns the Colorize structure that can be used for colorizing
// output. This is gauranteed to always return a non-nil value and so is useful
// as a helper to wrap any potentially colored strings.
func (b *Backend) Colorize() *colorstring.Colorize {
if b.CLIColor != nil {
return b.CLIColor
}
return &colorstring.Colorize{
Colors: colorstring.DefaultColors,
Disable: true,
}
}