github.com/hugorut/terraform@v1.1.3/src/backend/remote-state/azure/client.go (about)

     1  package azure
     2  
     3  import (
     4  	"context"
     5  	"encoding/base64"
     6  	"encoding/json"
     7  	"fmt"
     8  	"log"
     9  	"net/http"
    10  
    11  	"github.com/hashicorp/go-multierror"
    12  	"github.com/hashicorp/go-uuid"
    13  	"github.com/tombuildsstuff/giovanni/storage/2018-11-09/blob/blobs"
    14  
    15  	"github.com/hugorut/terraform/src/states/remote"
    16  	"github.com/hugorut/terraform/src/states/statemgr"
    17  )
    18  
    19  const (
    20  	leaseHeader = "x-ms-lease-id"
    21  	// Must be lower case
    22  	lockInfoMetaKey = "terraformlockid"
    23  )
    24  
    25  type RemoteClient struct {
    26  	giovanniBlobClient blobs.Client
    27  	accountName        string
    28  	containerName      string
    29  	keyName            string
    30  	leaseID            string
    31  	snapshot           bool
    32  }
    33  
    34  func (c *RemoteClient) Get() (*remote.Payload, error) {
    35  	options := blobs.GetInput{}
    36  	if c.leaseID != "" {
    37  		options.LeaseID = &c.leaseID
    38  	}
    39  
    40  	ctx := context.TODO()
    41  	blob, err := c.giovanniBlobClient.Get(ctx, c.accountName, c.containerName, c.keyName, options)
    42  	if err != nil {
    43  		if blob.Response.IsHTTPStatus(http.StatusNotFound) {
    44  			return nil, nil
    45  		}
    46  		return nil, err
    47  	}
    48  
    49  	payload := &remote.Payload{
    50  		Data: blob.Contents,
    51  	}
    52  
    53  	// If there was no data, then return nil
    54  	if len(payload.Data) == 0 {
    55  		return nil, nil
    56  	}
    57  
    58  	return payload, nil
    59  }
    60  
    61  func (c *RemoteClient) Put(data []byte) error {
    62  	getOptions := blobs.GetPropertiesInput{}
    63  	setOptions := blobs.SetPropertiesInput{}
    64  	putOptions := blobs.PutBlockBlobInput{}
    65  
    66  	options := blobs.GetInput{}
    67  	if c.leaseID != "" {
    68  		options.LeaseID = &c.leaseID
    69  		getOptions.LeaseID = &c.leaseID
    70  		setOptions.LeaseID = &c.leaseID
    71  		putOptions.LeaseID = &c.leaseID
    72  	}
    73  
    74  	ctx := context.TODO()
    75  
    76  	if c.snapshot {
    77  		snapshotInput := blobs.SnapshotInput{LeaseID: options.LeaseID}
    78  
    79  		log.Printf("[DEBUG] Snapshotting existing Blob %q (Container %q / Account %q)", c.keyName, c.containerName, c.accountName)
    80  		if _, err := c.giovanniBlobClient.Snapshot(ctx, c.accountName, c.containerName, c.keyName, snapshotInput); err != nil {
    81  			return fmt.Errorf("error snapshotting Blob %q (Container %q / Account %q): %+v", c.keyName, c.containerName, c.accountName, err)
    82  		}
    83  
    84  		log.Print("[DEBUG] Created blob snapshot")
    85  	}
    86  
    87  	blob, err := c.giovanniBlobClient.GetProperties(ctx, c.accountName, c.containerName, c.keyName, getOptions)
    88  	if err != nil {
    89  		if blob.StatusCode != 404 {
    90  			return err
    91  		}
    92  	}
    93  
    94  	contentType := "application/json"
    95  	putOptions.Content = &data
    96  	putOptions.ContentType = &contentType
    97  	putOptions.MetaData = blob.MetaData
    98  	_, err = c.giovanniBlobClient.PutBlockBlob(ctx, c.accountName, c.containerName, c.keyName, putOptions)
    99  
   100  	return err
   101  }
   102  
   103  func (c *RemoteClient) Delete() error {
   104  	options := blobs.DeleteInput{}
   105  
   106  	if c.leaseID != "" {
   107  		options.LeaseID = &c.leaseID
   108  	}
   109  
   110  	ctx := context.TODO()
   111  	resp, err := c.giovanniBlobClient.Delete(ctx, c.accountName, c.containerName, c.keyName, options)
   112  	if err != nil {
   113  		if !resp.IsHTTPStatus(http.StatusNotFound) {
   114  			return err
   115  		}
   116  	}
   117  	return nil
   118  }
   119  
   120  func (c *RemoteClient) Lock(info *statemgr.LockInfo) (string, error) {
   121  	stateName := fmt.Sprintf("%s/%s", c.containerName, c.keyName)
   122  	info.Path = stateName
   123  
   124  	if info.ID == "" {
   125  		lockID, err := uuid.GenerateUUID()
   126  		if err != nil {
   127  			return "", err
   128  		}
   129  
   130  		info.ID = lockID
   131  	}
   132  
   133  	getLockInfoErr := func(err error) error {
   134  		lockInfo, infoErr := c.getLockInfo()
   135  		if infoErr != nil {
   136  			err = multierror.Append(err, infoErr)
   137  		}
   138  
   139  		return &statemgr.LockError{
   140  			Err:  err,
   141  			Info: lockInfo,
   142  		}
   143  	}
   144  
   145  	leaseOptions := blobs.AcquireLeaseInput{
   146  		ProposedLeaseID: &info.ID,
   147  		LeaseDuration:   -1,
   148  	}
   149  	ctx := context.TODO()
   150  
   151  	// obtain properties to see if the blob lease is already in use. If the blob doesn't exist, create it
   152  	properties, err := c.giovanniBlobClient.GetProperties(ctx, c.accountName, c.containerName, c.keyName, blobs.GetPropertiesInput{})
   153  	if err != nil {
   154  		// error if we had issues getting the blob
   155  		if !properties.Response.IsHTTPStatus(http.StatusNotFound) {
   156  			return "", getLockInfoErr(err)
   157  		}
   158  		// if we don't find the blob, we need to build it
   159  
   160  		contentType := "application/json"
   161  		putGOptions := blobs.PutBlockBlobInput{
   162  			ContentType: &contentType,
   163  		}
   164  
   165  		_, err = c.giovanniBlobClient.PutBlockBlob(ctx, c.accountName, c.containerName, c.keyName, putGOptions)
   166  		if err != nil {
   167  			return "", getLockInfoErr(err)
   168  		}
   169  	}
   170  
   171  	// if the blob is already locked then error
   172  	if properties.LeaseStatus == blobs.Locked {
   173  		return "", getLockInfoErr(fmt.Errorf("state blob is already locked"))
   174  	}
   175  
   176  	leaseID, err := c.giovanniBlobClient.AcquireLease(ctx, c.accountName, c.containerName, c.keyName, leaseOptions)
   177  	if err != nil {
   178  		return "", getLockInfoErr(err)
   179  	}
   180  
   181  	info.ID = leaseID.LeaseID
   182  	c.leaseID = leaseID.LeaseID
   183  
   184  	if err := c.writeLockInfo(info); err != nil {
   185  		return "", err
   186  	}
   187  
   188  	return info.ID, nil
   189  }
   190  
   191  func (c *RemoteClient) getLockInfo() (*statemgr.LockInfo, error) {
   192  	options := blobs.GetPropertiesInput{}
   193  	if c.leaseID != "" {
   194  		options.LeaseID = &c.leaseID
   195  	}
   196  
   197  	ctx := context.TODO()
   198  	blob, err := c.giovanniBlobClient.GetProperties(ctx, c.accountName, c.containerName, c.keyName, options)
   199  	if err != nil {
   200  		return nil, err
   201  	}
   202  
   203  	raw := blob.MetaData[lockInfoMetaKey]
   204  	if raw == "" {
   205  		return nil, fmt.Errorf("blob metadata %q was empty", lockInfoMetaKey)
   206  	}
   207  
   208  	data, err := base64.StdEncoding.DecodeString(raw)
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  
   213  	lockInfo := &statemgr.LockInfo{}
   214  	err = json.Unmarshal(data, lockInfo)
   215  	if err != nil {
   216  		return nil, err
   217  	}
   218  
   219  	return lockInfo, nil
   220  }
   221  
   222  // writes info to blob meta data, deletes metadata entry if info is nil
   223  func (c *RemoteClient) writeLockInfo(info *statemgr.LockInfo) error {
   224  	ctx := context.TODO()
   225  	blob, err := c.giovanniBlobClient.GetProperties(ctx, c.accountName, c.containerName, c.keyName, blobs.GetPropertiesInput{LeaseID: &c.leaseID})
   226  	if err != nil {
   227  		return err
   228  	}
   229  	if err != nil {
   230  		return err
   231  	}
   232  
   233  	if info == nil {
   234  		delete(blob.MetaData, lockInfoMetaKey)
   235  	} else {
   236  		value := base64.StdEncoding.EncodeToString(info.Marshal())
   237  		blob.MetaData[lockInfoMetaKey] = value
   238  	}
   239  
   240  	opts := blobs.SetMetaDataInput{
   241  		LeaseID:  &c.leaseID,
   242  		MetaData: blob.MetaData,
   243  	}
   244  
   245  	_, err = c.giovanniBlobClient.SetMetaData(ctx, c.accountName, c.containerName, c.keyName, opts)
   246  	return err
   247  }
   248  
   249  func (c *RemoteClient) Unlock(id string) error {
   250  	lockErr := &statemgr.LockError{}
   251  
   252  	lockInfo, err := c.getLockInfo()
   253  	if err != nil {
   254  		lockErr.Err = fmt.Errorf("failed to retrieve lock info: %s", err)
   255  		return lockErr
   256  	}
   257  	lockErr.Info = lockInfo
   258  
   259  	if lockInfo.ID != id {
   260  		lockErr.Err = fmt.Errorf("lock id %q does not match existing lock", id)
   261  		return lockErr
   262  	}
   263  
   264  	c.leaseID = lockInfo.ID
   265  	if err := c.writeLockInfo(nil); err != nil {
   266  		lockErr.Err = fmt.Errorf("failed to delete lock info from metadata: %s", err)
   267  		return lockErr
   268  	}
   269  
   270  	ctx := context.TODO()
   271  	_, err = c.giovanniBlobClient.ReleaseLease(ctx, c.accountName, c.containerName, c.keyName, id)
   272  	if err != nil {
   273  		lockErr.Err = err
   274  		return lockErr
   275  	}
   276  
   277  	c.leaseID = ""
   278  
   279  	return nil
   280  }