github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/remote-state/azure/client.go (about)

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