package oss import ( "context" "encoding/json" "fmt" "io/ioutil" "log" "net/http" "net/url" "os" "regexp" "runtime" "strconv" "strings" "time" "github.com/aliyun/alibaba-cloud-sdk-go/sdk" "github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials" "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/location" "github.com/aliyun/alibaba-cloud-sdk-go/services/sts" "github.com/aliyun/aliyun-oss-go-sdk/oss" "github.com/aliyun/aliyun-tablestore-go-sdk/tablestore" "github.com/hashicorp/go-cleanhttp" "github.com/jmespath/go-jmespath" "github.com/mitchellh/go-homedir" "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/legacy/helper/schema" "github.com/hashicorp/terraform/version" ) // Deprecated in favor of flattening assume_role_* options func deprecatedAssumeRoleSchema() *schema.Schema { return &schema.Schema{ Type: schema.TypeSet, Optional: true, ConflictsWith: []string{"assume_role_role_arn", "assume_role_session_name", "assume_role_policy", "assume_role_session_expiration"}, MaxItems: 1, Deprecated: "use assume_role_* options instead", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "role_arn": { Type: schema.TypeString, Required: true, Description: "The ARN of a RAM role to assume prior to making API calls.", DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_ASSUME_ROLE_ARN", ""), }, "session_name": { Type: schema.TypeString, Optional: true, Description: "The session name to use when assuming the role.", DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_ASSUME_ROLE_SESSION_NAME", ""), }, "policy": { Type: schema.TypeString, Optional: true, Description: "The permissions applied when assuming a role. You cannot use this policy to grant permissions which exceed those of the role that is being assumed.", }, "session_expiration": { Type: schema.TypeInt, Optional: true, Description: "The time after which the established session for assuming role expires.", ValidateFunc: func(v interface{}, k string) ([]string, []error) { min := 900 max := 3600 value, ok := v.(int) if !ok { return nil, []error{fmt.Errorf("expected type of %s to be int", k)} } if value < min || value > max { return nil, []error{fmt.Errorf("expected %s to be in the range (%d - %d), got %d", k, min, max, v)} } return nil, nil }, }, }, }, } } // New creates a new backend for OSS remote state. func New() backend.Backend { s := &schema.Backend{ Schema: map[string]*schema.Schema{ "access_key": { Type: schema.TypeString, Optional: true, Description: "Alibaba Cloud Access Key ID", DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_ACCESS_KEY", os.Getenv("ALICLOUD_ACCESS_KEY_ID")), }, "secret_key": { Type: schema.TypeString, Optional: true, Description: "Alibaba Cloud Access Secret Key", DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_SECRET_KEY", os.Getenv("ALICLOUD_ACCESS_KEY_SECRET")), }, "security_token": { Type: schema.TypeString, Optional: true, Description: "Alibaba Cloud Security Token", 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": { Type: schema.TypeString, Optional: true, Description: "The region of the OSS bucket.", DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_REGION", os.Getenv("ALICLOUD_DEFAULT_REGION")), }, "tablestore_endpoint": { Type: schema.TypeString, Optional: true, Description: "A custom endpoint for the TableStore API", DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_TABLESTORE_ENDPOINT", ""), }, "endpoint": { Type: schema.TypeString, Optional: true, Description: "A custom endpoint for the OSS API", DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_OSS_ENDPOINT", os.Getenv("OSS_ENDPOINT")), }, "bucket": { Type: schema.TypeString, Required: true, Description: "The name of the OSS bucket", }, "prefix": { Type: schema.TypeString, Optional: true, Description: "The directory where state files will be saved inside the bucket", Default: "env:", ValidateFunc: func(v interface{}, s string) ([]string, []error) { prefix := v.(string) if strings.HasPrefix(prefix, "/") || strings.HasPrefix(prefix, "./") { return nil, []error{fmt.Errorf("workspace_key_prefix must not start with '/' or './'")} } return nil, nil }, }, "key": { Type: schema.TypeString, Optional: true, Description: "The path of the state file inside the bucket", ValidateFunc: func(v interface{}, s string) ([]string, []error) { if strings.HasPrefix(v.(string), "/") || strings.HasSuffix(v.(string), "/") { return nil, []error{fmt.Errorf("key can not start and end with '/'")} } return nil, nil }, Default: "terraform.tfstate", }, "tablestore_table": { Type: schema.TypeString, Optional: true, Description: "TableStore table for state locking and consistency", Default: "", }, "encrypt": { Type: schema.TypeBool, Optional: true, Description: "Whether to enable server side encryption of the state file", Default: false, }, "acl": { Type: schema.TypeString, Optional: true, Description: "Object ACL to be applied to the state file", Default: "", ValidateFunc: func(v interface{}, k string) ([]string, []error) { if value := v.(string); value != "" { acls := oss.ACLType(value) if acls != oss.ACLPrivate && acls != oss.ACLPublicRead && acls != oss.ACLPublicReadWrite { return nil, []error{fmt.Errorf( "%q must be a valid ACL value , expected %s, %s or %s, got %q", k, oss.ACLPrivate, oss.ACLPublicRead, oss.ACLPublicReadWrite, acls)} } } return nil, nil }, }, "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": { Type: schema.TypeString, Optional: true, Description: "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.", DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_PROFILE", ""), }, "assume_role": deprecatedAssumeRoleSchema(), "assume_role_role_arn": { Type: schema.TypeString, Optional: true, ConflictsWith: []string{"assume_role"}, Description: "The ARN of a RAM role to assume prior to making API calls.", DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_ASSUME_ROLE_ARN", ""), }, "assume_role_session_name": { Type: schema.TypeString, Optional: true, ConflictsWith: []string{"assume_role"}, Description: "The session name to use when assuming the role.", DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_ASSUME_ROLE_SESSION_NAME", ""), }, "assume_role_policy": { Type: schema.TypeString, Optional: true, ConflictsWith: []string{"assume_role"}, Description: "The permissions applied when assuming a role. You cannot use this policy to grant permissions which exceed those of the role that is being assumed.", }, "assume_role_session_expiration": { Type: schema.TypeInt, Optional: true, ConflictsWith: []string{"assume_role"}, Description: "The time after which the established session for assuming role expires.", ValidateFunc: func(v interface{}, k string) ([]string, []error) { min := 900 max := 3600 value, ok := v.(int) if !ok { return nil, []error{fmt.Errorf("expected type of %s to be int", k)} } if value < min || value > max { return nil, []error{fmt.Errorf("expected %s to be in the range (%d - %d), got %d", k, min, max, v)} } return nil, nil }, }, }, } result := &Backend{Backend: s} result.Backend.ConfigureFunc = result.configure return result } type Backend struct { *schema.Backend // The fields below are set from configure ossClient *oss.Client otsClient *tablestore.TableStoreClient bucketName string statePrefix string stateKey string serverSideEncryption bool acl string otsEndpoint string otsTable string } func (b *Backend) configure(ctx context.Context) error { if b.ossClient != nil { return nil } // Grab the resource data d := schema.FromContextBackendConfig(ctx) b.bucketName = d.Get("bucket").(string) b.statePrefix = strings.TrimPrefix(strings.Trim(d.Get("prefix").(string), "/"), "./") b.stateKey = d.Get("key").(string) b.serverSideEncryption = d.Get("encrypt").(bool) b.acl = d.Get("acl").(string) var getBackendConfig = func(str string, key string) string { if str == "" { value, err := getConfigFromProfile(d, key) if err == nil && value != nil { str = value.(string) } } return str } accessKey := getBackendConfig(d.Get("access_key").(string), "access_key_id") secretKey := getBackendConfig(d.Get("secret_key").(string), "access_key_secret") securityToken := getBackendConfig(d.Get("security_token").(string), "sts_token") region := getBackendConfig(d.Get("region").(string), "region_id") endpoint := d.Get("endpoint").(string) schma := "https" roleArn := getBackendConfig("", "ram_role_arn") sessionName := getBackendConfig("", "ram_session_name") var policy string var sessionExpiration int expiredSeconds, err := getConfigFromProfile(d, "expired_seconds") if err == nil && expiredSeconds != nil { sessionExpiration = (int)(expiredSeconds.(float64)) } if v, ok := d.GetOk("assume_role"); ok { // deprecated assume_role block for _, v := range v.(*schema.Set).List() { assumeRole := v.(map[string]interface{}) if assumeRole["role_arn"].(string) != "" { roleArn = assumeRole["role_arn"].(string) } if assumeRole["session_name"].(string) != "" { sessionName = assumeRole["session_name"].(string) } policy = assumeRole["policy"].(string) sessionExpiration = assumeRole["session_expiration"].(int) } } else { roleArn = d.Get("assume_role_role_arn").(string) sessionName = d.Get("assume_role_session_name").(string) policy = d.Get("assume_role_policy").(string) sessionExpiration = d.Get("assume_role_session_expiration").(int) } if sessionName == "" { sessionName = "terraform" } if sessionExpiration == 0 { if v := os.Getenv("ALICLOUD_ASSUME_ROLE_SESSION_EXPIRATION"); v != "" { if expiredSeconds, err := strconv.Atoi(v); err == nil { sessionExpiration = expiredSeconds } } if sessionExpiration == 0 { sessionExpiration = 3600 } } 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, securityToken, region, roleArn, sessionName, policy, sessionExpiration) if err != nil { return err } accessKey, secretKey, securityToken = subAccessKeyId, subAccessKeySecret, subSecurityToken } if endpoint == "" { endpointsResponse, err := b.getOSSEndpointByRegion(accessKey, secretKey, securityToken, region) if err != nil { return err } for _, endpointItem := range endpointsResponse.Endpoints.Endpoint { if endpointItem.Type == "openAPI" { endpoint = endpointItem.Endpoint break } } if endpoint == "" { endpoint = fmt.Sprintf("oss-%s.aliyuncs.com", region) } } if !strings.HasPrefix(endpoint, "http") { endpoint = fmt.Sprintf("%s://%s", schma, endpoint) } log.Printf("[DEBUG] Instantiate OSS client using endpoint: %#v", endpoint) var options []oss.ClientOption if securityToken != "" { options = append(options, oss.SecurityToken(securityToken)) } options = append(options, oss.UserAgent(fmt.Sprintf("%s/%s", TerraformUA, TerraformVersion))) proxyUrl := getHttpProxyUrl() if proxyUrl != nil { options = append(options, oss.Proxy(proxyUrl.String())) } client, err := oss.New(endpoint, accessKey, secretKey, options...) b.ossClient = client otsEndpoint := d.Get("tablestore_endpoint").(string) if otsEndpoint != "" { if !strings.HasPrefix(otsEndpoint, "http") { otsEndpoint = fmt.Sprintf("%s://%s", schma, otsEndpoint) } b.otsEndpoint = otsEndpoint parts := strings.Split(strings.TrimPrefix(strings.TrimPrefix(otsEndpoint, "https://"), "http://"), ".") b.otsClient = tablestore.NewClientWithConfig(otsEndpoint, parts[0], accessKey, secretKey, securityToken, tablestore.NewDefaultTableStoreConfig()) } b.otsTable = d.Get("tablestore_table").(string) return err } func (b *Backend) getOSSEndpointByRegion(access_key, secret_key, security_token, region string) (*location.DescribeEndpointsResponse, error) { args := location.CreateDescribeEndpointsRequest() args.ServiceCode = "oss" args.Id = region args.Domain = "location-readonly.aliyuncs.com" locationClient, err := location.NewClientWithOptions(region, getSdkConfig(), credentials.NewStsTokenCredential(access_key, secret_key, security_token)) if err != nil { return nil, fmt.Errorf("unable to initialize the location client: %#v", err) } locationClient.AppendUserAgent(TerraformUA, TerraformVersion) endpointsResponse, err := locationClient.DescribeEndpoints(args) if err != nil { return nil, fmt.Errorf("describe oss endpoint using region: %#v got an error: %#v", region, err) } return endpointsResponse, nil } 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 request.DurationSeconds = requests.NewInteger(sessionExpiration) request.Policy = policy request.Scheme = "https" 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 } response, err := client.AssumeRole(request) if err != nil { return "", "", "", err } return response.Credentials.AccessKeyId, response.Credentials.AccessKeySecret, response.Credentials.SecurityToken, nil } func getSdkConfig() *sdk.Config { return sdk.NewConfig(). WithMaxRetryTime(5). WithTimeout(time.Duration(30) * time.Second). WithGoRoutinePoolSize(10). WithDebug(false). WithHttpTransport(getTransport()). WithScheme("HTTPS") } func getTransport() *http.Transport { handshakeTimeout, err := strconv.Atoi(os.Getenv("TLSHandshakeTimeout")) if err != nil { handshakeTimeout = 120 } transport := cleanhttp.DefaultTransport() transport.TLSHandshakeTimeout = time.Duration(handshakeTimeout) * time.Second transport.Proxy = http.ProxyFromEnvironment return transport } type Invoker struct { catchers []*Catcher } type Catcher struct { Reason string RetryCount int RetryWaitSeconds int } const TerraformUA = "HashiCorp-Terraform" var TerraformVersion = strings.TrimSuffix(version.String(), "-dev") var ClientErrorCatcher = Catcher{"AliyunGoClientFailure", 10, 3} var ServiceBusyCatcher = Catcher{"ServiceUnavailable", 10, 3} func NewInvoker() Invoker { i := Invoker{} i.AddCatcher(ClientErrorCatcher) i.AddCatcher(ServiceBusyCatcher) return i } func (a *Invoker) AddCatcher(catcher Catcher) { a.catchers = append(a.catchers, &catcher) } func (a *Invoker) Run(f func() error) error { err := f() if err == nil { return nil } for _, catcher := range a.catchers { if strings.Contains(err.Error(), catcher.Reason) { catcher.RetryCount-- if catcher.RetryCount <= 0 { return fmt.Errorf("retry timeout and got an error: %#v", err) } else { time.Sleep(time.Duration(catcher.RetryWaitSeconds) * time.Second) return a.Run(f) } } } return err } var providerConfig map[string]interface{} func getConfigFromProfile(d *schema.ResourceData, ProfileKey string) (interface{}, error) { if providerConfig == nil { if v, ok := d.GetOk("profile"); !ok || v.(string) == "" { return nil, nil } current := d.Get("profile").(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" { profilePath = fmt.Sprintf("%s/.aliyun/config.json", os.Getenv("USERPROFILE")) } } providerConfig = make(map[string]interface{}) _, err = os.Stat(profilePath) if !os.IsNotExist(err) { data, err := ioutil.ReadFile(profilePath) if err != nil { return nil, err } config := map[string]interface{}{} err = json.Unmarshal(data, &config) if err != nil { return nil, err } for _, v := range config["profiles"].([]interface{}) { if current == v.(map[string]interface{})["name"] { providerConfig = v.(map[string]interface{}) } } } } mode := "" if v, ok := providerConfig["mode"]; ok { mode = v.(string) } else { return v, nil } switch ProfileKey { case "access_key_id", "access_key_secret": if mode == "EcsRamRole" { return "", nil } case "ram_role_name": if mode != "EcsRamRole" { return "", nil } case "sts_token": if mode != "StsToken" { return "", nil } case "ram_role_arn", "ram_session_name": if mode != "RamRoleArn" { return "", nil } case "expired_seconds": if mode != "RamRoleArn" { return float64(0), nil } } 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 } func getHttpProxyUrl() *url.URL { for _, v := range []string{"HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"} { value := strings.Trim(os.Getenv(v), " ") if value != "" { if !regexp.MustCompile(`^http(s)?://`).MatchString(value) { value = fmt.Sprintf("https://%s", value) } proxyUrl, err := url.Parse(value) if err == nil { return proxyUrl } break } } return nil }