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