terraform/internal/backend/remote-state/cos/client.go

404 lines
9.7 KiB
Go

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/states/remote"
"github.com/hashicorp/terraform/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)
}
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++ {
err = c.DeleteTag(lockTagKey, lockTagValue)
if err == nil {
return nil
}
time.Sleep(1 * time.Second)
}
return err
}
// 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
}