272 lines
6.6 KiB
Go
272 lines
6.6 KiB
Go
package azure
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
|
|
"encoding/base64"
|
|
"github.com/Azure/azure-sdk-for-go/storage"
|
|
multierror "github.com/hashicorp/go-multierror"
|
|
uuid "github.com/hashicorp/go-uuid"
|
|
"github.com/hashicorp/terraform/state"
|
|
"github.com/hashicorp/terraform/state/remote"
|
|
"github.com/hashicorp/terraform/terraform"
|
|
)
|
|
|
|
const (
|
|
leaseHeader = "x-ms-lease-id"
|
|
// Must be lower case
|
|
lockInfoMetaKey = "terraformlockid"
|
|
)
|
|
|
|
type RemoteClient struct {
|
|
blobClient storage.BlobStorageClient
|
|
containerName string
|
|
keyName string
|
|
leaseID string
|
|
}
|
|
|
|
func (c *RemoteClient) Get() (*remote.Payload, error) {
|
|
containerReference := c.blobClient.GetContainerReference(c.containerName)
|
|
blobReference := containerReference.GetBlobReference(c.keyName)
|
|
options := &storage.GetBlobOptions{}
|
|
|
|
if c.leaseID != "" {
|
|
options.LeaseID = c.leaseID
|
|
}
|
|
|
|
blob, err := blobReference.Get(options)
|
|
if err != nil {
|
|
if storErr, ok := err.(storage.AzureStorageServiceError); ok {
|
|
if storErr.Code == "BlobNotFound" {
|
|
return nil, nil
|
|
}
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
defer blob.Close()
|
|
|
|
buf := bytes.NewBuffer(nil)
|
|
if _, err := io.Copy(buf, blob); err != nil {
|
|
return nil, fmt.Errorf("Failed to read remote state: %s", err)
|
|
}
|
|
|
|
payload := &remote.Payload{
|
|
Data: buf.Bytes(),
|
|
}
|
|
|
|
// If there was no data, then return nil
|
|
if len(payload.Data) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
return payload, nil
|
|
}
|
|
|
|
func (c *RemoteClient) Put(data []byte) error {
|
|
getOptions := &storage.GetBlobMetadataOptions{}
|
|
setOptions := &storage.SetBlobPropertiesOptions{}
|
|
putOptions := &storage.PutBlobOptions{}
|
|
|
|
containerReference := c.blobClient.GetContainerReference(c.containerName)
|
|
blobReference := containerReference.GetBlobReference(c.keyName)
|
|
|
|
blobReference.Properties.ContentType = "application/json"
|
|
blobReference.Properties.ContentLength = int64(len(data))
|
|
|
|
if c.leaseID != "" {
|
|
getOptions.LeaseID = c.leaseID
|
|
setOptions.LeaseID = c.leaseID
|
|
putOptions.LeaseID = c.leaseID
|
|
}
|
|
|
|
exists, err := blobReference.Exists()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if exists {
|
|
err = blobReference.GetMetadata(getOptions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
reader := bytes.NewReader(data)
|
|
|
|
err = blobReference.CreateBlockBlobFromReader(reader, putOptions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return blobReference.SetProperties(setOptions)
|
|
}
|
|
|
|
func (c *RemoteClient) Delete() error {
|
|
containerReference := c.blobClient.GetContainerReference(c.containerName)
|
|
blobReference := containerReference.GetBlobReference(c.keyName)
|
|
options := &storage.DeleteBlobOptions{}
|
|
|
|
if c.leaseID != "" {
|
|
options.LeaseID = c.leaseID
|
|
}
|
|
|
|
return blobReference.Delete(options)
|
|
}
|
|
|
|
func (c *RemoteClient) Lock(info *state.LockInfo) (string, error) {
|
|
stateName := fmt.Sprintf("%s/%s", c.containerName, c.keyName)
|
|
info.Path = stateName
|
|
|
|
if info.ID == "" {
|
|
lockID, err := uuid.GenerateUUID()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
info.ID = lockID
|
|
}
|
|
|
|
getLockInfoErr := func(err error) error {
|
|
lockInfo, infoErr := c.getLockInfo()
|
|
if infoErr != nil {
|
|
err = multierror.Append(err, infoErr)
|
|
}
|
|
|
|
return &state.LockError{
|
|
Err: err,
|
|
Info: lockInfo,
|
|
}
|
|
}
|
|
|
|
containerReference := c.blobClient.GetContainerReference(c.containerName)
|
|
blobReference := containerReference.GetBlobReference(c.keyName)
|
|
leaseID, err := blobReference.AcquireLease(-1, info.ID, &storage.LeaseOptions{})
|
|
if err != nil {
|
|
if storErr, ok := err.(storage.AzureStorageServiceError); ok && storErr.Code != "BlobNotFound" {
|
|
return "", getLockInfoErr(err)
|
|
}
|
|
|
|
// failed to lock as there was no state blob, write empty state
|
|
stateMgr := &remote.State{Client: c}
|
|
|
|
// ensure state is actually empty
|
|
if err := stateMgr.RefreshState(); err != nil {
|
|
return "", fmt.Errorf("Failed to refresh state before writing empty state for locking: %s", err)
|
|
}
|
|
|
|
log.Print("[DEBUG] Could not lock as state blob did not exist, creating with empty state")
|
|
|
|
if v := stateMgr.State(); v == nil {
|
|
if err := stateMgr.WriteState(terraform.NewState()); err != nil {
|
|
return "", fmt.Errorf("Failed to write empty state for locking: %s", err)
|
|
}
|
|
if err := stateMgr.PersistState(); err != nil {
|
|
return "", fmt.Errorf("Failed to persist empty state for locking: %s", err)
|
|
}
|
|
}
|
|
|
|
leaseID, err = blobReference.AcquireLease(-1, info.ID, &storage.LeaseOptions{})
|
|
if err != nil {
|
|
return "", getLockInfoErr(err)
|
|
}
|
|
}
|
|
|
|
info.ID = leaseID
|
|
c.leaseID = leaseID
|
|
|
|
if err := c.writeLockInfo(info); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return info.ID, nil
|
|
}
|
|
|
|
func (c *RemoteClient) getLockInfo() (*state.LockInfo, error) {
|
|
containerReference := c.blobClient.GetContainerReference(c.containerName)
|
|
blobReference := containerReference.GetBlobReference(c.keyName)
|
|
err := blobReference.GetMetadata(&storage.GetBlobMetadataOptions{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
raw := blobReference.Metadata[lockInfoMetaKey]
|
|
if raw == "" {
|
|
return nil, fmt.Errorf("blob metadata %q was empty", lockInfoMetaKey)
|
|
}
|
|
|
|
data, err := base64.StdEncoding.DecodeString(raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
lockInfo := &state.LockInfo{}
|
|
err = json.Unmarshal(data, lockInfo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return lockInfo, nil
|
|
}
|
|
|
|
// writes info to blob meta data, deletes metadata entry if info is nil
|
|
func (c *RemoteClient) writeLockInfo(info *state.LockInfo) error {
|
|
containerReference := c.blobClient.GetContainerReference(c.containerName)
|
|
blobReference := containerReference.GetBlobReference(c.keyName)
|
|
err := blobReference.GetMetadata(&storage.GetBlobMetadataOptions{
|
|
LeaseID: c.leaseID,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if info == nil {
|
|
delete(blobReference.Metadata, lockInfoMetaKey)
|
|
} else {
|
|
value := base64.StdEncoding.EncodeToString(info.Marshal())
|
|
blobReference.Metadata[lockInfoMetaKey] = value
|
|
}
|
|
|
|
opts := &storage.SetBlobMetadataOptions{
|
|
LeaseID: c.leaseID,
|
|
}
|
|
return blobReference.SetMetadata(opts)
|
|
}
|
|
|
|
func (c *RemoteClient) Unlock(id string) error {
|
|
lockErr := &state.LockError{}
|
|
|
|
lockInfo, err := c.getLockInfo()
|
|
if err != nil {
|
|
lockErr.Err = fmt.Errorf("failed to retrieve lock info: %s", err)
|
|
return lockErr
|
|
}
|
|
lockErr.Info = lockInfo
|
|
|
|
if lockInfo.ID != id {
|
|
lockErr.Err = fmt.Errorf("lock id %q does not match existing lock", id)
|
|
return lockErr
|
|
}
|
|
|
|
if err := c.writeLockInfo(nil); err != nil {
|
|
lockErr.Err = fmt.Errorf("failed to delete lock info from metadata: %s", err)
|
|
return lockErr
|
|
}
|
|
|
|
containerReference := c.blobClient.GetContainerReference(c.containerName)
|
|
blobReference := containerReference.GetBlobReference(c.keyName)
|
|
err = blobReference.ReleaseLease(id, &storage.LeaseOptions{})
|
|
if err != nil {
|
|
lockErr.Err = err
|
|
return lockErr
|
|
}
|
|
|
|
c.leaseID = ""
|
|
|
|
return nil
|
|
}
|