opentofu/backend/atlas/backend.go
Martin Atkins 479c6b2466 move "configschema" from "config" to "configs"
The "config" package is no longer used and will be removed as part
of the 0.12 release cleanup. Since configschema is part of the
"new world" of configuration modelling, it makes more sense for
it to live as a subdirectory of the newer "configs" package.
2018-10-16 18:50:29 -07:00

192 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/helper/schema"
"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
// schema is the schema for configuration, set by init
schema *schema.Backend
// opLock locks operations
opLock sync.Mutex
}
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) ValidateConfig(obj 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 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 ValidateConfig 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 ValidateConfig, 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) States() ([]string, error) {
return nil, backend.ErrNamedStatesNotSupported
}
func (b *Backend) DeleteState(name string) error {
return backend.ErrNamedStatesNotSupported
}
func (b *Backend) State(name string) (state.State, error) {
if name != backend.DefaultStateName {
return nil, backend.ErrNamedStatesNotSupported
}
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,
}
}