opentofu/internal/backend/remote-state/azure/client.go
2023-08-27 12:03:53 +03:00

283 lines
6.9 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package azure
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-uuid"
"github.com/placeholderplaceholderplaceholder/opentf/internal/states/remote"
"github.com/placeholderplaceholderplaceholder/opentf/internal/states/statemgr"
"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
snapshot bool
}
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.IsHTTPStatus(http.StatusNotFound) {
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()
if c.snapshot {
snapshotInput := blobs.SnapshotInput{LeaseID: options.LeaseID}
log.Printf("[DEBUG] Snapshotting existing Blob %q (Container %q / Account %q)", c.keyName, c.containerName, c.accountName)
if _, err := c.giovanniBlobClient.Snapshot(ctx, c.accountName, c.containerName, c.keyName, snapshotInput); err != nil {
return fmt.Errorf("error snapshotting Blob %q (Container %q / Account %q): %+v", c.keyName, c.containerName, c.accountName, err)
}
log.Print("[DEBUG] Created blob snapshot")
}
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.IsHTTPStatus(http.StatusNotFound) {
return err
}
}
return nil
}
func (c *RemoteClient) Lock(info *statemgr.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 &statemgr.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.IsHTTPStatus(http.StatusNotFound) {
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() (*statemgr.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 := &statemgr.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 *statemgr.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 := &statemgr.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
}