-
Notifications
You must be signed in to change notification settings - Fork 9.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Migrate Manta Remote state to be a backend
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
Showing
29 changed files
with
3,331 additions
and
185 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
` |
Oops, something went wrong.