github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/backend/remote-state/oss/client.go (about) 1 package oss 2 3 import ( 4 "bytes" 5 "crypto/md5" 6 "encoding/hex" 7 "encoding/json" 8 "fmt" 9 "io" 10 "log" 11 "time" 12 13 "github.com/aliyun/aliyun-oss-go-sdk/oss" 14 "github.com/aliyun/aliyun-tablestore-go-sdk/tablestore" 15 "github.com/hashicorp/go-multierror" 16 uuid "github.com/hashicorp/go-uuid" 17 "github.com/pkg/errors" 18 19 "github.com/hashicorp/terraform/internal/states/remote" 20 "github.com/hashicorp/terraform/internal/states/statemgr" 21 ) 22 23 const ( 24 // Store the last saved serial in tablestore with this suffix for consistency checks. 25 stateIDSuffix = "-md5" 26 27 pkName = "LockID" 28 ) 29 30 var ( 31 // The amount of time we will retry a state waiting for it to match the 32 // expected checksum. 33 consistencyRetryTimeout = 10 * time.Second 34 35 // delay when polling the state 36 consistencyRetryPollInterval = 2 * time.Second 37 ) 38 39 // test hook called when checksums don't match 40 var testChecksumHook func() 41 42 type RemoteClient struct { 43 ossClient *oss.Client 44 otsClient *tablestore.TableStoreClient 45 bucketName string 46 stateFile string 47 lockFile string 48 serverSideEncryption bool 49 acl string 50 otsTable string 51 } 52 53 func (c *RemoteClient) Get() (payload *remote.Payload, err error) { 54 deadline := time.Now().Add(consistencyRetryTimeout) 55 56 // If we have a checksum, and the returned payload doesn't match, we retry 57 // up until deadline. 58 for { 59 payload, err = c.getObj() 60 if err != nil { 61 return nil, err 62 } 63 64 // If the remote state was manually removed the payload will be nil, 65 // but if there's still a digest entry for that state we will still try 66 // to compare the MD5 below. 67 var digest []byte 68 if payload != nil { 69 digest = payload.MD5 70 } 71 72 // verify that this state is what we expect 73 if expected, err := c.getMD5(); err != nil { 74 log.Printf("[WARN] failed to fetch state md5: %s", err) 75 } else if len(expected) > 0 && !bytes.Equal(expected, digest) { 76 log.Printf("[WARN] state md5 mismatch: expected '%x', got '%x'", expected, digest) 77 78 if testChecksumHook != nil { 79 testChecksumHook() 80 } 81 82 if time.Now().Before(deadline) { 83 time.Sleep(consistencyRetryPollInterval) 84 log.Println("[INFO] retrying OSS RemoteClient.Get...") 85 continue 86 } 87 88 return nil, fmt.Errorf(errBadChecksumFmt, digest) 89 } 90 91 break 92 } 93 return payload, nil 94 } 95 96 func (c *RemoteClient) Put(data []byte) error { 97 bucket, err := c.ossClient.Bucket(c.bucketName) 98 if err != nil { 99 return fmt.Errorf("error getting bucket: %#v", err) 100 } 101 102 body := bytes.NewReader(data) 103 104 var options []oss.Option 105 if c.acl != "" { 106 options = append(options, oss.ACL(oss.ACLType(c.acl))) 107 } 108 options = append(options, oss.ContentType("application/json")) 109 if c.serverSideEncryption { 110 options = append(options, oss.ServerSideEncryption("AES256")) 111 } 112 options = append(options, oss.ContentLength(int64(len(data)))) 113 114 if body != nil { 115 if err := bucket.PutObject(c.stateFile, body, options...); err != nil { 116 return fmt.Errorf("failed to upload state %s: %#v", c.stateFile, err) 117 } 118 } 119 120 sum := md5.Sum(data) 121 if err := c.putMD5(sum[:]); err != nil { 122 // if this errors out, we unfortunately have to error out altogether, 123 // since the next Get will inevitably fail. 124 return fmt.Errorf("failed to store state MD5: %s", err) 125 } 126 return nil 127 } 128 129 func (c *RemoteClient) Delete() error { 130 bucket, err := c.ossClient.Bucket(c.bucketName) 131 if err != nil { 132 return fmt.Errorf("error getting bucket %s: %#v", c.bucketName, err) 133 } 134 135 log.Printf("[DEBUG] Deleting remote state from OSS: %#v", c.stateFile) 136 137 if err := bucket.DeleteObject(c.stateFile); err != nil { 138 return fmt.Errorf("error deleting state %s: %#v", c.stateFile, err) 139 } 140 141 if err := c.deleteMD5(); err != nil { 142 log.Printf("[WARN] Error deleting state MD5: %s", err) 143 } 144 return nil 145 } 146 147 func (c *RemoteClient) Lock(info *statemgr.LockInfo) (string, error) { 148 if c.otsTable == "" { 149 return "", nil 150 } 151 152 info.Path = c.lockPath() 153 154 if info.ID == "" { 155 lockID, err := uuid.GenerateUUID() 156 if err != nil { 157 return "", err 158 } 159 info.ID = lockID 160 } 161 162 putParams := &tablestore.PutRowChange{ 163 TableName: c.otsTable, 164 PrimaryKey: &tablestore.PrimaryKey{ 165 PrimaryKeys: []*tablestore.PrimaryKeyColumn{ 166 { 167 ColumnName: pkName, 168 Value: c.lockPath(), 169 }, 170 }, 171 }, 172 Columns: []tablestore.AttributeColumn{ 173 { 174 ColumnName: "Info", 175 Value: string(info.Marshal()), 176 }, 177 }, 178 Condition: &tablestore.RowCondition{ 179 RowExistenceExpectation: tablestore.RowExistenceExpectation_EXPECT_NOT_EXIST, 180 }, 181 } 182 183 log.Printf("[DEBUG] Recording state lock in tablestore: %#v; LOCKID:%s", putParams, c.lockPath()) 184 185 _, err := c.otsClient.PutRow(&tablestore.PutRowRequest{ 186 PutRowChange: putParams, 187 }) 188 if err != nil { 189 err = fmt.Errorf("invoking PutRow got an error: %#v", err) 190 lockInfo, infoErr := c.getLockInfo() 191 if infoErr != nil { 192 err = multierror.Append(err, fmt.Errorf("\ngetting lock info got an error: %#v", infoErr)) 193 } 194 lockErr := &statemgr.LockError{ 195 Err: err, 196 Info: lockInfo, 197 } 198 log.Printf("[ERROR] state lock error: %s", lockErr.Error()) 199 return "", lockErr 200 } 201 202 return info.ID, nil 203 } 204 205 func (c *RemoteClient) getMD5() ([]byte, error) { 206 if c.otsTable == "" { 207 return nil, nil 208 } 209 210 getParams := &tablestore.SingleRowQueryCriteria{ 211 TableName: c.otsTable, 212 PrimaryKey: &tablestore.PrimaryKey{ 213 PrimaryKeys: []*tablestore.PrimaryKeyColumn{ 214 { 215 ColumnName: pkName, 216 Value: c.lockPath() + stateIDSuffix, 217 }, 218 }, 219 }, 220 ColumnsToGet: []string{pkName, "Digest"}, 221 MaxVersion: 1, 222 } 223 224 log.Printf("[DEBUG] Retrieving state serial in tablestore: %#v", getParams) 225 226 object, err := c.otsClient.GetRow(&tablestore.GetRowRequest{ 227 SingleRowQueryCriteria: getParams, 228 }) 229 230 if err != nil { 231 return nil, err 232 } 233 234 var val string 235 if v, ok := object.GetColumnMap().Columns["Digest"]; ok && len(v) > 0 { 236 val = v[0].Value.(string) 237 } 238 239 sum, err := hex.DecodeString(val) 240 if err != nil || len(sum) != md5.Size { 241 return nil, errors.New("invalid md5") 242 } 243 244 return sum, nil 245 } 246 247 // store the hash of the state to that clients can check for stale state files. 248 func (c *RemoteClient) putMD5(sum []byte) error { 249 if c.otsTable == "" { 250 return nil 251 } 252 253 if len(sum) != md5.Size { 254 return errors.New("invalid payload md5") 255 } 256 257 putParams := &tablestore.PutRowChange{ 258 TableName: c.otsTable, 259 PrimaryKey: &tablestore.PrimaryKey{ 260 PrimaryKeys: []*tablestore.PrimaryKeyColumn{ 261 { 262 ColumnName: pkName, 263 Value: c.lockPath() + stateIDSuffix, 264 }, 265 }, 266 }, 267 Columns: []tablestore.AttributeColumn{ 268 { 269 ColumnName: "Digest", 270 Value: hex.EncodeToString(sum), 271 }, 272 }, 273 Condition: &tablestore.RowCondition{ 274 RowExistenceExpectation: tablestore.RowExistenceExpectation_IGNORE, 275 }, 276 } 277 278 log.Printf("[DEBUG] Recoring state serial in tablestore: %#v", putParams) 279 280 _, err := c.otsClient.PutRow(&tablestore.PutRowRequest{ 281 PutRowChange: putParams, 282 }) 283 284 if err != nil { 285 log.Printf("[WARN] failed to record state serial in tablestore: %s", err) 286 } 287 288 return nil 289 } 290 291 // remove the hash value for a deleted state 292 func (c *RemoteClient) deleteMD5() error { 293 if c.otsTable == "" { 294 return nil 295 } 296 297 params := &tablestore.DeleteRowRequest{ 298 DeleteRowChange: &tablestore.DeleteRowChange{ 299 TableName: c.otsTable, 300 PrimaryKey: &tablestore.PrimaryKey{ 301 PrimaryKeys: []*tablestore.PrimaryKeyColumn{ 302 { 303 ColumnName: pkName, 304 Value: c.lockPath() + stateIDSuffix, 305 }, 306 }, 307 }, 308 Condition: &tablestore.RowCondition{ 309 RowExistenceExpectation: tablestore.RowExistenceExpectation_EXPECT_EXIST, 310 }, 311 }, 312 } 313 314 log.Printf("[DEBUG] Deleting state serial in tablestore: %#v", params) 315 316 if _, err := c.otsClient.DeleteRow(params); err != nil { 317 return err 318 } 319 320 return nil 321 } 322 323 func (c *RemoteClient) getLockInfo() (*statemgr.LockInfo, error) { 324 getParams := &tablestore.SingleRowQueryCriteria{ 325 TableName: c.otsTable, 326 PrimaryKey: &tablestore.PrimaryKey{ 327 PrimaryKeys: []*tablestore.PrimaryKeyColumn{ 328 { 329 ColumnName: pkName, 330 Value: c.lockPath(), 331 }, 332 }, 333 }, 334 ColumnsToGet: []string{pkName, "Info"}, 335 MaxVersion: 1, 336 } 337 338 log.Printf("[DEBUG] Retrieving state lock info from tablestore: %#v", getParams) 339 340 object, err := c.otsClient.GetRow(&tablestore.GetRowRequest{ 341 SingleRowQueryCriteria: getParams, 342 }) 343 if err != nil { 344 return nil, err 345 } 346 347 var infoData string 348 if v, ok := object.GetColumnMap().Columns["Info"]; ok && len(v) > 0 { 349 infoData = v[0].Value.(string) 350 } 351 lockInfo := &statemgr.LockInfo{} 352 err = json.Unmarshal([]byte(infoData), lockInfo) 353 if err != nil { 354 return nil, err 355 } 356 return lockInfo, nil 357 } 358 func (c *RemoteClient) Unlock(id string) error { 359 if c.otsTable == "" { 360 return nil 361 } 362 363 lockErr := &statemgr.LockError{} 364 365 lockInfo, err := c.getLockInfo() 366 if err != nil { 367 lockErr.Err = fmt.Errorf("failed to retrieve lock info: %s", err) 368 return lockErr 369 } 370 lockErr.Info = lockInfo 371 372 if lockInfo.ID != id { 373 lockErr.Err = fmt.Errorf("lock id %q does not match existing lock", id) 374 return lockErr 375 } 376 params := &tablestore.DeleteRowRequest{ 377 DeleteRowChange: &tablestore.DeleteRowChange{ 378 TableName: c.otsTable, 379 PrimaryKey: &tablestore.PrimaryKey{ 380 PrimaryKeys: []*tablestore.PrimaryKeyColumn{ 381 { 382 ColumnName: pkName, 383 Value: c.lockPath(), 384 }, 385 }, 386 }, 387 Condition: &tablestore.RowCondition{ 388 RowExistenceExpectation: tablestore.RowExistenceExpectation_IGNORE, 389 }, 390 }, 391 } 392 393 _, err = c.otsClient.DeleteRow(params) 394 395 if err != nil { 396 lockErr.Err = err 397 return lockErr 398 } 399 400 return nil 401 } 402 403 func (c *RemoteClient) lockPath() string { 404 return fmt.Sprintf("%s/%s", c.bucketName, c.stateFile) 405 } 406 407 func (c *RemoteClient) getObj() (*remote.Payload, error) { 408 bucket, err := c.ossClient.Bucket(c.bucketName) 409 if err != nil { 410 return nil, fmt.Errorf("error getting bucket %s: %#v", c.bucketName, err) 411 } 412 413 if exist, err := bucket.IsObjectExist(c.stateFile); err != nil { 414 return nil, fmt.Errorf("estimating object %s is exist got an error: %#v", c.stateFile, err) 415 } else if !exist { 416 return nil, nil 417 } 418 419 var options []oss.Option 420 output, err := bucket.GetObject(c.stateFile, options...) 421 if err != nil { 422 return nil, fmt.Errorf("error getting object: %#v", err) 423 } 424 425 buf := bytes.NewBuffer(nil) 426 if _, err := io.Copy(buf, output); err != nil { 427 return nil, fmt.Errorf("failed to read remote state: %s", err) 428 } 429 sum := md5.Sum(buf.Bytes()) 430 payload := &remote.Payload{ 431 Data: buf.Bytes(), 432 MD5: sum[:], 433 } 434 435 // If there was no data, then return nil 436 if len(payload.Data) == 0 { 437 return nil, nil 438 } 439 440 return payload, nil 441 } 442 443 const errBadChecksumFmt = `state data in OSS does not have the expected content. 444 445 This may be caused by unusually long delays in OSS processing a previous state 446 update. Please wait for a minute or two and try again. If this problem 447 persists, and neither OSS nor TableStore are experiencing an outage, you may need 448 to manually verify the remote state and update the Digest value stored in the 449 TableStore table to the following value: %x`