backend: remove deprecated atlas backend
This commit is contained in:
parent
ddf9635af6
commit
b8e3b8036a
|
@ -1,194 +0,0 @@
|
||||||
package atlas
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/states/statemgr"
|
|
||||||
"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/states/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) (statemgr.Full, 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,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
package atlas
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/zclconf/go-cty/cty"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/backend"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestImpl(t *testing.T) {
|
|
||||||
var _ backend.Backend = new(Backend)
|
|
||||||
var _ backend.CLI = new(Backend)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigure_envAddr(t *testing.T) {
|
|
||||||
defer os.Setenv("ATLAS_ADDRESS", os.Getenv("ATLAS_ADDRESS"))
|
|
||||||
os.Setenv("ATLAS_ADDRESS", "http://foo.com")
|
|
||||||
|
|
||||||
b := New()
|
|
||||||
diags := b.Configure(cty.ObjectVal(map[string]cty.Value{
|
|
||||||
"name": cty.StringVal("foo/bar"),
|
|
||||||
"address": cty.NullVal(cty.String),
|
|
||||||
"access_token": cty.StringVal("placeholder"),
|
|
||||||
}))
|
|
||||||
for _, diag := range diags {
|
|
||||||
t.Error(diag)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := b.stateClient.Server, "http://foo.com"; got != want {
|
|
||||||
t.Fatalf("wrong URL %#v; want %#v", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigure_envToken(t *testing.T) {
|
|
||||||
defer os.Setenv("ATLAS_TOKEN", os.Getenv("ATLAS_TOKEN"))
|
|
||||||
os.Setenv("ATLAS_TOKEN", "foo")
|
|
||||||
|
|
||||||
b := New()
|
|
||||||
diags := b.Configure(cty.ObjectVal(map[string]cty.Value{
|
|
||||||
"name": cty.StringVal("foo/bar"),
|
|
||||||
"address": cty.NullVal(cty.String),
|
|
||||||
"access_token": cty.NullVal(cty.String),
|
|
||||||
}))
|
|
||||||
for _, diag := range diags {
|
|
||||||
t.Error(diag)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := b.stateClient.AccessToken, "foo"; got != want {
|
|
||||||
t.Fatalf("wrong access token %#v; want %#v", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
package atlas
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/hashicorp/terraform/backend"
|
|
||||||
)
|
|
||||||
|
|
||||||
// backend.CLI impl.
|
|
||||||
func (b *Backend) CLIInit(opts *backend.CLIOpts) error {
|
|
||||||
b.CLI = opts.CLI
|
|
||||||
b.CLIColor = opts.CLIColor
|
|
||||||
b.ContextOpts = opts.ContextOpts
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,318 +0,0 @@
|
||||||
package atlas
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"crypto/md5"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"github.com/hashicorp/go-cleanhttp"
|
|
||||||
"github.com/hashicorp/go-retryablehttp"
|
|
||||||
"github.com/hashicorp/go-rootcerts"
|
|
||||||
"github.com/hashicorp/terraform/states/remote"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// defaultAtlasServer is used when no address is given
|
|
||||||
defaultAtlasServer = "https://atlas.hashicorp.com/"
|
|
||||||
atlasTokenHeader = "X-Atlas-Token"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AtlasClient implements the Client interface for an Atlas compatible server.
|
|
||||||
type stateClient struct {
|
|
||||||
Server string
|
|
||||||
ServerURL *url.URL
|
|
||||||
User string
|
|
||||||
Name string
|
|
||||||
AccessToken string
|
|
||||||
RunId string
|
|
||||||
HTTPClient *retryablehttp.Client
|
|
||||||
|
|
||||||
conflictHandlingAttempted bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *stateClient) Get() (*remote.Payload, error) {
|
|
||||||
// Make the HTTP request
|
|
||||||
req, err := retryablehttp.NewRequest("GET", c.url().String(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to make HTTP request: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set(atlasTokenHeader, c.AccessToken)
|
|
||||||
|
|
||||||
// Request the url
|
|
||||||
client, err := c.http()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// Handle the common status codes
|
|
||||||
switch resp.StatusCode {
|
|
||||||
case http.StatusOK:
|
|
||||||
// Handled after
|
|
||||||
case http.StatusNoContent:
|
|
||||||
return nil, nil
|
|
||||||
case http.StatusNotFound:
|
|
||||||
return nil, nil
|
|
||||||
case http.StatusUnauthorized:
|
|
||||||
return nil, fmt.Errorf("HTTP remote state endpoint requires auth")
|
|
||||||
case http.StatusForbidden:
|
|
||||||
return nil, fmt.Errorf("HTTP remote state endpoint invalid auth")
|
|
||||||
case http.StatusInternalServerError:
|
|
||||||
return nil, fmt.Errorf("HTTP remote state internal server error")
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"Unexpected HTTP response code: %d\n\nBody: %s",
|
|
||||||
resp.StatusCode, c.readBody(resp.Body))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read in the body
|
|
||||||
buf := bytes.NewBuffer(nil)
|
|
||||||
if _, err := io.Copy(buf, resp.Body); err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to read remote state: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the payload
|
|
||||||
payload := &remote.Payload{
|
|
||||||
Data: buf.Bytes(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(payload.Data) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for the MD5
|
|
||||||
if raw := resp.Header.Get("Content-MD5"); raw != "" {
|
|
||||||
md5, err := base64.StdEncoding.DecodeString(raw)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to decode Content-MD5 '%s': %v", raw, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
payload.MD5 = md5
|
|
||||||
} else {
|
|
||||||
// Generate the MD5
|
|
||||||
hash := md5.Sum(payload.Data)
|
|
||||||
payload.MD5 = hash[:]
|
|
||||||
}
|
|
||||||
|
|
||||||
return payload, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *stateClient) Put(state []byte) error {
|
|
||||||
// Get the target URL
|
|
||||||
base := c.url()
|
|
||||||
|
|
||||||
// Generate the MD5
|
|
||||||
hash := md5.Sum(state)
|
|
||||||
b64 := base64.StdEncoding.EncodeToString(hash[:])
|
|
||||||
|
|
||||||
// Make the HTTP client and request
|
|
||||||
req, err := retryablehttp.NewRequest("PUT", base.String(), bytes.NewReader(state))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to make HTTP request: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare the request
|
|
||||||
req.Header.Set(atlasTokenHeader, c.AccessToken)
|
|
||||||
req.Header.Set("Content-MD5", b64)
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
req.ContentLength = int64(len(state))
|
|
||||||
|
|
||||||
// Make the request
|
|
||||||
client, err := c.http()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to upload state: %v", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// Handle the error codes
|
|
||||||
switch resp.StatusCode {
|
|
||||||
case http.StatusOK:
|
|
||||||
return nil
|
|
||||||
case http.StatusConflict:
|
|
||||||
return c.handleConflict(c.readBody(resp.Body), state)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf(
|
|
||||||
"HTTP error: %d\n\nBody: %s",
|
|
||||||
resp.StatusCode, c.readBody(resp.Body))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *stateClient) Delete() error {
|
|
||||||
// Make the HTTP request
|
|
||||||
req, err := retryablehttp.NewRequest("DELETE", c.url().String(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to make HTTP request: %v", err)
|
|
||||||
}
|
|
||||||
req.Header.Set(atlasTokenHeader, c.AccessToken)
|
|
||||||
|
|
||||||
// Make the request
|
|
||||||
client, err := c.http()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to delete state: %v", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// Handle the error codes
|
|
||||||
switch resp.StatusCode {
|
|
||||||
case http.StatusOK:
|
|
||||||
return nil
|
|
||||||
case http.StatusNoContent:
|
|
||||||
return nil
|
|
||||||
case http.StatusNotFound:
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return fmt.Errorf(
|
|
||||||
"HTTP error: %d\n\nBody: %s",
|
|
||||||
resp.StatusCode, c.readBody(resp.Body))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *stateClient) readBody(b io.Reader) string {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
if _, err := io.Copy(&buf, b); err != nil {
|
|
||||||
return fmt.Sprintf("Error reading body: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := buf.String()
|
|
||||||
if result == "" {
|
|
||||||
result = "<empty>"
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *stateClient) url() *url.URL {
|
|
||||||
values := url.Values{}
|
|
||||||
|
|
||||||
values.Add("atlas_run_id", c.RunId)
|
|
||||||
|
|
||||||
return &url.URL{
|
|
||||||
Scheme: c.ServerURL.Scheme,
|
|
||||||
Host: c.ServerURL.Host,
|
|
||||||
Path: path.Join("api/v1/terraform/state", c.User, c.Name),
|
|
||||||
RawQuery: values.Encode(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *stateClient) http() (*retryablehttp.Client, error) {
|
|
||||||
if c.HTTPClient != nil {
|
|
||||||
return c.HTTPClient, nil
|
|
||||||
}
|
|
||||||
tlsConfig := &tls.Config{}
|
|
||||||
err := rootcerts.ConfigureTLS(tlsConfig, &rootcerts.Config{
|
|
||||||
CAFile: os.Getenv("ATLAS_CAFILE"),
|
|
||||||
CAPath: os.Getenv("ATLAS_CAPATH"),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rc := retryablehttp.NewClient()
|
|
||||||
|
|
||||||
rc.CheckRetry = func(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
|
||||||
if err != nil {
|
|
||||||
// don't bother retrying if the certs don't match
|
|
||||||
if err, ok := err.(*url.Error); ok {
|
|
||||||
if _, ok := err.Err.(x509.UnknownAuthorityError); ok {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return retryablehttp.DefaultRetryPolicy(ctx, resp, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t := cleanhttp.DefaultTransport()
|
|
||||||
t.TLSClientConfig = tlsConfig
|
|
||||||
rc.HTTPClient.Transport = t
|
|
||||||
|
|
||||||
c.HTTPClient = rc
|
|
||||||
return rc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Atlas returns an HTTP 409 - Conflict if the pushed state reports the same
|
|
||||||
// Serial number but the checksum of the raw content differs. This can
|
|
||||||
// sometimes happen when Terraform changes state representation internally
|
|
||||||
// between versions in a way that's semantically neutral but affects the JSON
|
|
||||||
// output and therefore the checksum.
|
|
||||||
//
|
|
||||||
// Here we detect and handle this situation by ticking the serial and retrying
|
|
||||||
// iff for the previous state and the proposed state:
|
|
||||||
//
|
|
||||||
// * the serials match
|
|
||||||
// * the parsed states are Equal (semantically equivalent)
|
|
||||||
//
|
|
||||||
// In other words, in this situation Terraform can override Atlas's detected
|
|
||||||
// conflict by asserting that the state it is pushing is indeed correct.
|
|
||||||
func (c *stateClient) handleConflict(msg string, state []byte) error {
|
|
||||||
log.Printf("[DEBUG] Handling Atlas conflict response: %s", msg)
|
|
||||||
|
|
||||||
if c.conflictHandlingAttempted {
|
|
||||||
log.Printf("[DEBUG] Already attempted conflict resolution; returning conflict.")
|
|
||||||
} else {
|
|
||||||
c.conflictHandlingAttempted = true
|
|
||||||
log.Printf("[DEBUG] Atlas reported conflict, checking for equivalent states.")
|
|
||||||
|
|
||||||
payload, err := c.Get()
|
|
||||||
if err != nil {
|
|
||||||
return conflictHandlingError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
currentState, err := terraform.ReadState(bytes.NewReader(payload.Data))
|
|
||||||
if err != nil {
|
|
||||||
return conflictHandlingError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
proposedState, err := terraform.ReadState(bytes.NewReader(state))
|
|
||||||
if err != nil {
|
|
||||||
return conflictHandlingError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if statesAreEquivalent(currentState, proposedState) {
|
|
||||||
log.Printf("[DEBUG] States are equivalent, incrementing serial and retrying.")
|
|
||||||
proposedState.Serial++
|
|
||||||
var buf bytes.Buffer
|
|
||||||
if err := terraform.WriteState(proposedState, &buf); err != nil {
|
|
||||||
return conflictHandlingError(err)
|
|
||||||
|
|
||||||
}
|
|
||||||
return c.Put(buf.Bytes())
|
|
||||||
} else {
|
|
||||||
log.Printf("[DEBUG] States are not equivalent, returning conflict.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf(
|
|
||||||
"Atlas detected a remote state conflict.\n\nMessage: %s", msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func conflictHandlingError(err error) error {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"Error while handling a conflict response from Atlas: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func statesAreEquivalent(current, proposed *terraform.State) bool {
|
|
||||||
return current.Serial == proposed.Serial && current.Equal(proposed)
|
|
||||||
}
|
|
|
@ -1,398 +0,0 @@
|
||||||
package atlas
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"crypto/md5"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/zclconf/go-cty/cty"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/backend"
|
|
||||||
"github.com/hashicorp/terraform/configs"
|
|
||||||
"github.com/hashicorp/terraform/helper/acctest"
|
|
||||||
"github.com/hashicorp/terraform/states/remote"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
|
||||||
)
|
|
||||||
|
|
||||||
func testStateClient(t *testing.T, c map[string]string) remote.Client {
|
|
||||||
vals := make(map[string]cty.Value)
|
|
||||||
for k, s := range c {
|
|
||||||
vals[k] = cty.StringVal(s)
|
|
||||||
}
|
|
||||||
synthBody := configs.SynthBody("<test>", vals)
|
|
||||||
|
|
||||||
b := backend.TestBackendConfig(t, New(), synthBody)
|
|
||||||
raw, err := b.StateMgr(backend.DefaultStateName)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s := raw.(*remote.State)
|
|
||||||
return s.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateClient_impl(t *testing.T) {
|
|
||||||
var _ remote.Client = new(stateClient)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateClient(t *testing.T) {
|
|
||||||
acctest.RemoteTestPrecheck(t)
|
|
||||||
|
|
||||||
token := os.Getenv("ATLAS_TOKEN")
|
|
||||||
if token == "" {
|
|
||||||
t.Skipf("skipping, ATLAS_TOKEN must be set")
|
|
||||||
}
|
|
||||||
|
|
||||||
client := testStateClient(t, map[string]string{
|
|
||||||
"access_token": token,
|
|
||||||
"name": "hashicorp/test-remote-state",
|
|
||||||
})
|
|
||||||
|
|
||||||
remote.TestClient(t, client)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateClient_noRetryOnBadCerts(t *testing.T) {
|
|
||||||
acctest.RemoteTestPrecheck(t)
|
|
||||||
|
|
||||||
client := testStateClient(t, map[string]string{
|
|
||||||
"access_token": "NOT_REQUIRED",
|
|
||||||
"name": "hashicorp/test-remote-state",
|
|
||||||
})
|
|
||||||
|
|
||||||
ac := client.(*stateClient)
|
|
||||||
// trigger the StateClient to build the http client and assign HTTPClient
|
|
||||||
httpClient, err := ac.http()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove the CA certs from the client
|
|
||||||
brokenCfg := &tls.Config{
|
|
||||||
RootCAs: new(x509.CertPool),
|
|
||||||
}
|
|
||||||
httpClient.HTTPClient.Transport.(*http.Transport).TLSClientConfig = brokenCfg
|
|
||||||
|
|
||||||
// Instrument CheckRetry to make sure we didn't retry
|
|
||||||
retries := 0
|
|
||||||
oldCheck := httpClient.CheckRetry
|
|
||||||
httpClient.CheckRetry = func(ctx context.Context, resp *http.Response, err error) (bool, error) {
|
|
||||||
if retries > 0 {
|
|
||||||
t.Fatal("retried after certificate error")
|
|
||||||
}
|
|
||||||
retries++
|
|
||||||
return oldCheck(ctx, resp, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = client.Get()
|
|
||||||
if err != nil {
|
|
||||||
if err, ok := err.(*url.Error); ok {
|
|
||||||
if _, ok := err.Err.(x509.UnknownAuthorityError); ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Fatalf("expected x509.UnknownAuthorityError, got %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateClient_ReportedConflictEqualStates(t *testing.T) {
|
|
||||||
fakeAtlas := newFakeAtlas(t, testStateModuleOrderChange)
|
|
||||||
srv := fakeAtlas.Server()
|
|
||||||
defer srv.Close()
|
|
||||||
|
|
||||||
client := testStateClient(t, map[string]string{
|
|
||||||
"access_token": "sometoken",
|
|
||||||
"name": "someuser/some-test-remote-state",
|
|
||||||
"address": srv.URL,
|
|
||||||
})
|
|
||||||
|
|
||||||
state, err := terraform.ReadState(bytes.NewReader(testStateModuleOrderChange))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var stateJson bytes.Buffer
|
|
||||||
if err := terraform.WriteState(state, &stateJson); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
if err := client.Put(stateJson.Bytes()); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateClient_NoConflict(t *testing.T) {
|
|
||||||
fakeAtlas := newFakeAtlas(t, testStateSimple)
|
|
||||||
srv := fakeAtlas.Server()
|
|
||||||
defer srv.Close()
|
|
||||||
|
|
||||||
client := testStateClient(t, map[string]string{
|
|
||||||
"access_token": "sometoken",
|
|
||||||
"name": "someuser/some-test-remote-state",
|
|
||||||
"address": srv.URL,
|
|
||||||
})
|
|
||||||
|
|
||||||
state, err := terraform.ReadState(bytes.NewReader(testStateSimple))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fakeAtlas.NoConflictAllowed(true)
|
|
||||||
|
|
||||||
var stateJson bytes.Buffer
|
|
||||||
if err := terraform.WriteState(state, &stateJson); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.Put(stateJson.Bytes()); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateClient_LegitimateConflict(t *testing.T) {
|
|
||||||
fakeAtlas := newFakeAtlas(t, testStateSimple)
|
|
||||||
srv := fakeAtlas.Server()
|
|
||||||
defer srv.Close()
|
|
||||||
|
|
||||||
client := testStateClient(t, map[string]string{
|
|
||||||
"access_token": "sometoken",
|
|
||||||
"name": "someuser/some-test-remote-state",
|
|
||||||
"address": srv.URL,
|
|
||||||
})
|
|
||||||
|
|
||||||
state, err := terraform.ReadState(bytes.NewReader(testStateSimple))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
terraform.WriteState(state, &buf)
|
|
||||||
|
|
||||||
// Changing the state but not the serial. Should generate a conflict.
|
|
||||||
state.RootModule().Outputs["drift"] = &terraform.OutputState{
|
|
||||||
Type: "string",
|
|
||||||
Sensitive: false,
|
|
||||||
Value: "happens",
|
|
||||||
}
|
|
||||||
|
|
||||||
var stateJson bytes.Buffer
|
|
||||||
if err := terraform.WriteState(state, &stateJson); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
if err := client.Put(stateJson.Bytes()); err == nil {
|
|
||||||
t.Fatal("Expected error from state conflict, got none.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStateClient_UnresolvableConflict(t *testing.T) {
|
|
||||||
fakeAtlas := newFakeAtlas(t, testStateSimple)
|
|
||||||
|
|
||||||
// Something unexpected causes Atlas to conflict in a way that we can't fix.
|
|
||||||
fakeAtlas.AlwaysConflict(true)
|
|
||||||
|
|
||||||
srv := fakeAtlas.Server()
|
|
||||||
defer srv.Close()
|
|
||||||
|
|
||||||
client := testStateClient(t, map[string]string{
|
|
||||||
"access_token": "sometoken",
|
|
||||||
"name": "someuser/some-test-remote-state",
|
|
||||||
"address": srv.URL,
|
|
||||||
})
|
|
||||||
|
|
||||||
state, err := terraform.ReadState(bytes.NewReader(testStateSimple))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var stateJson bytes.Buffer
|
|
||||||
if err := terraform.WriteState(state, &stateJson); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
errCh := make(chan error)
|
|
||||||
go func() {
|
|
||||||
defer close(errCh)
|
|
||||||
if err := client.Put(stateJson.Bytes()); err == nil {
|
|
||||||
errCh <- errors.New("expected error from state conflict, got none.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case err := <-errCh:
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error from anonymous test goroutine: %s", err)
|
|
||||||
}
|
|
||||||
case <-time.After(500 * time.Millisecond):
|
|
||||||
t.Fatalf("Timed out after 500ms, probably because retrying infinitely.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stub Atlas HTTP API for a given state JSON string; does checksum-based
|
|
||||||
// conflict detection equivalent to Atlas's.
|
|
||||||
type fakeAtlas struct {
|
|
||||||
state []byte
|
|
||||||
t *testing.T
|
|
||||||
|
|
||||||
// Used to test that we only do the special conflict handling retry once.
|
|
||||||
alwaysConflict bool
|
|
||||||
|
|
||||||
// Used to fail the test immediately if a conflict happens.
|
|
||||||
noConflictAllowed bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newFakeAtlas(t *testing.T, state []byte) *fakeAtlas {
|
|
||||||
return &fakeAtlas{
|
|
||||||
state: state,
|
|
||||||
t: t,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeAtlas) Server() *httptest.Server {
|
|
||||||
return httptest.NewServer(http.HandlerFunc(f.handler))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeAtlas) CurrentState() *terraform.State {
|
|
||||||
// we read the state manually here, because terraform may alter state
|
|
||||||
// during read
|
|
||||||
currentState := &terraform.State{}
|
|
||||||
err := json.Unmarshal(f.state, currentState)
|
|
||||||
if err != nil {
|
|
||||||
f.t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
return currentState
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeAtlas) CurrentSerial() int64 {
|
|
||||||
return f.CurrentState().Serial
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeAtlas) CurrentSum() [md5.Size]byte {
|
|
||||||
return md5.Sum(f.state)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeAtlas) AlwaysConflict(b bool) {
|
|
||||||
f.alwaysConflict = b
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeAtlas) NoConflictAllowed(b bool) {
|
|
||||||
f.noConflictAllowed = b
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeAtlas) handler(resp http.ResponseWriter, req *http.Request) {
|
|
||||||
// access tokens should only be sent as a header
|
|
||||||
if req.FormValue("access_token") != "" {
|
|
||||||
http.Error(resp, "access_token in request params", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Header.Get(atlasTokenHeader) == "" {
|
|
||||||
http.Error(resp, "missing access token", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch req.Method {
|
|
||||||
case "GET":
|
|
||||||
// Respond with the current stored state.
|
|
||||||
resp.Header().Set("Content-Type", "application/json")
|
|
||||||
resp.Write(f.state)
|
|
||||||
case "PUT":
|
|
||||||
var buf bytes.Buffer
|
|
||||||
buf.ReadFrom(req.Body)
|
|
||||||
sum := md5.Sum(buf.Bytes())
|
|
||||||
|
|
||||||
// we read the state manually here, because terraform may alter state
|
|
||||||
// during read
|
|
||||||
state := &terraform.State{}
|
|
||||||
err := json.Unmarshal(buf.Bytes(), state)
|
|
||||||
if err != nil {
|
|
||||||
f.t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
conflict := f.CurrentSerial() == state.Serial && f.CurrentSum() != sum
|
|
||||||
conflict = conflict || f.alwaysConflict
|
|
||||||
if conflict {
|
|
||||||
if f.noConflictAllowed {
|
|
||||||
f.t.Fatal("Got conflict when NoConflictAllowed was set.")
|
|
||||||
}
|
|
||||||
http.Error(resp, "Conflict", 409)
|
|
||||||
} else {
|
|
||||||
f.state = buf.Bytes()
|
|
||||||
resp.WriteHeader(200)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a tfstate file with the module order changed, which is a structural
|
|
||||||
// but not a semantic difference. Terraform will sort these modules as it
|
|
||||||
// loads the state.
|
|
||||||
var testStateModuleOrderChange = []byte(
|
|
||||||
`{
|
|
||||||
"version": 3,
|
|
||||||
"serial": 1,
|
|
||||||
"modules": [
|
|
||||||
{
|
|
||||||
"path": [
|
|
||||||
"root",
|
|
||||||
"child2",
|
|
||||||
"grandchild"
|
|
||||||
],
|
|
||||||
"outputs": {
|
|
||||||
"foo": {
|
|
||||||
"sensitive": false,
|
|
||||||
"type": "string",
|
|
||||||
"value": "bar"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"resources": null
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": [
|
|
||||||
"root",
|
|
||||||
"child1",
|
|
||||||
"grandchild"
|
|
||||||
],
|
|
||||||
"outputs": {
|
|
||||||
"foo": {
|
|
||||||
"sensitive": false,
|
|
||||||
"type": "string",
|
|
||||||
"value": "bar"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"resources": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
|
|
||||||
var testStateSimple = []byte(
|
|
||||||
`{
|
|
||||||
"version": 3,
|
|
||||||
"serial": 2,
|
|
||||||
"lineage": "c00ad9ac-9b35-42fe-846e-b06f0ef877e9",
|
|
||||||
"modules": [
|
|
||||||
{
|
|
||||||
"path": [
|
|
||||||
"root"
|
|
||||||
],
|
|
||||||
"outputs": {
|
|
||||||
"foo": {
|
|
||||||
"sensitive": false,
|
|
||||||
"type": "string",
|
|
||||||
"value": "bar"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"resources": {},
|
|
||||||
"depends_on": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
`)
|
|
|
@ -10,7 +10,6 @@ import (
|
||||||
"github.com/hashicorp/terraform/tfdiags"
|
"github.com/hashicorp/terraform/tfdiags"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
|
||||||
backendAtlas "github.com/hashicorp/terraform/backend/atlas"
|
|
||||||
backendLocal "github.com/hashicorp/terraform/backend/local"
|
backendLocal "github.com/hashicorp/terraform/backend/local"
|
||||||
backendRemote "github.com/hashicorp/terraform/backend/remote"
|
backendRemote "github.com/hashicorp/terraform/backend/remote"
|
||||||
backendArtifactory "github.com/hashicorp/terraform/backend/remote-state/artifactory"
|
backendArtifactory "github.com/hashicorp/terraform/backend/remote-state/artifactory"
|
||||||
|
@ -56,7 +55,6 @@ func Init(services *disco.Disco) {
|
||||||
|
|
||||||
// Remote State backends.
|
// Remote State backends.
|
||||||
"artifactory": func() backend.Backend { return backendArtifactory.New() },
|
"artifactory": func() backend.Backend { return backendArtifactory.New() },
|
||||||
"atlas": func() backend.Backend { return backendAtlas.New() },
|
|
||||||
"azurerm": func() backend.Backend { return backendAzure.New() },
|
"azurerm": func() backend.Backend { return backendAzure.New() },
|
||||||
"consul": func() backend.Backend { return backendConsul.New() },
|
"consul": func() backend.Backend { return backendConsul.New() },
|
||||||
"cos": func() backend.Backend { return backendCos.New() },
|
"cos": func() backend.Backend { return backendCos.New() },
|
||||||
|
|
|
@ -15,7 +15,6 @@ func TestInit_backend(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{"local", "*local.Local"},
|
{"local", "*local.Local"},
|
||||||
{"remote", "*remote.Remote"},
|
{"remote", "*remote.Remote"},
|
||||||
{"atlas", "*atlas.Backend"},
|
|
||||||
{"azurerm", "*azure.Backend"},
|
{"azurerm", "*azure.Backend"},
|
||||||
{"consul", "*consul.Backend"},
|
{"consul", "*consul.Backend"},
|
||||||
{"cos", "*cos.Backend"},
|
{"cos", "*cos.Backend"},
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
---
|
|
||||||
layout: "backend-types"
|
|
||||||
page_title: "Backend Type: terraform enterprise"
|
|
||||||
sidebar_current: "docs-backends-types-standard-terraform-enterprise"
|
|
||||||
description: |-
|
|
||||||
Terraform can store its state in Terraform Enterprise
|
|
||||||
---
|
|
||||||
|
|
||||||
# terraform enterprise
|
|
||||||
|
|
||||||
!> **The `atlas` backend is deprecated.** Please use the new enhanced
|
|
||||||
[remote](/docs/backends/types/remote.html) backend for storing state and running
|
|
||||||
remote operations in Terraform Cloud.
|
|
||||||
|
|
||||||
**Kind: Standard (with no locking)**
|
|
||||||
|
|
||||||
Reads and writes state from a [Terraform Enterprise](/docs/cloud/index.html)
|
|
||||||
workspace.
|
|
||||||
|
|
||||||
-> **Why is this called "atlas"?** Before it was a standalone offering,
|
|
||||||
Terraform Enterprise was part of an integrated suite of enterprise products
|
|
||||||
called Atlas. This backend predates the current version Terraform Enterprise, so
|
|
||||||
it uses the old name.
|
|
||||||
|
|
||||||
We no longer recommend using this backend, as it does not support collaboration
|
|
||||||
features like [workspace
|
|
||||||
locking](/docs/cloud/run/index.html). Please use the new enhanced
|
|
||||||
[remote](/docs/backends/types/remote.html) backend for storing state and running
|
|
||||||
remote operations in Terraform Cloud.
|
|
||||||
|
|
||||||
## Example Configuration
|
|
||||||
|
|
||||||
```hcl
|
|
||||||
terraform {
|
|
||||||
backend "atlas" {
|
|
||||||
name = "example_corp/networking-prod"
|
|
||||||
address = "https://app.terraform.io" # optional
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
We recommend using a [partial configuration](/docs/backends/config.html) and
|
|
||||||
omitting the access token, which can be provided as an environment variable.
|
|
||||||
|
|
||||||
## Data Source Configuration
|
|
||||||
|
|
||||||
```hcl
|
|
||||||
data "terraform_remote_state" "foo" {
|
|
||||||
backend = "atlas"
|
|
||||||
config = {
|
|
||||||
name = "example_corp/networking-prod"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration variables
|
|
||||||
|
|
||||||
The following configuration options / environment variables are supported:
|
|
||||||
|
|
||||||
* `name` - (Required) Full name of the workspace (`<ORGANIZATION>/<WORKSPACE>`).
|
|
||||||
* `ATLAS_TOKEN`/ `access_token` - (Optional) A Terraform Enterprise [user API
|
|
||||||
token](/docs/cloud/users-teams-organizations/users.html#api-tokens). We
|
|
||||||
recommend using the `ATLAS_TOKEN` environment variable rather than setting
|
|
||||||
`access_token` in the configuration. If not set, the token will be requested
|
|
||||||
during a `terraform init` and saved locally.
|
|
||||||
* `address` - (Optional) The URL of a Terraform Enterprise instance. Defaults to
|
|
||||||
the SaaS version of Terraform Enterprise, at `"https://app.terraform.io"`; if
|
|
||||||
you use a private install, provide its URL here.
|
|
|
@ -1,31 +0,0 @@
|
||||||
---
|
|
||||||
layout: "docs"
|
|
||||||
page_title: "Configuring Terraform Push"
|
|
||||||
sidebar_current: "docs-config-push"
|
|
||||||
description: |-
|
|
||||||
Terraform's push command was a way to interact with the legacy version of Terraform Enterprise. It is not supported in the current version of Terraform Enterprise.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Terraform Push Configuration
|
|
||||||
|
|
||||||
-> **Note:** This page is about Terraform 0.12 and later. For Terraform 0.11 and
|
|
||||||
earlier, see
|
|
||||||
[0.11 Configuration Language: Configuring Terraform Push](../configuration-0-11/terraform-enterprise.html).
|
|
||||||
|
|
||||||
Prior to v0.12, Terraform included mechanisms to interact with a legacy version
|
|
||||||
of Terraform Enterprise, formerly known as "Atlas".
|
|
||||||
|
|
||||||
These features relied on a special configuration block named `atlas`:
|
|
||||||
|
|
||||||
```hcl
|
|
||||||
atlas {
|
|
||||||
name = "acme-corp/production"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
These features are no longer available on Terraform Enterprise and so the
|
|
||||||
corresponding configuration elements and commands have been removed in
|
|
||||||
Terraform v0.12.
|
|
||||||
|
|
||||||
After upgrading to the current version of Terraform Enterprise,
|
|
||||||
any `atlas` blocks in your configuration can be safely removed.
|
|
Loading…
Reference in New Issue