github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/backend/remote-state/cos/backend.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package cos 5 6 import ( 7 "context" 8 "fmt" 9 "net/http" 10 "net/url" 11 "os" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" 17 "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" 18 sts "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sts/v20180813" 19 tag "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813" 20 "github.com/tencentyun/cos-go-sdk-v5" 21 "github.com/terramate-io/tf/backend" 22 "github.com/terramate-io/tf/legacy/helper/schema" 23 ) 24 25 // Default value from environment variable 26 const ( 27 PROVIDER_SECRET_ID = "TENCENTCLOUD_SECRET_ID" 28 PROVIDER_SECRET_KEY = "TENCENTCLOUD_SECRET_KEY" 29 PROVIDER_SECURITY_TOKEN = "TENCENTCLOUD_SECURITY_TOKEN" 30 PROVIDER_REGION = "TENCENTCLOUD_REGION" 31 PROVIDER_ASSUME_ROLE_ARN = "TENCENTCLOUD_ASSUME_ROLE_ARN" 32 PROVIDER_ASSUME_ROLE_SESSION_NAME = "TENCENTCLOUD_ASSUME_ROLE_SESSION_NAME" 33 PROVIDER_ASSUME_ROLE_SESSION_DURATION = "TENCENTCLOUD_ASSUME_ROLE_SESSION_DURATION" 34 ) 35 36 // Backend implements "backend".Backend for tencentCloud cos 37 type Backend struct { 38 *schema.Backend 39 credential *common.Credential 40 41 cosContext context.Context 42 cosClient *cos.Client 43 tagClient *tag.Client 44 stsClient *sts.Client 45 46 region string 47 bucket string 48 prefix string 49 key string 50 encrypt bool 51 acl string 52 } 53 54 // New creates a new backend for TencentCloud cos remote state. 55 func New() backend.Backend { 56 s := &schema.Backend{ 57 Schema: map[string]*schema.Schema{ 58 "secret_id": { 59 Type: schema.TypeString, 60 Optional: true, 61 DefaultFunc: schema.EnvDefaultFunc(PROVIDER_SECRET_ID, nil), 62 Description: "Secret id of Tencent Cloud", 63 }, 64 "secret_key": { 65 Type: schema.TypeString, 66 Optional: true, 67 DefaultFunc: schema.EnvDefaultFunc(PROVIDER_SECRET_KEY, nil), 68 Description: "Secret key of Tencent Cloud", 69 Sensitive: true, 70 }, 71 "security_token": { 72 Type: schema.TypeString, 73 Optional: true, 74 DefaultFunc: schema.EnvDefaultFunc(PROVIDER_SECURITY_TOKEN, nil), 75 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).", 76 Sensitive: true, 77 }, 78 "region": { 79 Type: schema.TypeString, 80 Required: true, 81 DefaultFunc: schema.EnvDefaultFunc(PROVIDER_REGION, nil), 82 Description: "The region of the COS bucket", 83 InputDefault: "ap-guangzhou", 84 }, 85 "bucket": { 86 Type: schema.TypeString, 87 Required: true, 88 Description: "The name of the COS bucket", 89 }, 90 "prefix": { 91 Type: schema.TypeString, 92 Optional: true, 93 Description: "The directory for saving the state file in bucket", 94 ValidateFunc: func(v interface{}, s string) ([]string, []error) { 95 prefix := v.(string) 96 if strings.HasPrefix(prefix, "/") || strings.HasPrefix(prefix, "./") { 97 return nil, []error{fmt.Errorf("prefix must not start with '/' or './'")} 98 } 99 return nil, nil 100 }, 101 }, 102 "key": { 103 Type: schema.TypeString, 104 Optional: true, 105 Description: "The path for saving the state file in bucket", 106 Default: "terraform.tfstate", 107 ValidateFunc: func(v interface{}, s string) ([]string, []error) { 108 if strings.HasPrefix(v.(string), "/") || strings.HasSuffix(v.(string), "/") { 109 return nil, []error{fmt.Errorf("key can not start and end with '/'")} 110 } 111 return nil, nil 112 }, 113 }, 114 "encrypt": { 115 Type: schema.TypeBool, 116 Optional: true, 117 Description: "Whether to enable server side encryption of the state file", 118 Default: true, 119 }, 120 "acl": { 121 Type: schema.TypeString, 122 Optional: true, 123 Description: "Object ACL to be applied to the state file", 124 Default: "private", 125 ValidateFunc: func(v interface{}, s string) ([]string, []error) { 126 value := v.(string) 127 if value != "private" && value != "public-read" { 128 return nil, []error{fmt.Errorf( 129 "acl value invalid, expected %s or %s, got %s", 130 "private", "public-read", value)} 131 } 132 return nil, nil 133 }, 134 }, 135 "accelerate": { 136 Type: schema.TypeBool, 137 Optional: true, 138 Description: "Whether to enable global Acceleration", 139 Default: false, 140 }, 141 "assume_role": { 142 Type: schema.TypeSet, 143 Optional: true, 144 MaxItems: 1, 145 Description: "The `assume_role` block. If provided, terraform will attempt to assume this role using the supplied credentials.", 146 Elem: &schema.Resource{ 147 Schema: map[string]*schema.Schema{ 148 "role_arn": { 149 Type: schema.TypeString, 150 Required: true, 151 DefaultFunc: schema.EnvDefaultFunc(PROVIDER_ASSUME_ROLE_ARN, nil), 152 Description: "The ARN of the role to assume. It can be sourced from the `TENCENTCLOUD_ASSUME_ROLE_ARN`.", 153 }, 154 "session_name": { 155 Type: schema.TypeString, 156 Required: true, 157 DefaultFunc: schema.EnvDefaultFunc(PROVIDER_ASSUME_ROLE_SESSION_NAME, nil), 158 Description: "The session name to use when making the AssumeRole call. It can be sourced from the `TENCENTCLOUD_ASSUME_ROLE_SESSION_NAME`.", 159 }, 160 "session_duration": { 161 Type: schema.TypeInt, 162 Required: true, 163 DefaultFunc: func() (interface{}, error) { 164 if v := os.Getenv(PROVIDER_ASSUME_ROLE_SESSION_DURATION); v != "" { 165 return strconv.Atoi(v) 166 } 167 return 7200, nil 168 }, 169 ValidateFunc: validateIntegerInRange(0, 43200), 170 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`.", 171 }, 172 "policy": { 173 Type: schema.TypeString, 174 Optional: true, 175 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).", 176 }, 177 }, 178 }, 179 }, 180 }, 181 } 182 183 result := &Backend{Backend: s} 184 result.Backend.ConfigureFunc = result.configure 185 186 return result 187 } 188 189 func validateIntegerInRange(min, max int64) schema.SchemaValidateFunc { 190 return func(v interface{}, k string) (ws []string, errors []error) { 191 value := int64(v.(int)) 192 if value < min { 193 errors = append(errors, fmt.Errorf( 194 "%q cannot be lower than %d: %d", k, min, value)) 195 } 196 if value > max { 197 errors = append(errors, fmt.Errorf( 198 "%q cannot be higher than %d: %d", k, max, value)) 199 } 200 return 201 } 202 } 203 204 // configure init cos client 205 func (b *Backend) configure(ctx context.Context) error { 206 if b.cosClient != nil { 207 return nil 208 } 209 210 b.cosContext = ctx 211 data := schema.FromContextBackendConfig(b.cosContext) 212 213 b.region = data.Get("region").(string) 214 b.bucket = data.Get("bucket").(string) 215 b.prefix = data.Get("prefix").(string) 216 b.key = data.Get("key").(string) 217 b.encrypt = data.Get("encrypt").(bool) 218 b.acl = data.Get("acl").(string) 219 220 var ( 221 u *url.URL 222 err error 223 ) 224 accelerate := data.Get("accelerate").(bool) 225 if accelerate { 226 u, err = url.Parse(fmt.Sprintf("https://%s.cos.accelerate.myqcloud.com", b.bucket)) 227 } else { 228 u, err = url.Parse(fmt.Sprintf("https://%s.cos.%s.myqcloud.com", b.bucket, b.region)) 229 } 230 if err != nil { 231 return err 232 } 233 234 secretId := data.Get("secret_id").(string) 235 secretKey := data.Get("secret_key").(string) 236 securityToken := data.Get("security_token").(string) 237 238 // init credential by AKSK & TOKEN 239 b.credential = common.NewTokenCredential(secretId, secretKey, securityToken) 240 // update credential if assume role exist 241 err = handleAssumeRole(data, b) 242 if err != nil { 243 return err 244 } 245 246 b.cosClient = cos.NewClient( 247 &cos.BaseURL{BucketURL: u}, 248 &http.Client{ 249 Timeout: 60 * time.Second, 250 Transport: &cos.AuthorizationTransport{ 251 SecretID: b.credential.SecretId, 252 SecretKey: b.credential.SecretKey, 253 SessionToken: b.credential.Token, 254 }, 255 }, 256 ) 257 258 b.tagClient = b.UseTagClient() 259 return err 260 } 261 262 func handleAssumeRole(data *schema.ResourceData, b *Backend) error { 263 assumeRoleList := data.Get("assume_role").(*schema.Set).List() 264 if len(assumeRoleList) == 1 { 265 assumeRole := assumeRoleList[0].(map[string]interface{}) 266 assumeRoleArn := assumeRole["role_arn"].(string) 267 assumeRoleSessionName := assumeRole["session_name"].(string) 268 assumeRoleSessionDuration := assumeRole["session_duration"].(int) 269 assumeRolePolicy := assumeRole["policy"].(string) 270 271 err := b.updateCredentialWithSTS(assumeRoleArn, assumeRoleSessionName, assumeRoleSessionDuration, assumeRolePolicy) 272 if err != nil { 273 return err 274 } 275 } 276 return nil 277 } 278 279 func (b *Backend) updateCredentialWithSTS(assumeRoleArn, assumeRoleSessionName string, assumeRoleSessionDuration int, assumeRolePolicy string) error { 280 // assume role by STS 281 request := sts.NewAssumeRoleRequest() 282 request.RoleArn = &assumeRoleArn 283 request.RoleSessionName = &assumeRoleSessionName 284 duration := uint64(assumeRoleSessionDuration) 285 request.DurationSeconds = &duration 286 if assumeRolePolicy != "" { 287 policy := url.QueryEscape(assumeRolePolicy) 288 request.Policy = &policy 289 } 290 291 response, err := b.UseStsClient().AssumeRole(request) 292 if err != nil { 293 return err 294 } 295 // update credentials by result of assume role 296 b.credential = common.NewTokenCredential( 297 *response.Response.Credentials.TmpSecretId, 298 *response.Response.Credentials.TmpSecretKey, 299 *response.Response.Credentials.Token, 300 ) 301 302 return nil 303 } 304 305 // UseStsClient returns sts client for service 306 func (b *Backend) UseStsClient() *sts.Client { 307 if b.stsClient != nil { 308 return b.stsClient 309 } 310 cpf := b.NewClientProfile(300) 311 b.stsClient, _ = sts.NewClient(b.credential, b.region, cpf) 312 b.stsClient.WithHttpTransport(&LogRoundTripper{}) 313 314 return b.stsClient 315 } 316 317 // UseTagClient returns tag client for service 318 func (b *Backend) UseTagClient() *tag.Client { 319 if b.tagClient != nil { 320 return b.tagClient 321 } 322 cpf := b.NewClientProfile(300) 323 cpf.Language = "en-US" 324 b.tagClient, _ = tag.NewClient(b.credential, b.region, cpf) 325 return b.tagClient 326 } 327 328 // NewClientProfile returns a new ClientProfile 329 func (b *Backend) NewClientProfile(timeout int) *profile.ClientProfile { 330 cpf := profile.NewClientProfile() 331 332 // all request use method POST 333 cpf.HttpProfile.ReqMethod = "POST" 334 // request timeout 335 cpf.HttpProfile.ReqTimeout = timeout 336 337 return cpf 338 }