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