From bfae627112e9b42e4d94010fbd5437dfca1d91b4 Mon Sep 17 00:00:00 2001 From: He Guimin Date: Sat, 2 Nov 2019 00:09:30 +0800 Subject: [PATCH] add a new field ecs_role_name to support more scenario --- backend/remote-state/oss/backend.go | 115 ++++++++++++++++++++++-- website/docs/backends/types/oss.html.md | 8 +- website/docs/state/remote.html.md | 2 +- 3 files changed, 118 insertions(+), 7 deletions(-) diff --git a/backend/remote-state/oss/backend.go b/backend/remote-state/oss/backend.go index 0b0a4bd71..8e868c176 100644 --- a/backend/remote-state/oss/backend.go +++ b/backend/remote-state/oss/backend.go @@ -5,11 +5,13 @@ import ( "encoding/json" "fmt" "github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests" + "github.com/aliyun/alibaba-cloud-sdk-go/sdk/responses" "github.com/aliyun/alibaba-cloud-sdk-go/services/sts" "github.com/aliyun/aliyun-oss-go-sdk/oss" "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/validation" + "github.com/jmespath/go-jmespath" "io/ioutil" "os" "runtime" @@ -21,6 +23,7 @@ import ( "github.com/aliyun/aliyun-tablestore-go-sdk/tablestore" "github.com/hashicorp/go-cleanhttp" "github.com/hashicorp/terraform/version" + "github.com/mitchellh/go-homedir" "log" "net/http" "strconv" @@ -52,6 +55,13 @@ func New() backend.Backend { DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_SECURITY_TOKEN", ""), }, + "ecs_role_name": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_ECS_ROLE_NAME", os.Getenv("ALICLOUD_ECS_ROLE_NAME")), + Description: "The RAM Role Name attached on a ECS instance for API operations. You can retrieve this from the 'Access Control' section of the Alibaba Cloud console.", + }, + "region": &schema.Schema{ Type: schema.TypeString, Optional: true, @@ -140,6 +150,7 @@ func New() backend.Backend { "shared_credentials_file": { Type: schema.TypeString, Optional: true, + DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_SHARED_CREDENTIALS_FILE", ""), Description: "This is the path to the shared credentials file. If this is not set and a profile is specified, `~/.aliyun/config.json` will be used.", }, "profile": { @@ -276,8 +287,17 @@ func (b *Backend) configure(ctx context.Context) error { } } + if accessKey == "" { + ecsRoleName := getBackendConfig(d.Get("ecs_role_name").(string), "ram_role_name") + subAccessKeyId, subAccessKeySecret, subSecurityToken, err := getAuthCredentialByEcsRoleName(ecsRoleName) + if err != nil { + return err + } + accessKey, secretKey, securityToken = subAccessKeyId, subAccessKeySecret, subSecurityToken + } + if roleArn != "" { - subAccessKeyId, subAccessKeySecret, subSecurityToken, err := getAssumeRoleAK(accessKey, secretKey, region, roleArn, sessionName, policy, sessionExpiration) + subAccessKeyId, subAccessKeySecret, subSecurityToken, err := getAssumeRoleAK(accessKey, secretKey, securityToken, region, roleArn, sessionName, policy, sessionExpiration) if err != nil { return err } @@ -347,7 +367,7 @@ func (b *Backend) getOSSEndpointByRegion(access_key, secret_key, security_token, return endpointsResponse, nil } -func getAssumeRoleAK(accessKey, secretKey, region, roleArn, sessionName, policy string, sessionExpiration int) (string, string, string, error) { +func getAssumeRoleAK(accessKey, secretKey, stsToken, region, roleArn, sessionName, policy string, sessionExpiration int) (string, string, string, error) { request := sts.CreateAssumeRoleRequest() request.RoleArn = roleArn request.RoleSessionName = sessionName @@ -355,7 +375,13 @@ func getAssumeRoleAK(accessKey, secretKey, region, roleArn, sessionName, policy request.Policy = policy request.Scheme = "https" - client, err := sts.NewClientWithAccessKey(region, accessKey, secretKey) + var client *sts.Client + var err error + if stsToken == "" { + client, err = sts.NewClientWithAccessKey(region, accessKey, secretKey) + } else { + client, err = sts.NewClientWithStsToken(region, accessKey, secretKey, stsToken) + } if err != nil { return "", "", "", err } @@ -445,7 +471,11 @@ func getConfigFromProfile(d *schema.ResourceData, ProfileKey string) (interface{ return nil, nil } current := d.Get("profile").(string) - profilePath := d.Get("shared_credentials_file").(string) + // Set CredsFilename, expanding home directory + profilePath, err := homedir.Expand(d.Get("shared_credentials_file").(string)) + if err != nil { + return nil, err + } if profilePath == "" { profilePath = fmt.Sprintf("%s/.aliyun/config.json", os.Getenv("HOME")) if runtime.GOOS == "windows" { @@ -453,7 +483,7 @@ func getConfigFromProfile(d *schema.ResourceData, ProfileKey string) (interface{ } } providerConfig = make(map[string]interface{}) - _, err := os.Stat(profilePath) + _, err = os.Stat(profilePath) if !os.IsNotExist(err) { data, err := ioutil.ReadFile(profilePath) if err != nil { @@ -503,3 +533,78 @@ func getConfigFromProfile(d *schema.ResourceData, ProfileKey string) (interface{ return providerConfig[ProfileKey], nil } + +var securityCredURL = "http://100.100.100.200/latest/meta-data/ram/security-credentials/" + +// getAuthCredentialByEcsRoleName aims to access meta to get sts credential +// Actually, the job should be done by sdk, but currently not all resources and products support alibaba-cloud-sdk-go, +// and their go sdk does support ecs role name. +// This method is a temporary solution and it should be removed after all go sdk support ecs role name +// The related PR: https://github.com/terraform-providers/terraform-provider-alicloud/pull/731 +func getAuthCredentialByEcsRoleName(ecsRoleName string) (accessKey, secretKey, token string, err error) { + + if ecsRoleName == "" { + return + } + requestUrl := securityCredURL + ecsRoleName + httpRequest, err := http.NewRequest(requests.GET, requestUrl, strings.NewReader("")) + if err != nil { + err = fmt.Errorf("build sts requests err: %s", err.Error()) + return + } + httpClient := &http.Client{} + httpResponse, err := httpClient.Do(httpRequest) + if err != nil { + err = fmt.Errorf("get Ecs sts token err : %s", err.Error()) + return + } + + response := responses.NewCommonResponse() + err = responses.Unmarshal(response, httpResponse, "") + if err != nil { + err = fmt.Errorf("Unmarshal Ecs sts token response err : %s", err.Error()) + return + } + + if response.GetHttpStatus() != http.StatusOK { + err = fmt.Errorf("get Ecs sts token err, httpStatus: %d, message = %s", response.GetHttpStatus(), response.GetHttpContentString()) + return + } + var data interface{} + err = json.Unmarshal(response.GetHttpContentBytes(), &data) + if err != nil { + err = fmt.Errorf("refresh Ecs sts token err, json.Unmarshal fail: %s", err.Error()) + return + } + code, err := jmespath.Search("Code", data) + if err != nil { + err = fmt.Errorf("refresh Ecs sts token err, fail to get Code: %s", err.Error()) + return + } + if code.(string) != "Success" { + err = fmt.Errorf("refresh Ecs sts token err, Code is not Success") + return + } + accessKeyId, err := jmespath.Search("AccessKeyId", data) + if err != nil { + err = fmt.Errorf("refresh Ecs sts token err, fail to get AccessKeyId: %s", err.Error()) + return + } + accessKeySecret, err := jmespath.Search("AccessKeySecret", data) + if err != nil { + err = fmt.Errorf("refresh Ecs sts token err, fail to get AccessKeySecret: %s", err.Error()) + return + } + securityToken, err := jmespath.Search("SecurityToken", data) + if err != nil { + err = fmt.Errorf("refresh Ecs sts token err, fail to get SecurityToken: %s", err.Error()) + return + } + + if accessKeyId == nil || accessKeySecret == nil || securityToken == nil { + err = fmt.Errorf("there is no any available accesskey, secret and security token for Ecs role %s", ecsRoleName) + return + } + + return accessKeyId.(string), accessKeySecret.(string), securityToken.(string), nil +} diff --git a/website/docs/backends/types/oss.html.md b/website/docs/backends/types/oss.html.md index 08e3a11f8..4cd04dda3 100644 --- a/website/docs/backends/types/oss.html.md +++ b/website/docs/backends/types/oss.html.md @@ -16,6 +16,11 @@ This backend also supports state locking and consistency checking via [Alibaba Cloud Table Store](https://www.alibabacloud.com/help/doc-detail/27280.htm), which can be enabled by setting the `tablestore_table` field to an existing TableStore table name. +-> **Note:** The OSS backend is available from terraform version 0.12.2. + +!> **Warning:** If you set `tablestore_table`, please ensure the table does not contain primary key named +`LockID`, `Info` and `Digest`. Otherwise, there will throw an error `OTSParameterInvalid Duplicated attribute column ...`. + ## Example Configuration ```hcl @@ -78,6 +83,7 @@ The following configuration options or environment variables are supported: * `access_key` - (Optional) Alibaba Cloud access key. It supports environment variables `ALICLOUD_ACCESS_KEY` and `ALICLOUD_ACCESS_KEY_ID`. * `secret_key` - (Optional) Alibaba Cloud secret access key. It supports environment variables `ALICLOUD_SECRET_KEY` and `ALICLOUD_ACCESS_KEY_SECRET`. * `security_token` - (Optional) STS access token. It supports environment variable `ALICLOUD_SECURITY_TOKEN`. + * `ecs_role_name` - (Optional, Available in 0.12.14+) The RAM Role Name attached on a ECS instance for API operations. You can retrieve this from the 'Access Control' section of the Alibaba Cloud console. * `region` - (Optional) The region of the OSS bucket. It supports environment variables `ALICLOUD_REGION` and `ALICLOUD_DEFAULT_REGION`. * `endpoint` - (Optional) A custom endpoint for the OSS API. It supports environment variables `ALICLOUD_OSS_ENDPOINT` and `OSS_ENDPOINT`. * `bucket` - (Required) The name of the OSS bucket. @@ -90,7 +96,7 @@ The following configuration options or environment variables are supported: * `acl` - (Optional) [Object ACL](https://www.alibabacloud.com/help/doc-detail/52284.htm) to be applied to the state file. - * `shared_credentials_file` - (Optional, Available in 0.12.8+) This is the path to the shared credentials file. If this is not set and a profile is specified, `~/.aliyun/config.json` will be used. + * `shared_credentials_file` - (Optional, Available in 0.12.8+) This is the path to the shared credentials file. It can also be sourced from the `ALICLOUD_SHARED_CREDENTIALS_FILE` environment variable. If this is not set and a profile is specified, `~/.aliyun/config.json` will be used. * `profile` - (Optional, Available in 0.12.8+) This is the Alibaba Cloud profile name as set in the shared credentials file. It can also be sourced from the `ALICLOUD_PROFILE` environment variable. * `assume_role` - (Optional, Available in 0.12.6+) If provided with a role ARN, will attempt to assume this role using the supplied credentials. diff --git a/website/docs/state/remote.html.md b/website/docs/state/remote.html.md index bb3c05396..ace197fac 100644 --- a/website/docs/state/remote.html.md +++ b/website/docs/state/remote.html.md @@ -17,7 +17,7 @@ Terraform at the same time. With _remote_ state, Terraform writes the state data to a remote data store, which can then be shared between all members of a team. Terraform supports storing state in [Terraform Cloud](https://www.hashicorp.com/products/terraform/), -[HashiCorp Consul](https://www.consul.io/), Amazon S3, and more. +[HashiCorp Consul](https://www.consul.io/), Amazon S3, Alibaba Cloud OSS, and more. Remote state is a feature of [backends](/docs/backends). Configuring and using remote backends is easy and you can get started with remote state