github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/internal/backend/remote-state/oss/backend.go (about) 1 package oss 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io/ioutil" 8 "log" 9 "net/http" 10 "net/url" 11 "os" 12 "regexp" 13 "runtime" 14 "strconv" 15 "strings" 16 "time" 17 18 "github.com/aliyun/alibaba-cloud-sdk-go/sdk" 19 "github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials" 20 "github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests" 21 "github.com/aliyun/alibaba-cloud-sdk-go/sdk/responses" 22 "github.com/aliyun/alibaba-cloud-sdk-go/services/location" 23 "github.com/aliyun/alibaba-cloud-sdk-go/services/sts" 24 "github.com/aliyun/aliyun-oss-go-sdk/oss" 25 "github.com/aliyun/aliyun-tablestore-go-sdk/tablestore" 26 "github.com/hashicorp/go-cleanhttp" 27 "github.com/hashicorp/terraform/internal/backend" 28 "github.com/hashicorp/terraform/internal/legacy/helper/schema" 29 "github.com/hashicorp/terraform/version" 30 "github.com/jmespath/go-jmespath" 31 "github.com/mitchellh/go-homedir" 32 ) 33 34 // New creates a new backend for OSS remote state. 35 func New() backend.Backend { 36 s := &schema.Backend{ 37 Schema: map[string]*schema.Schema{ 38 "access_key": &schema.Schema{ 39 Type: schema.TypeString, 40 Optional: true, 41 Description: "Alibaba Cloud Access Key ID", 42 DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_ACCESS_KEY", os.Getenv("ALICLOUD_ACCESS_KEY_ID")), 43 }, 44 45 "secret_key": &schema.Schema{ 46 Type: schema.TypeString, 47 Optional: true, 48 Description: "Alibaba Cloud Access Secret Key", 49 DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_SECRET_KEY", os.Getenv("ALICLOUD_ACCESS_KEY_SECRET")), 50 }, 51 52 "security_token": &schema.Schema{ 53 Type: schema.TypeString, 54 Optional: true, 55 Description: "Alibaba Cloud Security Token", 56 DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_SECURITY_TOKEN", ""), 57 }, 58 59 "ecs_role_name": { 60 Type: schema.TypeString, 61 Optional: true, 62 DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_ECS_ROLE_NAME", os.Getenv("ALICLOUD_ECS_ROLE_NAME")), 63 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.", 64 }, 65 66 "region": &schema.Schema{ 67 Type: schema.TypeString, 68 Optional: true, 69 Description: "The region of the OSS bucket.", 70 DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_REGION", os.Getenv("ALICLOUD_DEFAULT_REGION")), 71 }, 72 "tablestore_endpoint": { 73 Type: schema.TypeString, 74 Optional: true, 75 Description: "A custom endpoint for the TableStore API", 76 DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_TABLESTORE_ENDPOINT", ""), 77 }, 78 "endpoint": { 79 Type: schema.TypeString, 80 Optional: true, 81 Description: "A custom endpoint for the OSS API", 82 DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_OSS_ENDPOINT", os.Getenv("OSS_ENDPOINT")), 83 }, 84 85 "bucket": &schema.Schema{ 86 Type: schema.TypeString, 87 Required: true, 88 Description: "The name of the OSS bucket", 89 }, 90 91 "prefix": &schema.Schema{ 92 Type: schema.TypeString, 93 Optional: true, 94 Description: "The directory where state files will be saved inside the bucket", 95 Default: "env:", 96 ValidateFunc: func(v interface{}, s string) ([]string, []error) { 97 prefix := v.(string) 98 if strings.HasPrefix(prefix, "/") || strings.HasPrefix(prefix, "./") { 99 return nil, []error{fmt.Errorf("workspace_key_prefix must not start with '/' or './'")} 100 } 101 return nil, nil 102 }, 103 }, 104 105 "key": &schema.Schema{ 106 Type: schema.TypeString, 107 Optional: true, 108 Description: "The path of the state file inside the bucket", 109 ValidateFunc: func(v interface{}, s string) ([]string, []error) { 110 if strings.HasPrefix(v.(string), "/") || strings.HasSuffix(v.(string), "/") { 111 return nil, []error{fmt.Errorf("key can not start and end with '/'")} 112 } 113 return nil, nil 114 }, 115 Default: "terraform.tfstate", 116 }, 117 118 "tablestore_table": { 119 Type: schema.TypeString, 120 Optional: true, 121 Description: "TableStore table for state locking and consistency", 122 Default: "", 123 }, 124 125 "encrypt": &schema.Schema{ 126 Type: schema.TypeBool, 127 Optional: true, 128 Description: "Whether to enable server side encryption of the state file", 129 Default: false, 130 }, 131 132 "acl": &schema.Schema{ 133 Type: schema.TypeString, 134 Optional: true, 135 Description: "Object ACL to be applied to the state file", 136 Default: "", 137 ValidateFunc: func(v interface{}, k string) ([]string, []error) { 138 if value := v.(string); value != "" { 139 acls := oss.ACLType(value) 140 if acls != oss.ACLPrivate && acls != oss.ACLPublicRead && acls != oss.ACLPublicReadWrite { 141 return nil, []error{fmt.Errorf( 142 "%q must be a valid ACL value , expected %s, %s or %s, got %q", 143 k, oss.ACLPrivate, oss.ACLPublicRead, oss.ACLPublicReadWrite, acls)} 144 } 145 } 146 return nil, nil 147 }, 148 }, 149 150 "assume_role": assumeRoleSchema(), 151 "shared_credentials_file": { 152 Type: schema.TypeString, 153 Optional: true, 154 DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_SHARED_CREDENTIALS_FILE", ""), 155 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.", 156 }, 157 "profile": { 158 Type: schema.TypeString, 159 Optional: true, 160 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.", 161 DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_PROFILE", ""), 162 }, 163 }, 164 } 165 166 result := &Backend{Backend: s} 167 result.Backend.ConfigureFunc = result.configure 168 return result 169 } 170 171 func assumeRoleSchema() *schema.Schema { 172 return &schema.Schema{ 173 Type: schema.TypeSet, 174 Optional: true, 175 MaxItems: 1, 176 Elem: &schema.Resource{ 177 Schema: map[string]*schema.Schema{ 178 "role_arn": { 179 Type: schema.TypeString, 180 Required: true, 181 Description: "The ARN of a RAM role to assume prior to making API calls.", 182 DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_ASSUME_ROLE_ARN", ""), 183 }, 184 "session_name": { 185 Type: schema.TypeString, 186 Optional: true, 187 Description: "The session name to use when assuming the role.", 188 DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_ASSUME_ROLE_SESSION_NAME", ""), 189 }, 190 "policy": { 191 Type: schema.TypeString, 192 Optional: true, 193 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.", 194 }, 195 "session_expiration": { 196 Type: schema.TypeInt, 197 Optional: true, 198 Description: "The time after which the established session for assuming role expires.", 199 ValidateFunc: func(v interface{}, k string) ([]string, []error) { 200 min := 900 201 max := 3600 202 value, ok := v.(int) 203 if !ok { 204 return nil, []error{fmt.Errorf("expected type of %s to be int", k)} 205 } 206 207 if value < min || value > max { 208 return nil, []error{fmt.Errorf("expected %s to be in the range (%d - %d), got %d", k, min, max, v)} 209 } 210 211 return nil, nil 212 }, 213 }, 214 }, 215 }, 216 } 217 } 218 219 type Backend struct { 220 *schema.Backend 221 222 // The fields below are set from configure 223 ossClient *oss.Client 224 otsClient *tablestore.TableStoreClient 225 226 bucketName string 227 statePrefix string 228 stateKey string 229 serverSideEncryption bool 230 acl string 231 endpoint string 232 otsEndpoint string 233 otsTable string 234 } 235 236 func (b *Backend) configure(ctx context.Context) error { 237 if b.ossClient != nil { 238 return nil 239 } 240 241 // Grab the resource data 242 d := schema.FromContextBackendConfig(ctx) 243 244 b.bucketName = d.Get("bucket").(string) 245 b.statePrefix = strings.TrimPrefix(strings.Trim(d.Get("prefix").(string), "/"), "./") 246 b.stateKey = d.Get("key").(string) 247 b.serverSideEncryption = d.Get("encrypt").(bool) 248 b.acl = d.Get("acl").(string) 249 250 var getBackendConfig = func(str string, key string) string { 251 if str == "" { 252 value, err := getConfigFromProfile(d, key) 253 if err == nil && value != nil { 254 str = value.(string) 255 } 256 } 257 return str 258 } 259 260 accessKey := getBackendConfig(d.Get("access_key").(string), "access_key_id") 261 secretKey := getBackendConfig(d.Get("secret_key").(string), "access_key_secret") 262 securityToken := getBackendConfig(d.Get("security_token").(string), "sts_token") 263 region := getBackendConfig(d.Get("region").(string), "region_id") 264 265 endpoint := d.Get("endpoint").(string) 266 schma := "https" 267 268 roleArn := getBackendConfig("", "ram_role_arn") 269 sessionName := getBackendConfig("", "ram_session_name") 270 var policy string 271 var sessionExpiration int 272 expiredSeconds, err := getConfigFromProfile(d, "expired_seconds") 273 if err == nil && expiredSeconds != nil { 274 sessionExpiration = (int)(expiredSeconds.(float64)) 275 } 276 277 if v, ok := d.GetOk("assume_role"); ok { 278 for _, v := range v.(*schema.Set).List() { 279 assumeRole := v.(map[string]interface{}) 280 if assumeRole["role_arn"].(string) != "" { 281 roleArn = assumeRole["role_arn"].(string) 282 } 283 if assumeRole["session_name"].(string) != "" { 284 sessionName = assumeRole["session_name"].(string) 285 } 286 if sessionName == "" { 287 sessionName = "terraform" 288 } 289 policy = assumeRole["policy"].(string) 290 sessionExpiration = assumeRole["session_expiration"].(int) 291 if sessionExpiration == 0 { 292 if v := os.Getenv("ALICLOUD_ASSUME_ROLE_SESSION_EXPIRATION"); v != "" { 293 if expiredSeconds, err := strconv.Atoi(v); err == nil { 294 sessionExpiration = expiredSeconds 295 } 296 } 297 if sessionExpiration == 0 { 298 sessionExpiration = 3600 299 } 300 } 301 } 302 } 303 304 if accessKey == "" { 305 ecsRoleName := getBackendConfig(d.Get("ecs_role_name").(string), "ram_role_name") 306 subAccessKeyId, subAccessKeySecret, subSecurityToken, err := getAuthCredentialByEcsRoleName(ecsRoleName) 307 if err != nil { 308 return err 309 } 310 accessKey, secretKey, securityToken = subAccessKeyId, subAccessKeySecret, subSecurityToken 311 } 312 313 if roleArn != "" { 314 subAccessKeyId, subAccessKeySecret, subSecurityToken, err := getAssumeRoleAK(accessKey, secretKey, securityToken, region, roleArn, sessionName, policy, sessionExpiration) 315 if err != nil { 316 return err 317 } 318 accessKey, secretKey, securityToken = subAccessKeyId, subAccessKeySecret, subSecurityToken 319 } 320 321 if endpoint == "" { 322 endpointsResponse, _ := b.getOSSEndpointByRegion(accessKey, secretKey, securityToken, region) 323 for _, endpointItem := range endpointsResponse.Endpoints.Endpoint { 324 if endpointItem.Type == "openAPI" { 325 endpoint = endpointItem.Endpoint 326 break 327 } 328 } 329 if endpoint == "" { 330 endpoint = fmt.Sprintf("oss-%s.aliyuncs.com", region) 331 } 332 } 333 if !strings.HasPrefix(endpoint, "http") { 334 endpoint = fmt.Sprintf("%s://%s", schma, endpoint) 335 } 336 log.Printf("[DEBUG] Instantiate OSS client using endpoint: %#v", endpoint) 337 var options []oss.ClientOption 338 if securityToken != "" { 339 options = append(options, oss.SecurityToken(securityToken)) 340 } 341 options = append(options, oss.UserAgent(fmt.Sprintf("%s/%s", TerraformUA, TerraformVersion))) 342 343 proxyUrl := getHttpProxyUrl() 344 if proxyUrl != nil { 345 options = append(options, oss.Proxy(proxyUrl.String())) 346 } 347 348 client, err := oss.New(endpoint, accessKey, secretKey, options...) 349 b.ossClient = client 350 otsEndpoint := d.Get("tablestore_endpoint").(string) 351 if otsEndpoint != "" { 352 if !strings.HasPrefix(otsEndpoint, "http") { 353 otsEndpoint = fmt.Sprintf("%s://%s", schma, otsEndpoint) 354 } 355 b.otsEndpoint = otsEndpoint 356 parts := strings.Split(strings.TrimPrefix(strings.TrimPrefix(otsEndpoint, "https://"), "http://"), ".") 357 b.otsClient = tablestore.NewClientWithConfig(otsEndpoint, parts[0], accessKey, secretKey, securityToken, tablestore.NewDefaultTableStoreConfig()) 358 } 359 b.otsTable = d.Get("tablestore_table").(string) 360 361 return err 362 } 363 364 func (b *Backend) getOSSEndpointByRegion(access_key, secret_key, security_token, region string) (*location.DescribeEndpointsResponse, error) { 365 args := location.CreateDescribeEndpointsRequest() 366 args.ServiceCode = "oss" 367 args.Id = region 368 args.Domain = "location-readonly.aliyuncs.com" 369 370 locationClient, err := location.NewClientWithOptions(region, getSdkConfig(), credentials.NewStsTokenCredential(access_key, secret_key, security_token)) 371 if err != nil { 372 return nil, fmt.Errorf("Unable to initialize the location client: %#v", err) 373 374 } 375 locationClient.AppendUserAgent(TerraformUA, TerraformVersion) 376 endpointsResponse, err := locationClient.DescribeEndpoints(args) 377 if err != nil { 378 return nil, fmt.Errorf("Describe oss endpoint using region: %#v got an error: %#v.", region, err) 379 } 380 return endpointsResponse, nil 381 } 382 383 func getAssumeRoleAK(accessKey, secretKey, stsToken, region, roleArn, sessionName, policy string, sessionExpiration int) (string, string, string, error) { 384 request := sts.CreateAssumeRoleRequest() 385 request.RoleArn = roleArn 386 request.RoleSessionName = sessionName 387 request.DurationSeconds = requests.NewInteger(sessionExpiration) 388 request.Policy = policy 389 request.Scheme = "https" 390 391 var client *sts.Client 392 var err error 393 if stsToken == "" { 394 client, err = sts.NewClientWithAccessKey(region, accessKey, secretKey) 395 } else { 396 client, err = sts.NewClientWithStsToken(region, accessKey, secretKey, stsToken) 397 } 398 if err != nil { 399 return "", "", "", err 400 } 401 response, err := client.AssumeRole(request) 402 if err != nil { 403 return "", "", "", err 404 } 405 return response.Credentials.AccessKeyId, response.Credentials.AccessKeySecret, response.Credentials.SecurityToken, nil 406 } 407 408 func getSdkConfig() *sdk.Config { 409 return sdk.NewConfig(). 410 WithMaxRetryTime(5). 411 WithTimeout(time.Duration(30) * time.Second). 412 WithGoRoutinePoolSize(10). 413 WithDebug(false). 414 WithHttpTransport(getTransport()). 415 WithScheme("HTTPS") 416 } 417 418 func getTransport() *http.Transport { 419 handshakeTimeout, err := strconv.Atoi(os.Getenv("TLSHandshakeTimeout")) 420 if err != nil { 421 handshakeTimeout = 120 422 } 423 transport := cleanhttp.DefaultTransport() 424 transport.TLSHandshakeTimeout = time.Duration(handshakeTimeout) * time.Second 425 transport.Proxy = http.ProxyFromEnvironment 426 return transport 427 } 428 429 type Invoker struct { 430 catchers []*Catcher 431 } 432 433 type Catcher struct { 434 Reason string 435 RetryCount int 436 RetryWaitSeconds int 437 } 438 439 const TerraformUA = "HashiCorp-Terraform" 440 441 var TerraformVersion = strings.TrimSuffix(version.String(), "-dev") 442 var ClientErrorCatcher = Catcher{"AliyunGoClientFailure", 10, 3} 443 var ServiceBusyCatcher = Catcher{"ServiceUnavailable", 10, 3} 444 445 func NewInvoker() Invoker { 446 i := Invoker{} 447 i.AddCatcher(ClientErrorCatcher) 448 i.AddCatcher(ServiceBusyCatcher) 449 return i 450 } 451 452 func (a *Invoker) AddCatcher(catcher Catcher) { 453 a.catchers = append(a.catchers, &catcher) 454 } 455 456 func (a *Invoker) Run(f func() error) error { 457 err := f() 458 459 if err == nil { 460 return nil 461 } 462 463 for _, catcher := range a.catchers { 464 if strings.Contains(err.Error(), catcher.Reason) { 465 catcher.RetryCount-- 466 467 if catcher.RetryCount <= 0 { 468 return fmt.Errorf("Retry timeout and got an error: %#v.", err) 469 } else { 470 time.Sleep(time.Duration(catcher.RetryWaitSeconds) * time.Second) 471 return a.Run(f) 472 } 473 } 474 } 475 return err 476 } 477 478 var providerConfig map[string]interface{} 479 480 func getConfigFromProfile(d *schema.ResourceData, ProfileKey string) (interface{}, error) { 481 482 if providerConfig == nil { 483 if v, ok := d.GetOk("profile"); !ok || v.(string) == "" { 484 return nil, nil 485 } 486 current := d.Get("profile").(string) 487 // Set CredsFilename, expanding home directory 488 profilePath, err := homedir.Expand(d.Get("shared_credentials_file").(string)) 489 if err != nil { 490 return nil, err 491 } 492 if profilePath == "" { 493 profilePath = fmt.Sprintf("%s/.aliyun/config.json", os.Getenv("HOME")) 494 if runtime.GOOS == "windows" { 495 profilePath = fmt.Sprintf("%s/.aliyun/config.json", os.Getenv("USERPROFILE")) 496 } 497 } 498 providerConfig = make(map[string]interface{}) 499 _, err = os.Stat(profilePath) 500 if !os.IsNotExist(err) { 501 data, err := ioutil.ReadFile(profilePath) 502 if err != nil { 503 return nil, err 504 } 505 config := map[string]interface{}{} 506 err = json.Unmarshal(data, &config) 507 if err != nil { 508 return nil, err 509 } 510 for _, v := range config["profiles"].([]interface{}) { 511 if current == v.(map[string]interface{})["name"] { 512 providerConfig = v.(map[string]interface{}) 513 } 514 } 515 } 516 } 517 518 mode := "" 519 if v, ok := providerConfig["mode"]; ok { 520 mode = v.(string) 521 } else { 522 return v, nil 523 } 524 switch ProfileKey { 525 case "access_key_id", "access_key_secret": 526 if mode == "EcsRamRole" { 527 return "", nil 528 } 529 case "ram_role_name": 530 if mode != "EcsRamRole" { 531 return "", nil 532 } 533 case "sts_token": 534 if mode != "StsToken" { 535 return "", nil 536 } 537 case "ram_role_arn", "ram_session_name": 538 if mode != "RamRoleArn" { 539 return "", nil 540 } 541 case "expired_seconds": 542 if mode != "RamRoleArn" { 543 return float64(0), nil 544 } 545 } 546 547 return providerConfig[ProfileKey], nil 548 } 549 550 var securityCredURL = "http://100.100.100.200/latest/meta-data/ram/security-credentials/" 551 552 // getAuthCredentialByEcsRoleName aims to access meta to get sts credential 553 // Actually, the job should be done by sdk, but currently not all resources and products support alibaba-cloud-sdk-go, 554 // and their go sdk does support ecs role name. 555 // This method is a temporary solution and it should be removed after all go sdk support ecs role name 556 // The related PR: https://github.com/terraform-providers/terraform-provider-alicloud/pull/731 557 func getAuthCredentialByEcsRoleName(ecsRoleName string) (accessKey, secretKey, token string, err error) { 558 559 if ecsRoleName == "" { 560 return 561 } 562 requestUrl := securityCredURL + ecsRoleName 563 httpRequest, err := http.NewRequest(requests.GET, requestUrl, strings.NewReader("")) 564 if err != nil { 565 err = fmt.Errorf("build sts requests err: %s", err.Error()) 566 return 567 } 568 httpClient := &http.Client{} 569 httpResponse, err := httpClient.Do(httpRequest) 570 if err != nil { 571 err = fmt.Errorf("get Ecs sts token err : %s", err.Error()) 572 return 573 } 574 575 response := responses.NewCommonResponse() 576 err = responses.Unmarshal(response, httpResponse, "") 577 if err != nil { 578 err = fmt.Errorf("Unmarshal Ecs sts token response err : %s", err.Error()) 579 return 580 } 581 582 if response.GetHttpStatus() != http.StatusOK { 583 err = fmt.Errorf("get Ecs sts token err, httpStatus: %d, message = %s", response.GetHttpStatus(), response.GetHttpContentString()) 584 return 585 } 586 var data interface{} 587 err = json.Unmarshal(response.GetHttpContentBytes(), &data) 588 if err != nil { 589 err = fmt.Errorf("refresh Ecs sts token err, json.Unmarshal fail: %s", err.Error()) 590 return 591 } 592 code, err := jmespath.Search("Code", data) 593 if err != nil { 594 err = fmt.Errorf("refresh Ecs sts token err, fail to get Code: %s", err.Error()) 595 return 596 } 597 if code.(string) != "Success" { 598 err = fmt.Errorf("refresh Ecs sts token err, Code is not Success") 599 return 600 } 601 accessKeyId, err := jmespath.Search("AccessKeyId", data) 602 if err != nil { 603 err = fmt.Errorf("refresh Ecs sts token err, fail to get AccessKeyId: %s", err.Error()) 604 return 605 } 606 accessKeySecret, err := jmespath.Search("AccessKeySecret", data) 607 if err != nil { 608 err = fmt.Errorf("refresh Ecs sts token err, fail to get AccessKeySecret: %s", err.Error()) 609 return 610 } 611 securityToken, err := jmespath.Search("SecurityToken", data) 612 if err != nil { 613 err = fmt.Errorf("refresh Ecs sts token err, fail to get SecurityToken: %s", err.Error()) 614 return 615 } 616 617 if accessKeyId == nil || accessKeySecret == nil || securityToken == nil { 618 err = fmt.Errorf("there is no any available accesskey, secret and security token for Ecs role %s", ecsRoleName) 619 return 620 } 621 622 return accessKeyId.(string), accessKeySecret.(string), securityToken.(string), nil 623 } 624 625 func getHttpProxyUrl() *url.URL { 626 for _, v := range []string{"HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"} { 627 value := strings.Trim(os.Getenv(v), " ") 628 if value != "" { 629 if !regexp.MustCompile(`^http(s)?://`).MatchString(value) { 630 value = fmt.Sprintf("https://%s", value) 631 } 632 proxyUrl, err := url.Parse(value) 633 if err == nil { 634 return proxyUrl 635 } 636 break 637 } 638 } 639 return nil 640 }