diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 122d97ec8c..5b9f3c9171 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -9,6 +9,10 @@ across different versions. Following the [announcement](https://github.com/Snowflake-Labs/terraform-provider-snowflake/discussions/2736) we have removed the old grant resources. The two resources [snowflake_role_ownership_grant](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/role_ownership_grant) and [snowflake_user_ownership_grant](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/resources/user_ownership_grant) were not listed in the announcement, but they were also marked as deprecated ones. We are removing them too to conclude the grants redesign saga. ### snowflake_scim_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 field `provisioner_role` to `run_as_role` to align with Snowflake docs. Please rename this field in your configuration files. State will be migrated automatically. diff --git a/docs/resources/scim_integration.md b/docs/resources/scim_integration.md index f9d68cbb94..819e1a2cda 100644 --- a/docs/resources/scim_integration.md +++ b/docs/resources/scim_integration.md @@ -45,12 +45,92 @@ resource "snowflake_scim_integration" "test" { - `comment` (String) Specifies a comment for the integration. - `network_policy` (String) Specifies an existing network policy that controls SCIM network traffic. -- `sync_password` (Boolean) Specifies whether to enable or disable the synchronization of a user password from an Okta SCIM client as part of the API request to Snowflake. +- `sync_password` (String) Specifies whether to enable or disable the synchronization of a user password from an Okta SCIM client as part of the API request to Snowflake. Available options are: `true` or `false`. When the value is not set in the configuration the provider will put `unknown` there which means to use the Snowflake default for this value. ### Read-Only -- `created_on` (String) Date and time when the SCIM integration was created. +- `describe_output` (List of Object) Outputs the result of `DESCRIBE SECURITY INTEGRATIONS` for the given security integration. (see [below for nested schema](#nestedatt--describe_output)) - `id` (String) The ID of this resource. +- `show_output` (List of Object) Outputs the result of `SHOW SECURITY INTEGRATIONS` for the given security integration. (see [below for nested schema](#nestedatt--show_output)) + + +### Nested Schema for `describe_output` + +Read-Only: + +- `comment` (List of Object) (see [below for nested schema](#nestedobjatt--describe_output--comment)) +- `enabled` (List of Object) (see [below for nested schema](#nestedobjatt--describe_output--enabled)) +- `network_policy` (List of Object) (see [below for nested schema](#nestedobjatt--describe_output--network_policy)) +- `run_as_role` (List of Object) (see [below for nested schema](#nestedobjatt--describe_output--run_as_role)) +- `sync_password` (List of Object) (see [below for nested schema](#nestedobjatt--describe_output--sync_password)) + + +### Nested Schema for `describe_output.comment` + +Read-Only: + +- `default` (String) +- `name` (String) +- `type` (String) +- `value` (String) + + + +### Nested Schema for `describe_output.enabled` + +Read-Only: + +- `default` (String) +- `name` (String) +- `type` (String) +- `value` (String) + + + +### Nested Schema for `describe_output.network_policy` + +Read-Only: + +- `default` (String) +- `name` (String) +- `type` (String) +- `value` (String) + + + +### Nested Schema for `describe_output.run_as_role` + +Read-Only: + +- `default` (String) +- `name` (String) +- `type` (String) +- `value` (String) + + + +### Nested Schema for `describe_output.sync_password` + +Read-Only: + +- `default` (String) +- `name` (String) +- `type` (String) +- `value` (String) + + + + +### Nested Schema for `show_output` + +Read-Only: + +- `category` (String) +- `comment` (String) +- `created_on` (String) +- `enabled` (Boolean) +- `integration_type` (String) +- `name` (String) ## Import diff --git a/pkg/resources/custom_diffs.go b/pkg/resources/custom_diffs.go index c3e417e523..14382da0af 100644 --- a/pkg/resources/custom_diffs.go +++ b/pkg/resources/custom_diffs.go @@ -9,8 +9,6 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -61,24 +59,6 @@ func ParameterValueComputedIf(key string, parameters []*sdk.Parameter, objectPar } } -func BoolComputedIf(key string, getDefault func(client *sdk.Client, id sdk.AccountObjectIdentifier) (string, error)) schema.CustomizeDiffFunc { - return customdiff.ComputedIf(key, func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) bool { - configValue := d.GetRawConfig().AsValueMap()[key] - if !configValue.IsNull() { - return false - } - - client := meta.(*provider.Context).Client - - def, err := getDefault(client, helpers.DecodeSnowflakeID(d.Id()).(sdk.AccountObjectIdentifier)) - if err != nil { - return false - } - stateValue := d.Get(key).(bool) - return def != strconv.FormatBool(stateValue) - }) -} - // TODO [follow-up PR]: test func ComputedIfAnyAttributeChanged(key string, changedAttributeKeys ...string) schema.CustomizeDiffFunc { return customdiff.ComputedIf(key, func(ctx context.Context, diff *schema.ResourceDiff, meta interface{}) bool { diff --git a/pkg/resources/diff_suppressions.go b/pkg/resources/diff_suppressions.go index d2de195517..59edce5c83 100644 --- a/pkg/resources/diff_suppressions.go +++ b/pkg/resources/diff_suppressions.go @@ -27,17 +27,17 @@ func IgnoreAfterCreation(_, _, _ string, d *schema.ResourceData) bool { return d.Id() != "" } -func IgnoreChangeToCurrentSnowflakeValue(keyInShowOutput string) schema.SchemaDiffSuppressFunc { +func IgnoreChangeToCurrentSnowflakeValueInShow(keyInShowOutput string) schema.SchemaDiffSuppressFunc { return func(_, _, new string, d *schema.ResourceData) bool { if d.Id() == "" { return false } - if showOutput, ok := d.GetOk(showOutputAttributeName); ok { - showOutputList := showOutput.([]any) - if len(showOutputList) == 1 { - result := showOutputList[0].(map[string]any) - log.Printf("[DEBUG] IgnoreChangeToCurrentSnowflakeValue: value for key %s is %v, new value is %s, comparison result is: %t", keyInShowOutput, result[keyInShowOutput], new, new == fmt.Sprintf("%v", result[keyInShowOutput])) + if queryOutput, ok := d.GetOk(showOutputAttributeName); ok { + queryOutputList := queryOutput.([]any) + if len(queryOutputList) == 1 { + result := queryOutputList[0].(map[string]any) + log.Printf("[DEBUG] IgnoreChangeToCurrentSnowflakeValueInShow: value for key %s is %v, new value is %s, comparison result is: %t", keyInShowOutput, result[keyInShowOutput], new, new == fmt.Sprintf("%v", result[keyInShowOutput])) if new == fmt.Sprintf("%v", result[keyInShowOutput]) { return true } @@ -47,6 +47,30 @@ func IgnoreChangeToCurrentSnowflakeValue(keyInShowOutput string) schema.SchemaDi } } +func IgnoreChangeToCurrentSnowflakeValueInDescribe(keyInDescribeOutput string) schema.SchemaDiffSuppressFunc { + return func(_, _, new string, d *schema.ResourceData) bool { + if d.Id() == "" { + return false + } + + if queryOutput, ok := d.GetOk(describeOutputAttributeName); ok { + queryOutputList := queryOutput.([]any) + if len(queryOutputList) == 1 { + result := queryOutputList[0].(map[string]any) + newValueInDescribeList := result[keyInDescribeOutput].([]any) + if len(newValueInDescribeList) == 1 { + newValueInDescribe := newValueInDescribeList[0].(map[string]any)["value"] + log.Printf("[DEBUG] IgnoreChangeToCurrentSnowflakeValueInDescribe: value for key %s is %v, new value is %s, comparison result is: %t", keyInDescribeOutput, newValueInDescribe, new, new == fmt.Sprintf("%v", newValueInDescribe)) + if new == fmt.Sprintf("%v", newValueInDescribe) { + return true + } + } + } + } + return false + } +} + func SuppressIfAny(diffSuppressFunctions ...schema.SchemaDiffSuppressFunc) schema.SchemaDiffSuppressFunc { return func(k, old, new string, d *schema.ResourceData) bool { var suppress bool diff --git a/pkg/resources/scim_integration.go b/pkg/resources/scim_integration.go index b6d83b3ece..4bfacb3d1f 100644 --- a/pkg/resources/scim_integration.go +++ b/pkg/resources/scim_integration.go @@ -4,16 +4,20 @@ import ( "context" "errors" "fmt" - "log" + "strconv" "strings" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/collections" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/logging" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/schemas" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" "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" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) @@ -26,12 +30,10 @@ var scimIntegrationSchema = map[string]*schema.Schema{ Description: "String that specifies the identifier (i.e. name) for the integration; must be unique in your account.", }, "enabled": { - Type: schema.TypeBool, - Required: true, - Description: "Specify whether the security integration is enabled. ", - DiffSuppressFunc: func(k, oldValue, newValue string, d *schema.ResourceData) bool { - return d.GetRawConfig().AsValueMap()["enabled"].IsNull() - }, + Type: schema.TypeBool, + Required: true, + Description: "Specify whether the security integration is enabled.", + DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInDescribe("enabled"), }, "scim_client": { Type: schema.TypeString, @@ -60,25 +62,36 @@ var scimIntegrationSchema = map[string]*schema.Schema{ ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](), Optional: true, Description: "Specifies an existing network policy that controls SCIM network traffic.", - DiffSuppressFunc: func(_, old, new string, d *schema.ResourceData) bool { - return sdk.NewAccountObjectIdentifierFromFullyQualifiedName(old) == sdk.NewAccountObjectIdentifierFromFullyQualifiedName(new) - }, + DiffSuppressFunc: SuppressIfAny(suppressIdentifierQuoting, IgnoreChangeToCurrentSnowflakeValueInDescribe("network_policy")), }, "sync_password": { - Type: schema.TypeBool, - Optional: true, - Computed: true, - Description: "Specifies whether to enable or disable the synchronization of a user password from an Okta SCIM client as part of the API request to Snowflake.", + Type: schema.TypeString, + Optional: true, + Default: "unknown", + ValidateFunc: validation.StringInSlice([]string{"true", "false"}, true), + DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInDescribe("sync_password"), + Description: "Specifies whether to enable or disable the synchronization of a user password from an Okta SCIM client as part of the API request to Snowflake. Available options are: `true` or `false`. When the value is not set in the configuration the provider will put `unknown` there which means to use the Snowflake default for this value.", }, "comment": { Type: schema.TypeString, Optional: true, Description: "Specifies a comment for the integration.", }, - "created_on": { - Type: schema.TypeString, + showOutputAttributeName: { + Type: schema.TypeList, Computed: true, - Description: "Date and time when the SCIM integration was created.", + Description: "Outputs the result of `SHOW SECURITY INTEGRATIONS` for the given security integration.", + Elem: &schema.Resource{ + Schema: schemas.ShowSecurityIntegrationSchema, + }, + }, + describeOutputAttributeName: { + Type: schema.TypeList, + Computed: true, + Description: "Outputs the result of `DESCRIBE SECURITY INTEGRATIONS` for the given security integration.", + Elem: &schema.Resource{ + Schema: schemas.DescribeScimSecurityIntegrationSchema, + }, }, } @@ -87,27 +100,20 @@ func SCIMIntegration() *schema.Resource { SchemaVersion: 1, CreateContext: CreateContextSCIMIntegration, - ReadContext: ReadContextSCIMIntegration, + ReadContext: ReadContextSCIMIntegration(true), UpdateContext: UpdateContextSCIMIntegration, DeleteContext: DeleteContextSCIMIntegration, - CustomizeDiff: customdiff.All( - BoolComputedIf("sync_password", func(client *sdk.Client, id sdk.AccountObjectIdentifier) (string, error) { - props, err := client.SecurityIntegrations.Describe(context.Background(), id) - if err != nil { - return "", err - } - for _, prop := range props { - if prop.GetName() == "SYNC_PASSWORD" { - return prop.GetDefault(), nil - } - } - return "", fmt.Errorf("") - }), - ), + Schema: scimIntegrationSchema, Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, + StateContext: ImportScimIntegration, }, + + CustomizeDiff: customdiff.All( + ComputedIfAnyAttributeChanged(showOutputAttributeName, "enabled", "scim_client", "comment"), + ComputedIfAnyAttributeChanged(describeOutputAttributeName, "enabled", "comment", "network_policy", "run_as_role", "sync_password"), + ), + StateUpgraders: []schema.StateUpgrader{ { Version: 0, @@ -119,152 +125,266 @@ func SCIMIntegration() *schema.Resource { } } +func ImportScimIntegration(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + logging.DebugLogger.Printf("[DEBUG] Starting scim integration import") + client := meta.(*provider.Context).Client + id := helpers.DecodeSnowflakeID(d.Id()).(sdk.AccountObjectIdentifier) + + integration, err := client.SecurityIntegrations.ShowByID(ctx, id) + if err != nil { + return nil, err + } + + integrationProperties, err := client.SecurityIntegrations.Describe(ctx, id) + if err != nil { + return nil, err + } + + if err = d.Set("name", integration.Name); err != nil { + return nil, err + } + if err = d.Set("enabled", integration.Enabled); err != nil { + return nil, err + } + if scimClient, err := integration.SubType(); err == nil { + if err = d.Set("scim_client", scimClient); err != nil { + return nil, err + } + } + if runAsRoleProperty, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { return property.Name == "RUN_AS_ROLE" }); err == nil { + if err = d.Set("run_as_role", runAsRoleProperty.Value); err != nil { + return nil, err + } + } + if networkPolicyProperty, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { return property.Name == "NETWORK_POLICY" }); err == nil { + if err = d.Set("network_policy", networkPolicyProperty.Value); err != nil { + return nil, err + } + } + if syncPasswordProperty, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { return property.Name == "SYNC_PASSWORD" }); err == nil { + if err = d.Set("sync_password", syncPasswordProperty.Value); err != nil { + return nil, err + } + } + if err = d.Set("comment", integration.Comment); err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} + func CreateContextSCIMIntegration(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*provider.Context).Client - name := d.Get("name").(string) - id := sdk.NewAccountObjectIdentifier(name) - scimClientRaw := d.Get("scim_client").(string) - runAsRoleRaw := d.Get("run_as_role").(string) - scimClient, err := sdk.ToScimSecurityIntegrationScimClientOption(scimClientRaw) + + id := sdk.NewAccountObjectIdentifier(d.Get("name").(string)) + + scimClient, err := sdk.ToScimSecurityIntegrationScimClientOption(d.Get("scim_client").(string)) if err != nil { return diag.FromErr(err) } - runAsRole, err := sdk.ToScimSecurityIntegrationRunAsRoleOption(runAsRoleRaw) + + runAsRole, err := sdk.ToScimSecurityIntegrationRunAsRoleOption(d.Get("run_as_role").(string)) if err != nil { return diag.FromErr(err) } + req := sdk.NewCreateScimSecurityIntegrationRequest(id, scimClient, runAsRole).WithEnabled(d.Get("enabled").(bool)) if v, ok := d.GetOk("network_policy"); ok { req.WithNetworkPolicy(sdk.NewAccountObjectIdentifier(v.(string))) } - if !d.GetRawConfig().AsValueMap()["sync_password"].IsNull() { - req.WithSyncPassword(d.Get("sync_password").(bool)) + + if v := d.Get("sync_password").(string); v != "unknown" { + parsed, err := strconv.ParseBool(v) + if err != nil { + return diag.FromErr(err) + } + + req.WithSyncPassword(parsed) } + if v, ok := d.GetOk("comment"); ok { req.WithComment(v.(string)) } + if err := client.SecurityIntegrations.CreateScim(ctx, req); err != nil { return diag.FromErr(err) } d.SetId(helpers.EncodeSnowflakeID(id)) - return ReadContextSCIMIntegration(ctx, d, meta) + return ReadContextSCIMIntegration(false)(ctx, d, meta) } -func ReadContextSCIMIntegration(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*provider.Context).Client - id := helpers.DecodeSnowflakeID(d.Id()).(sdk.AccountObjectIdentifier) +func ReadContextSCIMIntegration(withExternalChangesMarking bool) schema.ReadContextFunc { + return func(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + id := helpers.DecodeSnowflakeID(d.Id()).(sdk.AccountObjectIdentifier) - integration, err := client.SecurityIntegrations.ShowByID(ctx, id) - if err != nil { - if errors.Is(err, sdk.ErrObjectNotFound) { - d.SetId("") - return diag.Diagnostics{ - diag.Diagnostic{ - Severity: diag.Warning, - Summary: "Failed to query security integration. Marking the resource as removed.", - Detail: fmt.Sprintf("Security integration name: %s, Err: %s", id.FullyQualifiedName(), err), - }, + integration, err := client.SecurityIntegrations.ShowByID(ctx, id) + if err != nil { + if errors.Is(err, sdk.ErrObjectNotFound) { + d.SetId("") + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Failed to query security integration. Marking the resource as removed.", + Detail: fmt.Sprintf("Security integration name: %s, Err: %s", id.FullyQualifiedName(), err), + }, + } } + return diag.FromErr(err) } - return diag.FromErr(err) - } - if c := integration.Category; c != sdk.SecurityIntegrationCategory { - return diag.FromErr(fmt.Errorf("expected %v to be a SECURITY integration, got %v", id, c)) - } + integrationProperties, err := client.SecurityIntegrations.Describe(ctx, id) + if err != nil { + if errors.Is(err, sdk.ErrObjectNotFound) { + d.SetId("") + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Failed to query security integration properties. Marking the resource as removed.", + Detail: fmt.Sprintf("Security integration name: %s, Err: %s", id.FullyQualifiedName(), err), + }, + } + } + return diag.FromErr(err) + } - if err := d.Set("name", integration.Name); err != nil { - return diag.FromErr(err) - } + if c := integration.Category; c != sdk.SecurityIntegrationCategory { + return diag.FromErr(fmt.Errorf("expected %v to be a SECURITY integration, got %v", id, c)) + } - if err := d.Set("comment", integration.Comment); err != nil { - return diag.FromErr(err) - } + if err := d.Set("name", integration.Name); err != nil { + return diag.FromErr(err) + } - if err := d.Set("created_on", integration.CreatedOn.String()); err != nil { - return diag.FromErr(err) - } + if err := d.Set("enabled", integration.Enabled); err != nil { + return diag.FromErr(err) + } - if err := d.Set("enabled", integration.Enabled); err != nil { - return diag.FromErr(err) - } + scimClient, err := integration.SubType() + if err != nil { + return diag.FromErr(err) + } - scimClient, err := integration.SubType() - if err != nil { - return diag.FromErr(err) - } - if err := d.Set("scim_client", scimClient); err != nil { - return diag.FromErr(err) - } - integrationProperties, err := client.SecurityIntegrations.Describe(ctx, id) - if err != nil { - return diag.FromErr(err) - } - for _, property := range integrationProperties { - name := property.Name - value := property.Value - switch name { - case "ENABLED", "COMMENT": - // We set this using the SHOW INTEGRATION call so let's ignore it here - case "NETWORK_POLICY": - networkPolicyID := sdk.NewAccountObjectIdentifier(value) - if err := d.Set("network_policy", networkPolicyID.FullyQualifiedName()); err != nil { + if err := d.Set("scim_client", scimClient); err != nil { + return diag.FromErr(err) + } + + runAsRoleProperty, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { return property.Name == "RUN_AS_ROLE" }) + if err != nil { + return diag.FromErr(err) + } + + if err := d.Set("run_as_role", runAsRoleProperty.Value); err != nil { + return diag.FromErr(err) + } + + if withExternalChangesMarking { + if err = handleExternalChangesToObjectInShow(d, + showMapping{"comment", "comment", integration.Comment, integration.Comment, nil}, + ); err != nil { return diag.FromErr(err) } - case "SYNC_PASSWORD": - if err := d.Set("sync_password", helpers.StringToBool(value)); err != nil { + + networkPolicyProperty, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { return property.Name == "NETWORK_POLICY" }) + if err != nil { return diag.FromErr(err) } - case "RUN_AS_ROLE": - if err := d.Set("run_as_role", value); err != nil { + + syncPasswordProperty, err := collections.FindOne(integrationProperties, func(property sdk.SecurityIntegrationProperty) bool { return property.Name == "SYNC_PASSWORD" }) + if err != nil { + return diag.FromErr(err) + } + + if err = handleExternalChangesToObjectInDescribe(d, + describeMapping{"network_policy", "network_policy", networkPolicyProperty.Value, networkPolicyProperty.Value, nil}, + describeMapping{"sync_password", "sync_password", syncPasswordProperty.Value, syncPasswordProperty.Value, nil}, + ); err != nil { return diag.FromErr(err) } - default: - log.Printf("[WARN] unexpected property %v returned from Snowflake", name) } - } - return nil + // These are all identity sets, needed for the case where: + // - previous config was empty (therefore Snowflake defaults had been used) + // - new config have the same values that are already in SF + if !d.GetRawConfig().IsNull() { + if v := d.GetRawConfig().AsValueMap()["network_policy"]; !v.IsNull() { + if err = d.Set("network_policy", v.AsString()); err != nil { + return diag.FromErr(err) + } + } + if v := d.GetRawConfig().AsValueMap()["sync_password"]; !v.IsNull() { + if err = d.Set("sync_password", v.AsString()); err != nil { + return diag.FromErr(err) + } + } + if v := d.GetRawConfig().AsValueMap()["comment"]; !v.IsNull() { + if err = d.Set("comment", v.AsString()); err != nil { + return diag.FromErr(err) + } + } + } + + if err = d.Set(showOutputAttributeName, []map[string]any{schemas.SecurityIntegrationToSchema(integration)}); err != nil { + return diag.FromErr(err) + } + + if err = d.Set(describeOutputAttributeName, []map[string]any{schemas.ScimSecurityIntegrationPropertiesToSchema(integrationProperties)}); err != nil { + return diag.FromErr(err) + } + + return nil + } } func UpdateContextSCIMIntegration(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*provider.Context).Client id := helpers.DecodeSnowflakeID(d.Id()).(sdk.AccountObjectIdentifier) set, unset := sdk.NewScimIntegrationSetRequest(), sdk.NewScimIntegrationUnsetRequest() + if d.HasChange("enabled") { set.WithEnabled(d.Get("enabled").(bool)) } + if d.HasChange("network_policy") { - networkPolicyID := sdk.NewAccountObjectIdentifier(d.Get("network_policy").(string)) - if networkPolicyID.Name() != "" { - set.WithNetworkPolicy(networkPolicyID) + if v := d.Get("network_policy").(string); v != "" { + set.WithNetworkPolicy(sdk.NewAccountObjectIdentifier(v)) } else { unset.WithNetworkPolicy(true) } } - if !d.GetRawConfig().AsValueMap()["sync_password"].IsNull() { - set.WithSyncPassword(d.Get("sync_password").(bool)) - } else { - unset.WithSyncPassword(true) + + if d.HasChange("sync_password") { + if v := d.Get("sync_password").(string); v != "unknown" { + parsed, err := strconv.ParseBool(v) + if err != nil { + return diag.FromErr(err) + } + set.WithSyncPassword(parsed) + } else { + unset.WithSyncPassword(true) + } } if d.HasChange("comment") { set.WithComment(sdk.StringAllowEmpty{Value: d.Get("comment").(string)}) } + if (*set != sdk.ScimIntegrationSetRequest{}) { if err := client.SecurityIntegrations.AlterScim(ctx, sdk.NewAlterScimSecurityIntegrationRequest(id).WithSet(*set)); err != nil { return diag.FromErr(err) } } + if (*unset != sdk.ScimIntegrationUnsetRequest{}) { if err := client.SecurityIntegrations.AlterScim(ctx, sdk.NewAlterScimSecurityIntegrationRequest(id).WithUnset(*unset)); err != nil { return diag.FromErr(err) } } - return ReadContextSCIMIntegration(ctx, d, meta) + + return ReadContextSCIMIntegration(false)(ctx, d, meta) } func DeleteContextSCIMIntegration(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { diff --git a/pkg/resources/scim_integration_acceptance_test.go b/pkg/resources/scim_integration_acceptance_test.go index f175a453cb..d7df385d87 100644 --- a/pkg/resources/scim_integration_acceptance_test.go +++ b/pkg/resources/scim_integration_acceptance_test.go @@ -2,9 +2,13 @@ package resources_test import ( "fmt" - "strings" "testing" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/importchecks" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/planchecks" + tfjson "github.com/hashicorp/terraform-json" + acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/helpers" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/snowflakeroles" @@ -20,8 +24,10 @@ import ( func TestAcc_ScimIntegration_basic(t *testing.T) { networkPolicy, networkPolicyCleanup := acc.TestClient().NetworkPolicy.CreateNetworkPolicy(t) t.Cleanup(networkPolicyCleanup) + role, role2 := snowflakeroles.GenericScimProvisioner, snowflakeroles.OktaProvisioner id := acc.TestClient().Ids.RandomAccountObjectIdentifier() + m := func(enabled bool, scimClient sdk.ScimSecurityIntegrationScimClientOption, runAsRole sdk.AccountObjectIdentifier, complete bool) map[string]config.Variable { c := map[string]config.Variable{ "name": config.StringVariable(id.Name()), @@ -36,6 +42,7 @@ func TestAcc_ScimIntegration_basic(t *testing.T) { } return c } + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, PreCheck: func() { acc.TestAccPreCheck(t) }, @@ -44,6 +51,7 @@ func TestAcc_ScimIntegration_basic(t *testing.T) { }, CheckDestroy: acc.CheckDestroy(t, resources.ScimSecurityIntegration), Steps: []resource.TestStep{ + // create with empty optionals { ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ScimIntegration/basic"), ConfigVariables: m(false, sdk.ScimSecurityIntegrationScimClientGeneric, role, false), @@ -52,10 +60,43 @@ func TestAcc_ScimIntegration_basic(t *testing.T) { resource.TestCheckResourceAttr("snowflake_scim_integration.test", "enabled", "false"), resource.TestCheckResourceAttr("snowflake_scim_integration.test", "scim_client", "GENERIC"), resource.TestCheckResourceAttr("snowflake_scim_integration.test", "run_as_role", role.Name()), - resource.TestCheckResourceAttrSet("snowflake_scim_integration.test", "sync_password"), - resource.TestCheckResourceAttrSet("snowflake_scim_integration.test", "created_on"), + resource.TestCheckNoResourceAttr("snowflake_scim_integration.test", "network_policy"), + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "sync_password", "unknown"), + resource.TestCheckNoResourceAttr("snowflake_scim_integration.test", "comment"), + + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "show_output.#", "1"), + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "show_output.0.name", id.Name()), + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "show_output.0.integration_type", "SCIM - GENERIC"), + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "show_output.0.category", "SECURITY"), + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "show_output.0.enabled", "false"), + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "show_output.0.comment", ""), + resource.TestCheckResourceAttrSet("snowflake_scim_integration.test", "show_output.0.created_on"), + + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "describe_output.#", "1"), + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "describe_output.0.enabled.0.value", "false"), + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "describe_output.0.network_policy.0.value", ""), + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "describe_output.0.run_as_role.0.value", role.Name()), + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "describe_output.0.sync_password.0.value", "true"), + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "describe_output.0.comment.0.value", ""), ), }, + // import - without optionals + { + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ScimIntegration/basic"), + ConfigVariables: m(false, sdk.ScimSecurityIntegrationScimClientGeneric, role, false), + ResourceName: "snowflake_scim_integration.test", + ImportState: true, + ImportStateCheck: importchecks.ComposeImportStateCheck( + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "name", id.Name()), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "enabled", "false"), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "scim_client", "GENERIC"), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "run_as_role", role.Name()), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "network_policy", ""), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "sync_password", "true"), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "comment", ""), + ), + }, + // set optionals { ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ScimIntegration/complete"), ConfigVariables: m(true, sdk.ScimSecurityIntegrationScimClientOkta, role2, true), @@ -64,18 +105,41 @@ func TestAcc_ScimIntegration_basic(t *testing.T) { resource.TestCheckResourceAttr("snowflake_scim_integration.test", "enabled", "true"), resource.TestCheckResourceAttr("snowflake_scim_integration.test", "scim_client", "OKTA"), resource.TestCheckResourceAttr("snowflake_scim_integration.test", "run_as_role", role2.Name()), - resource.TestCheckResourceAttr("snowflake_scim_integration.test", "network_policy", sdk.NewAccountObjectIdentifier(networkPolicy.Name).FullyQualifiedName()), + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "network_policy", sdk.NewAccountObjectIdentifier(networkPolicy.Name).Name()), // TODO(SNOW-999049): Fix during identifiers rework resource.TestCheckResourceAttr("snowflake_scim_integration.test", "sync_password", "false"), resource.TestCheckResourceAttr("snowflake_scim_integration.test", "comment", "foo"), - resource.TestCheckResourceAttrSet("snowflake_scim_integration.test", "created_on"), + + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "show_output.#", "1"), + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "show_output.0.name", id.Name()), + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "show_output.0.integration_type", "SCIM - OKTA"), + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "show_output.0.category", "SECURITY"), + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "show_output.0.enabled", "true"), + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "show_output.0.comment", "foo"), + resource.TestCheckResourceAttrSet("snowflake_scim_integration.test", "show_output.0.created_on"), + + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "describe_output.#", "1"), + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "describe_output.0.enabled.0.value", "true"), + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "describe_output.0.network_policy.0.value", sdk.NewAccountObjectIdentifier(networkPolicy.Name).Name()), // TODO(SNOW-999049): Fix during identifiers rework + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "describe_output.0.run_as_role.0.value", role2.Name()), + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "describe_output.0.sync_password.0.value", "false"), + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "describe_output.0.comment.0.value", "foo"), ), }, + // import - complete { - ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ScimIntegration/basic"), - ConfigVariables: m(true, sdk.ScimSecurityIntegrationScimClientOkta, role2, true), - ResourceName: "snowflake_scim_integration.test", - ImportState: true, - ImportStateVerify: true, + ConfigDirectory: acc.ConfigurationDirectory("TestAcc_ScimIntegration/complete"), + ConfigVariables: m(true, sdk.ScimSecurityIntegrationScimClientOkta, role2, true), + ResourceName: "snowflake_scim_integration.test", + ImportState: true, + ImportStateCheck: importchecks.ComposeImportStateCheck( + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "name", id.Name()), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "enabled", "true"), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "scim_client", "OKTA"), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "run_as_role", role2.Name()), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "network_policy", sdk.NewAccountObjectIdentifier(networkPolicy.Name).Name()), // TODO(SNOW-999049): Fix during identifiers rework + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "sync_password", "false"), + importchecks.TestCheckResourceAttrInstanceState(id.Name(), "comment", "foo"), + ), }, // unset { @@ -87,9 +151,8 @@ func TestAcc_ScimIntegration_basic(t *testing.T) { resource.TestCheckResourceAttr("snowflake_scim_integration.test", "scim_client", "OKTA"), resource.TestCheckResourceAttr("snowflake_scim_integration.test", "run_as_role", role2.Name()), resource.TestCheckResourceAttr("snowflake_scim_integration.test", "network_policy", ""), - resource.TestCheckResourceAttr("snowflake_scim_integration.test", "sync_password", "true"), + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "sync_password", "unknown"), resource.TestCheckResourceAttr("snowflake_scim_integration.test", "comment", ""), - resource.TestCheckResourceAttrSet("snowflake_scim_integration.test", "created_on"), ), }, }, @@ -105,7 +168,7 @@ func TestAcc_ScimIntegration_complete(t *testing.T) { return map[string]config.Variable{ "name": config.StringVariable(id.Name()), "enabled": config.BoolVariable(false), - "scim_client": config.StringVariable(strings.ToLower(string(sdk.ScimSecurityIntegrationScimClientGeneric))), + "scim_client": config.StringVariable(string(sdk.ScimSecurityIntegrationScimClientGeneric)), "sync_password": config.BoolVariable(false), "network_policy_name": config.StringVariable(networkPolicy.Name), "run_as_role": config.StringVariable(role.Name()), @@ -128,10 +191,9 @@ func TestAcc_ScimIntegration_complete(t *testing.T) { resource.TestCheckResourceAttr("snowflake_scim_integration.test", "enabled", "false"), resource.TestCheckResourceAttr("snowflake_scim_integration.test", "scim_client", "GENERIC"), resource.TestCheckResourceAttr("snowflake_scim_integration.test", "run_as_role", role.Name()), - resource.TestCheckResourceAttr("snowflake_scim_integration.test", "network_policy", sdk.NewAccountObjectIdentifier(networkPolicy.Name).FullyQualifiedName()), + resource.TestCheckResourceAttr("snowflake_scim_integration.test", "network_policy", sdk.NewAccountObjectIdentifier(networkPolicy.Name).Name()), // TODO(SNOW-999049): Fix during identifiers rework resource.TestCheckResourceAttr("snowflake_scim_integration.test", "sync_password", "false"), resource.TestCheckResourceAttr("snowflake_scim_integration.test", "comment", "foo"), - resource.TestCheckResourceAttrSet("snowflake_scim_integration.test", "created_on"), ), }, { @@ -231,7 +293,15 @@ func TestAcc_ScimIntegration_migrateFromVersion091(t *testing.T) { ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, Config: scimIntegrationv092(id.Name(), role.Name()), ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{plancheck.ExpectEmptyPlan()}, + PreApply: []plancheck.PlanCheck{ + planchecks.ExpectChange("snowflake_scim_integration.test", "name", tfjson.ActionUpdate, sdk.String(id.Name()), sdk.String(id.Name())), + planchecks.ExpectChange("snowflake_scim_integration.test", "enabled", tfjson.ActionUpdate, sdk.String("true"), sdk.String("true")), + planchecks.ExpectChange("snowflake_scim_integration.test", "scim_client", tfjson.ActionUpdate, sdk.String("GENERIC"), sdk.String("GENERIC")), + planchecks.ExpectChange("snowflake_scim_integration.test", "run_as_role", tfjson.ActionUpdate, sdk.String(role.Name()), sdk.String(role.Name())), + planchecks.ExpectChange("snowflake_scim_integration.test", "network_policy", tfjson.ActionUpdate, sdk.String(""), sdk.String("")), + planchecks.ExpectChange("snowflake_scim_integration.test", "sync_password", tfjson.ActionUpdate, nil, sdk.String("unknown")), + planchecks.ExpectChange("snowflake_scim_integration.test", "comment", tfjson.ActionUpdate, nil, nil), + }, }, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "name", id.Name()), diff --git a/pkg/resources/scim_integration_state_upgraders.go b/pkg/resources/scim_integration_state_upgraders.go index 130ae41076..4c8f60f8c1 100644 --- a/pkg/resources/scim_integration_state_upgraders.go +++ b/pkg/resources/scim_integration_state_upgraders.go @@ -1,13 +1,21 @@ package resources -import "context" +import ( + "context" + "strconv" +) -func v091ScimIntegrationStateUpgrader(ctx context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { +func v091ScimIntegrationStateUpgrader(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) { if rawState == nil { return rawState, nil } rawState["run_as_role"] = rawState["provisioner_role"] delete(rawState, "provisioner_role") + + if v, ok := rawState["enabled"]; ok { + rawState["enabled"] = strconv.FormatBool(v.(bool)) + } + return rawState, nil } diff --git a/pkg/resources/warehouse.go b/pkg/resources/warehouse.go index 71ab1d0ba1..b44b843e8f 100644 --- a/pkg/resources/warehouse.go +++ b/pkg/resources/warehouse.go @@ -30,42 +30,42 @@ var warehouseSchema = map[string]*schema.Schema{ Type: schema.TypeString, Optional: true, ValidateDiagFunc: sdkValidation(sdk.ToWarehouseType), - DiffSuppressFunc: SuppressIfAny(NormalizeAndCompare(sdk.ToWarehouseType), IgnoreChangeToCurrentSnowflakeValue("type")), + DiffSuppressFunc: SuppressIfAny(NormalizeAndCompare(sdk.ToWarehouseType), IgnoreChangeToCurrentSnowflakeValueInShow("type")), Description: fmt.Sprintf("Specifies warehouse type. Valid values are (case-insensitive): %s. Warehouse needs to be suspended to change its type. Provider will handle automatic suspension and resumption if needed.", possibleValuesListed(sdk.ValidWarehouseTypesString)), }, "warehouse_size": { Type: schema.TypeString, Optional: true, ValidateDiagFunc: sdkValidation(sdk.ToWarehouseSize), - DiffSuppressFunc: SuppressIfAny(NormalizeAndCompare(sdk.ToWarehouseSize), IgnoreChangeToCurrentSnowflakeValue("size")), + DiffSuppressFunc: SuppressIfAny(NormalizeAndCompare(sdk.ToWarehouseSize), IgnoreChangeToCurrentSnowflakeValueInShow("size")), Description: fmt.Sprintf("Specifies the size of the virtual warehouse. Valid values are (case-insensitive): %s. Consult [warehouse documentation](https://docs.snowflake.com/en/sql-reference/sql/create-warehouse#optional-properties-objectproperties) for the details. Note: removing the size from config will result in the resource recreation.", possibleValuesListed(sdk.ValidWarehouseSizesString)), }, "max_cluster_count": { Type: schema.TypeInt, Optional: true, ValidateFunc: validation.IntBetween(1, 10), - DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValue("max_cluster_count"), + DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInShow("max_cluster_count"), Description: "Specifies the maximum number of server clusters for the warehouse.", }, "min_cluster_count": { Type: schema.TypeInt, Optional: true, ValidateFunc: validation.IntBetween(1, 10), - DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValue("min_cluster_count"), + DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInShow("min_cluster_count"), Description: "Specifies the minimum number of server clusters for the warehouse (only applies to multi-cluster warehouses).", }, "scaling_policy": { Type: schema.TypeString, Optional: true, ValidateDiagFunc: sdkValidation(sdk.ToScalingPolicy), - DiffSuppressFunc: SuppressIfAny(NormalizeAndCompare(sdk.ToWarehouseType), IgnoreChangeToCurrentSnowflakeValue("scaling_policy")), + DiffSuppressFunc: SuppressIfAny(NormalizeAndCompare(sdk.ToWarehouseType), IgnoreChangeToCurrentSnowflakeValueInShow("scaling_policy")), Description: fmt.Sprintf("Specifies the policy for automatically starting and shutting down clusters in a multi-cluster warehouse running in Auto-scale mode. Valid values are (case-insensitive): %s.", possibleValuesListed(sdk.ValidWarehouseScalingPoliciesString)), }, "auto_suspend": { Type: schema.TypeInt, Optional: true, ValidateFunc: validation.IntAtLeast(0), - DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValue("auto_suspend"), + DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInShow("auto_suspend"), Description: "Specifies the number of seconds of inactivity after which a warehouse is automatically suspended.", Default: -1, }, @@ -73,7 +73,7 @@ var warehouseSchema = map[string]*schema.Schema{ Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringInSlice([]string{"true", "false"}, true), - DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValue("auto_resume"), + DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInShow("auto_resume"), Description: "Specifies whether to automatically resume a warehouse when a SQL statement (e.g. query) is submitted to it.", Default: "unknown", }, @@ -87,7 +87,7 @@ var warehouseSchema = map[string]*schema.Schema{ Type: schema.TypeString, Optional: true, ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](), - DiffSuppressFunc: SuppressIfAny(suppressIdentifierQuoting, IgnoreChangeToCurrentSnowflakeValue("resource_monitor")), + DiffSuppressFunc: SuppressIfAny(suppressIdentifierQuoting, IgnoreChangeToCurrentSnowflakeValueInShow("resource_monitor")), Description: "Specifies the name of a resource monitor that is explicitly assigned to the warehouse.", }, "comment": { @@ -99,7 +99,7 @@ var warehouseSchema = map[string]*schema.Schema{ Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringInSlice([]string{"true", "false"}, true), - DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValue("enable_query_acceleration"), + DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInShow("enable_query_acceleration"), Description: "Specifies whether to enable the query acceleration service for queries that rely on this warehouse for compute resources.", Default: "unknown", }, @@ -107,7 +107,7 @@ var warehouseSchema = map[string]*schema.Schema{ Type: schema.TypeInt, Optional: true, ValidateFunc: validation.IntBetween(0, 100), - DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValue("query_acceleration_max_scale_factor"), + DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInShow("query_acceleration_max_scale_factor"), Description: "Specifies the maximum scale factor for leasing compute resources for query acceleration. The scale factor is used as a multiplier based on warehouse size.", Default: -1, }, @@ -348,7 +348,7 @@ func GetReadWarehouseFunc(withExternalChangesMarking bool) schema.ReadContextFun } if withExternalChangesMarking { - if err = handleExternalChangesToObject(d, + if err = handleExternalChangesToObjectInShow(d, showMapping{"type", "warehouse_type", string(w.Type), w.Type, nil}, showMapping{"size", "warehouse_size", string(w.Size), w.Size, nil}, showMapping{"max_cluster_count", "max_cluster_count", w.MaxClusterCount, w.MaxClusterCount, nil}, diff --git a/pkg/resources/warehouse_rework_show_output_proposal.go b/pkg/resources/warehouse_rework_show_output_proposal.go index 8c9908953c..654fec57e7 100644 --- a/pkg/resources/warehouse_rework_show_output_proposal.go +++ b/pkg/resources/warehouse_rework_show_output_proposal.go @@ -4,10 +4,13 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -const showOutputAttributeName = "show_output" +const ( + showOutputAttributeName = "show_output" + describeOutputAttributeName = "describe_output" +) -// handleExternalChangesToObject assumes that show output is kept in showOutputAttributeName attribute -func handleExternalChangesToObject(d *schema.ResourceData, mappings ...showMapping) error { +// handleExternalChangesToObjectInShow assumes that show output is kept in showOutputAttributeName attribute +func handleExternalChangesToObjectInShow(d *schema.ResourceData, mappings ...showMapping) error { if showOutput, ok := d.GetOk(showOutputAttributeName); ok { showOutputList := showOutput.([]any) if len(showOutputList) == 1 { @@ -35,3 +38,43 @@ type showMapping struct { valueToSet any normalizeFunc func(any) any } + +// handleExternalChangesToObjectInDescribe assumes that show output is kept in describeOutputAttributeName attribute +func handleExternalChangesToObjectInDescribe(d *schema.ResourceData, mappings ...describeMapping) error { + if describeOutput, ok := d.GetOk(describeOutputAttributeName); ok { + describeOutputList := describeOutput.([]any) + if len(describeOutputList) == 1 { + result := describeOutputList[0].(map[string]any) + + for _, mapping := range mappings { + if result[mapping.nameInDescribe] == nil { + continue + } + + valueToCompareFromList := result[mapping.nameInDescribe].([]any) + if len(valueToCompareFromList) != 1 { + continue + } + + valueToCompareFrom := valueToCompareFromList[0].(map[string]any)["value"] + if mapping.normalizeFunc != nil { + valueToCompareFrom = mapping.normalizeFunc(valueToCompareFrom) + } + if valueToCompareFrom != mapping.valueToCompare { + if err := d.Set(mapping.nameInConfig, mapping.valueToSet); err != nil { + return err + } + } + } + } + } + return nil +} + +type describeMapping struct { + nameInDescribe string + nameInConfig string + valueToCompare any + valueToSet any + normalizeFunc func(any) any +} diff --git a/pkg/schemas/common_types.go b/pkg/schemas/common_types.go new file mode 100644 index 0000000000..36db0c3692 --- /dev/null +++ b/pkg/schemas/common_types.go @@ -0,0 +1,42 @@ +package schemas + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// ParameterListSchema represents Snowflake parameter object. +var ParameterListSchema = &schema.Schema{ + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: ShowParameterSchema, + }, +} + +// DescribePropertyListSchema represents Snowflake property object returned by DESCRIBE query. +var DescribePropertyListSchema = &schema.Schema{ + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: DescribePropertySchema, + }, +} + +var DescribePropertySchema = map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Computed: true, + }, + "value": { + Type: schema.TypeString, + Computed: true, + }, + "default": { + Type: schema.TypeString, + Computed: true, + }, +} diff --git a/pkg/schemas/parameters.go b/pkg/schemas/parameters.go deleted file mode 100644 index b81d3b0786..0000000000 --- a/pkg/schemas/parameters.go +++ /dev/null @@ -1,12 +0,0 @@ -package schemas - -import "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - -// ParameterListSchema represents Snowflake parameter object. -var ParameterListSchema = &schema.Schema{ - Type: schema.TypeList, - Computed: true, - Elem: &schema.Resource{ - Schema: ShowParameterSchema, - }, -} diff --git a/pkg/schemas/scim_security_integration.go b/pkg/schemas/scim_security_integration.go new file mode 100644 index 0000000000..a07f9ffb61 --- /dev/null +++ b/pkg/schemas/scim_security_integration.go @@ -0,0 +1,43 @@ +package schemas + +import ( + "strings" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// DescribeScimSecurityIntegrationSchema represents output of DESCRIBE query for the single SecurityIntegration. +var DescribeScimSecurityIntegrationSchema = map[string]*schema.Schema{ + "enabled": DescribePropertyListSchema, + "network_policy": DescribePropertyListSchema, + "run_as_role": DescribePropertyListSchema, + "sync_password": DescribePropertyListSchema, + "comment": DescribePropertyListSchema, +} + +var _ = DescribeScimSecurityIntegrationSchema + +func ScimSecurityIntegrationPropertiesToSchema(securityIntegrationProperties []sdk.SecurityIntegrationProperty) map[string]any { + securityIntegrationSchema := make(map[string]any) + for _, securityIntegrationProperty := range securityIntegrationProperties { + switch securityIntegrationProperty.Name { + case "ENABLED", + "NETWORK_POLICY", + "RUN_AS_ROLE", + "SYNC_PASSWORD", + "COMMENT": + securityIntegrationSchema[strings.ToLower(securityIntegrationProperty.Name)] = []map[string]any{ + { + "name": securityIntegrationProperty.Name, + "type": securityIntegrationProperty.Type, + "value": securityIntegrationProperty.Value, + "default": securityIntegrationProperty.Default, + }, + } + } + } + return securityIntegrationSchema +} + +var _ = ScimSecurityIntegrationPropertiesToSchema