Skip to content

Commit

Permalink
Add state upgraders and update migration guide
Browse files Browse the repository at this point in the history
  • Loading branch information
sfc-gh-jmichalak committed Jul 4, 2024
1 parent 2ead100 commit 5ff48da
Show file tree
Hide file tree
Showing 4 changed files with 279 additions and 18 deletions.
10 changes: 0 additions & 10 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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`
Expand Down
30 changes: 22 additions & 8 deletions pkg/resources/external_oauth_integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
},
},
}
}

Expand Down
219 changes: 219 additions & 0 deletions pkg/resources/external_oauth_integration_acceptance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
38 changes: 38 additions & 0 deletions pkg/resources/external_oauth_integration_stage_upgraders.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 5ff48da

Please sign in to comment.