github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/backend/remote-state/cos/backend.go (about)

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