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  }