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 }