Skip to content

Commit

Permalink
Migrate Manta Remote state to be a backend
Browse files Browse the repository at this point in the history
This PR changes manta from being a legacy remote state client to a new backend type. This also includes creating a simple lock within manta

This PR also unifies the way the triton client is configured (the schema) and also uses the same env vars to set the backend up

It is important to note that if the remote state path does not exist, then the backend will create that path. This means the user doesn't need to fall into a chicken and egg situation of creating the directory in advance before interacting with it
  • Loading branch information
stack72 committed Oct 30, 2017
1 parent 4a01bf0 commit 1fd0f80
Show file tree
Hide file tree
Showing 29 changed files with 3,331 additions and 185 deletions.
2 changes: 2 additions & 0 deletions backend/init/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 16,7 @@ import (
backendetcdv3 "github.com/hashicorp/terraform/backend/remote-state/etcdv3"
backendGCS "github.com/hashicorp/terraform/backend/remote-state/gcs"
backendinmem "github.com/hashicorp/terraform/backend/remote-state/inmem"
backendManta "github.com/hashicorp/terraform/backend/remote-state/manta"
backendS3 "github.com/hashicorp/terraform/backend/remote-state/s3"
backendSwift "github.com/hashicorp/terraform/backend/remote-state/swift"
)
Expand Down Expand Up @@ -49,6 50,7 @@ func init() {
"azurerm": func() backend.Backend { return backendAzure.New() },
"etcdv3": func() backend.Backend { return backendetcdv3.New() },
"gcs": func() backend.Backend { return backendGCS.New() },
"manta": func() backend.Backend { return backendManta.New() },
}

// Add the legacy remote backends that haven't yet been convertd to
Expand Down
177 changes: 177 additions & 0 deletions backend/remote-state/manta/backend.go
Original file line number Diff line number Diff line change
@@ -0,0 1,177 @@
package manta

import (
"context"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"os"

"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/helper/schema"
triton "github.com/joyent/triton-go"
"github.com/joyent/triton-go/authentication"
"github.com/joyent/triton-go/storage"
)

func New() backend.Backend {
s := &schema.Backend{
Schema: map[string]*schema.Schema{
"account": {
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{"TRITON_ACCOUNT", "SDC_ACCOUNT"}, ""),
},

"url": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{"MANTA_URL"}, "https://us-east.manta.joyent.com"),
},

"key_material": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{"TRITON_KEY_MATERIAL", "SDC_KEY_MATERIAL"}, ""),
},

"key_id": {
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.MultiEnvDefaultFunc([]string{"TRITON_KEY_ID", "SDC_KEY_ID"}, ""),
},

"insecure_skip_tls_verify": {
Type: schema.TypeBool,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("TRITON_SKIP_TLS_VERIFY", ""),
},

"path": {
Type: schema.TypeString,
Required: true,
},

"objectName": {
Type: schema.TypeString,
Optional: true,
Default: "terraform.tfstate",
},
},
}

result := &Backend{Backend: s}
result.Backend.ConfigureFunc = result.configure
return result
}

type Backend struct {
*schema.Backend
data *schema.ResourceData

// The fields below are set from configure
storageClient *storage.StorageClient
path string
objectName string
}

type BackendConfig struct {
AccountId string
KeyId string
AccountUrl string
KeyMaterial string
SkipTls bool
}

