mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 01:41:48 -06:00
f40800b3a4
This is part of a general effort to move all of Terraform's non-library package surface under internal in order to reinforce that these are for internal use within Terraform only. If you were previously importing packages under this prefix into an external codebase, you could pin to an earlier release tag as an interim solution until you've make a plan to achieve the same functionality some other way.
281 lines
6.8 KiB
Go
281 lines
6.8 KiB
Go
package azure
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
|
|
"github.com/hashicorp/go-multierror"
|
|
"github.com/hashicorp/go-uuid"
|
|
"github.com/tombuildsstuff/giovanni/storage/2018-11-09/blob/blobs"
|
|
|
|
"github.com/hashicorp/terraform/internal/states/remote"
|
|
"github.com/hashicorp/terraform/internal/states/statemgr"
|
|
)
|
|
|
|
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
|
|
}
|