Merge pull request #12352 from hashicorp/f-atlas-backend

backend/atlas: convert to new style
This commit is contained in:
Mitchell Hashimoto 2017-03-01 13:37:29 -08:00 committed by GitHub
commit e7a88ce089
7 changed files with 236 additions and 95 deletions

163
backend/atlas/backend.go Normal file
View File

@ -0,0 +1,163 @@
package atlas
import (
"context"
"fmt"
"net/url"
"os"
"strings"
"sync"
"github.com/hashicorp/terraform/backend"
"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"
)
// 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
once sync.Once
// opLock locks operations
opLock sync.Mutex
}
func (b *Backend) Input(
ui terraform.UIInput, c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) {
b.once.Do(b.init)
return b.schema.Input(ui, c)
}
func (b *Backend) Validate(c *terraform.ResourceConfig) ([]string, []error) {
b.once.Do(b.init)
return b.schema.Validate(c)
}
func (b *Backend) Configure(c *terraform.ResourceConfig) error {
b.once.Do(b.init)
return b.schema.Configure(c)
}
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,
}
}
func (b *Backend) init() {
b.schema = &schema.Backend{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: schemaDescriptions["name"],
},
"access_token": &schema.Schema{
Type: schema.TypeString,
Required: true,
Description: schemaDescriptions["access_token"],
DefaultFunc: schema.EnvDefaultFunc("ATLAS_TOKEN", nil),
},
"address": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: defaultAtlasServer,
Description: schemaDescriptions["address"],
},
},
ConfigureFunc: b.schemaConfigure,
}
}
func (b *Backend) schemaConfigure(ctx context.Context) error {
d := schema.FromContextBackendConfig(ctx)
// Parse the address
addr := d.Get("address").(string)
addrUrl, err := url.Parse(addr)
if err != nil {
return fmt.Errorf("Error parsing 'address': %s", err)
}
// Parse the org/env
name := d.Get("name").(string)
parts := strings.Split(name, "/")
if len(parts) != 2 {
return fmt.Errorf("malformed name '%s', expected format '<org>/<name>'", name)
}
org := parts[0]
env := parts[1]
// Setup the client
b.stateClient = &stateClient{
Server: addr,
ServerURL: addrUrl,
AccessToken: d.Get("access_token").(string),
User: org,
Name: env,
// This is optionally set during Atlas Terraform runs.
RunId: os.Getenv("ATLAS_RUN_ID"),
}
return nil
}
var schemaDescriptions = map[string]string{
"name": "Full name of the environment in Atlas, such as 'hashicorp/myenv'",
"access_token": "Access token to use to access Atlas. If ATLAS_TOKEN is set then\n" +
"this will override any saved value for this.",
"address": "Address to your Atlas installation. This defaults to the publicly\n" +
"hosted version at 'https://atlas.hashicorp.com/'. This address\n" +
"should contain the full HTTP scheme to use.",
}

View File

@ -0,0 +1,12 @@
package atlas
import (
"testing"
"github.com/hashicorp/terraform/backend"
)
func TestImpl(t *testing.T) {
var _ backend.Backend = new(Backend)
var _ backend.CLI = new(Backend)
}

13
backend/atlas/cli.go Normal file
View File

