Support assume role for cos backend (#32631)

* go get github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sts/v20180813@v1.0.588

* feat:support assume_role for COS backend

* update go.mod and go.sum

* change secret_id and secret_key from required to optional

* update cos doc

* update logic by comments

* rm sensitive info in log
This commit is contained in:
Yin Luo 2023-02-13 17:25:58 +08:00 committed by GitHub
parent 26e85685d1
commit e9b066f514
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 332 additions and 22 deletions

3
go.mod
View File

@ -70,7 +70,8 @@ require (
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/posener/complete v1.2.3 github.com/posener/complete v1.2.3
github.com/spf13/afero v1.2.2 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/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.233
github.com/tencentyun/cos-go-sdk-v5 v0.7.29 github.com/tencentyun/cos-go-sdk-v5 v0.7.29
github.com/tombuildsstuff/giovanni v0.15.1 github.com/tombuildsstuff/giovanni v0.15.1

6
go.sum
View File

@ -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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 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.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.588 h1:DYtBXB7sVc3EOW5horg8j55cLZynhsLYhHrvQ/jXKKM=
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/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/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 h1:5Tbi+jyZ2MojC6GK8V6hchwtnkP2IuENUTqSisbYOlA=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.233/go.mod h1:sX14+NSvMjOhNFaMtP2aDy6Bss8PyFXij21gpY6+DAs= 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= github.com/tencentyun/cos-go-sdk-v5 v0.7.29 h1:uwRBzc70Wgtc5iQQCowqecfRT0OpCXUOZzodZHOOEDs=

View File

@ -5,6 +5,8 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"os"
"strconv"
"strings" "strings"
"time" "time"
@ -12,24 +14,31 @@ import (
"github.com/hashicorp/terraform/internal/legacy/helper/schema" "github.com/hashicorp/terraform/internal/legacy/helper/schema"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" "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" tag "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813"
"github.com/tencentyun/cos-go-sdk-v5" "github.com/tencentyun/cos-go-sdk-v5"
) )
// Default value from environment variable // Default value from environment variable
const ( const (
PROVIDER_SECRET_ID = "TENCENTCLOUD_SECRET_ID" PROVIDER_SECRET_ID = "TENCENTCLOUD_SECRET_ID"
PROVIDER_SECRET_KEY = "TENCENTCLOUD_SECRET_KEY" PROVIDER_SECRET_KEY = "TENCENTCLOUD_SECRET_KEY"
PROVIDER_REGION = "TENCENTCLOUD_REGION" 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 // Backend implements "backend".Backend for tencentCloud cos
type Backend struct { type Backend struct {
*schema.Backend *schema.Backend
credential *common.Credential
cosContext context.Context cosContext context.Context
cosClient *cos.Client cosClient *cos.Client
tagClient *tag.Client tagClient *tag.Client
stsClient *sts.Client
region string region string
bucket string bucket string
@ -45,17 +54,24 @@ func New() backend.Backend {
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"secret_id": { "secret_id": {
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Optional: true,
DefaultFunc: schema.EnvDefaultFunc(PROVIDER_SECRET_ID, nil), DefaultFunc: schema.EnvDefaultFunc(PROVIDER_SECRET_ID, nil),
Description: "Secret id of Tencent Cloud", Description: "Secret id of Tencent Cloud",
}, },
"secret_key": { "secret_key": {
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Optional: true,
DefaultFunc: schema.EnvDefaultFunc(PROVIDER_SECRET_KEY, nil), DefaultFunc: schema.EnvDefaultFunc(PROVIDER_SECRET_KEY, nil),
Description: "Secret key of Tencent Cloud", Description: "Secret key of Tencent Cloud",
Sensitive: true, 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": { "region": {
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
@ -119,6 +135,45 @@ func New() backend.Backend {
Description: "Whether to enable global Acceleration", Description: "Whether to enable global Acceleration",
Default: false, 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 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 // configure init cos client
func (b *Backend) configure(ctx context.Context) error { func (b *Backend) configure(ctx context.Context) error {
if b.cosClient != nil { if b.cosClient != nil {
@ -158,27 +228,108 @@ func (b *Backend) configure(ctx context.Context) error {
return err 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( b.cosClient = cos.NewClient(
&cos.BaseURL{BucketURL: u}, &cos.BaseURL{BucketURL: u},
&http.Client{ &http.Client{
Timeout: 60 * time.Second, Timeout: 60 * time.Second,
Transport: &cos.AuthorizationTransport{ Transport: &cos.AuthorizationTransport{
SecretID: data.Get("secret_id").(string), SecretID: b.credential.SecretId,
SecretKey: data.Get("secret_key").(string), SecretKey: b.credential.SecretKey,
SessionToken: b.credential.Token,
}, },
}, },
) )
credential := common.NewCredential( b.tagClient = b.UseTagClient()
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)
return err 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
}

View File

@ -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())
}

View File

@ -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_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`. - `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`. - `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. - `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:". - `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`. - `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. - `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`. - `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`. - `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
```