Skip to content

Commit

Permalink
Merge pull request hashicorp#12352 from hashicorp/f-atlas-backend
Browse files Browse the repository at this point in the history
backend/atlas: convert to new style
  • Loading branch information
mitchellh authored Mar 1, 2017
2 parents 868230e + 08b47cf commit e7a88ce
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 95 deletions.
163 changes: 163 additions & 0 deletions backend/atlas/backend.go
Original file line number Diff line number Diff line change
@@ -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.",
}
12 changes: 12 additions & 0 deletions backend/atlas/backend_test.go
Original file line number Diff line number Diff line change
@@ -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 changes: 13 additions & 0 deletions backend/atlas/cli.go
Original file line number Diff line number Diff line change
@@ -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
}
69 changes: 11 additions & 58 deletions state/remote/atlas.go → backend/atlas/state_client.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package remote
package atlas

import (
"bytes"
Expand All @@ -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"
)

Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -134,7 +87,7 @@ func (c *AtlasClient) Get() (*Payload, error) {
}

// Create the payload
payload := &Payload{
payload := &remote.Payload{
Data: buf.Bytes(),
}

Expand All @@ -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()

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit e7a88ce

Please sign in to comment.