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