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