mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
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:
parent
26e85685d1
commit
e9b066f514
3
go.mod
3
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
|
||||
|
6
go.sum
6
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=
|
||||
|
@ -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
|
||||
}
|
||||
|
112
internal/backend/remote-state/cos/transport.go
Normal file
112
internal/backend/remote-state/cos/transport.go
Normal 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())
|
||||
}
|
@ -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`.
|
||||
- `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
|
||||
```
|
Loading…
Reference in New Issue
Block a user