| package cos |
| |
| import ( |
| "bytes" |
| "context" |
| "crypto/md5" |
| "encoding/json" |
| "fmt" |
| "io/ioutil" |
| "log" |
| "net/http" |
| "strings" |
| "time" |
| |
| multierror "github.com/hashicorp/go-multierror" |
| "github.com/hashicorp/terraform/internal/states/remote" |
| "github.com/hashicorp/terraform/internal/states/statemgr" |
| tag "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813" |
| "github.com/tencentyun/cos-go-sdk-v5" |
| ) |
| |
| const ( |
| lockTagKey = "tencentcloud-terraform-lock" |
| ) |
| |
| // RemoteClient implements the client of remote state |
| type remoteClient struct { |
| cosContext context.Context |
| cosClient *cos.Client |
| tagClient *tag.Client |
| |
| bucket string |
| stateFile string |
| lockFile string |
| encrypt bool |
| acl string |
| } |
| |
| // Get returns remote state file |
| func (c *remoteClient) Get() (*remote.Payload, error) { |
| log.Printf("[DEBUG] get remote state file %s", c.stateFile) |
| |
| exists, data, checksum, err := c.getObject(c.stateFile) |
| if err != nil { |
| return nil, err |
| } |
| |
| if !exists { |
| return nil, nil |
| } |
| |
| payload := &remote.Payload{ |
| Data: data, |
| MD5: []byte(checksum), |
| } |
| |
| return payload, nil |
| } |
| |
| // Put put state file to remote |
| func (c *remoteClient) Put(data []byte) error { |
| log.Printf("[DEBUG] put remote state file %s", c.stateFile) |
| |
| return c.putObject(c.stateFile, data) |
| } |
| |
| // Delete delete remote state file |
| func (c *remoteClient) Delete() error { |
| log.Printf("[DEBUG] delete remote state file %s", c.stateFile) |
| |
| return c.deleteObject(c.stateFile) |
| } |
| |
| // Lock lock remote state file for writing |
| func (c *remoteClient) Lock(info *statemgr.LockInfo) (string, error) { |
| log.Printf("[DEBUG] lock remote state file %s", c.lockFile) |
| |
| err := c.cosLock(c.bucket, c.lockFile) |
| if err != nil { |
| return "", c.lockError(err) |
| } |
| defer c.cosUnlock(c.bucket, c.lockFile) |
| |
| exists, _, _, err := c.getObject(c.lockFile) |
| if err != nil { |
| return "", c.lockError(err) |
| } |
| |
| if exists { |
| return "", c.lockError(fmt.Errorf("lock file %s exists", c.lockFile)) |
| } |
| |
| info.Path = c.lockFile |
| data, err := json.Marshal(info) |
| if err != nil { |
| return "", c.lockError(err) |
| } |
| |
| check := fmt.Sprintf("%x", md5.Sum(data)) |
| err = c.putObject(c.lockFile, data) |
| if err != nil { |
| return "", c.lockError(err) |
| } |
| |
| return check, nil |
| } |
| |
| // Unlock unlock remote state file |
| func (c *remoteClient) Unlock(check string) error { |
| log.Printf("[DEBUG] unlock remote state file %s", c.lockFile) |
| |
| info, err := c.lockInfo() |
| if err != nil { |
| return c.lockError(err) |
| } |
| |
| if info.ID != check { |
| return c.lockError(fmt.Errorf("lock id mismatch, %v != %v", info.ID, check)) |
| } |
| |
| err = c.deleteObject(c.lockFile) |
| if err != nil { |
| return c.lockError(err) |
| } |
| |
| err = c.cosUnlock(c.bucket, c.lockFile) |
| if err != nil { |
| return c.lockError(err) |
| } |
| |
| return nil |
| } |
| |
| // lockError returns statemgr.LockError |
| func (c *remoteClient) lockError(err error) *statemgr.LockError { |
| log.Printf("[DEBUG] failed to lock or unlock %s: %v", c.lockFile, err) |
| |
| lockErr := &statemgr.LockError{ |
| Err: err, |
| } |
| |
| info, infoErr := c.lockInfo() |
| if infoErr != nil { |
| lockErr.Err = multierror.Append(lockErr.Err, infoErr) |
| } else { |
| lockErr.Info = info |
| } |
| |
| return lockErr |
| } |
| |
| // lockInfo returns LockInfo from lock file |
| func (c *remoteClient) lockInfo() (*statemgr.LockInfo, error) { |
| exists, data, checksum, err := c.getObject(c.lockFile) |
| if err != nil { |
| return nil, err |
| } |
| |
| if !exists { |
| return nil, fmt.Errorf("lock file %s not exists", c.lockFile) |
| } |
| |
| info := &statemgr.LockInfo{} |
| if err := json.Unmarshal(data, info); err != nil { |
| return nil, err |
| } |
| |
| info.ID = checksum |
| |
| return info, nil |
| } |
| |
| // getObject get remote object |
| func (c *remoteClient) getObject(cosFile string) (exists bool, data []byte, checksum string, err error) { |
| rsp, err := c.cosClient.Object.Get(c.cosContext, cosFile, nil) |
| if rsp == nil { |
| log.Printf("[DEBUG] getObject %s: error: %v", cosFile, err) |
| err = fmt.Errorf("failed to open file at %v: %v", cosFile, err) |
| return |
| } |
| defer rsp.Body.Close() |
| |
| log.Printf("[DEBUG] getObject %s: code: %d, error: %v", cosFile, rsp.StatusCode, err) |
| if err != nil { |
| if rsp.StatusCode == 404 { |
| err = nil |
| } else { |
| err = fmt.Errorf("failed to open file at %v: %v", cosFile, err) |
| } |
| return |
| } |
| |
| checksum = rsp.Header.Get("X-Cos-Meta-Md5") |
| log.Printf("[DEBUG] getObject %s: checksum: %s", cosFile, checksum) |
| if len(checksum) != 32 { |
| err = fmt.Errorf("failed to open file at %v: checksum %s invalid", cosFile, checksum) |
| return |
| } |
| |
| exists = true |
| data, err = ioutil.ReadAll(rsp.Body) |
| log.Printf("[DEBUG] getObject %s: data length: %d", cosFile, len(data)) |
| if err != nil { |
| err = fmt.Errorf("failed to open file at %v: %v", cosFile, err) |
| return |
| } |
| |
| check := fmt.Sprintf("%x", md5.Sum(data)) |
| log.Printf("[DEBUG] getObject %s: check: %s", cosFile, check) |
| if check != checksum { |
| err = fmt.Errorf("failed to open file at %v: checksum mismatch, %s != %s", cosFile, check, checksum) |
| return |
| } |
| |
| return |
| } |
| |
| // putObject put object to remote |
| func (c *remoteClient) putObject(cosFile string, data []byte) error { |
| opt := &cos.ObjectPutOptions{ |
| ObjectPutHeaderOptions: &cos.ObjectPutHeaderOptions{ |
| XCosMetaXXX: &http.Header{ |
| "X-Cos-Meta-Md5": []string{fmt.Sprintf("%x", md5.Sum(data))}, |
| }, |
| }, |
| ACLHeaderOptions: &cos.ACLHeaderOptions{ |
| XCosACL: c.acl, |
| }, |
| } |
| |
| if c.encrypt { |
| opt.ObjectPutHeaderOptions.XCosServerSideEncryption = "AES256" |
| } |
| |
| r := bytes.NewReader(data) |
| rsp, err := c.cosClient.Object.Put(c.cosContext, cosFile, r, opt) |
| if rsp == nil { |
| log.Printf("[DEBUG] putObject %s: error: %v", cosFile, err) |
| return fmt.Errorf("failed to save file to %v: %v", cosFile, err) |
| } |
| defer rsp.Body.Close() |
| |
| log.Printf("[DEBUG] putObject %s: code: %d, error: %v", cosFile, rsp.StatusCode, err) |
| if err != nil { |
| return fmt.Errorf("failed to save file to %v: %v", cosFile, err) |
| } |
| |
| return nil |
| } |
| |
| // deleteObject delete remote object |
| func (c *remoteClient) deleteObject(cosFile string) error { |
| rsp, err := c.cosClient.Object.Delete(c.cosContext, cosFile) |
| if rsp == nil { |
| log.Printf("[DEBUG] deleteObject %s: error: %v", cosFile, err) |
| return fmt.Errorf("failed to delete file %v: %v", cosFile, err) |
| } |
| defer rsp.Body.Close() |
| |
| log.Printf("[DEBUG] deleteObject %s: code: %d, error: %v", cosFile, rsp.StatusCode, err) |
| if rsp.StatusCode == 404 { |
| return nil |
| } |
| |
| if err != nil { |
| return fmt.Errorf("failed to delete file %v: %v", cosFile, err) |
| } |
| |
| return nil |
| } |
| |
| // getBucket list bucket by prefix |
| func (c *remoteClient) getBucket(prefix string) (obs []cos.Object, err error) { |
| fs, rsp, err := c.cosClient.Bucket.Get(c.cosContext, &cos.BucketGetOptions{Prefix: prefix}) |
| if rsp == nil { |
| log.Printf("[DEBUG] getBucket %s/%s: error: %v", c.bucket, prefix, err) |
| err = fmt.Errorf("bucket %s not exists", c.bucket) |
| return |
| } |
| defer rsp.Body.Close() |
| |
| log.Printf("[DEBUG] getBucket %s/%s: code: %d, error: %v", c.bucket, prefix, rsp.StatusCode, err) |
| if rsp.StatusCode == 404 { |
| err = fmt.Errorf("bucket %s not exists", c.bucket) |
| return |
| } |
| |
| if err != nil { |
| return |
| } |
| |
| return fs.Contents, nil |
| } |
| |
| // putBucket create cos bucket |
| func (c *remoteClient) putBucket() error { |
| rsp, err := c.cosClient.Bucket.Put(c.cosContext, nil) |
| if rsp == nil { |
| log.Printf("[DEBUG] putBucket %s: error: %v", c.bucket, err) |
| return fmt.Errorf("failed to create bucket %v: %v", c.bucket, err) |
| } |
| defer rsp.Body.Close() |
| |
| log.Printf("[DEBUG] putBucket %s: code: %d, error: %v", c.bucket, rsp.StatusCode, err) |
| if rsp.StatusCode == 409 { |
| return nil |
| } |
| |
| if err != nil { |
| return fmt.Errorf("failed to create bucket %v: %v", c.bucket, err) |
| } |
| |
| return nil |
| } |
| |
| // deleteBucket delete cos bucket |
| func (c *remoteClient) deleteBucket(recursive bool) error { |
| if recursive { |
| obs, err := c.getBucket("") |
| if err != nil { |
| if strings.Contains(err.Error(), "not exists") { |
| return nil |
| } |
| log.Printf("[DEBUG] deleteBucket %s: empty bucket error: %v", c.bucket, err) |
| return fmt.Errorf("failed to empty bucket %v: %v", c.bucket, err) |
| } |
| for _, v := range obs { |
| c.deleteObject(v.Key) |
| } |
| } |
| |
| rsp, err := c.cosClient.Bucket.Delete(c.cosContext) |
| if rsp == nil { |
| log.Printf("[DEBUG] deleteBucket %s: error: %v", c.bucket, err) |
| return fmt.Errorf("failed to delete bucket %v: %v", c.bucket, err) |
| } |
| defer rsp.Body.Close() |
| |
| log.Printf("[DEBUG] deleteBucket %s: code: %d, error: %v", c.bucket, rsp.StatusCode, err) |
| if rsp.StatusCode == 404 { |
| return nil |
| } |
| |
| if err != nil { |
| return fmt.Errorf("failed to delete bucket %v: %v", c.bucket, err) |
| } |
| |
| return nil |
| } |
| |
| // cosLock lock cos for writing |
| func (c *remoteClient) cosLock(bucket, cosFile string) error { |
| log.Printf("[DEBUG] lock cos file %s:%s", bucket, cosFile) |
| |
| cosPath := fmt.Sprintf("%s:%s", bucket, cosFile) |
| lockTagValue := fmt.Sprintf("%x", md5.Sum([]byte(cosPath))) |
| |
| return c.CreateTag(lockTagKey, lockTagValue) |
| } |
| |
| // cosUnlock unlock cos writing |
| func (c *remoteClient) cosUnlock(bucket, cosFile string) error { |
| log.Printf("[DEBUG] unlock cos file %s:%s", bucket, cosFile) |
| |
| cosPath := fmt.Sprintf("%s:%s", bucket, cosFile) |
| lockTagValue := fmt.Sprintf("%x", md5.Sum([]byte(cosPath))) |
| |
| var err error |
| for i := 0; i < 30; i++ { |
| tagExists, err := c.CheckTag(lockTagKey, lockTagValue) |
| |
| if err != nil { |
| return err |
| } |
| |
| if !tagExists { |
| return nil |
| } |
| |
| err = c.DeleteTag(lockTagKey, lockTagValue) |
| if err == nil { |
| return nil |
| } |
| time.Sleep(1 * time.Second) |
| } |
| |
| return err |
| } |
| |
| // CheckTag checks if tag key:value exists |
| func (c *remoteClient) CheckTag(key, value string) (exists bool, err error) { |
| request := tag.NewDescribeTagsRequest() |
| request.TagKey = &key |
| request.TagValue = &value |
| |
| response, err := c.tagClient.DescribeTags(request) |
| log.Printf("[DEBUG] create tag %s:%s: error: %v", key, value, err) |
| if err != nil { |
| return |
| } |
| |
| if len(response.Response.Tags) == 0 { |
| return |
| } |
| |
| tagKey := response.Response.Tags[0].TagKey |
| tagValue := response.Response.Tags[0].TagValue |
| |
| exists = key == *tagKey && value == *tagValue |
| |
| return |
| } |
| |
| // CreateTag create tag by key and value |
| func (c *remoteClient) CreateTag(key, value string) error { |
| request := tag.NewCreateTagRequest() |
| request.TagKey = &key |
| request.TagValue = &value |
| |
| _, err := c.tagClient.CreateTag(request) |
| log.Printf("[DEBUG] create tag %s:%s: error: %v", key, value, err) |
| if err != nil { |
| return fmt.Errorf("failed to create tag: %s -> %s: %s", key, value, err) |
| } |
| |
| return nil |
| } |
| |
| // DeleteTag create tag by key and value |
| func (c *remoteClient) DeleteTag(key, value string) error { |
| request := tag.NewDeleteTagRequest() |
| request.TagKey = &key |
| request.TagValue = &value |
| |
| _, err := c.tagClient.DeleteTag(request) |
| log.Printf("[DEBUG] delete tag %s:%s: error: %v", key, value, err) |
| if err != nil { |
| return fmt.Errorf("failed to delete tag: %s -> %s: %s", key, value, err) |
| } |
| |
| return nil |
| } |