github.com/opentofu/opentofu@v1.7.1/internal/backend/remote-state/cos/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 cos 7 8 import ( 9 "bytes" 10 "context" 11 "crypto/md5" 12 "encoding/json" 13 "fmt" 14 "io" 15 "log" 16 "net/http" 17 "strings" 18 "time" 19 20 multierror "github.com/hashicorp/go-multierror" 21 "github.com/opentofu/opentofu/internal/states/remote" 22 "github.com/opentofu/opentofu/internal/states/statemgr" 23 tag "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813" 24 "github.com/tencentyun/cos-go-sdk-v5" 25 ) 26 27 const ( 28 lockTagKey = "tencentcloud-terraform-lock" 29 ) 30 31 // RemoteClient implements the client of remote state 32 type remoteClient struct { 33 cosContext context.Context 34 cosClient *cos.Client 35 tagClient *tag.Client 36 37 bucket string 38 stateFile string 39 lockFile string 40 encrypt bool 41 acl string 42 } 43 44 // Get returns remote state file 45 func (c *remoteClient) Get() (*remote.Payload, error) { 46 log.Printf("[DEBUG] get remote state file %s", c.stateFile) 47 48 exists, data, checksum, err := c.getObject(c.stateFile) 49 if err != nil { 50 return nil, err 51 } 52 53 if !exists { 54 return nil, nil 55 } 56 57 payload := &remote.Payload{ 58 Data: data, 59 MD5: []byte(checksum), 60 } 61 62 return payload, nil 63 } 64 65 // Put put state file to remote 66 func (c *remoteClient) Put(data []byte) error { 67 log.Printf("[DEBUG] put remote state file %s", c.stateFile) 68 69 return c.putObject(c.stateFile, data) 70 } 71 72 // Delete delete remote state file 73 func (c *remoteClient) Delete() error { 74 log.Printf("[DEBUG] delete remote state file %s", c.stateFile) 75 76 return c.deleteObject(c.stateFile) 77 } 78 79 // Lock lock remote state file for writing 80 func (c *remoteClient) Lock(info *statemgr.LockInfo) (string, error) { 81 log.Printf("[DEBUG] lock remote state file %s", c.lockFile) 82 83 err := c.cosLock(c.bucket, c.lockFile) 84 if err != nil { 85 return "", c.lockError(err) 86 } 87 defer c.cosUnlock(c.bucket, c.lockFile) 88 89 exists, _, _, err := c.getObject(c.lockFile) 90 if err != nil { 91 return "", c.lockError(err) 92 } 93 94 if exists { 95 return "", c.lockError(fmt.Errorf("lock file %s exists", c.lockFile)) 96 } 97 98 info.Path = c.lockFile 99 data, err := json.Marshal(info) 100 if err != nil { 101 return "", c.lockError(err) 102 } 103 104 check := fmt.Sprintf("%x", md5.Sum(data)) 105 err = c.putObject(c.lockFile, data) 106 if err != nil { 107 return "", c.lockError(err) 108 } 109 110 return check, nil 111 } 112 113 // Unlock unlock remote state file 114 func (c *remoteClient) Unlock(check string) error { 115 log.Printf("[DEBUG] unlock remote state file %s", c.lockFile) 116 117 info, err := c.lockInfo() 118 if err != nil { 119 return c.lockError(err) 120 } 121 122 if info.ID != check { 123 return c.lockError(fmt.Errorf("lock id mismatch, %v != %v", info.ID, check)) 124 } 125 126 err = c.deleteObject(c.lockFile) 127 if err != nil { 128 return c.lockError(err) 129 } 130 131 err = c.cosUnlock(c.bucket, c.lockFile) 132 if err != nil { 133 return c.lockError(err) 134 } 135 136 return nil 137 } 138 139 // lockError returns statemgr.LockError 140 func (c *remoteClient) lockError(err error) *statemgr.LockError { 141 log.Printf("[DEBUG] failed to lock or unlock %s: %v", c.lockFile, err) 142 143 lockErr := &statemgr.LockError{ 144 Err: err, 145 } 146 147 info, infoErr := c.lockInfo() 148 if infoErr != nil { 149 lockErr.Err = multierror.Append(lockErr.Err, infoErr) 150 } else { 151 lockErr.Info = info 152 } 153 154 return lockErr 155 } 156 157 // lockInfo returns LockInfo from lock file 158 func (c *remoteClient) lockInfo() (*statemgr.LockInfo, error) { 159 exists, data, checksum, err := c.getObject(c.lockFile) 160 if err != nil { 161 return nil, err 162 } 163 164 if !exists { 165 return nil, fmt.Errorf("lock file %s not exists", c.lockFile) 166 } 167 168 info := &statemgr.LockInfo{} 169 if err := json.Unmarshal(data, info); err != nil { 170 return nil, err 171 } 172 173 info.ID = checksum 174 175 return info, nil 176 } 177 178 // getObject get remote object 179 func (c *remoteClient) getObject(cosFile string) (exists bool, data []byte, checksum string, err error) { 180 rsp, err := c.cosClient.Object.Get(c.cosContext, cosFile, nil) 181 if rsp == nil { 182 log.Printf("[DEBUG] getObject %s: error: %v", cosFile, err) 183 err = fmt.Errorf("failed to open file at %v: %w", cosFile, err) 184 return 185 } 186 defer rsp.Body.Close() 187 188 log.Printf("[DEBUG] getObject %s: code: %d, error: %v", cosFile, rsp.StatusCode, err) 189 if err != nil { 190 if rsp.StatusCode == 404 { 191 err = nil 192 } else { 193 err = fmt.Errorf("failed to open file at %v: %w", cosFile, err) 194 } 195 return 196 } 197 198 checksum = rsp.Header.Get("X-Cos-Meta-Md5") 199 log.Printf("[DEBUG] getObject %s: checksum: %s", cosFile, checksum) 200 if len(checksum) != 32 { 201 err = fmt.Errorf("failed to open file at %v: checksum %s invalid", cosFile, checksum) 202 return 203 } 204 205 exists = true 206 data, err = io.ReadAll(rsp.Body) 207 log.Printf("[DEBUG] getObject %s: data length: %d", cosFile, len(data)) 208 if err != nil { 209 err = fmt.Errorf("failed to open file at %v: %w", cosFile, err) 210 return 211 } 212 213 check := fmt.Sprintf("%x", md5.Sum(data)) 214 log.Printf("[DEBUG] getObject %s: check: %s", cosFile, check) 215 if check != checksum { 216 err = fmt.Errorf("failed to open file at %v: checksum mismatch, %s != %s", cosFile, check, checksum) 217 return 218 } 219 220 return 221 } 222 223 // putObject put object to remote 224 func (c *remoteClient) putObject(cosFile string, data []byte) error { 225 opt := &cos.ObjectPutOptions{ 226 ObjectPutHeaderOptions: &cos.ObjectPutHeaderOptions{ 227 XCosMetaXXX: &http.Header{ 228 "X-Cos-Meta-Md5": []string{fmt.Sprintf("%x", md5.Sum(data))}, 229 }, 230 }, 231 ACLHeaderOptions: &cos.ACLHeaderOptions{ 232 XCosACL: c.acl, 233 }, 234 } 235 236 if c.encrypt { 237 opt.ObjectPutHeaderOptions.XCosServerSideEncryption = "AES256" 238 } 239 240 r := bytes.NewReader(data) 241 rsp, err := c.cosClient.Object.Put(c.cosContext, cosFile, r, opt) 242 if rsp == nil { 243 log.Printf("[DEBUG] putObject %s: error: %v", cosFile, err) 244 return fmt.Errorf("failed to save file to %v: %w", cosFile, err) 245 } 246 defer rsp.Body.Close() 247 248 log.Printf("[DEBUG] putObject %s: code: %d, error: %v", cosFile, rsp.StatusCode, err) 249 if err != nil { 250 return fmt.Errorf("failed to save file to %v: %w", cosFile, err) 251 } 252 253 return nil 254 } 255 256 // deleteObject delete remote object 257 func (c *remoteClient) deleteObject(cosFile string) error { 258 rsp, err := c.cosClient.Object.Delete(c.cosContext, cosFile) 259 if rsp == nil { 260 log.Printf("[DEBUG] deleteObject %s: error: %v", cosFile, err) 261 return fmt.Errorf("failed to delete file %v: %w", cosFile, err) 262 } 263 defer rsp.Body.Close() 264 265 log.Printf("[DEBUG] deleteObject %s: code: %d, error: %v", cosFile, rsp.StatusCode, err) 266 if rsp.StatusCode == 404 { 267 return nil 268 } 269 270 if err != nil { 271 return fmt.Errorf("failed to delete file %v: %w", cosFile, err) 272 } 273 274 return nil 275 } 276 277 // getBucket list bucket by prefix 278 func (c *remoteClient) getBucket(prefix string) (obs []cos.Object, err error) { 279 fs, rsp, err := c.cosClient.Bucket.Get(c.cosContext, &cos.BucketGetOptions{Prefix: prefix}) 280 if rsp == nil { 281 log.Printf("[DEBUG] getBucket %s/%s: error: %v", c.bucket, prefix, err) 282 err = fmt.Errorf("bucket %s not exists", c.bucket) 283 return 284 } 285 defer rsp.Body.Close() 286 287 log.Printf("[DEBUG] getBucket %s/%s: code: %d, error: %v", c.bucket, prefix, rsp.StatusCode, err) 288 if rsp.StatusCode == 404 { 289 err = fmt.Errorf("bucket %s not exists", c.bucket) 290 return 291 } 292 293 if err != nil { 294 return 295 } 296 297 return fs.Contents, nil 298 } 299 300 // putBucket create cos bucket 301 func (c *remoteClient) putBucket() error { 302 rsp, err := c.cosClient.Bucket.Put(c.cosContext, nil) 303 if rsp == nil { 304 log.Printf("[DEBUG] putBucket %s: error: %v", c.bucket, err) 305 return fmt.Errorf("failed to create bucket %v: %w", c.bucket, err) 306 } 307 defer rsp.Body.Close() 308 309 log.Printf("[DEBUG] putBucket %s: code: %d, error: %v", c.bucket, rsp.StatusCode, err) 310 if rsp.StatusCode == 409 { 311 return nil 312 } 313 314 if err != nil { 315 return fmt.Errorf("failed to create bucket %v: %w", c.bucket, err) 316 } 317 318 return nil 319 } 320 321 // deleteBucket delete cos bucket 322 func (c *remoteClient) deleteBucket(recursive bool) error { 323 if recursive { 324 obs, err := c.getBucket("") 325 if err != nil { 326 if strings.Contains(err.Error(), "not exists") { 327 return nil 328 } 329 log.Printf("[DEBUG] deleteBucket %s: empty bucket error: %v", c.bucket, err) 330 return fmt.Errorf("failed to empty bucket %v: %w", c.bucket, err) 331 } 332 for _, v := range obs { 333 c.deleteObject(v.Key) 334 } 335 } 336 337 rsp, err := c.cosClient.Bucket.Delete(c.cosContext) 338 if rsp == nil { 339 log.Printf("[DEBUG] deleteBucket %s: error: %v", c.bucket, err) 340 return fmt.Errorf("failed to delete bucket %v: %w", c.bucket, err) 341 } 342 defer rsp.Body.Close() 343 344 log.Printf("[DEBUG] deleteBucket %s: code: %d, error: %v", c.bucket, rsp.StatusCode, err) 345 if rsp.StatusCode == 404 { 346 return nil 347 } 348 349 if err != nil { 350 return fmt.Errorf("failed to delete bucket %v: %w", c.bucket, err) 351 } 352 353 return nil 354 } 355 356 // cosLock lock cos for writing 357 func (c *remoteClient) cosLock(bucket, cosFile string) error { 358 log.Printf("[DEBUG] lock cos file %s:%s", bucket, cosFile) 359 360 cosPath := fmt.Sprintf("%s:%s", bucket, cosFile) 361 lockTagValue := fmt.Sprintf("%x", md5.Sum([]byte(cosPath))) 362 363 return c.CreateTag(lockTagKey, lockTagValue) 364 } 365 366 // cosUnlock unlock cos writing 367 func (c *remoteClient) cosUnlock(bucket, cosFile string) error { 368 log.Printf("[DEBUG] unlock cos file %s:%s", bucket, cosFile) 369 370 cosPath := fmt.Sprintf("%s:%s", bucket, cosFile) 371 lockTagValue := fmt.Sprintf("%x", md5.Sum([]byte(cosPath))) 372 373 var err error 374 for i := 0; i < 30; i++ { 375 tagExists, err := c.CheckTag(lockTagKey, lockTagValue) 376 377 if err != nil { 378 return err 379 } 380 381 if !tagExists { 382 return nil 383 } 384 385 err = c.DeleteTag(lockTagKey, lockTagValue) 386 if err == nil { 387 return nil 388 } 389 time.Sleep(1 * time.Second) 390 } 391 392 return err 393 } 394 395 // CheckTag checks if tag key:value exists 396 func (c *remoteClient) CheckTag(key, value string) (exists bool, err error) { 397 request := tag.NewDescribeTagsRequest() 398 request.TagKey = &key 399 request.TagValue = &value 400 401 response, err := c.tagClient.DescribeTags(request) 402 log.Printf("[DEBUG] create tag %s:%s: error: %v", key, value, err) 403 if err != nil { 404 return 405 } 406 407 if len(response.Response.Tags) == 0 { 408 return 409 } 410 411 tagKey := response.Response.Tags[0].TagKey 412 tagValue := response.Response.Tags[0].TagValue 413 414 exists = key == *tagKey && value == *tagValue 415 416 return 417 } 418 419 // CreateTag create tag by key and value 420 func (c *remoteClient) CreateTag(key, value string) error { 421 request := tag.NewCreateTagRequest() 422 request.TagKey = &key 423 request.TagValue = &value 424 425 _, err := c.tagClient.CreateTag(request) 426 log.Printf("[DEBUG] create tag %s:%s: error: %v", key, value, err) 427 if err != nil { 428 return fmt.Errorf("failed to create tag: %s -> %s: %w", key, value, err) 429 } 430 431 return nil 432 } 433 434 // DeleteTag create tag by key and value 435 func (c *remoteClient) DeleteTag(key, value string) error { 436 request := tag.NewDeleteTagRequest() 437 request.TagKey = &key 438 request.TagValue = &value 439 440 _, err := c.tagClient.DeleteTag(request) 441 log.Printf("[DEBUG] delete tag %s:%s: error: %v", key, value, err) 442 if err != nil { 443 return fmt.Errorf("failed to delete tag: %s -> %s: %w", key, value, err) 444 } 445 446 return nil 447 }