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