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