backend: remove deprecated atlas backend

This commit is contained in:
Kristin Laemmert 2020-10-20 15:53:52 -04:00
parent ddf9635af6
commit b8e3b8036a
9 changed files with 0 additions and 1078 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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": []
}
]
}
`)

View File

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

View File

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

View File

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

View File

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