diff --git a/go.mod b/go.mod index 6693e949c7..61a6f491d9 100644 --- a/go.mod +++ b/go.mod @@ -70,7 +70,8 @@ require ( github.com/pkg/errors v0.9.1 github.com/posener/complete v1.2.3 github.com/spf13/afero v1.2.2 - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.232 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.588 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sts v1.0.588 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.233 github.com/tencentyun/cos-go-sdk-v5 v0.7.29 github.com/tombuildsstuff/giovanni v0.15.1 diff --git a/go.sum b/go.sum index 36a5f78cfc..6a70cce852 100644 --- a/go.sum +++ b/go.sum @@ -608,9 +608,11 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.194/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.232 h1:kwsWbh4rEw42ZDe9/812ebhbwNZxlQyZ2sTmxBOKhN4= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.232/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.588 h1:DYtBXB7sVc3EOW5horg8j55cLZynhsLYhHrvQ/jXKKM= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.588/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.194/go.mod h1:yrBKWhChnDqNz1xuXdSbWXG56XawEq0G5j1lg4VwBD4= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sts v1.0.588 h1:PlkFOALQZ9BLUyX8EalATUQD5xEn1Sz34C+Rw5VSpvk= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sts v1.0.588/go.mod h1:vPvXNb+zBZVJfZCIKWcYxLpGzgScKKgiPUArobWZ+nU= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.233 h1:5Tbi+jyZ2MojC6GK8V6hchwtnkP2IuENUTqSisbYOlA= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.233/go.mod h1:sX14+NSvMjOhNFaMtP2aDy6Bss8PyFXij21gpY6+DAs= github.com/tencentyun/cos-go-sdk-v5 v0.7.29 h1:uwRBzc70Wgtc5iQQCowqecfRT0OpCXUOZzodZHOOEDs= diff --git a/internal/backend/remote-state/cos/backend.go b/internal/backend/remote-state/cos/backend.go index 667fdd81dd..aebf69aabb 100644 --- a/internal/backend/remote-state/cos/backend.go +++ b/internal/backend/remote-state/cos/backend.go @@ -5,6 +5,8 @@ import ( "fmt" "net/http" "net/url" + "os" + "strconv" "strings" "time" @@ -12,24 +14,31 @@ import ( "github.com/hashicorp/terraform/internal/legacy/helper/schema" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" + sts "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sts/v20180813" tag "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813" "github.com/tencentyun/cos-go-sdk-v5" ) // Default value from environment variable const ( - PROVIDER_SECRET_ID = "TENCENTCLOUD_SECRET_ID" - PROVIDER_SECRET_KEY = "TENCENTCLOUD_SECRET_KEY" - PROVIDER_REGION = "TENCENTCLOUD_REGION" + PROVIDER_SECRET_ID = "TENCENTCLOUD_SECRET_ID" + PROVIDER_SECRET_KEY = "TENCENTCLOUD_SECRET_KEY" + PROVIDER_SECURITY_TOKEN = "TENCENTCLOUD_SECURITY_TOKEN" + PROVIDER_REGION = "TENCENTCLOUD_REGION" + PROVIDER_ASSUME_ROLE_ARN = "TENCENTCLOUD_ASSUME_ROLE_ARN" + PROVIDER_ASSUME_ROLE_SESSION_NAME = "TENCENTCLOUD_ASSUME_ROLE_SESSION_NAME" + PROVIDER_ASSUME_ROLE_SESSION_DURATION = "TENCENTCLOUD_ASSUME_ROLE_SESSION_DURATION" ) // Backend implements "backend".Backend for tencentCloud cos type Backend struct { *schema.Backend + credential *common.Credential cosContext context.Context cosClient *cos.Client tagClient *tag.Client + stsClient *sts.Client region string bucket string @@ -45,17 +54,24 @@ func New() backend.Backend { Schema: map[string]*schema.Schema{ "secret_id": { Type: schema.TypeString, - Required: true, + Optional: true, DefaultFunc: schema.EnvDefaultFunc(PROVIDER_SECRET_ID, nil), Description: "Secret id of Tencent Cloud", }, "secret_key": { Type: schema.TypeString, - Required: true, + Optional: true, DefaultFunc: schema.EnvDefaultFunc(PROVIDER_SECRET_KEY, nil), Description: "Secret key of Tencent Cloud", Sensitive: true, }, + "security_token": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(PROVIDER_SECURITY_TOKEN, nil), + 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).", + Sensitive: true, + }, "region": { Type: schema.TypeString, Required: true, @@ -119,6 +135,45 @@ func New() backend.Backend { Description: "Whether to enable global Acceleration", Default: false, }, + "assume_role": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + Description: "The `assume_role` block. If provided, terraform will attempt to assume this role using the supplied credentials.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "role_arn": { + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc(PROVIDER_ASSUME_ROLE_ARN, nil), + Description: "The ARN of the role to assume. It can be sourced from the `TENCENTCLOUD_ASSUME_ROLE_ARN`.", + }, + "session_name": { + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc(PROVIDER_ASSUME_ROLE_SESSION_NAME, nil), + Description: "The session name to use when making the AssumeRole call. It can be sourced from the `TENCENTCLOUD_ASSUME_ROLE_SESSION_NAME`.", + }, + "session_duration": { + Type: schema.TypeInt, + Required: true, + DefaultFunc: func() (interface{}, error) { + if v := os.Getenv(PROVIDER_ASSUME_ROLE_SESSION_DURATION); v != "" { + return strconv.Atoi(v) + } + return 7200, nil + }, + ValidateFunc: validateIntegerInRange(0, 43200), + 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`.", + }, + "policy": { + Type: schema.TypeString, + Optional: true, + 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).", + }, + }, + }, + }, }, } @@ -128,6 +183,21 @@ func New() backend.Backend { return result } +func validateIntegerInRange(min, max int64) schema.SchemaValidateFunc { + return func(v interface{}, k string) (ws []string, errors []error) { + value := int64(v.(int)) + if value < min { + errors = append(errors, fmt.Errorf( + "%q cannot be lower than %d: %d", k, min, value)) + } + if value > max { + errors = append(errors, fmt.Errorf( + "%q cannot be higher than %d: %d", k, max, value)) + } + return + } +} + // configure init cos client func (b *Backend) configure(ctx context.Context) error { if b.cosClient != nil { @@ -158,27 +228,108 @@ func (b *Backend) configure(ctx context.Context) error { return err } + secretId := data.Get("secret_id").(string) + secretKey := data.Get("secret_key").(string) + securityToken := data.Get("security_token").(string) + + // init credential by AKSK & TOKEN + b.credential = common.NewTokenCredential(secretId, secretKey, securityToken) + // update credential if assume role exist + err = handleAssumeRole(data, b) + if err != nil { + return err + } + b.cosClient = cos.NewClient( &cos.BaseURL{BucketURL: u}, &http.Client{ Timeout: 60 * time.Second, Transport: &cos.AuthorizationTransport{ - SecretID: data.Get("secret_id").(string), - SecretKey: data.Get("secret_key").(string), + SecretID: b.credential.SecretId, + SecretKey: b.credential.SecretKey, + SessionToken: b.credential.Token, }, }, ) - credential := common.NewCredential( - data.Get("secret_id").(string), - data.Get("secret_key").(string), - ) - - cpf := profile.NewClientProfile() - cpf.HttpProfile.ReqMethod = "POST" - cpf.HttpProfile.ReqTimeout = 300 - cpf.Language = "en-US" - b.tagClient, err = tag.NewClient(credential, b.region, cpf) - + b.tagClient = b.UseTagClient() return err } + +func handleAssumeRole(data *schema.ResourceData, b *Backend) error { + assumeRoleList := data.Get("assume_role").(*schema.Set).List() + if len(assumeRoleList) == 1 { + assumeRole := assumeRoleList[0].(map[string]interface{}) + assumeRoleArn := assumeRole["role_arn"].(string) + assumeRoleSessionName := assumeRole["session_name"].(string) + assumeRoleSessionDuration := assumeRole["session_duration"].(int) + assumeRolePolicy := assumeRole["policy"].(string) + + err := b.updateCredentialWithSTS(assumeRoleArn, assumeRoleSessionName, assumeRoleSessionDuration, assumeRolePolicy) + if err != nil { + return err + } + } + return nil +} + +func (b *Backend) updateCredentialWithSTS(assumeRoleArn, assumeRoleSessionName string, assumeRoleSessionDuration int, assumeRolePolicy string) error { + // assume role by STS + request := sts.NewAssumeRoleRequest() + request.RoleArn = &assumeRoleArn + request.RoleSessionName = &assumeRoleSessionName + duration := uint64(assumeRoleSessionDuration) + request.DurationSeconds = &duration + if assumeRolePolicy != "" { + policy := url.QueryEscape(assumeRolePolicy) + request.Policy = &policy + } + + response, err := b.UseStsClient().AssumeRole(request) + if err != nil { + return err + } + // update credentials by result of assume role + b.credential = common.NewTokenCredential( + *response.Response.Credentials.TmpSecretId, + *response.Response.Credentials.TmpSecretKey, + *response.Response.Credentials.Token, + ) + + return nil +} + +// UseStsClient returns sts client for service +func (b *Backend) UseStsClient() *sts.Client { + if b.stsClient != nil { + return b.stsClient + } + cpf := b.NewClientProfile(300) + b.stsClient, _ = sts.NewClient(b.credential, b.region, cpf) + b.stsClient.WithHttpTransport(&LogRoundTripper{}) + + return b.stsClient +} + +// UseTagClient returns tag client for service +func (b *Backend) UseTagClient() *tag.Client { + if b.tagClient != nil { + return b.tagClient + } + cpf := b.NewClientProfile(300) + cpf.Language = "en-US" + b.tagClient, _ = tag.NewClient(b.credential, b.region, cpf) + return b.tagClient +} + +// NewClientProfile returns a new ClientProfile +func (b *Backend) NewClientProfile(timeout int) *profile.ClientProfile { + cpf := profile.NewClientProfile() + + // all request use method POST + cpf.HttpProfile.ReqMethod = "POST" + // request timeout + cpf.HttpProfile.ReqTimeout = timeout + + return cpf +} diff --git a/internal/backend/remote-state/cos/transport.go b/internal/backend/remote-state/cos/transport.go new file mode 100644 index 0000000000..dc4a57dccf --- /dev/null +++ b/internal/backend/remote-state/cos/transport.go @@ -0,0 +1,112 @@ +package cos + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "time" +) + +const REQUEST_CLIENT = "TENCENTCLOUD_API_REQUEST_CLIENT" + +var ReqClient = "Terraform-latest" + +func SetReqClient(name string) { + if name == "" { + return + } + ReqClient = name +} + +type LogRoundTripper struct { +} + +func (me *LogRoundTripper) RoundTrip(request *http.Request) (response *http.Response, errRet error) { + + var inBytes, outBytes []byte + + var start = time.Now() + + defer func() { me.log(inBytes, outBytes, errRet, start) }() + + bodyReader, errRet := request.GetBody() + if errRet != nil { + return + } + var headName = "X-TC-Action" + + if envReqClient := os.Getenv(REQUEST_CLIENT); envReqClient != "" { + ReqClient = envReqClient + } + + request.Header.Set("X-TC-RequestClient", ReqClient) + inBytes = []byte(fmt.Sprintf("%s, request: ", request.Header[headName])) + requestBody, errRet := ioutil.ReadAll(bodyReader) + if errRet != nil { + return + } + inBytes = append(inBytes, requestBody...) + + headName = "X-TC-Region" + appendMessage := []byte(fmt.Sprintf( + ", (host %+v, region:%+v)", + request.Header["Host"], + request.Header[headName], + )) + + inBytes = append(inBytes, appendMessage...) + + response, errRet = http.DefaultTransport.RoundTrip(request) + if errRet != nil { + return + } + outBytes, errRet = ioutil.ReadAll(response.Body) + if errRet != nil { + return + } + response.Body = ioutil.NopCloser(bytes.NewBuffer(outBytes)) + return +} + +func (me *LogRoundTripper) log(in []byte, out []byte, err error, start time.Time) { + var buf bytes.Buffer + buf.WriteString("######") + tag := "[DEBUG]" + if err != nil { + tag = "[CRITICAL]" + } + buf.WriteString(tag) + if len(in) > 0 { + buf.WriteString("tencentcloud-sdk-go: ") + buf.Write(in) + } + if len(out) > 0 { + buf.WriteString("; response:") + err := json.Compact(&buf, out) + if err != nil { + out := bytes.Replace(out, + []byte("\n"), + []byte(""), + -1) + out = bytes.Replace(out, + []byte(" "), + []byte(""), + -1) + buf.Write(out) + } + } + + if err != nil { + buf.WriteString("; error:") + buf.WriteString(err.Error()) + } + + costFormat := fmt.Sprintf(",cost %s", time.Since(start).String()) + buf.WriteString(costFormat) + + log.Println(buf.String()) +} diff --git a/website/docs/language/settings/backends/cos.mdx b/website/docs/language/settings/backends/cos.mdx index 033f57a82e..94037c2ca6 100644 --- a/website/docs/language/settings/backends/cos.mdx +++ b/website/docs/language/settings/backends/cos.mdx @@ -53,10 +53,54 @@ The following configuration options or environment variables are supported: - `secret_id` - (Optional) Secret id of Tencent Cloud. It supports environment variables `TENCENTCLOUD_SECRET_ID`. - `secret_key` - (Optional) Secret key of Tencent Cloud. It supports environment variables `TENCENTCLOUD_SECRET_KEY`. +- `security_token` - (Optional) TencentCloud Security Token of temporary access credentials. It supports environment variables `TENCENTCLOUD_SECURITY_TOKEN`. - `region` - (Optional) The region of the COS bucket. It supports environment variables `TENCENTCLOUD_REGION`. - `bucket` - (Required) The name of the COS bucket. You shall manually create it first. - `prefix` - (Optional) The directory for saving the state file in bucket. Default to "env:". - `key` - (Optional) The path for saving the state file in bucket. Defaults to `terraform.tfstate`. - `encrypt` - (Optional) Whether to enable server side encryption of the state file. If it is true, COS will use 'AES256' encryption algorithm to encrypt state file. - `acl` - (Optional) Object ACL to be applied to the state file, allows `private` and `public-read`. Defaults to `private`. -- `accelerate` - (Optional) Whether to enable global Acceleration. Defaults to `false`. \ No newline at end of file +- `accelerate` - (Optional) Whether to enable global Acceleration. Defaults to `false`. + +### Assume Role +If provided with an assume role, Terraform will attempt to assume this role using the supplied credentials. +Assume role can be provided by adding an `assume_role` block in the cos backend block. + +- `assume_role` - (Optional) The `assume_role` block. If provided, terraform will attempt to assume this role using the supplied credentials. + +The details of `assume_role` block as following: +- `role_arn` - (Required) The ARN of the role to assume. It can be sourced from the `TENCENTCLOUD_ASSUME_ROLE_ARN`. +- `session_name` - (Required) The session name to use when making the AssumeRole call. It can be sourced from the `TENCENTCLOUD_ASSUME_ROLE_SESSION_NAME`. +- `session_duration` - (Required) 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`. +- `policy` - (Optional) 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). + +Usage: + +```hcl +terraform { + backend "cos" { + region = "ap-guangzhou" + bucket = "bucket-for-terraform-state-{appid}" + prefix = "terraform/state" + assume_role { + role_arn = "qcs::cam::uin/xxx:roleName/yyy" + session_name = "my-session-name" + session_duration = 3600 + } + } +} +``` + +In addition, these `assume_role` configurations can also be provided by environment variables. + +Usage: + +```shell +$ export TENCENTCLOUD_SECRET_ID="my-secret-id" +$ export TENCENTCLOUD_SECRET_KEY="my-secret-key" +$ export TENCENTCLOUD_REGION="ap-guangzhou" +$ export TENCENTCLOUD_ASSUME_ROLE_ARN="qcs::cam::uin/xxx:roleName/yyy" +$ export TENCENTCLOUD_ASSUME_ROLE_SESSION_NAME="my-session-name" +$ export TENCENTCLOUD_ASSUME_ROLE_SESSION_DURATION=3600 +$ terraform plan +``` \ No newline at end of file