@ -0,0 +1,13 @@
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,4 +1,4 @@
package remote
package atlas
import (
"bytes"
@ -13,11 +13,11 @@ import (
"net/url"
"os"
"path"
"strings"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-retryablehttp"
"github.com/hashicorp/go-rootcerts"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/terraform"
)
@ -27,55 +27,8 @@ const (
atlasTokenHeader = "X-Atlas-Token"
)
func atlasFactory(conf map[string]string) (Client, error) {
var client AtlasClient
server, ok := conf["address"]
if !ok || server == "" {
server = defaultAtlasServer
}
url, err := url.Parse(server)
if err != nil {
return nil, err
}
token, ok := conf["access_token"]
if token == "" {
token = os.Getenv("ATLAS_TOKEN")
ok = true
}
if !ok || token == "" {
return nil, fmt.Errorf(
"missing 'access_token' configuration or ATLAS_TOKEN environmental variable")
}
name, ok := conf["name"]
if !ok || name == "" {
return nil, fmt.Errorf("missing 'name' configuration")
}
parts := strings.Split(name, "/")
if len(parts) != 2 {
return nil, fmt.Errorf("malformed name '%s', expected format '<account>/<name>'", name)
}
// If it exists, add the `ATLAS_RUN_ID` environment
// variable as a param, which is injected during Atlas Terraform
// runs. This is completely optional.
client.RunId = os.Getenv("ATLAS_RUN_ID")
client.Server = server
client.ServerURL = url
client.AccessToken = token
client.User = parts[0]
client.Name = parts[1]
return &client, nil
}
// AtlasClient implements the Client interface for an Atlas compatible server.
type AtlasClient struct {
type stateClient struct {
Server string
ServerURL *url.URL
User string
@ -87,7 +40,7 @@ type AtlasClient struct {
conflictHandlingAttempted bool
}
func (c *AtlasClient) Get() (*Payload, error) {
func (c *stateClient) Get() (*remote.Payload, error) {
// Make the HTTP request
req, err := retryablehttp.NewRequest("GET", c.url().String(), nil)
if err != nil {
@ -134,7 +87,7 @@ func (c *AtlasClient) Get() (*Payload, error) {
}
// Create the payload
payload := &Payload{
payload := &remote.Payload{
Data: buf.Bytes(),
}
@ -159,7 +112,7 @@ func (c *AtlasClient) Get() (*Payload, error) {
return payload, nil
}
func (c *AtlasClient) Put(state []byte) error {
func (c *stateClient) Put(state []byte) error {
// Get the target URL
base := c.url()
@ -203,7 +156,7 @@ func (c *AtlasClient) Put(state []byte) error {
}
}
func (c *AtlasClient) Delete() error {
func (c *stateClient) Delete() error {
// Make the HTTP request
req, err := retryablehttp.NewRequest("DELETE", c.url().String(), nil)
if err != nil {
@ -237,7 +190,7 @@ func (c *AtlasClient) Delete() error {
}
}
func (c *AtlasClient) readBody(b io.Reader) string {
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)
@ -251,7 +204,7 @@ func (c *AtlasClient) readBody(b io.Reader) string {
return result
}
func (c *AtlasClient) url() *url.URL {
func (c *stateClient) url() *url.URL {
values := url.Values{}
values.Add("atlas_run_id", c.RunId)
@ -264,7 +217,7 @@ func (c *AtlasClient) url() *url.URL {
}
}
func (c *AtlasClient) http() (*retryablehttp.Client, error) {
func (c *stateClient) http() (*retryablehttp.Client, error) {
if c.HTTPClient != nil {
return c.HTTPClient, nil
}
@ -314,7 +267,7 @@ func (c *AtlasClient) http() (*retryablehttp.Client, error) {
//
// 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 *AtlasClient) handleConflict(msg string, state []byte) error {
func (c *stateClient) handleConflict(msg string, state []byte) error {
log.Printf("[DEBUG] Handling Atlas conflict response: %s", msg)
if c.conflictHandlingAttempted {

View File

@ -1,4 +1,4 @@
package remote
package atlas
import (
"bytes"
@ -13,15 +13,28 @@ import (
"testing"
"time"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/terraform"
)
func TestAtlasClient_impl(t *testing.T) {
var _ Client = new(AtlasClient)
func testStateClient(t *testing.T, c map[string]interface{}) remote.Client {
b := backend.TestBackendConfig(t, &Backend{}, c)
raw, err := b.State(backend.DefaultStateName)
if err != nil {
t.Fatalf("err: %s", err)
}
s := raw.(*remote.State)
return s.Client
}
func TestAtlasClient(t *testing.T) {
func TestStateClient_impl(t *testing.T) {
var _ remote.Client = new(stateClient)
}
func TestStateClient(t *testing.T) {
acctest.RemoteTestPrecheck(t)
token := os.Getenv("ATLAS_TOKEN")
@ -29,30 +42,24 @@ func TestAtlasClient(t *testing.T) {
t.Skipf("skipping, ATLAS_TOKEN must be set")
}
client, err := atlasFactory(map[string]string{
client := testStateClient(t, map[string]interface{}{
"access_token": token,
"name": "hashicorp/test-remote-state",
})
if err != nil {
t.Fatalf("bad: %s", err)
}
testClient(t, client)
remote.TestClient(t, client)
}
func TestAtlasClient_noRetryOnBadCerts(t *testing.T) {
func TestStateClient_noRetryOnBadCerts(t *testing.T) {
acctest.RemoteTestPrecheck(t)
client, err := atlasFactory(map[string]string{
client := testStateClient(t, map[string]interface{}{
"access_token": "NOT_REQUIRED",
"name": "hashicorp/test-remote-state",
})
if err != nil {
t.Fatalf("bad: %s", err)
}
ac := client.(*AtlasClient)
// trigger the AtlasClient to build the http client and assign HTTPClient
ac := client.(*stateClient)
// trigger the StateClient to build the http client and assign HTTPClient
httpClient, err := ac.http()
if err != nil {
t.Fatal(err)
@ -87,18 +94,16 @@ func TestAtlasClient_noRetryOnBadCerts(t *testing.T) {
t.Fatalf("expected x509.UnknownAuthorityError, got %v", err)
}
func TestAtlasClient_ReportedConflictEqualStates(t *testing.T) {
func TestStateClient_ReportedConflictEqualStates(t *testing.T) {
fakeAtlas := newFakeAtlas(t, testStateModuleOrderChange)
srv := fakeAtlas.Server()
defer srv.Close()
client, err := atlasFactory(map[string]string{
client := testStateClient(t, map[string]interface{}{
"access_token": "sometoken",
"name": "someuser/some-test-remote-state",
"address": srv.URL,
})
if err != nil {
t.Fatalf("err: %s", err)
}
state, err := terraform.ReadState(bytes.NewReader(testStateModuleOrderChange))
if err != nil {
@ -114,18 +119,16 @@ func TestAtlasClient_ReportedConflictEqualStates(t *testing.T) {
}
}
func TestAtlasClient_NoConflict(t *testing.T) {
func TestStateClient_NoConflict(t *testing.T) {
fakeAtlas := newFakeAtlas(t, testStateSimple)
srv := fakeAtlas.Server()
defer srv.Close()
client, err := atlasFactory(map[string]string{
client := testStateClient(t, map[string]interface{}{
"access_token": "sometoken",
"name": "someuser/some-test-remote-state",
"address": srv.URL,
})
if err != nil {
t.Fatalf("err: %s", err)
}
state, err := terraform.ReadState(bytes.NewReader(testStateSimple))
if err != nil {
@ -144,18 +147,16 @@ func TestAtlasClient_NoConflict(t *testing.T) {
}
}
func TestAtlasClient_LegitimateConflict(t *testing.T) {
func TestStateClient_LegitimateConflict(t *testing.T) {
fakeAtlas := newFakeAtlas(t, testStateSimple)
srv := fakeAtlas.Server()
defer srv.Close()
client, err := atlasFactory(map[string]string{
client := testStateClient(t, map[string]interface{}{
"access_token": "sometoken",
"name": "someuser/some-test-remote-state",
"address": srv.URL,
})
if err != nil {
t.Fatalf("err: %s", err)
}
state, err := terraform.ReadState(bytes.NewReader(testStateSimple))
if err != nil {
@ -181,7 +182,7 @@ func TestAtlasClient_LegitimateConflict(t *testing.T) {
}
}
func TestAtlasClient_UnresolvableConflict(t *testing.T) {
func TestStateClient_UnresolvableConflict(t *testing.T) {
fakeAtlas := newFakeAtlas(t, testStateSimple)
// Something unexpected causes Atlas to conflict in a way that we can't fix.
@ -189,14 +190,12 @@ func TestAtlasClient_UnresolvableConflict(t *testing.T) {
srv := fakeAtlas.Server()
defer srv.Close()
client, err := atlasFactory(map[string]string{
client := testStateClient(t, map[string]interface{}{
"access_token": "sometoken",
"name": "someuser/some-test-remote-state",
"address": srv.URL,
})
if err != nil {
t.Fatalf("err: %s", err)
}
state, err := terraform.ReadState(bytes.NewReader(testStateSimple))
if err != nil {

View File

@ -7,6 +7,7 @@ import (
"github.com/hashicorp/terraform/backend"
backendatlas "github.com/hashicorp/terraform/backend/atlas"
backendlegacy "github.com/hashicorp/terraform/backend/legacy"
backendlocal "github.com/hashicorp/terraform/backend/local"
backendconsul "github.com/hashicorp/terraform/backend/remote-state/consul"
@ -31,6 +32,7 @@ func init() {
// Our hardcoded backends. We don't need to acquire a lock here
// since init() code is serial and can't spawn goroutines.
backends = map[string]func() backend.Backend{
"atlas": func() backend.Backend { return &backendatlas.Backend{} },
"local": func() backend.Backend { return &backendlocal.Local{} },
"consul": func() backend.Backend { return backendconsul.New() },
"inmem": func() backend.Backend { return backendinmem.New() },

View File

@ -46,7 +46,6 @@ func NewClient(t string, conf map[string]string) (Client, error) {
// NewClient.
var BuiltinClients = map[string]Factory{
"artifactory": artifactoryFactory,
"atlas": atlasFactory,
"azure": azureFactory,
"etcd": etcdFactory,
"gcs": gcsFactory,