package azure import ( "context" "encoding/base64" "encoding/json" "fmt" "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-uuid" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state/remote" "github.com/tombuildsstuff/giovanni/storage/2018-11-09/blob/blobs" ) const ( leaseHeader = "x-ms-lease-id" // Must be lower case lockInfoMetaKey = "terraformlockid" ) type RemoteClient struct { giovanniBlobClient blobs.Client accountName string containerName string keyName string leaseID string } func (c *RemoteClient) Get() (*remote.Payload, error) { options := blobs.GetInput{} if c.leaseID != "" { options.LeaseID = &c.leaseID } ctx := context.TODO() blob, err := c.giovanniBlobClient.Get(ctx, c.accountName, c.containerName, c.keyName, options) if err != nil { if blob.Response.StatusCode == 404 { return nil, nil } return nil, err } payload := &remote.Payload{ Data: blob.Contents, } // 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 := blobs.GetPropertiesInput{} setOptions := blobs.SetPropertiesInput{} putOptions := blobs.PutBlockBlobInput{} options := blobs.GetInput{} if c.leaseID != "" { options.LeaseID = &c.leaseID getOptions.LeaseID = &c.leaseID setOptions.LeaseID = &c.leaseID putOptions.LeaseID = &c.leaseID } ctx := context.TODO() blob, err := c.giovanniBlobClient.GetProperties(ctx, c.accountName, c.containerName, c.keyName, getOptions) if err != nil { if blob.StatusCode != 404 { return err } } contentType := "application/json" putOptions.Content = &data putOptions.ContentType = &contentType putOptions.MetaData = blob.MetaData _, err = c.giovanniBlobClient.PutBlockBlob(ctx, c.accountName, c.containerName, c.keyName, putOptions) return err } func (c *RemoteClient) Delete() error { options := blobs.DeleteInput{} if c.leaseID != "" { options.LeaseID = &c.leaseID } ctx := context.TODO() resp, err := c.giovanniBlobClient.Delete(ctx, c.accountName, c.containerName, c.keyName, options) if err != nil { if resp.Response.StatusCode != 404 { return err } } return nil } 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, } } leaseOptions := blobs.AcquireLeaseInput{ ProposedLeaseID: &info.ID, LeaseDuration: -1, } ctx := context.TODO() // obtain properties to see if the blob lease is already in use. If the blob doesn't exist, create it properties, err := c.giovanniBlobClient.GetProperties(ctx, c.accountName, c.containerName, c.keyName, blobs.GetPropertiesInput{}) if err != nil { // error if we had issues getting the blob if properties.Response.StatusCode != 404 { return "", getLockInfoErr(err) } // if we don't find the blob, we need to build it contentType := "application/json" putGOptions := blobs.PutBlockBlobInput{ ContentType: &contentType, } _, err = c.giovanniBlobClient.PutBlockBlob(ctx, c.accountName, c.containerName, c.keyName, putGOptions) if err != nil { return "", getLockInfoErr(err) } } // if the blob is already locked then error if properties.LeaseStatus == blobs.Locked { return "", getLockInfoErr(fmt.Errorf("state blob is already locked")) } leaseID, err := c.giovanniBlobClient.AcquireLease(ctx, c.accountName, c.containerName, c.keyName, leaseOptions) if err != nil { return "", getLockInfoErr(err) } info.ID = leaseID.LeaseID c.leaseID = leaseID.LeaseID if err := c.writeLockInfo(info); err != nil { return "", err } return info.ID, nil } func (c *RemoteClient) getLockInfo() (*state.LockInfo, error) { options := blobs.GetPropertiesInput{} if c.leaseID != "" { options.LeaseID = &c.leaseID } ctx := context.TODO() blob, err := c.giovanniBlobClient.GetProperties(ctx, c.accountName, c.containerName, c.keyName, options) if err != nil { return nil, err } raw := blob.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 { ctx := context.TODO() blob, err := c.giovanniBlobClient.GetProperties(ctx, c.accountName, c.containerName, c.keyName, blobs.GetPropertiesInput{LeaseID: &c.leaseID}) if err != nil { return err } if err != nil { return err } if info == nil { delete(blob.MetaData, lockInfoMetaKey) } else { value := base64.StdEncoding.EncodeToString(info.Marshal()) blob.MetaData[lockInfoMetaKey] = value } opts := blobs.SetMetaDataInput{ LeaseID: &c.leaseID, MetaData: blob.MetaData, } _, err = c.giovanniBlobClient.SetMetaData(ctx, c.accountName, c.containerName, c.keyName, opts) return err } 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 } c.leaseID = lockInfo.ID if err := c.writeLockInfo(nil); err != nil { lockErr.Err = fmt.Errorf("failed to delete lock info from metadata: %s", err) return lockErr } ctx := context.TODO() _, err = c.giovanniBlobClient.ReleaseLease(ctx, c.accountName, c.containerName, c.keyName, id) if err != nil { lockErr.Err = err return lockErr } c.leaseID = "" return nil }