github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/remote-state/cos/backend.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package cos
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"net/http"
    10  	"net/url"
    11  	"os"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
    17  	"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
    18  	sts "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sts/v20180813"
    19  	tag "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813"
    20  	"github.com/tencentyun/cos-go-sdk-v5"
    21  	"github.com/terramate-io/tf/backend"
    22  	"github.com/terramate-io/tf/legacy/helper/schema"
    23  )
    24  
    25  // Default value from environment variable
    26  const (
    27  	PROVIDER_SECRET_ID                    = "TENCENTCLOUD_SECRET_ID"
    28  	PROVIDER_SECRET_KEY                   = "TENCENTCLOUD_SECRET_KEY"
    29  	PROVIDER_SECURITY_TOKEN               = "TENCENTCLOUD_SECURITY_TOKEN"
    30  	PROVIDER_REGION                       = "TENCENTCLOUD_REGION"
    31  	PROVIDER_ASSUME_ROLE_ARN              = "TENCENTCLOUD_ASSUME_ROLE_ARN"
    32  	PROVIDER_ASSUME_ROLE_SESSION_NAME     = "TENCENTCLOUD_ASSUME_ROLE_SESSION_NAME"
    33  	PROVIDER_ASSUME_ROLE_SESSION_DURATION = "TENCENTCLOUD_ASSUME_ROLE_SESSION_DURATION"
    34  )
    35  
    36  // Backend implements "backend".Backend for tencentCloud cos
    37  type Backend struct {
    38  	*schema.Backend
    39  	credential *common.Credential
    40  
    41  	cosContext context.Context
    42  	cosClient  *cos.Client
    43  	tagClient  *tag.Client
    44  	stsClient  *sts.Client
    45  
    46  	region  string
    47  	bucket  string
    48  	prefix  string
    49  	key     string
    50  	encrypt bool
    51  	acl     string
    52  }
    53  
    54  // New creates a new backend for TencentCloud cos remote state.
    55  func New() backend.Backend {
    56  	s := &schema.Backend{
    57  		Schema: map[string]*schema.Schema{
    58  			"secret_id": {
    59  				Type:        schema.TypeString,
    60  				Optional:    true,
    61  				DefaultFunc: schema.EnvDefaultFunc(PROVIDER_SECRET_ID, nil),
    62  				Description: "Secret id of Tencent Cloud",
    63  			},
    64  			"secret_key": {
    65  				Type:        schema.TypeString,
    66  				Optional:    true,
    67  				DefaultFunc: schema.EnvDefaultFunc(PROVIDER_SECRET_KEY, nil),
    68  				Description: "Secret key of Tencent Cloud",
    69  				Sensitive:   true,
    70  			},
    71  			"security_token": {
    72  				Type:        schema.TypeString,
    73  				Optional:    true,
    74  				DefaultFunc: schema.EnvDefaultFunc(PROVIDER_SECURITY_TOKEN, nil),
    75  				Description: "TencentCloud Security Token of temporary access credentials. It can be sourced from the `TENCENTCLOUD_SECURITY_TOKEN` environment variable. Notice: for supported products, please refer to: [temporary key supported products](https://intl.cloud.tencent.com/document/product/598/10588).",
    76  				Sensitive:   true,
    77  			},
    78  			"region": {
    79  				Type:         schema.TypeString,
    80  				Required:     true,
    81  				DefaultFunc:  schema.EnvDefaultFunc(PROVIDER_REGION, nil),
    82  				Description:  "The region of the COS bucket",
    83  				InputDefault: "ap-guangzhou",
    84  			},
    85  			"bucket": {
    86  				Type:        schema.TypeString,
    87  				Required:    true,
    88  				Description: "The name of the COS bucket",
    89  			},
    90  			"prefix": {
    91  				Type:        schema.TypeString,
    92  				Optional:    true,
    93  				Description: "The directory for saving the state file in bucket",
    94  				ValidateFunc: func(v interface{}, s string) ([]string, []error) {
    95  					prefix := v.(string)
    96  					if strings.HasPrefix(prefix, "/") || strings.HasPrefix(prefix, "./") {
    97  						return nil, []error{fmt.Errorf("prefix must not start with '/' or './'")}
    98  					}
    99  					return nil, nil
   100  				},
   101  			},
   102  			"key": {
   103  				Type:        schema.TypeString,
   104  				Optional:    true,
   105  				Description: "The path for saving the state file in bucket",
   106  				Default:     "terraform.tfstate",
   107  				ValidateFunc: func(v interface{}, s string) ([]string, []error) {
   108  					if strings.HasPrefix(v.(string), "/") || strings.HasSuffix(v.(string), "/") {
   109  						return nil, []error{fmt.Errorf("key can not start and end with '/'")}
   110  					}
   111  					return nil, nil
   112  				},
   113  			},
   114  			"encrypt": {
   115  				Type:        schema.TypeBool,
   116  				Optional:    true,
   117  				Description: "Whether to enable server side encryption of the state file",
   118  				Default:     true,
   119  			},
   120  			"acl": {
   121  				Type:        schema.TypeString,
   122  				Optional:    true,
   123  				Description: "Object ACL to be applied to the state file",
   124  				Default:     "private",
   125  				ValidateFunc: func(v interface{}, s string) ([]string, []error) {
   126  					value := v.(string)
   127  					if value != "private" && value != "public-read" {
   128  						return nil, []error{fmt.Errorf(
   129  							"acl value invalid, expected %s or %s, got %s",
   130  							"private", "public-read", value)}
   131  					}
   132  					return nil, nil
   133  				},
   134  			},
   135  			"accelerate": {
   136  				Type:        schema.TypeBool,
   137  				Optional:    true,
   138  				Description: "Whether to enable global Acceleration",
   139  				Default:     false,
   140  			},
   141  			"assume_role": {
   142  				Type:        schema.TypeSet,
   143  				Optional:    true,
   144  				MaxItems:    1,
   145  				Description: "The `assume_role` block. If provided, terraform will attempt to assume this role using the supplied credentials.",
   146  				Elem: &schema.Resource{
   147  					Schema: map[string]*schema.Schema{
   148  						"role_arn": {
   149  							Type:        schema.TypeString,
   150  							Required:    true,
   151  							DefaultFunc: schema.EnvDefaultFunc(PROVIDER_ASSUME_ROLE_ARN, nil),
   152  							Description: "The ARN of the role to assume. It can be sourced from the `TENCENTCLOUD_ASSUME_ROLE_ARN`.",
   153  						},
   154  						"session_name": {
   155  							Type:        schema.TypeString,
   156  							Required:    true,
   157  							DefaultFunc: schema.EnvDefaultFunc(PROVIDER_ASSUME_ROLE_SESSION_NAME, nil),
   158  							Description: "The session name to use when making the AssumeRole call. It can be sourced from the `TENCENTCLOUD_ASSUME_ROLE_SESSION_NAME`.",
   159  						},
   160  						"session_duration": {
   161  							Type:     schema.TypeInt,
   162  							Required: true,
   163  							DefaultFunc: func() (interface{}, error) {
   164  								if v := os.Getenv(PROVIDER_ASSUME_ROLE_SESSION_DURATION); v != "" {
   165  									return strconv.Atoi(v)
   166  								}
   167  								return 7200, nil
   168  							},
   169  							ValidateFunc: validateIntegerInRange(0, 43200),
   170  							Description:  "The duration of the session when making the AssumeRole call. Its value ranges from 0 to 43200(seconds), and default is 7200 seconds. It can be sourced from the `TENCENTCLOUD_ASSUME_ROLE_SESSION_DURATION`.",
   171  						},
   172  						"policy": {
   173  							Type:        schema.TypeString,
   174  							Optional:    true,
   175  							Description: "A more restrictive policy when making the AssumeRole call. Its content must not contains `principal` elements. Notice: more syntax references, please refer to: [policies syntax logic](https://intl.cloud.tencent.com/document/product/598/10603).",
   176  						},
   177  					},
   178  				},
   179  			},
   180  		},
   181  	}
   182  
   183  	result := &Backend{Backend: s}
   184  	result.Backend.ConfigureFunc = result.configure
   185  
   186  	return result
   187  }
   188  
   189  func validateIntegerInRange(min, max int64) schema.SchemaValidateFunc {
   190  	return func(v interface{}, k string) (ws []string, errors []error) {
   191  		value := int64(v.(int))
   192  		if value < min {
   193  			errors = append(errors, fmt.Errorf(
   194  				"%q cannot be lower than %d: %d", k, min, value))
   195  		}
   196  		if value > max {
   197  			errors = append(errors, fmt.Errorf(
   198  				"%q cannot be higher than %d: %d", k, max, value))
   199  		}
   200  		return
   201  	}
   202  }
   203  
   204  // configure init cos client
   205  func (b *Backend) configure(ctx context.Context) error {
   206  	if b.cosClient != nil {
   207  		return nil
   208  	}
   209  
   210  	b.cosContext = ctx
   211  	data := schema.FromContextBackendConfig(b.cosContext)
   212  
   213  	b.region = data.Get("region").(string)
   214  	b.bucket = data.Get("bucket").(string)
   215  	b.prefix = data.Get("prefix").(string)
   216  	b.key = data.Get("key").(string)
   217  	b.encrypt = data.Get("encrypt").(bool)
   218  	b.acl = data.Get("acl").(string)
   219  
   220  	var (
   221  		u   *url.URL
   222  		err error
   223  	)
   224  	accelerate := data.Get("accelerate").(bool)
   225  	if accelerate {
   226  		u, err = url.Parse(fmt.Sprintf("https://%s.cos.accelerate.myqcloud.com", b.bucket))
   227  	} else {
   228  		u, err = url.Parse(fmt.Sprintf("https://%s.cos.%s.myqcloud.com", b.bucket, b.region))
   229  	}
   230  	if err != nil {
   231  		return err
   232  	}
   233  
   234  	secretId := data.Get("secret_id").(string)
   235  	secretKey := data.Get("secret_key").(string)
   236  	securityToken := data.Get("security_token").(string)
   237  
   238  	// init credential by AKSK & TOKEN
   239  	b.credential = common.NewTokenCredential(secretId, secretKey, securityToken)
   240  	// update credential if assume role exist
   241  	err = handleAssumeRole(data, b)
   242  	if err != nil {
   243  		return err
   244  	}
   245  
   246  	b.cosClient = cos.NewClient(
   247  		&cos.BaseURL{BucketURL: u},
   248  		&http.Client{
   249  			Timeout: 60 * time.Second,
   250  			Transport: &cos.AuthorizationTransport{
   251  				SecretID:     b.credential.SecretId,
   252  				SecretKey:    b.credential.SecretKey,
   253  				SessionToken: b.credential.Token,
   254  			},
   255  		},
   256  	)
   257  
   258  	b.tagClient = b.UseTagClient()
   259  	return err
   260  }
   261  
   262  func handleAssumeRole(data *schema.ResourceData, b *Backend) error {
   263  	assumeRoleList := data.Get("assume_role").(*schema.Set).List()
   264  	if len(assumeRoleList) == 1 {
   265  		assumeRole := assumeRoleList[0].(map[string]interface{})
   266  		assumeRoleArn := assumeRole["role_arn"].(string)
   267  		assumeRoleSessionName := assumeRole["session_name"].(string)
   268  		assumeRoleSessionDuration := assumeRole["session_duration"].(int)
   269  		assumeRolePolicy := assumeRole["policy"].(string)
   270  
   271  		err := b.updateCredentialWithSTS(assumeRoleArn, assumeRoleSessionName, assumeRoleSessionDuration, assumeRolePolicy)
   272  		if err != nil {
   273  			return err
   274  		}
   275  	}
   276  	return nil
   277  }
   278  
   279  func (b *Backend) updateCredentialWithSTS(assumeRoleArn, assumeRoleSessionName string, assumeRoleSessionDuration int, assumeRolePolicy string) error {
   280  	// assume role by STS
   281  	request := sts.NewAssumeRoleRequest()
   282  	request.RoleArn = &assumeRoleArn
   283  	request.RoleSessionName = &assumeRoleSessionName
   284  	duration := uint64(assumeRoleSessionDuration)
   285  	request.DurationSeconds = &duration
   286  	if assumeRolePolicy != "" {
   287  		policy := url.QueryEscape(assumeRolePolicy)
   288  		request.Policy = &policy
   289  	}
   290  
   291  	response, err := b.UseStsClient().AssumeRole(request)
   292  	if err != nil {
   293  		return err
   294  	}
   295  	// update credentials by result of assume role
   296  	b.credential = common.NewTokenCredential(
   297  		*response.Response.Credentials.TmpSecretId,
   298  		*response.Response.Credentials.TmpSecretKey,
   299  		*response.Response.Credentials.Token,
   300  	)
   301  
   302  	return nil
   303  }
   304  
   305  // UseStsClient returns sts client for service
   306  func (b *Backend) UseStsClient() *sts.Client {
   307  	if b.stsClient != nil {
   308  		return b.stsClient
   309  	}
   310  	cpf := b.NewClientProfile(300)
   311  	b.stsClient, _ = sts.NewClient(b.credential, b.region, cpf)
   312  	b.stsClient.WithHttpTransport(&LogRoundTripper{})
   313  
   314  	return b.stsClient
   315  }
   316  
   317  // UseTagClient returns tag client for service
   318  func (b *Backend) UseTagClient() *tag.Client {
   319  	if b.tagClient != nil {
   320  		return b.tagClient
   321  	}
   322  	cpf := b.NewClientProfile(300)
   323  	cpf.Language = "en-US"
   324  	b.tagClient, _ = tag.NewClient(b.credential, b.region, cpf)
   325  	return b.tagClient
   326  }
   327  
   328  // NewClientProfile returns a new ClientProfile
   329  func (b *Backend) NewClientProfile(timeout int) *profile.ClientProfile {
   330  	cpf := profile.NewClientProfile()
   331  
   332  	// all request use method POST
   333  	cpf.HttpProfile.ReqMethod = "POST"
   334  	// request timeout
   335  	cpf.HttpProfile.ReqTimeout = timeout
   336  
   337  	return cpf
   338  }