func (b *Backend) configure(ctx context.Context) error {
if b.path != "" {
return nil
}

data := schema.FromContextBackendConfig(ctx)

config := &BackendConfig{
AccountId: data.Get("account").(string),
AccountUrl: data.Get("url").(string),
KeyId: data.Get("key_id").(string),
SkipTls: data.Get("insecure_skip_tls_verify").(bool),
}

if v, ok := data.GetOk("key_material"); ok {
config.KeyMaterial = v.(string)
}

b.path = data.Get("path").(string)
b.objectName = data.Get("objectName").(string)

var validationError *multierror.Error

if data.Get("account").(string) == "" {
validationError = multierror.Append(validationError, errors.New("`Account` must be configured for the Triton provider"))
}
if data.Get("key_id").(string) == "" {
validationError = multierror.Append(validationError, errors.New("`Key ID` must be configured for the Triton provider"))
}
if b.path == "" {
validationError = multierror.Append(validationError, errors.New("`Path` must be configured for the Triton provider"))
}

if validationError != nil {
return validationError
}

var signer authentication.Signer
var err error

if config.KeyMaterial == "" {
signer, err = authentication.NewSSHAgentSigner(config.KeyId, config.AccountId)
if err != nil {
return errwrap.Wrapf("Error Creating SSH Agent Signer: {{err}}", err)
}
} else {
var keyBytes []byte
if _, err = os.Stat(config.KeyMaterial); err == nil {
keyBytes, err = ioutil.ReadFile(config.KeyMaterial)
if err != nil {
return fmt.Errorf("Error reading key material from %s: %s",
config.KeyMaterial, err)
}
block, _ := pem.Decode(keyBytes)
if block == nil {
return fmt.Errorf(
"Failed to read key material '%s': no key found", config.KeyMaterial)
}

if block.Headers["Proc-Type"] == "4,ENCRYPTED" {
return fmt.Errorf(
"Failed to read key '%s': password protected keys are\n"
"not currently supported. Please decrypt the key prior to use.", config.KeyMaterial)
}

} else {
keyBytes = []byte(config.KeyMaterial)
}

signer, err = authentication.NewPrivateKeySigner(config.KeyId, keyBytes, config.AccountId)
if err != nil {
return errwrap.Wrapf("Error Creating SSH Private Key Signer: {{err}}", err)
}
}

clientConfig := &triton.ClientConfig{
MantaURL: config.AccountUrl,
AccountName: config.AccountId,
Signers: []authentication.Signer{signer},
}
triton, err := storage.NewClient(clientConfig)
if err != nil {
return err
}

b.storageClient = triton

return nil
}
144 changes: 144 additions & 0 deletions backend/remote-state/manta/backend_state.go
Original file line number Diff line number Diff line change
@@ -0,0 1,144 @@
package manta

import (
"context"
"errors"
"fmt"
"path"
"sort"
"strings"

"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/terraform"
"github.com/joyent/triton-go/storage"
)

func (b *Backend) States() ([]string, error) {
result := []string{backend.DefaultStateName}

objs, err := b.storageClient.Dir().List(context.Background(), &storage.ListDirectoryInput{
DirectoryName: path.Join(mantaDefaultRootStore, b.path),
})
if err != nil {
if strings.Contains(err.Error(), "ResourceNotFound") {
return result, nil
}
return nil, err
}

for _, obj := range objs.Entries {
if obj.Type == "directory" && obj.Name != "" {
result = append(result, obj.Name)
}
}

sort.Strings(result[1:])
return result, nil
}

func (b *Backend) DeleteState(name string) error {
if name == backend.DefaultStateName || name == "" {
return fmt.Errorf("can't delete default state")
}

//firstly we need to delete the state file
err := b.storageClient.Objects().Delete(context.Background(), &storage.DeleteObjectInput{
ObjectPath: path.Join(mantaDefaultRootStore, b.statePath(name), b.objectName),
})
if err != nil {
return err
}

//then we need to delete the state folder
err = b.storageClient.Objects().Delete(context.Background(), &storage.DeleteObjectInput{
ObjectPath: path.Join(mantaDefaultRootStore, b.statePath(name)),
})
if err != nil {
return err
}

return nil
}

func (b *Backend) State(name string) (state.State, error) {
if name == "" {
return nil, errors.New("missing state name")
}

client := &RemoteClient{
storageClient: b.storageClient,
directoryName: b.statePath(name),
keyName: b.objectName,
}

stateMgr := &remote.State{Client: client}

//if this isn't the default state name, we need to create the object so
//it's listed by States.
if name != backend.DefaultStateName {

// take a lock on this state while we write it
lockInfo := state.NewLockInfo()
lockInfo.Operation = "init"
lockId, err := client.Lock(lockInfo)
if err != nil {
return nil, fmt.Errorf("failed to lock manta state: %s", err)
}

// Local helper function so we can call it multiple places
lockUnlock := func(parent error) error {
if err := stateMgr.Unlock(lockId); err != nil {
return fmt.Errorf(strings.TrimSpace(errStateUnlock), lockId, err)
}
return parent
}

// Grab the value
if err := stateMgr.RefreshState(); err != nil {
err = lockUnlock(err)
return nil, err
}

// If we have no state, we have to create an empty state
if v := stateMgr.State(); v == nil {
if err := stateMgr.WriteState(terraform.NewState()); err != nil {
err = lockUnlock(err)
return nil, err
}
if err := stateMgr.PersistState(); err != nil {
err = lockUnlock(err)
return nil, err
}
}

// Unlock, the state should now be initialized
if err := lockUnlock(nil); err != nil {
return nil, err
}

}

return stateMgr, nil
}

func (b *Backend) client() *RemoteClient {
return &RemoteClient{}
}

func (b *Backend) statePath(name string) string {
if name == backend.DefaultStateName {
return b.path
}

return path.Join(b.path, name)
}

const errStateUnlock = `
Error unlocking Manta state. Lock ID: %s
Error: %s
You may have to force-unlock this state in order to use it again.
`
Loading

0 comments on commit 1fd0f80

Please sign in to comment.