From 5ff48da066c60c93f99b35f3e9e0a285b6972ba2 Mon Sep 17 00:00:00 2001 From: Jakub Michalak Date: Thu, 4 Jul 2024 08:18:15 +0200 Subject: [PATCH] Add state upgraders and update migration guide --- MIGRATION_GUIDE.md | 10 - pkg/resources/external_oauth_integration.go | 30 ++- ...ernal_oauth_integration_acceptance_test.go | 219 ++++++++++++++++++ ...ernal_oauth_integration_stage_upgraders.go | 38 +++ 4 files changed, 279 insertions(+), 18 deletions(-) create mode 100644 pkg/resources/external_oauth_integration_stage_upgraders.go diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 7b6b4aeb32..a52c3c897d 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -31,12 +31,8 @@ Added a new datasource enabling querying and filtering all types of security int It's important to limit the records and calls to Snowflake to the minimum. That's why we recommend assessing which information you need from the data source and then providing strong filters and turning off additional fields for better plan performance. ### snowflake_external_oauth_integration resource changes -#### *(behavior change)* Changed behavior of `sync_password` - -Now, the `sync_password` field will set the state value to `unknown` whenever the value is not set in the config. This indicates that the value on the Snowflake side is set to the Snowflake default. #### *(behavior change)* Renamed fields - Renamed fields: - `type` to `external_oauth_type` - `issuer` to `external_oauth_issuer` @@ -53,12 +49,6 @@ Renamed fields: - `scope_delimiter` to `external_oauth_scope_delimiter` to align with Snowflake docs. Please rename this field in your configuration files. State will be migrated automatically. -#### *(feature)* New fields -Fields added to the resource: -- `enabled` -- `sync_password` -- `comment` - #### *(behavior change)* Force new for multiple attributes after removing from config Force new was added for the following attributes (because no usable SQL alter statements for them): - `external_oauth_rsa_public_key` diff --git a/pkg/resources/external_oauth_integration.go b/pkg/resources/external_oauth_integration.go index 95871c6273..432a2956d4 100644 --- a/pkg/resources/external_oauth_integration.go +++ b/pkg/resources/external_oauth_integration.go @@ -15,6 +15,7 @@ import ( "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/schemas" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -77,16 +78,18 @@ var oauthExternalIntegrationSchema = map[string]*schema.Schema{ Description: "Specifies the endpoint or a list of endpoints from which to download public keys or certificates to validate an External OAuth access token. The maximum number of URLs that can be specified in the list is 3.", }, "external_oauth_rsa_public_key": { - Type: schema.TypeString, - Optional: true, - ConflictsWith: []string{"external_oauth_jws_keys_url"}, - Description: "Specifies a Base64-encoded RSA public key, without the -----BEGIN PUBLIC KEY----- and -----END PUBLIC KEY----- headers.", + Type: schema.TypeString, + Optional: true, + Description: "Specifies a Base64-encoded RSA public key, without the -----BEGIN PUBLIC KEY----- and -----END PUBLIC KEY----- headers.", + DiffSuppressFunc: ignoreTrimSpaceSuppressFunc, + ConflictsWith: []string{"external_oauth_jws_keys_url"}, }, "external_oauth_rsa_public_key_2": { - Type: schema.TypeString, - Optional: true, - Description: "Specifies a second RSA public key, without the -----BEGIN PUBLIC KEY----- and -----END PUBLIC KEY----- headers. Used for key rotation.", - ConflictsWith: []string{"external_oauth_jws_keys_url"}, + Type: schema.TypeString, + Optional: true, + Description: "Specifies a second RSA public key, without the -----BEGIN PUBLIC KEY----- and -----END PUBLIC KEY----- headers. Used for key rotation.", + DiffSuppressFunc: ignoreTrimSpaceSuppressFunc, + ConflictsWith: []string{"external_oauth_jws_keys_url"}, }, "external_oauth_blocked_roles_list": { Type: schema.TypeSet, @@ -178,6 +181,8 @@ var oauthExternalIntegrationSchema = map[string]*schema.Schema{ func ExternalOauthIntegration() *schema.Resource { return &schema.Resource{ + SchemaVersion: 1, + CreateContext: CreateContextExternalOauthIntegration, ReadContext: ReadContextExternalOauthIntegration(true), UpdateContext: UpdateContextExternalOauthIntegration, @@ -197,6 +202,15 @@ func ExternalOauthIntegration() *schema.Resource { Importer: &schema.ResourceImporter{ StateContext: ImportExternalOauthIntegration, }, + + StateUpgraders: []schema.StateUpgrader{ + { + Version: 0, + // setting type to cty.EmptyObject is a bit hacky here but following https://developer.hashicorp.com/terraform/plugin/framework/migrating/resources/state-upgrade#sdkv2-1 would require lots of repetitive code; this should work with cty.EmptyObject + Type: cty.EmptyObject, + Upgrade: v092ExternalOauthIntegrationStateUpgrader, + }, + }, } } diff --git a/pkg/resources/external_oauth_integration_acceptance_test.go b/pkg/resources/external_oauth_integration_acceptance_test.go index 38f4e4f2ba..8e5d4d55e5 100644 --- a/pkg/resources/external_oauth_integration_acceptance_test.go +++ b/pkg/resources/external_oauth_integration_acceptance_test.go @@ -618,3 +618,222 @@ func TestAcc_ExternalOauthIntegration_InvalidIncomplete(t *testing.T) { }, }) } + +func TestAcc_ExternalOauthIntegration_migrateFromVersion092_withRsaPublicKeysAndBlockedRolesList(t *testing.T) { + id := acc.TestClient().Ids.RandomAccountObjectIdentifier() + role1, role1Cleanup := acc.TestClient().Role.CreateRole(t) + t.Cleanup(role1Cleanup) + issuer := random.String() + rsaKey := random.GenerateRSAPublicKey(t) + resourceName := "snowflake_external_oauth_integration.test" + resource.Test(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "snowflake": { + VersionConstraint: "=0.92.0", + Source: "Snowflake-Labs/snowflake", + }, + }, + Config: externalOauthIntegrationWithRsaPublicKeysAndBlockedRolesListv092(id.Name(), issuer, rsaKey, role1.Name), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", id.Name()), + resource.TestCheckResourceAttr(resourceName, "type", string(sdk.ExternalOauthSecurityIntegrationTypeCustom)), + resource.TestCheckResourceAttr(resourceName, "issuer", issuer), + resource.TestCheckResourceAttr(resourceName, "token_user_mapping_claims.#", "1"), + resource.TestCheckResourceAttr(resourceName, "token_user_mapping_claims.0", "foo"), + resource.TestCheckResourceAttr(resourceName, "snowflake_user_mapping_attribute", string(sdk.ExternalOauthSecurityIntegrationSnowflakeUserMappingAttributeLoginName)), + resource.TestCheckResourceAttr(resourceName, "scope_mapping_attribute", "foo"), + resource.TestCheckResourceAttr(resourceName, "rsa_public_key", rsaKey), + resource.TestCheckResourceAttr(resourceName, "rsa_public_key_2", rsaKey), + resource.TestCheckResourceAttr(resourceName, "blocked_roles.#", "1"), + resource.TestCheckResourceAttr(resourceName, "blocked_roles.0", role1.Name), + resource.TestCheckResourceAttr(resourceName, "audience_urls.#", "1"), + resource.TestCheckResourceAttr(resourceName, "audience_urls.0", "foo"), + resource.TestCheckResourceAttr(resourceName, "any_role_mode", string(sdk.ExternalOauthSecurityIntegrationAnyRoleModeDisable)), + resource.TestCheckResourceAttr(resourceName, "scope_delimiter", ":"), + ), + }, + { + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + Config: externalOauthIntegrationWithRsaPublicKeysAndBlockedRolesListv093(id.Name(), issuer, rsaKey, role1.Name), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", id.Name()), + resource.TestCheckResourceAttr(resourceName, "external_oauth_type", string(sdk.ExternalOauthSecurityIntegrationTypeCustom)), + resource.TestCheckResourceAttr(resourceName, "external_oauth_issuer", issuer), + resource.TestCheckResourceAttr(resourceName, "external_oauth_token_user_mapping_claim.#", "1"), + resource.TestCheckResourceAttr(resourceName, "external_oauth_token_user_mapping_claim.0", "foo"), + resource.TestCheckResourceAttr(resourceName, "external_oauth_snowflake_user_mapping_attribute", string(sdk.ExternalOauthSecurityIntegrationSnowflakeUserMappingAttributeLoginName)), + resource.TestCheckResourceAttr(resourceName, "external_oauth_scope_mapping_attribute", "foo"), + resource.TestCheckResourceAttr(resourceName, "external_oauth_rsa_public_key", rsaKey), + resource.TestCheckResourceAttr(resourceName, "external_oauth_rsa_public_key_2", rsaKey), + resource.TestCheckResourceAttr(resourceName, "external_oauth_blocked_roles_list.#", "1"), + resource.TestCheckResourceAttr(resourceName, "external_oauth_blocked_roles_list.0", role1.Name), + resource.TestCheckResourceAttr(resourceName, "external_oauth_audience_list.#", "1"), + resource.TestCheckResourceAttr(resourceName, "external_oauth_audience_list.0", "foo"), + resource.TestCheckResourceAttr(resourceName, "external_oauth_any_role_mode", string(sdk.ExternalOauthSecurityIntegrationAnyRoleModeDisable)), + resource.TestCheckResourceAttr(resourceName, "external_oauth_scope_delimiter", ":"), + ), + }, + }, + }) +} + +func externalOauthIntegrationWithRsaPublicKeysAndBlockedRolesListv092(name, issuer, rsaKey, roleName string) string { + s := ` +locals { + key_raw = <<-EOT +%s + EOT + key = trimsuffix(local.key_raw, "\n") +} +resource "snowflake_external_oauth_integration" "test" { + name = "%s" + enabled = true + type = "CUSTOM" + issuer = "%s" + token_user_mapping_claims = ["foo"] + snowflake_user_mapping_attribute = "LOGIN_NAME" + scope_mapping_attribute = "foo" + rsa_public_key = local.key + rsa_public_key_2 = local.key + blocked_roles = ["%s"] + audience_urls = ["foo"] + any_role_mode = "DISABLE" + scope_delimiter = ":" +}` + return fmt.Sprintf(s, rsaKey, name, issuer, roleName) +} + +func externalOauthIntegrationWithRsaPublicKeysAndBlockedRolesListv093(name, issuer, rsaKey, roleName string) string { + s := ` +locals { + key_raw = <<-EOT +%s + EOT + key = trimsuffix(local.key_raw, "\n") +} +resource "snowflake_external_oauth_integration" "test" { + name = "%s" + enabled = true + external_oauth_type = "CUSTOM" + external_oauth_issuer = "%s" + external_oauth_token_user_mapping_claim = ["foo"] + external_oauth_snowflake_user_mapping_attribute = "LOGIN_NAME" + external_oauth_scope_mapping_attribute = "foo" + external_oauth_rsa_public_key = local.key + external_oauth_rsa_public_key_2 = local.key + external_oauth_blocked_roles_list = ["%s"] + external_oauth_audience_list = ["foo"] + external_oauth_any_role_mode = "DISABLE" + external_oauth_scope_delimiter = ":" +}` + return fmt.Sprintf(s, rsaKey, name, issuer, roleName) +} + +func TestAcc_ExternalOauthIntegration_migrateFromVersion092_withJwsKeysUrlAndAllowedRolesList(t *testing.T) { + id := acc.TestClient().Ids.RandomAccountObjectIdentifier() + role1, role1Cleanup := acc.TestClient().Role.CreateRole(t) + t.Cleanup(role1Cleanup) + issuer := random.String() + resourceName := "snowflake_external_oauth_integration.test" + resource.Test(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "snowflake": { + VersionConstraint: "=0.92.0", + Source: "Snowflake-Labs/snowflake", + }, + }, + Config: externalOauthIntegrationWithJwsKeysUrlAndAllowedRolesListv092(id.Name(), issuer, role1.Name), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", id.Name()), + resource.TestCheckResourceAttr(resourceName, "type", string(sdk.ExternalOauthSecurityIntegrationTypeCustom)), + resource.TestCheckResourceAttr(resourceName, "issuer", issuer), + resource.TestCheckResourceAttr(resourceName, "token_user_mapping_claims.#", "1"), + resource.TestCheckResourceAttr(resourceName, "token_user_mapping_claims.0", "foo"), + resource.TestCheckResourceAttr(resourceName, "snowflake_user_mapping_attribute", string(sdk.ExternalOauthSecurityIntegrationSnowflakeUserMappingAttributeLoginName)), + resource.TestCheckResourceAttr(resourceName, "scope_mapping_attribute", "foo"), + resource.TestCheckResourceAttr(resourceName, "jws_keys_urls.#", "1"), + resource.TestCheckResourceAttr(resourceName, "jws_keys_urls.0", "https://example.com"), + resource.TestCheckResourceAttr(resourceName, "allowed_roles.#", "1"), + resource.TestCheckResourceAttr(resourceName, "allowed_roles.0", role1.Name), + resource.TestCheckResourceAttr(resourceName, "audience_urls.#", "1"), + resource.TestCheckResourceAttr(resourceName, "audience_urls.0", "foo"), + resource.TestCheckResourceAttr(resourceName, "any_role_mode", string(sdk.ExternalOauthSecurityIntegrationAnyRoleModeDisable)), + resource.TestCheckResourceAttr(resourceName, "scope_delimiter", ":"), + ), + }, + { + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + Config: externalOauthIntegrationWithJwsKeysUrlAndAllowedRolesListv093(id.Name(), issuer, role1.Name), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "name", id.Name()), + resource.TestCheckResourceAttr(resourceName, "external_oauth_type", string(sdk.ExternalOauthSecurityIntegrationTypeCustom)), + resource.TestCheckResourceAttr(resourceName, "external_oauth_issuer", issuer), + resource.TestCheckResourceAttr(resourceName, "external_oauth_token_user_mapping_claim.#", "1"), + resource.TestCheckResourceAttr(resourceName, "external_oauth_token_user_mapping_claim.0", "foo"), + resource.TestCheckResourceAttr(resourceName, "external_oauth_snowflake_user_mapping_attribute", string(sdk.ExternalOauthSecurityIntegrationSnowflakeUserMappingAttributeLoginName)), + resource.TestCheckResourceAttr(resourceName, "external_oauth_scope_mapping_attribute", "foo"), + resource.TestCheckResourceAttr(resourceName, "external_oauth_jws_keys_url.#", "1"), + resource.TestCheckResourceAttr(resourceName, "external_oauth_jws_keys_url.0", "https://example.com"), + resource.TestCheckResourceAttr(resourceName, "external_oauth_allowed_roles_list.#", "1"), + resource.TestCheckResourceAttr(resourceName, "external_oauth_allowed_roles_list.0", role1.Name), + resource.TestCheckResourceAttr(resourceName, "external_oauth_audience_list.#", "1"), + resource.TestCheckResourceAttr(resourceName, "external_oauth_audience_list.0", "foo"), + resource.TestCheckResourceAttr(resourceName, "external_oauth_any_role_mode", string(sdk.ExternalOauthSecurityIntegrationAnyRoleModeDisable)), + resource.TestCheckResourceAttr(resourceName, "external_oauth_scope_delimiter", ":"), + ), + }, + }, + }) +} + +func externalOauthIntegrationWithJwsKeysUrlAndAllowedRolesListv092(name, issuer, roleName string) string { + s := ` +resource "snowflake_external_oauth_integration" "test" { + name = "%s" + enabled = true + type = "CUSTOM" + issuer = "%s" + token_user_mapping_claims = ["foo"] + snowflake_user_mapping_attribute = "LOGIN_NAME" + scope_mapping_attribute = "foo" + jws_keys_urls = ["https://example.com"] + allowed_roles = ["%s"] + audience_urls = ["foo"] + any_role_mode = "DISABLE" + scope_delimiter = ":" +}` + return fmt.Sprintf(s, name, issuer, roleName) +} + +func externalOauthIntegrationWithJwsKeysUrlAndAllowedRolesListv093(name, issuer, roleName string) string { + s := ` +resource "snowflake_external_oauth_integration" "test" { + name = "%s" + enabled = true + external_oauth_type = "CUSTOM" + external_oauth_issuer = "%s" + external_oauth_token_user_mapping_claim = ["foo"] + external_oauth_snowflake_user_mapping_attribute = "LOGIN_NAME" + external_oauth_scope_mapping_attribute = "foo" + external_oauth_jws_keys_url = ["https://example.com"] + external_oauth_allowed_roles_list = ["%s"] + external_oauth_audience_list = ["foo"] + external_oauth_any_role_mode = "DISABLE" + external_oauth_scope_delimiter = ":" +}` + return fmt.Sprintf(s, name, issuer, roleName) +} diff --git a/pkg/resources/external_oauth_integration_stage_upgraders.go b/pkg/resources/external_oauth_integration_stage_upgraders.go new file mode 100644 index 0000000000..88ba0dd65a --- /dev/null +++ b/pkg/resources/external_oauth_integration_stage_upgraders.go @@ -0,0 +1,38 @@ +package resources + +import ( + "context" +) + +func v092ExternalOauthIntegrationStateUpgrader(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) { + if rawState == nil { + return rawState, nil + } + + type renameField struct { + from string + to string + } + fieldsToRename := []renameField{ + {from: "type", to: "external_oauth_type"}, + {from: "issuer", to: "external_oauth_issuer"}, + {from: "token_user_mapping_claims", to: "external_oauth_token_user_mapping_claim"}, + {from: "snowflake_user_mapping_attribute", to: "external_oauth_snowflake_user_mapping_attribute"}, + {from: "scope_mapping_attribute", to: "external_oauth_scope_mapping_attribute"}, + {from: "jws_keys_urls", to: "external_oauth_jws_keys_url"}, + {from: "rsa_public_key", to: "external_oauth_rsa_public_key"}, + {from: "rsa_public_key_2", to: "external_oauth_rsa_public_key_2"}, + {from: "blocked_roles", to: "external_oauth_blocked_roles_list"}, + {from: "allowed_roles", to: "external_oauth_allowed_roles_list"}, + {from: "audience_urls", to: "external_oauth_audience_list"}, + {from: "any_role_mode", to: "external_oauth_any_role_mode"}, + {from: "scope_delimiter", to: "external_oauth_scope_delimiter"}, + } + + for _, field := range fieldsToRename { + rawState[field.to] = rawState[field.from] + delete(rawState, field.from) + } + + return rawState, nil +}