diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 9fe0fec214..eea774dd44 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -9,6 +9,22 @@ across different versions. ## v0.97.0 ➞ v0.98.0 +### *(new feature)* connection resources + +Added a new resources for managing connections. We decided to split connection into two separate resources based on whether the connection is primary or a replica (secondary). i.e.: + +- `snowflake_connection` is used as primary connection, with ability to enable failover to other accounts. +- `snowflake_secondary_connection` is used as replica (secondary) connection. + +In order to promote secondary_connection to primary, resources need to be migrated (check [resource migration](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/resource_migration.md)) or re-created and imported using the following SQL statements on Snowflake Worksheet: + +``` +CREATE CONNECTION AS REPLICA OF ..; +ALTER CONNECTION PRIMARY; +``` + +See reference [docs](https://docs.snowflake.com/en/sql-reference/sql/create-connection). + ### snowflake_streams data source changes New filtering options: - `like` diff --git a/docs/resources/primary_connection.md b/docs/resources/primary_connection.md new file mode 100644 index 0000000000..cb7bca2221 --- /dev/null +++ b/docs/resources/primary_connection.md @@ -0,0 +1,76 @@ +--- +page_title: "snowflake_primary_connection Resource - terraform-provider-snowflake" +subcategory: "" +description: |- + Resource used to manage primary (not replicated) connections. For more information, check connection documentation https://docs.snowflake.com/en/sql-reference/sql/create-connection.html. +--- + +!> **V1 release candidate** This resource is a release candidate for the V1. It is on the list of remaining GA objects for V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0970--v0980) to use it. + +# snowflake_primary_connection (Resource) + +Resource used to manage primary (not replicated) connections. For more information, check [connection documentation](https://docs.snowflake.com/en/sql-reference/sql/create-connection.html). + +## Example Usage + +```terraform +## Minimal +resource "snowflake_primary_connection" "basic" { + name = "connection_name" +} + +## Complete (with every optional set) +resource "snowflake_primary_connection" "complete" { + name = "connection_name" + comment = "my complete connection" + enable_failover_to_accounts = [ + "." + ] +} +``` +-> **Note** Instead of using fully_qualified_name, you can reference objects managed outside Terraform by constructing a correct ID, consult [identifiers guide](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/guides/identifiers#new-computed-fully-qualified-name-field-in-resources). + + + +## Schema + +### Required + +- `name` (String) String that specifies the identifier (i.e. name) for the connection. Must start with an alphabetic character and may only contain letters, decimal digits (0-9), and underscores (_). For a primary connection, the name must be unique across connection names and account names in the organization. Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: `|`, `.`, `(`, `)`, `"` + +### Optional + +- `comment` (String) Specifies a comment for the connection. +- `enable_failover_to_accounts` (List of String) Enables failover for given connection to provided accounts. Specifies a list of accounts in your organization where a secondary connection for this primary connection can be promoted to serve as the primary connection. Include your organization name for each account in the list. + +### Read-Only + +- `fully_qualified_name` (String) Fully qualified name of the resource. For more information, see [object name resolution](https://docs.snowflake.com/en/sql-reference/name-resolution). +- `id` (String) The ID of this resource. +- `show_output` (List of Object) Outputs the result of `SHOW CONNECTIONS` for the given connection. (see [below for nested schema](#nestedatt--show_output)) + + +### Nested Schema for `show_output` + +Read-Only: + +- `account_locator` (String) +- `account_name` (String) +- `comment` (String) +- `connection_url` (String) +- `created_on` (String) +- `failover_allowed_to_accounts` (List of String) +- `is_primary` (Boolean) +- `name` (String) +- `organization_name` (String) +- `primary` (String) +- `region_group` (String) +- `snowflake_region` (String) + +## Import + +Import is supported using the following syntax: + +```shell +terraform import snowflake_primary_connection.example 'connection_name' +``` diff --git a/docs/resources/secondary_connection.md b/docs/resources/secondary_connection.md new file mode 100644 index 0000000000..e1a6a4d969 --- /dev/null +++ b/docs/resources/secondary_connection.md @@ -0,0 +1,76 @@ +--- +page_title: "snowflake_secondary_connection Resource - terraform-provider-snowflake" +subcategory: "" +description: |- + Resource used to manage secondary connections. To promote secondary connection to primary check migraton guide https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#connection-resources. For more information, check connection documentation https://docs.snowflake.com/en/sql-reference/sql/create-connection.html. +--- + +!> **V1 release candidate** This resource is a release candidate for the V1. It is on the list of remaining GA objects for V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0970--v0980) to use it. + +# snowflake_secondary_connection (Resource) + +Resource used to manage secondary connections. To promote secondary connection to primary check [migraton guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#connection-resources). For more information, check [connection documentation](https://docs.snowflake.com/en/sql-reference/sql/create-connection.html). + +## Example Usage + +```terraform +## Minimal +resource "snowflake_secondary_connection" "basic" { + name = "connection_name" + as_replica_of = ".." +} + +## Complete (with every optional set) +resource "snowflake_secondary_connection" "complete" { + name = "connection_name" + as_replica_of = ".." + comment = "my complete secondary connection" +} +``` +-> **Note** Instead of using fully_qualified_name, you can reference objects managed outside Terraform by constructing a correct ID, consult [identifiers guide](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/guides/identifiers#new-computed-fully-qualified-name-field-in-resources). + + + +## Schema + +### Required + +- `as_replica_of` (String) Specifies the identifier for a primary connection from which to create a replica (i.e. a secondary connection). +- `name` (String) String that specifies the identifier (i.e. name) for the connection. Must start with an alphabetic character and may only contain letters, decimal digits (0-9), and underscores (_). For a secondary connection, the name must match the name of its primary connection. Due to technical limitations (read more [here](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/docs/technical-documentation/identifiers_rework_design_decisions.md#known-limitations-and-identifier-recommendations)), avoid using the following characters: `|`, `.`, `(`, `)`, `"` + +### Optional + +- `comment` (String) Specifies a comment for the secondary connection. + +### Read-Only + +- `fully_qualified_name` (String) Fully qualified name of the resource. For more information, see [object name resolution](https://docs.snowflake.com/en/sql-reference/name-resolution). +- `id` (String) The ID of this resource. +- `is_primary` (Boolean) Indicates if the connection has been changed to primary. If change is detected, the secondary connection will be recreated. +- `show_output` (List of Object) Outputs the result of `SHOW CONNECTIONS` for the given connection. (see [below for nested schema](#nestedatt--show_output)) + + +### Nested Schema for `show_output` + +Read-Only: + +- `account_locator` (String) +- `account_name` (String) +- `comment` (String) +- `connection_url` (String) +- `created_on` (String) +- `failover_allowed_to_accounts` (List of String) +- `is_primary` (Boolean) +- `name` (String) +- `organization_name` (String) +- `primary` (String) +- `region_group` (String) +- `snowflake_region` (String) + +## Import + +Import is supported using the following syntax: + +```shell +terraform import snowflake_secondary_connection.example 'secondary_connection_name' +``` diff --git a/examples/resources/snowflake_primary_connection/import.sh b/examples/resources/snowflake_primary_connection/import.sh new file mode 100644 index 0000000000..743bf79921 --- /dev/null +++ b/examples/resources/snowflake_primary_connection/import.sh @@ -0,0 +1 @@ +terraform import snowflake_primary_connection.example 'connection_name' diff --git a/examples/resources/snowflake_primary_connection/resource.tf b/examples/resources/snowflake_primary_connection/resource.tf new file mode 100644 index 0000000000..b9fe410b72 --- /dev/null +++ b/examples/resources/snowflake_primary_connection/resource.tf @@ -0,0 +1,13 @@ +## Minimal +resource "snowflake_primary_connection" "basic" { + name = "connection_name" +} + +## Complete (with every optional set) +resource "snowflake_primary_connection" "complete" { + name = "connection_name" + comment = "my complete connection" + enable_failover_to_accounts = [ + "." + ] +} diff --git a/examples/resources/snowflake_secondary_connection/import.sh b/examples/resources/snowflake_secondary_connection/import.sh new file mode 100644 index 0000000000..4de28135f7 --- /dev/null +++ b/examples/resources/snowflake_secondary_connection/import.sh @@ -0,0 +1 @@ +terraform import snowflake_secondary_connection.example 'secondary_connection_name' diff --git a/examples/resources/snowflake_secondary_connection/resource.tf b/examples/resources/snowflake_secondary_connection/resource.tf new file mode 100644 index 0000000000..17d32c0820 --- /dev/null +++ b/examples/resources/snowflake_secondary_connection/resource.tf @@ -0,0 +1,12 @@ +## Minimal +resource "snowflake_secondary_connection" "basic" { + name = "connection_name" + as_replica_of = ".." +} + +## Complete (with every optional set) +resource "snowflake_secondary_connection" "complete" { + name = "connection_name" + as_replica_of = ".." + comment = "my complete secondary connection" +} diff --git a/pkg/acceptance/bettertestspoc/assert/objectassert/connection_snowflake_ext.go b/pkg/acceptance/bettertestspoc/assert/objectassert/connection_snowflake_ext.go index 4e237798ef..3ab10cc0e9 100644 --- a/pkg/acceptance/bettertestspoc/assert/objectassert/connection_snowflake_ext.go +++ b/pkg/acceptance/bettertestspoc/assert/objectassert/connection_snowflake_ext.go @@ -3,13 +3,12 @@ package objectassert import ( "fmt" "slices" - "strings" "testing" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" ) -func (c *ConnectionAssert) HasFailoverAllowedToAccounts(expected []string) *ConnectionAssert { +func (c *ConnectionAssert) HasFailoverAllowedToAccounts(expected ...sdk.AccountIdentifier) *ConnectionAssert { c.AddAssertion(func(t *testing.T, o *sdk.Connection) error { t.Helper() if !slices.Equal(expected, o.FailoverAllowedToAccounts) { @@ -46,9 +45,8 @@ func (c *ConnectionAssert) HasConnectionUrlNotEmpty() *ConnectionAssert { func (c *ConnectionAssert) HasPrimaryIdentifier(expected sdk.ExternalObjectIdentifier) *ConnectionAssert { c.AddAssertion(func(t *testing.T, o *sdk.Connection) error { t.Helper() - expectedString := strings.ReplaceAll(expected.FullyQualifiedName(), `"`, "") - if o.Primary != expectedString { - return fmt.Errorf("expected primary identifier: %v; got: %v", expectedString, o.Primary) + if o.Primary != expected { + return fmt.Errorf("expected primary identifier: %v; got: %v", expected.FullyQualifiedName(), o.Primary) } return nil }) diff --git a/pkg/acceptance/bettertestspoc/assert/objectassert/connection_snowflake_gen.go b/pkg/acceptance/bettertestspoc/assert/objectassert/connection_snowflake_gen.go index d1c0946276..c6e8e2361e 100644 --- a/pkg/acceptance/bettertestspoc/assert/objectassert/connection_snowflake_gen.go +++ b/pkg/acceptance/bettertestspoc/assert/objectassert/connection_snowflake_gen.go @@ -31,6 +31,20 @@ func ConnectionFromObject(t *testing.T, connection *sdk.Connection) *ConnectionA } } +func (c *ConnectionAssert) HasRegionGroup(expected string) *ConnectionAssert { + c.AddAssertion(func(t *testing.T, o *sdk.Connection) error { + t.Helper() + if o.RegionGroup == nil { + return fmt.Errorf("expected region group to have value; got: nil") + } + if *o.RegionGroup != expected { + return fmt.Errorf("expected region group: %v; got: %v", expected, *o.RegionGroup) + } + return nil + }) + return c +} + func (c *ConnectionAssert) HasSnowflakeRegion(expected string) *ConnectionAssert { c.AddAssertion(func(t *testing.T, o *sdk.Connection) error { t.Helper() @@ -100,7 +114,7 @@ func (c *ConnectionAssert) HasIsPrimary(expected bool) *ConnectionAssert { return c } -func (c *ConnectionAssert) HasPrimary(expected string) *ConnectionAssert { +func (c *ConnectionAssert) HasPrimary(expected sdk.ExternalObjectIdentifier) *ConnectionAssert { c.AddAssertion(func(t *testing.T, o *sdk.Connection) error { t.Helper() if o.Primary != expected { @@ -111,6 +125,19 @@ func (c *ConnectionAssert) HasPrimary(expected string) *ConnectionAssert { return c } +/* +func (c *ConnectionAssert) HasFailoverAllowedToAccounts(expected []sdk.AccountIdentifier) *ConnectionAssert { + c.AddAssertion(func(t *testing.T, o *sdk.Connection) error { + t.Helper() + if o.FailoverAllowedToAccounts != expected { + return fmt.Errorf("expected failover allowed to accounts: %v; got: %v", expected, o.FailoverAllowedToAccounts) + } + return nil + }) + return c +} +*/ + func (c *ConnectionAssert) HasConnectionUrl(expected string) *ConnectionAssert { c.AddAssertion(func(t *testing.T, o *sdk.Connection) error { t.Helper() diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/gen/resource_schema_def.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/gen/resource_schema_def.go index 6eaa0cb8c8..692552496c 100644 --- a/pkg/acceptance/bettertestspoc/assert/resourceassert/gen/resource_schema_def.go +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/gen/resource_schema_def.go @@ -97,4 +97,12 @@ var allResourceSchemaDefs = []ResourceSchemaDef{ name: "StreamOnView", schema: resources.StreamOnView().Schema, }, + { + name: "PrimaryConnection", + schema: resources.PrimaryConnection().Schema, + }, + { + name: "SecondaryConnection", + schema: resources.SecondaryConnection().Schema, + }, } diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/primary_connection_resource_ext.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/primary_connection_resource_ext.go new file mode 100644 index 0000000000..b22dbfd4ee --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/primary_connection_resource_ext.go @@ -0,0 +1,21 @@ +package resourceassert + +import ( + "fmt" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" +) + +func (c *PrimaryConnectionResourceAssert) HasExactlyFailoverToAccountsInOrder(expected ...sdk.AccountIdentifier) *PrimaryConnectionResourceAssert { + c.AddAssertion(assert.ValueSet("enable_failover_to_accounts.#", fmt.Sprintf("%d", len(expected)))) + for i, v := range expected { + c.AddAssertion(assert.ValueSet(fmt.Sprintf("enable_failover_to_accounts.%d", i), v.Name())) + } + return c +} + +func (c *PrimaryConnectionResourceAssert) HasNoEnableFailoverToAccounts() *PrimaryConnectionResourceAssert { + c.AddAssertion(assert.ValueSet("enable_failover_to_accounts.#", "0")) + return c +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/primary_connection_resource_gen.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/primary_connection_resource_gen.go new file mode 100644 index 0000000000..9297a6408f --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/primary_connection_resource_gen.go @@ -0,0 +1,79 @@ +// Code generated by assertions generator; DO NOT EDIT. + +package resourceassert + +import ( + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" +) + +type PrimaryConnectionResourceAssert struct { + *assert.ResourceAssert +} + +func PrimaryConnectionResource(t *testing.T, name string) *PrimaryConnectionResourceAssert { + t.Helper() + + return &PrimaryConnectionResourceAssert{ + ResourceAssert: assert.NewResourceAssert(name, "resource"), + } +} + +func ImportedPrimaryConnectionResource(t *testing.T, id string) *PrimaryConnectionResourceAssert { + t.Helper() + + return &PrimaryConnectionResourceAssert{ + ResourceAssert: assert.NewImportedResourceAssert(id, "imported resource"), + } +} + +/////////////////////////////////// +// Attribute value string checks // +/////////////////////////////////// + +func (p *PrimaryConnectionResourceAssert) HasCommentString(expected string) *PrimaryConnectionResourceAssert { + p.AddAssertion(assert.ValueSet("comment", expected)) + return p +} + +func (p *PrimaryConnectionResourceAssert) HasEnableFailoverToAccountsString(expected string) *PrimaryConnectionResourceAssert { + p.AddAssertion(assert.ValueSet("enable_failover_to_accounts", expected)) + return p +} + +func (p *PrimaryConnectionResourceAssert) HasFullyQualifiedNameString(expected string) *PrimaryConnectionResourceAssert { + p.AddAssertion(assert.ValueSet("fully_qualified_name", expected)) + return p +} + +func (p *PrimaryConnectionResourceAssert) HasNameString(expected string) *PrimaryConnectionResourceAssert { + p.AddAssertion(assert.ValueSet("name", expected)) + return p +} + +//////////////////////////// +// Attribute empty checks // +//////////////////////////// + +func (p *PrimaryConnectionResourceAssert) HasNoComment() *PrimaryConnectionResourceAssert { + p.AddAssertion(assert.ValueNotSet("comment")) + return p +} + +/* +func (p *PrimaryConnectionResourceAssert) HasNoEnableFailoverToAccounts() *PrimaryConnectionResourceAssert { + p.AddAssertion(assert.ValueNotSet("enable_failover_to_accounts")) + return p +} +*/ + +func (p *PrimaryConnectionResourceAssert) HasNoFullyQualifiedName() *PrimaryConnectionResourceAssert { + p.AddAssertion(assert.ValueNotSet("fully_qualified_name")) + return p +} + +func (p *PrimaryConnectionResourceAssert) HasNoName() *PrimaryConnectionResourceAssert { + p.AddAssertion(assert.ValueNotSet("name")) + return p +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/secondary_connection_resource_ext.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/secondary_connection_resource_ext.go new file mode 100644 index 0000000000..4bf24106c7 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/secondary_connection_resource_ext.go @@ -0,0 +1,11 @@ +package resourceassert + +import ( + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" +) + +func (s *SecondaryConnectionResourceAssert) HasAsReplicaOfIdentifier(expected sdk.ExternalObjectIdentifier) *SecondaryConnectionResourceAssert { + s.AddAssertion(assert.ValueSet("as_replica_of", expected.Name())) + return s +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/secondary_connection_resource_gen.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/secondary_connection_resource_gen.go new file mode 100644 index 0000000000..363cd9c804 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/secondary_connection_resource_gen.go @@ -0,0 +1,87 @@ +// Code generated by assertions generator; DO NOT EDIT. + +package resourceassert + +import ( + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" +) + +type SecondaryConnectionResourceAssert struct { + *assert.ResourceAssert +} + +func SecondaryConnectionResource(t *testing.T, name string) *SecondaryConnectionResourceAssert { + t.Helper() + + return &SecondaryConnectionResourceAssert{ + ResourceAssert: assert.NewResourceAssert(name, "resource"), + } +} + +func ImportedSecondaryConnectionResource(t *testing.T, id string) *SecondaryConnectionResourceAssert { + t.Helper() + + return &SecondaryConnectionResourceAssert{ + ResourceAssert: assert.NewImportedResourceAssert(id, "imported resource"), + } +} + +/////////////////////////////////// +// Attribute value string checks // +/////////////////////////////////// + +func (s *SecondaryConnectionResourceAssert) HasAsReplicaOfString(expected string) *SecondaryConnectionResourceAssert { + s.AddAssertion(assert.ValueSet("as_replica_of", expected)) + return s +} + +func (s *SecondaryConnectionResourceAssert) HasCommentString(expected string) *SecondaryConnectionResourceAssert { + s.AddAssertion(assert.ValueSet("comment", expected)) + return s +} + +func (s *SecondaryConnectionResourceAssert) HasFullyQualifiedNameString(expected string) *SecondaryConnectionResourceAssert { + s.AddAssertion(assert.ValueSet("fully_qualified_name", expected)) + return s +} + +func (s *SecondaryConnectionResourceAssert) HasIsPrimaryString(expected string) *SecondaryConnectionResourceAssert { + s.AddAssertion(assert.ValueSet("is_primary", expected)) + return s +} + +func (s *SecondaryConnectionResourceAssert) HasNameString(expected string) *SecondaryConnectionResourceAssert { + s.AddAssertion(assert.ValueSet("name", expected)) + return s +} + +//////////////////////////// +// Attribute empty checks // +//////////////////////////// + +func (s *SecondaryConnectionResourceAssert) HasNoAsReplicaOf() *SecondaryConnectionResourceAssert { + s.AddAssertion(assert.ValueNotSet("as_replica_of")) + return s +} + +func (s *SecondaryConnectionResourceAssert) HasNoComment() *SecondaryConnectionResourceAssert { + s.AddAssertion(assert.ValueNotSet("comment")) + return s +} + +func (s *SecondaryConnectionResourceAssert) HasNoFullyQualifiedName() *SecondaryConnectionResourceAssert { + s.AddAssertion(assert.ValueNotSet("fully_qualified_name")) + return s +} + +func (s *SecondaryConnectionResourceAssert) HasNoIsPrimary() *SecondaryConnectionResourceAssert { + s.AddAssertion(assert.ValueNotSet("is_primary")) + return s +} + +func (s *SecondaryConnectionResourceAssert) HasNoName() *SecondaryConnectionResourceAssert { + s.AddAssertion(assert.ValueNotSet("name")) + return s +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/connection_show_output_ext.go b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/connection_show_output_ext.go new file mode 100644 index 0000000000..3bbf12c196 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/connection_show_output_ext.go @@ -0,0 +1,21 @@ +package resourceshowoutputassert + +import ( + "fmt" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" +) + +func (c *ConnectionShowOutputAssert) HasPrimaryIdentifier(expected sdk.ExternalObjectIdentifier) *ConnectionShowOutputAssert { + c.AddAssertion(assert.ResourceShowOutputValueSet("primary", expected.FullyQualifiedName())) + return c +} + +func (c *ConnectionShowOutputAssert) HasFailoverAllowedToAccounts(expected ...sdk.AccountIdentifier) *ConnectionShowOutputAssert { + c.AddAssertion(assert.ResourceShowOutputValueSet("failover_allowed_to_accounts.#", fmt.Sprintf("%d", len(expected)))) + for i, v := range expected { + c.AddAssertion(assert.ResourceShowOutputValueSet(fmt.Sprintf("failover_allowed_to_accounts.%d", i), v.Name())) + } + return c +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/connection_show_output_gen.go b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/connection_show_output_gen.go new file mode 100644 index 0000000000..d0e76f836a --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/connection_show_output_gen.go @@ -0,0 +1,104 @@ +// Code generated by assertions generator; DO NOT EDIT. + +package resourceshowoutputassert + +import ( + "testing" + "time" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" +) + +// to ensure sdk package is used +var _ = sdk.Object{} + +type ConnectionShowOutputAssert struct { + *assert.ResourceAssert +} + +func ConnectionShowOutput(t *testing.T, name string) *ConnectionShowOutputAssert { + t.Helper() + + c := ConnectionShowOutputAssert{ + ResourceAssert: assert.NewResourceAssert(name, "show_output"), + } + c.AddAssertion(assert.ValueSet("show_output.#", "1")) + return &c +} + +func ImportedConnectionShowOutput(t *testing.T, id string) *ConnectionShowOutputAssert { + t.Helper() + + c := ConnectionShowOutputAssert{ + ResourceAssert: assert.NewImportedResourceAssert(id, "show_output"), + } + c.AddAssertion(assert.ValueSet("show_output.#", "1")) + return &c +} + +//////////////////////////// +// Attribute value checks // +//////////////////////////// + +func (c *ConnectionShowOutputAssert) HasRegionGroup(expected string) *ConnectionShowOutputAssert { + c.AddAssertion(assert.ResourceShowOutputValueSet("region_group", expected)) + return c +} + +func (c *ConnectionShowOutputAssert) HasSnowflakeRegion(expected string) *ConnectionShowOutputAssert { + c.AddAssertion(assert.ResourceShowOutputValueSet("snowflake_region", expected)) + return c +} + +func (c *ConnectionShowOutputAssert) HasCreatedOn(expected time.Time) *ConnectionShowOutputAssert { + c.AddAssertion(assert.ResourceShowOutputValueSet("created_on", expected.String())) + return c +} + +func (c *ConnectionShowOutputAssert) HasAccountName(expected string) *ConnectionShowOutputAssert { + c.AddAssertion(assert.ResourceShowOutputValueSet("account_name", expected)) + return c +} + +func (c *ConnectionShowOutputAssert) HasName(expected string) *ConnectionShowOutputAssert { + c.AddAssertion(assert.ResourceShowOutputValueSet("name", expected)) + return c +} + +func (c *ConnectionShowOutputAssert) HasComment(expected string) *ConnectionShowOutputAssert { + c.AddAssertion(assert.ResourceShowOutputValueSet("comment", expected)) + return c +} + +func (c *ConnectionShowOutputAssert) HasIsPrimary(expected bool) *ConnectionShowOutputAssert { + c.AddAssertion(assert.ResourceShowOutputBoolValueSet("is_primary", expected)) + return c +} + +func (c *ConnectionShowOutputAssert) HasPrimary(expected string) *ConnectionShowOutputAssert { + c.AddAssertion(assert.ResourceShowOutputValueSet("primary", expected)) + return c +} + +/* +func (c *ConnectionShowOutputAssert) HasFailoverAllowedToAccounts(expected []string) *ConnectionShowOutputAssert { + c.AddAssertion(assert.ResourceShowOutputValueSet("failover_allowed_to_accounts", expected)) + return c +} +*/ + +func (c *ConnectionShowOutputAssert) HasConnectionUrl(expected string) *ConnectionShowOutputAssert { + c.AddAssertion(assert.ResourceShowOutputValueSet("connection_url", expected)) + return c +} + +func (c *ConnectionShowOutputAssert) HasOrganizationName(expected string) *ConnectionShowOutputAssert { + c.AddAssertion(assert.ResourceShowOutputValueSet("organization_name", expected)) + return c +} + +func (c *ConnectionShowOutputAssert) HasAccountLocator(expected string) *ConnectionShowOutputAssert { + c.AddAssertion(assert.ResourceShowOutputValueSet("account_locator", expected)) + return c +} diff --git a/pkg/acceptance/bettertestspoc/config/model/primary_connection_model_ext.go b/pkg/acceptance/bettertestspoc/config/model/primary_connection_model_ext.go new file mode 100644 index 0000000000..0e7da1b1dc --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/model/primary_connection_model_ext.go @@ -0,0 +1,17 @@ +package model + +import ( + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-testing/config" +) + +func (c *PrimaryConnectionModel) WithEnableFailover(toAccount ...sdk.AccountIdentifier) *PrimaryConnectionModel { + variables := make([]config.Variable, 0) + for _, v := range toAccount { + variables = append(variables, config.StringVariable(v.Name())) + } + + c.EnableFailoverToAccounts = config.ListVariable(variables...) + + return c +} diff --git a/pkg/acceptance/bettertestspoc/config/model/primary_connection_model_gen.go b/pkg/acceptance/bettertestspoc/config/model/primary_connection_model_gen.go new file mode 100644 index 0000000000..f8f29bf1cf --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/model/primary_connection_model_gen.go @@ -0,0 +1,85 @@ +// Code generated by config model builder generator; DO NOT EDIT. + +package model + +import ( + tfconfig "github.com/hashicorp/terraform-plugin-testing/config" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" +) + +type PrimaryConnectionModel struct { + Comment tfconfig.Variable `json:"comment,omitempty"` + EnableFailoverToAccounts tfconfig.Variable `json:"enable_failover_to_accounts,omitempty"` + FullyQualifiedName tfconfig.Variable `json:"fully_qualified_name,omitempty"` + Name tfconfig.Variable `json:"name,omitempty"` + + *config.ResourceModelMeta +} + +///////////////////////////////////////////////// +// Basic builders (resource name and required) // +///////////////////////////////////////////////// + +func PrimaryConnection( + resourceName string, + name string, +) *PrimaryConnectionModel { + p := &PrimaryConnectionModel{ResourceModelMeta: config.Meta(resourceName, resources.PrimaryConnection)} + p.WithName(name) + return p +} + +func PrimaryConnectionWithDefaultMeta( + name string, +) *PrimaryConnectionModel { + p := &PrimaryConnectionModel{ResourceModelMeta: config.DefaultMeta(resources.PrimaryConnection)} + p.WithName(name) + return p +} + +///////////////////////////////// +// below all the proper values // +///////////////////////////////// + +func (p *PrimaryConnectionModel) WithComment(comment string) *PrimaryConnectionModel { + p.Comment = tfconfig.StringVariable(comment) + return p +} + +// enable_failover_to_accounts attribute type is not yet supported, so WithEnableFailoverToAccounts can't be generated + +func (p *PrimaryConnectionModel) WithFullyQualifiedName(fullyQualifiedName string) *PrimaryConnectionModel { + p.FullyQualifiedName = tfconfig.StringVariable(fullyQualifiedName) + return p +} + +func (p *PrimaryConnectionModel) WithName(name string) *PrimaryConnectionModel { + p.Name = tfconfig.StringVariable(name) + return p +} + +////////////////////////////////////////// +// below it's possible to set any value // +////////////////////////////////////////// + +func (p *PrimaryConnectionModel) WithCommentValue(value tfconfig.Variable) *PrimaryConnectionModel { + p.Comment = value + return p +} + +func (p *PrimaryConnectionModel) WithEnableFailoverToAccountsValue(value tfconfig.Variable) *PrimaryConnectionModel { + p.EnableFailoverToAccounts = value + return p +} + +func (p *PrimaryConnectionModel) WithFullyQualifiedNameValue(value tfconfig.Variable) *PrimaryConnectionModel { + p.FullyQualifiedName = value + return p +} + +func (p *PrimaryConnectionModel) WithNameValue(value tfconfig.Variable) *PrimaryConnectionModel { + p.Name = value + return p +} diff --git a/pkg/acceptance/bettertestspoc/config/model/secondary_connection_model_gen.go b/pkg/acceptance/bettertestspoc/config/model/secondary_connection_model_gen.go new file mode 100644 index 0000000000..f1ee4f7024 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/model/secondary_connection_model_gen.go @@ -0,0 +1,103 @@ +// Code generated by config model builder generator; DO NOT EDIT. + +package model + +import ( + tfconfig "github.com/hashicorp/terraform-plugin-testing/config" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" +) + +type SecondaryConnectionModel struct { + AsReplicaOf tfconfig.Variable `json:"as_replica_of,omitempty"` + Comment tfconfig.Variable `json:"comment,omitempty"` + FullyQualifiedName tfconfig.Variable `json:"fully_qualified_name,omitempty"` + IsPrimary tfconfig.Variable `json:"is_primary,omitempty"` + Name tfconfig.Variable `json:"name,omitempty"` + + *config.ResourceModelMeta +} + +///////////////////////////////////////////////// +// Basic builders (resource name and required) // +///////////////////////////////////////////////// + +func SecondaryConnection( + resourceName string, + asReplicaOf string, + name string, +) *SecondaryConnectionModel { + s := &SecondaryConnectionModel{ResourceModelMeta: config.Meta(resourceName, resources.SecondaryConnection)} + s.WithAsReplicaOf(asReplicaOf) + s.WithName(name) + return s +} + +func SecondaryConnectionWithDefaultMeta( + asReplicaOf string, + name string, +) *SecondaryConnectionModel { + s := &SecondaryConnectionModel{ResourceModelMeta: config.DefaultMeta(resources.SecondaryConnection)} + s.WithAsReplicaOf(asReplicaOf) + s.WithName(name) + return s +} + +///////////////////////////////// +// below all the proper values // +///////////////////////////////// + +func (s *SecondaryConnectionModel) WithAsReplicaOf(asReplicaOf string) *SecondaryConnectionModel { + s.AsReplicaOf = tfconfig.StringVariable(asReplicaOf) + return s +} + +func (s *SecondaryConnectionModel) WithComment(comment string) *SecondaryConnectionModel { + s.Comment = tfconfig.StringVariable(comment) + return s +} + +func (s *SecondaryConnectionModel) WithFullyQualifiedName(fullyQualifiedName string) *SecondaryConnectionModel { + s.FullyQualifiedName = tfconfig.StringVariable(fullyQualifiedName) + return s +} + +func (s *SecondaryConnectionModel) WithIsPrimary(isPrimary bool) *SecondaryConnectionModel { + s.IsPrimary = tfconfig.BoolVariable(isPrimary) + return s +} + +func (s *SecondaryConnectionModel) WithName(name string) *SecondaryConnectionModel { + s.Name = tfconfig.StringVariable(name) + return s +} + +////////////////////////////////////////// +// below it's possible to set any value // +////////////////////////////////////////// + +func (s *SecondaryConnectionModel) WithAsReplicaOfValue(value tfconfig.Variable) *SecondaryConnectionModel { + s.AsReplicaOf = value + return s +} + +func (s *SecondaryConnectionModel) WithCommentValue(value tfconfig.Variable) *SecondaryConnectionModel { + s.Comment = value + return s +} + +func (s *SecondaryConnectionModel) WithFullyQualifiedNameValue(value tfconfig.Variable) *SecondaryConnectionModel { + s.FullyQualifiedName = value + return s +} + +func (s *SecondaryConnectionModel) WithIsPrimaryValue(value tfconfig.Variable) *SecondaryConnectionModel { + s.IsPrimary = value + return s +} + +func (s *SecondaryConnectionModel) WithNameValue(value tfconfig.Variable) *SecondaryConnectionModel { + s.Name = value + return s +} diff --git a/pkg/acceptance/check_destroy.go b/pkg/acceptance/check_destroy.go index f9fd627104..57145b726f 100644 --- a/pkg/acceptance/check_destroy.go +++ b/pkg/acceptance/check_destroy.go @@ -102,6 +102,9 @@ var showByIdFunctions = map[resources.Resource]showByIdFunc{ resources.AuthenticationPolicy: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error { return runShowById(ctx, id, client.AuthenticationPolicies.ShowByID) }, + resources.PrimaryConnection: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error { + return runShowById(ctx, id, client.Connections.ShowByID) + }, resources.CortexSearchService: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error { return runShowById(ctx, id, client.CortexSearchServices.ShowByID) }, @@ -195,6 +198,9 @@ var showByIdFunctions = map[resources.Resource]showByIdFunc{ resources.ScimSecurityIntegration: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error { return runShowById(ctx, id, client.SecurityIntegrations.ShowByID) }, + resources.SecondaryConnection: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error { + return runShowById(ctx, id, client.Connections.ShowByID) + }, resources.SecondaryDatabase: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error { return runShowById(ctx, id, client.Databases.ShowByID) }, diff --git a/pkg/acceptance/helpers/connection_client.go b/pkg/acceptance/helpers/connection_client.go index 24fdf9384a..72d37924b5 100644 --- a/pkg/acceptance/helpers/connection_client.go +++ b/pkg/acceptance/helpers/connection_client.go @@ -2,6 +2,8 @@ package helpers import ( "context" + "fmt" + "strings" "testing" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" @@ -46,7 +48,7 @@ func (c *ConnectionClient) CreateReplication(t *testing.T, id sdk.AccountObjectI return connection, c.DropFunc(t, id) } -func (c *ConnectionClient) Alter(t *testing.T, id sdk.AccountObjectIdentifier, req *sdk.AlterConnectionRequest) { +func (c *ConnectionClient) Alter(t *testing.T, req *sdk.AlterConnectionRequest) { t.Helper() ctx := context.Background() @@ -70,3 +72,7 @@ func (c *ConnectionClient) Show(t *testing.T, id sdk.AccountObjectIdentifier) (* return c.client().ShowByID(ctx, id) } + +func (c *ConnectionClient) GetConnectionUrl(organizationName, objectName string) string { + return strings.ToLower(fmt.Sprintf("%s-%s.snowflakecomputing.com", organizationName, objectName)) +} diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 922e8b3f72..0721189115 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -465,6 +465,7 @@ func getResources() map[string]*schema.Resource { "snowflake_object_parameter": resources.ObjectParameter(), "snowflake_password_policy": resources.PasswordPolicy(), "snowflake_pipe": resources.Pipe(), + "snowflake_primary_connection": resources.PrimaryConnection(), "snowflake_procedure": resources.Procedure(), "snowflake_resource_monitor": resources.ResourceMonitor(), "snowflake_role": resources.Role(), @@ -473,6 +474,7 @@ func getResources() map[string]*schema.Resource { "snowflake_saml2_integration": resources.SAML2Integration(), "snowflake_schema": resources.Schema(), "snowflake_scim_integration": resources.SCIMIntegration(), + "snowflake_secondary_connection": resources.SecondaryConnection(), "snowflake_secondary_database": resources.SecondaryDatabase(), "snowflake_secret_with_authorization_code_grant": resources.SecretWithAuthorizationCodeGrant(), "snowflake_secret_with_basic_authentication": resources.SecretWithBasicAuthentication(), diff --git a/pkg/provider/resources/resources.go b/pkg/provider/resources/resources.go index 2076ccbe21..9437469ef9 100644 --- a/pkg/provider/resources/resources.go +++ b/pkg/provider/resources/resources.go @@ -35,6 +35,7 @@ const ( OauthIntegrationForPartnerApplications resource = "snowflake_oauth_integration_for_partner_applications" PasswordPolicy resource = "snowflake_password_policy" Pipe resource = "snowflake_pipe" + PrimaryConnection resource = "snowflake_primary_connection" Procedure resource = "snowflake_procedure" ResourceMonitor resource = "snowflake_resource_monitor" Role resource = "snowflake_role" @@ -42,6 +43,7 @@ const ( Saml2SecurityIntegration resource = "snowflake_saml2_integration" Schema resource = "snowflake_schema" ScimSecurityIntegration resource = "snowflake_scim_integration" + SecondaryConnection resource = "snowflake_secondary_connection" SecondaryDatabase resource = "snowflake_secondary_database" SecretWithAuthorizationCodeGrant resource = "snowflake_secret_with_authorization_code_grant" SecretWithBasicAuthentication resource = "snowflake_secret_with_basic_authentication" diff --git a/pkg/resources/custom_diffs.go b/pkg/resources/custom_diffs.go index 5d2ac7de3c..e4cfe35eae 100644 --- a/pkg/resources/custom_diffs.go +++ b/pkg/resources/custom_diffs.go @@ -270,3 +270,15 @@ func RecreateWhenStreamIsStale() schema.CustomizeDiffFunc { return nil } } + +// TODO: [SNOW-1763442] unable to test now, as there is no test accounts with different regions +// RecreateWhenSecondaryConnectionChangedExternally detects if the secondary connection was promoted externally to serve as primary. +// If so, it sets the `is_primary` field to `false` which is our desired value for secondary_connection +func RecreateWhenSecondaryConnectionPromotedExternally() schema.CustomizeDiffFunc { + return func(_ context.Context, diff *schema.ResourceDiff, _ any) error { + if _, newValue := diff.GetChange("is_primary"); newValue.(bool) { + return diff.SetNew("is_primary", false) + } + return nil + } +} diff --git a/pkg/resources/custom_diffs_test.go b/pkg/resources/custom_diffs_test.go index 8942eca24c..50949578ff 100644 --- a/pkg/resources/custom_diffs_test.go +++ b/pkg/resources/custom_diffs_test.go @@ -2,11 +2,10 @@ package resources_test import ( "context" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "strings" "testing" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/resources" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" @@ -1082,3 +1081,40 @@ func Test_RecreateWhenSecretTypeChangedExternallyForOAuth2(t *testing.T) { }) } } + +func Test_RecreateWhenSecondaryConnectionChangedExternally(t *testing.T) { + tests := []struct { + name string + expectedIsPrimary string + stateValue map[string]string + }{ + { + name: "changed from is_primary from false to true", + expectedIsPrimary: "false", + stateValue: map[string]string{ + "is_primary": "true", + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + customDiff := resources.RecreateWhenSecondaryConnectionPromotedExternally() + testProvider := createProviderWithCustomSchemaAndCustomDiff(t, + map[string]*schema.Schema{ + "is_primary": { + Type: schema.TypeBool, + Computed: true, + }, + }, + customDiff) + diff := calculateDiffFromAttributes( + t, + testProvider, + tt.stateValue, + map[string]any{}, + ) + assert.Equal(t, tt.expectedIsPrimary, diff.Attributes["is_primary"].New) + }) + } +} diff --git a/pkg/resources/primary_connection.go b/pkg/resources/primary_connection.go new file mode 100644 index 0000000000..ca55ce1af0 --- /dev/null +++ b/pkg/resources/primary_connection.go @@ -0,0 +1,265 @@ +package resources + +import ( + "context" + "errors" + "fmt" + + "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/schemas" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "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" +) + +var primaryConnectionSchema = map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: blocklistedCharactersFieldDescription("String that specifies the identifier (i.e. name) for the connection. Must start with an alphabetic character and may only contain letters, decimal digits (0-9), and underscores (_). For a primary connection, the name must be unique across connection names and account names in the organization. "), + DiffSuppressFunc: suppressIdentifierQuoting, + }, + "enable_failover_to_accounts": { + Type: schema.TypeList, + Optional: true, + Description: "Enables failover for given connection to provided accounts. Specifies a list of accounts in your organization where a secondary connection for this primary connection can be promoted to serve as the primary connection. Include your organization name for each account in the list.", + Elem: &schema.Schema{ + Type: schema.TypeString, + DiffSuppressFunc: suppressIdentifierQuoting, + }, + }, + "comment": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies a comment for the connection.", + }, + ShowOutputAttributeName: { + Type: schema.TypeList, + Computed: true, + Description: "Outputs the result of `SHOW CONNECTIONS` for the given connection.", + Elem: &schema.Resource{ + Schema: schemas.ShowConnectionSchema, + }, + }, + FullyQualifiedNameAttributeName: schemas.FullyQualifiedNameSchema, +} + +func PrimaryConnection() *schema.Resource { + return &schema.Resource{ + CreateContext: CreateContextPrimaryConnection, + ReadContext: ReadContextPrimaryConnection, + UpdateContext: UpdateContextPrimaryConnection, + DeleteContext: DeleteContextPrimaryConnection, + + CustomizeDiff: customdiff.All( + ComputedIfAnyAttributeChanged(primaryConnectionSchema, ShowOutputAttributeName, "comment", "failover_allowed_to_accounts"), + ), + + Description: "Resource used to manage primary (not replicated) connections. For more information, check [connection documentation](https://docs.snowflake.com/en/sql-reference/sql/create-connection.html).", + Schema: primaryConnectionSchema, + Importer: &schema.ResourceImporter{ + StateContext: ImportName[sdk.AccountObjectIdentifier], + }, + } +} + +func CreateContextPrimaryConnection(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + + id, err := sdk.ParseAccountObjectIdentifier(d.Get("name").(string)) + if err != nil { + return diag.FromErr(err) + } + + request := sdk.NewCreateConnectionRequest(id) + + if v, ok := d.GetOk("comment"); ok { + request.WithComment(v.(string)) + } + + err = client.Connections.Create(ctx, request) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(helpers.EncodeResourceIdentifier(id)) + + if v, ok := d.GetOk("enable_failover_to_accounts"); ok { + enableFailoverConfig := v.([]any) + + enableFailoverToAccountsList := make([]sdk.AccountIdentifier, 0) + for _, enableToAccount := range enableFailoverConfig { + accountInConfig := enableToAccount.(string) + accountIdentifier := sdk.NewAccountIdentifierFromFullyQualifiedName(accountInConfig) + + enableFailoverToAccountsList = append(enableFailoverToAccountsList, accountIdentifier) + } + + err := client.Connections.Alter(ctx, sdk.NewAlterConnectionRequest(id). + WithEnableConnectionFailover(*sdk.NewEnableConnectionFailoverRequest(enableFailoverToAccountsList))) + if err != nil { + return diag.FromErr(err) + } + } + + return ReadContextPrimaryConnection(ctx, d, meta) +} + +func ReadContextPrimaryConnection(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + id, err := sdk.ParseAccountObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + connection, err := client.Connections.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 retrieve connection. Target object not found. Marking the resource as removed.", + Detail: fmt.Sprintf("Connection name: %s, Err: %s", id.FullyQualifiedName(), err), + }, + } + } + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to retrieve connection.", + Detail: fmt.Sprintf("Connection name: %s, Err: %s", id.FullyQualifiedName(), err), + }, + } + } + + errs := errors.Join( + d.Set(FullyQualifiedNameAttributeName, id.FullyQualifiedName()), + d.Set(ShowOutputAttributeName, []map[string]any{schemas.ConnectionToSchema(connection)}), + d.Set("comment", connection.Comment), + ) + if errs != nil { + return diag.FromErr(errs) + } + + sessionDetails, err := client.ContextFunctions.CurrentSessionDetails(ctx) + if err != nil { + return diag.FromErr(err) + } + currentAccountIdentifier := sdk.NewAccountIdentifier(sessionDetails.OrganizationName, sessionDetails.AccountName) + + enableFailoverToAccounts := make([]string, 0) + for _, allowedAccount := range connection.FailoverAllowedToAccounts { + if currentAccountIdentifier.FullyQualifiedName() == allowedAccount.FullyQualifiedName() { + continue + } + enableFailoverToAccounts = append(enableFailoverToAccounts, allowedAccount.Name()) + } + + if len(enableFailoverToAccounts) == 0 { + err := d.Set("enable_failover_to_accounts", []any{}) + if err != nil { + return diag.FromErr(err) + } + } else { + err := d.Set("enable_failover_to_accounts", enableFailoverToAccounts) + if err != nil { + return diag.FromErr(err) + } + } + + return nil +} + +func UpdateContextPrimaryConnection(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + id, err := sdk.ParseAccountObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + connectionSetRequest := new(sdk.ConnectionSetRequest) + connectionUnsetRequest := new(sdk.ConnectionUnsetRequest) + + if d.HasChange("enable_failover_to_accounts") { + before, after := d.GetChange("enable_failover_to_accounts") + + getFailoverToAccounts := func(failoverConfig []any) []sdk.AccountIdentifier { + failoverEnabledToAccounts := make([]sdk.AccountIdentifier, 0) + + for _, allowedAccount := range failoverConfig { + accountIdentifier := sdk.NewAccountIdentifierFromFullyQualifiedName(allowedAccount.(string)) + failoverEnabledToAccounts = append(failoverEnabledToAccounts, accountIdentifier) + } + return failoverEnabledToAccounts + } + + beforeFailover := getFailoverToAccounts(before.([]any)) + afterFailover := getFailoverToAccounts(after.([]any)) + + addedFailovers, removedFailovers := ListDiff(beforeFailover, afterFailover) + + if len(addedFailovers) > 0 { + err := client.Connections.Alter(ctx, sdk.NewAlterConnectionRequest(id). + WithEnableConnectionFailover(*sdk.NewEnableConnectionFailoverRequest(addedFailovers)), + ) + if err != nil { + return diag.FromErr(err) + } + } + + if len(removedFailovers) > 0 { + err := client.Connections.Alter(ctx, sdk.NewAlterConnectionRequest(id). + WithDisableConnectionFailover(*sdk.NewDisableConnectionFailoverRequest(). + WithToAccounts(*sdk.NewToAccountsRequest(removedFailovers)), + ), + ) + if err != nil { + return diag.FromErr(err) + } + } + } + + if d.HasChange("comment") { + comment := d.Get("comment").(string) + if len(comment) > 0 { + connectionSetRequest.WithComment(comment) + } else { + connectionUnsetRequest.WithComment(true) + } + } + + if (*connectionSetRequest != sdk.ConnectionSetRequest{}) { + err := client.Connections.Alter(ctx, sdk.NewAlterConnectionRequest(id).WithSet(*connectionSetRequest)) + if err != nil { + return diag.FromErr(err) + } + } + + if (*connectionUnsetRequest != sdk.ConnectionUnsetRequest{}) { + err := client.Connections.Alter(ctx, sdk.NewAlterConnectionRequest(id).WithUnset(*connectionUnsetRequest)) + if err != nil { + return diag.FromErr(err) + } + } + + return ReadContextPrimaryConnection(ctx, d, meta) +} + +func DeleteContextPrimaryConnection(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + id, err := sdk.ParseAccountObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } + err = client.Connections.Drop(ctx, sdk.NewDropConnectionRequest(id).WithIfExists(true)) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + return nil +} diff --git a/pkg/resources/primary_connection_acceptance_test.go b/pkg/resources/primary_connection_acceptance_test.go new file mode 100644 index 0000000000..6273621c9b --- /dev/null +++ b/pkg/resources/primary_connection_acceptance_test.go @@ -0,0 +1,277 @@ +package resources_test + +import ( + "fmt" + "testing" + + acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert/resourceassert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config/model" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/helpers/random" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/importchecks" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/planchecks" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/testenvs" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + tfjson "github.com/hashicorp/terraform-json" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestAcc_PrimaryConnection_Basic(t *testing.T) { + // TODO: [SNOW-1002023]: Unskip; Business Critical Snowflake Edition needed + _ = testenvs.GetOrSkipTest(t, testenvs.TestFailoverGroups) + + id := acc.TestClient().Ids.RandomAccountObjectIdentifier() + comment := random.Comment() + + accountId := acc.TestClient().Account.GetAccountIdentifier(t) + secondaryAccountId := acc.SecondaryTestClient().Account.GetAccountIdentifier(t) + primaryConnectionAsExternalId := sdk.NewExternalObjectIdentifier(accountId, id) + + connectionModel := model.PrimaryConnection("t", id.Name()) + connectionModelWithComment := model.PrimaryConnection("t", id.Name()).WithComment(comment) + connectionModelWithFailover := model.PrimaryConnection("t", id.Name()).WithEnableFailover(secondaryAccountId) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.PrimaryConnection), + Steps: []resource.TestStep{ + // create + { + Config: config.FromModel(t, connectionModel), + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.PrimaryConnectionResource(t, connectionModel.ResourceReference()). + HasNameString(id.Name()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasNoEnableFailoverToAccounts(). + HasCommentString(""), + + resourceshowoutputassert.ConnectionShowOutput(t, connectionModel.ResourceReference()). + HasName(id.Name()). + HasSnowflakeRegion(acc.TestClient().Context.CurrentRegion(t)). + HasAccountLocator(acc.TestClient().GetAccountLocator()). + HasAccountName(accountId.AccountName()). + HasOrganizationName(accountId.OrganizationName()). + HasComment(""). + HasIsPrimary(true). + HasPrimaryIdentifier(primaryConnectionAsExternalId). + HasFailoverAllowedToAccounts(accountId). + HasConnectionUrl( + acc.TestClient().Connection.GetConnectionUrl(accountId.OrganizationName(), id.Name()), + ), + ), + ), + }, + // set comment + { + Config: config.FromModel(t, connectionModelWithComment), + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.PrimaryConnectionResource(t, connectionModelWithComment.ResourceReference()). + HasNameString(id.Name()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasNoEnableFailoverToAccounts(). + HasCommentString(comment), + + resourceshowoutputassert.ConnectionShowOutput(t, connectionModelWithComment.ResourceReference()). + HasComment(comment), + ), + ), + }, + // import + { + ResourceName: connectionModelWithComment.ResourceReference(), + ImportState: true, + ImportStateVerify: true, + ImportStateCheck: importchecks.ComposeImportStateCheck( + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "name", id.Name()), + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "comment", comment), + ), + }, + // unset comment + { + Config: config.FromModel(t, connectionModel), + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.PrimaryConnectionResource(t, connectionModel.ResourceReference()). + HasCommentString(""), + + resourceshowoutputassert.ConnectionShowOutput(t, connectionModel.ResourceReference()). + HasComment(""), + ), + ), + }, + // enable failover to second account + { + Config: config.FromModel(t, connectionModelWithFailover), + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.PrimaryConnectionResource(t, connectionModelWithFailover.ResourceReference()). + HasNameString(id.Name()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasExactlyFailoverToAccountsInOrder(secondaryAccountId). + HasCommentString(""), + + resourceshowoutputassert.ConnectionShowOutput(t, connectionModelWithFailover.ResourceReference()). + HasFailoverAllowedToAccounts(accountId, secondaryAccountId), + ), + ), + }, + // disable failover to second account + { + Config: config.FromModel(t, connectionModel), + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.PrimaryConnectionResource(t, connectionModel.ResourceReference()). + HasNameString(id.Name()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasNoEnableFailoverToAccounts(). + HasCommentString(""), + + resourceshowoutputassert.ConnectionShowOutput(t, connectionModel.ResourceReference()). + HasFailoverAllowedToAccounts(accountId), + ), + ), + }, + }, + }) +} + +func TestAcc_PrimaryConnection_ExternalChanges(t *testing.T) { + // TODO: [SNOW-1002023]: Unskip; Business Critical Snowflake Edition needed + _ = testenvs.GetOrSkipTest(t, testenvs.TestFailoverGroups) + + id := acc.TestClient().Ids.RandomAccountObjectIdentifier() + accountId := acc.TestClient().Account.GetAccountIdentifier(t) + secondaryAccountId := acc.SecondaryTestClient().Account.GetAccountIdentifier(t) + primaryConnectionAsExternalId := sdk.NewExternalObjectIdentifier(accountId, id) + + connectionModel := model.PrimaryConnection("t", id.Name()).WithComment("config comment") + connectionModelWithFailover := model.PrimaryConnection("t", id.Name()).WithEnableFailover(secondaryAccountId) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.PrimaryConnection), + Steps: []resource.TestStep{ + // create + { + Config: config.FromModel(t, connectionModel), + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.PrimaryConnectionResource(t, connectionModel.ResourceReference()). + HasNameString(id.Name()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasNoEnableFailoverToAccounts(). + HasCommentString("config comment"), + + resourceshowoutputassert.ConnectionShowOutput(t, connectionModel.ResourceReference()). + HasName(id.Name()). + HasSnowflakeRegion(acc.TestClient().Context.CurrentRegion(t)). + HasAccountLocator(acc.TestClient().GetAccountLocator()). + HasAccountName(accountId.AccountName()). + HasOrganizationName(accountId.OrganizationName()). + HasComment("config comment"). + HasIsPrimary(true). + HasPrimaryIdentifier(primaryConnectionAsExternalId). + HasFailoverAllowedToAccounts(accountId), + ), + ), + }, + // change comment externally + { + PreConfig: func() { + acc.TestClient().Connection.Alter(t, sdk.NewAlterConnectionRequest(id). + WithSet(*sdk.NewConnectionSetRequest(). + WithComment("external comment"))) + }, + Config: config.FromModel(t, connectionModel), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(connectionModel.ResourceReference(), plancheck.ResourceActionUpdate), + planchecks.ExpectChange(connectionModel.ResourceReference(), "comment", tfjson.ActionUpdate, sdk.String("external comment"), sdk.String("config comment")), + }, + }, + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.PrimaryConnectionResource(t, connectionModel.ResourceReference()). + HasCommentString("config comment"), + resourceshowoutputassert.ConnectionShowOutput(t, connectionModel.ResourceReference()). + HasComment("config comment"), + ), + ), + }, + // externally change enable failover accounts list - add second account + { + PreConfig: func() { + acc.TestClient().Connection.Alter(t, sdk.NewAlterConnectionRequest(id). + WithEnableConnectionFailover(*sdk.NewEnableConnectionFailoverRequest( + []sdk.AccountIdentifier{secondaryAccountId})), + ) + }, + Config: config.FromModel(t, connectionModel), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(connectionModel.ResourceReference(), plancheck.ResourceActionUpdate), + planchecks.ExpectChange( + connectionModel.ResourceReference(), + "enable_failover_to_accounts", + tfjson.ActionUpdate, + sdk.String(fmt.Sprintf("[%s]", secondaryAccountId.FullyQualifiedName())), + nil, + ), + }, + }, + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.PrimaryConnectionResource(t, connectionModel.ResourceReference()). + HasNoEnableFailoverToAccounts(), + resourceshowoutputassert.ConnectionShowOutput(t, connectionModel.ResourceReference()). + HasFailoverAllowedToAccounts(accountId), + ), + ), + }, + // externally change disable failover + { + PreConfig: func() { + acc.TestClient().Connection.Alter(t, sdk.NewAlterConnectionRequest(id).WithDisableConnectionFailover(*sdk.NewDisableConnectionFailoverRequest())) + }, + Config: config.FromModel(t, connectionModelWithFailover), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(connectionModelWithFailover.ResourceReference(), plancheck.ResourceActionUpdate), + planchecks.ExpectChange( + connectionModel.ResourceReference(), + "enable_failover_to_accounts", + tfjson.ActionUpdate, + nil, + sdk.String(fmt.Sprintf("[%s]", secondaryAccountId.FullyQualifiedName())), + ), + }, + }, + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.PrimaryConnectionResource(t, connectionModelWithFailover.ResourceReference()). + HasExactlyFailoverToAccountsInOrder(secondaryAccountId), + resourceshowoutputassert.ConnectionShowOutput(t, connectionModelWithFailover.ResourceReference()). + HasFailoverAllowedToAccounts(accountId, secondaryAccountId), + ), + ), + }, + }, + }) +} diff --git a/pkg/resources/secondary_connection.go b/pkg/resources/secondary_connection.go new file mode 100644 index 0000000000..711a6074ec --- /dev/null +++ b/pkg/resources/secondary_connection.go @@ -0,0 +1,191 @@ +package resources + +import ( + "context" + "errors" + "fmt" + + "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/schemas" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "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" +) + +var secondaryConnectionSchema = map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: blocklistedCharactersFieldDescription("String that specifies the identifier (i.e. name) for the connection. Must start with an alphabetic character and may only contain letters, decimal digits (0-9), and underscores (_). For a secondary connection, the name must match the name of its primary connection."), + DiffSuppressFunc: suppressIdentifierQuoting, + }, + "is_primary": { + Type: schema.TypeBool, + Computed: true, + Description: "Indicates if the connection has been changed to primary. If change is detected, the secondary connection will be recreated.", + }, + "as_replica_of": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Specifies the identifier for a primary connection from which to create a replica (i.e. a secondary connection).", + DiffSuppressFunc: suppressIdentifierQuoting, + }, + "comment": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies a comment for the secondary connection.", + }, + ShowOutputAttributeName: { + Type: schema.TypeList, + Computed: true, + Description: "Outputs the result of `SHOW CONNECTIONS` for the given connection.", + Elem: &schema.Resource{ + Schema: schemas.ShowConnectionSchema, + }, + }, + FullyQualifiedNameAttributeName: schemas.FullyQualifiedNameSchema, +} + +func SecondaryConnection() *schema.Resource { + return &schema.Resource{ + CreateContext: CreateContextSecondaryConnection, + ReadContext: ReadContextSecondaryConnection, + UpdateContext: UpdateContextSecondaryConnection, + DeleteContext: DeleteContextSecondaryConnection, + Description: "Resource used to manage secondary connections. To promote secondary connection to primary check [migraton guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#connection-resources). For more information, check [connection documentation](https://docs.snowflake.com/en/sql-reference/sql/create-connection.html).", + + CustomizeDiff: customdiff.All( + ComputedIfAnyAttributeChanged(secondaryConnectionSchema, ShowOutputAttributeName, "comment", "is_primary", "failover_allowed_to_accounts"), + RecreateWhenSecondaryConnectionPromotedExternally(), + ), + + Schema: secondaryConnectionSchema, + Importer: &schema.ResourceImporter{ + StateContext: ImportName[sdk.AccountObjectIdentifier], + }, + } +} + +func CreateContextSecondaryConnection(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + + id, err := sdk.ParseAccountObjectIdentifier(d.Get("name").(string)) + if err != nil { + return diag.FromErr(err) + } + + request := sdk.NewCreateConnectionRequest(id) + + if v, ok := d.GetOk("as_replica_of"); ok { + externalObjectId, err := sdk.ParseExternalObjectIdentifier(v.(string)) + if err != nil { + return diag.FromErr(err) + } + request.WithAsReplicaOf(externalObjectId) + } + + if v, ok := d.GetOk("comment"); ok { + request.WithComment(v.(string)) + } + + err = client.Connections.Create(ctx, request) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(helpers.EncodeResourceIdentifier(id)) + + return ReadContextSecondaryConnection(ctx, d, meta) +} + +func ReadContextSecondaryConnection(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + id, err := sdk.ParseAccountObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + connection, err := client.Connections.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 retrieve connection. Target object not found. Marking the resource as removed.", + Detail: fmt.Sprintf("Connection name: %s, Err: %s", id.FullyQualifiedName(), err), + }, + } + } + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to retrieve connection.", + Detail: fmt.Sprintf("Connection name: %s, Err: %s", id.FullyQualifiedName(), err), + }, + } + } + + return diag.FromErr(errors.Join( + d.Set("is_primary", connection.IsPrimary), + d.Set(FullyQualifiedNameAttributeName, id.FullyQualifiedName()), + d.Set(ShowOutputAttributeName, []map[string]any{schemas.ConnectionToSchema(connection)}), + d.Set("comment", connection.Comment), + d.Set("as_replica_of", connection.Primary), + )) +} + +func UpdateContextSecondaryConnection(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + id, err := sdk.ParseAccountObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + connectionSetRequest := new(sdk.ConnectionSetRequest) + connectionUnsetRequest := new(sdk.ConnectionUnsetRequest) + + if d.HasChange("comment") { + comment := d.Get("comment").(string) + if len(comment) > 0 { + connectionSetRequest.WithComment(comment) + } else { + connectionUnsetRequest.WithComment(true) + } + } + + if (*connectionSetRequest != sdk.ConnectionSetRequest{}) { + err := client.Connections.Alter(ctx, sdk.NewAlterConnectionRequest(id).WithSet(*connectionSetRequest)) + if err != nil { + return diag.FromErr(err) + } + } + + if (*connectionUnsetRequest != sdk.ConnectionUnsetRequest{}) { + err := client.Connections.Alter(ctx, sdk.NewAlterConnectionRequest(id).WithUnset(*connectionUnsetRequest)) + if err != nil { + return diag.FromErr(err) + } + } + + return ReadContextSecondaryConnection(ctx, d, meta) +} + +func DeleteContextSecondaryConnection(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + id, err := sdk.ParseAccountObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } + err = client.Connections.Drop(ctx, sdk.NewDropConnectionRequest(id).WithIfExists(true)) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + return nil +} diff --git a/pkg/resources/secondary_connection_acceptance_test.go b/pkg/resources/secondary_connection_acceptance_test.go new file mode 100644 index 0000000000..c0d8b47dbb --- /dev/null +++ b/pkg/resources/secondary_connection_acceptance_test.go @@ -0,0 +1,135 @@ +package resources_test + +import ( + "testing" + + acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert/resourceassert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config/model" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/importchecks" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/testenvs" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestAcc_SecondaryConnection_Basic(t *testing.T) { + // TODO: [SNOW-1002023]: Unskip; Business Critical Snowflake Edition needed + _ = testenvs.GetOrSkipTest(t, testenvs.TestFailoverGroups) + + id := acc.SecondaryTestClient().Ids.RandomAccountObjectIdentifier() + accountId := acc.TestClient().Account.GetAccountIdentifier(t) + primaryConnectionAsExternalId := sdk.NewExternalObjectIdentifier(accountId, id) + + // create primary connection + _, cleanup := acc.SecondaryTestClient().Connection.Create(t, id) + t.Cleanup(cleanup) + acc.SecondaryTestClient().Connection.Alter(t, sdk.NewAlterConnectionRequest(id).WithEnableConnectionFailover( + *sdk.NewEnableConnectionFailoverRequest([]sdk.AccountIdentifier{accountId}))) + + secondartyConnectionModel := model.SecondaryConnection("t", primaryConnectionAsExternalId.FullyQualifiedName(), id.Name()) + secondartyConnectionModelWithComment := model.SecondaryConnection("t", primaryConnectionAsExternalId.FullyQualifiedName(), id.Name()).WithComment("secondary connection test comment") + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.SecondaryConnection), + Steps: []resource.TestStep{ + { + Config: config.FromModel(t, secondartyConnectionModel), + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.SecondaryConnectionResource(t, secondartyConnectionModel.ResourceReference()). + HasNameString(id.Name()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasAsReplicaOfIdentifier(primaryConnectionAsExternalId). + HasIsPrimaryString("false"). + HasCommentString(""), + + resourceshowoutputassert.ConnectionShowOutput(t, secondartyConnectionModel.ResourceReference()). + HasName(id.Name()). + HasSnowflakeRegion(acc.SecondaryTestClient().Context.CurrentRegion(t)). + HasAccountLocator(acc.SecondaryTestClient().GetAccountLocator()). + HasAccountName(accountId.AccountName()). + HasOrganizationName(accountId.OrganizationName()). + HasComment(""). + HasIsPrimary(false). + HasPrimaryIdentifier(primaryConnectionAsExternalId). + HasFailoverAllowedToAccounts(accountId). + HasConnectionUrl( + acc.SecondaryTestClient().Connection.GetConnectionUrl(accountId.OrganizationName(), id.Name()), + ), + ), + ), + }, + // set comment + { + Config: config.FromModel(t, secondartyConnectionModelWithComment), + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.SecondaryConnectionResource(t, secondartyConnectionModelWithComment.ResourceReference()). + HasNameString(id.Name()). + HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasCommentString("secondary connection test comment"), + + resourceshowoutputassert.ConnectionShowOutput(t, secondartyConnectionModelWithComment.ResourceReference()). + HasComment("secondary connection test comment"), + ), + ), + }, + // import + { + ResourceName: secondartyConnectionModelWithComment.ResourceReference(), + ImportState: true, + ImportStateVerify: true, + ImportStateCheck: importchecks.ComposeImportStateCheck( + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "name", id.Name()), + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "comment", "secondary connection test comment"), + ), + }, + // unset comment + { + Config: config.FromModel(t, secondartyConnectionModel), + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.SecondaryConnectionResource(t, secondartyConnectionModel.ResourceReference()). + HasCommentString(""), + + resourceshowoutputassert.ConnectionShowOutput(t, secondartyConnectionModel.ResourceReference()). + HasComment(""), + ), + ), + }, + // recreate when exteranlly promoted to primary + { + PreConfig: func() { + acc.TestClient().Connection.Alter(t, sdk.NewAlterConnectionRequest(id).WithPrimary(true)) + }, + Config: config.FromModel(t, secondartyConnectionModel), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(secondartyConnectionModel.ResourceReference(), plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.SecondaryConnectionResource(t, secondartyConnectionModel.ResourceReference()). + HasIsPrimaryString("false"), + + resourceshowoutputassert.ConnectionShowOutput(t, secondartyConnectionModel.ResourceReference()). + HasIsPrimary(false), + ), + ), + }, + }, + }) +} diff --git a/pkg/schemas/connection_gen.go b/pkg/schemas/connection_gen.go new file mode 100644 index 0000000000..530dea13a2 --- /dev/null +++ b/pkg/schemas/connection_gen.go @@ -0,0 +1,91 @@ +// Code generated by sdk-to-schema generator; DO NOT EDIT. + +package schemas + +import ( + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// ShowConnectionSchema represents output of SHOW query for the single Connection. +var ShowConnectionSchema = map[string]*schema.Schema{ + "region_group": { + Type: schema.TypeString, + Computed: true, + }, + "snowflake_region": { + Type: schema.TypeString, + Computed: true, + }, + "created_on": { + Type: schema.TypeString, + Computed: true, + }, + "account_name": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "comment": { + Type: schema.TypeString, + Computed: true, + }, + "is_primary": { + Type: schema.TypeBool, + Computed: true, + ForceNew: true, + }, + "primary": { + Type: schema.TypeString, + Computed: true, + }, + "failover_allowed_to_accounts": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + }, + "connection_url": { + Type: schema.TypeString, + Computed: true, + }, + "organization_name": { + Type: schema.TypeString, + Computed: true, + }, + "account_locator": { + Type: schema.TypeString, + Computed: true, + }, +} + +var _ = ShowConnectionSchema + +func ConnectionToSchema(connection *sdk.Connection) map[string]any { + connectionSchema := make(map[string]any) + if connection.RegionGroup != nil { + connectionSchema["region_group"] = connection.RegionGroup + } + connectionSchema["snowflake_region"] = connection.SnowflakeRegion + connectionSchema["created_on"] = connection.CreatedOn.String() + connectionSchema["account_name"] = connection.AccountName + connectionSchema["name"] = connection.Name + if connection.Comment != nil { + connectionSchema["comment"] = connection.Comment + } + connectionSchema["is_primary"] = connection.IsPrimary + connectionSchema["primary"] = connection.Primary.FullyQualifiedName() + var allowedAccounts []string + for _, accountId := range connection.FailoverAllowedToAccounts { + allowedAccounts = append(allowedAccounts, accountId.Name()) + } + connectionSchema["failover_allowed_to_accounts"] = allowedAccounts + connectionSchema["connection_url"] = connection.ConnectionUrl + connectionSchema["organization_name"] = connection.OrganizationName + connectionSchema["account_locator"] = connection.AccountLocator + return connectionSchema +} + +var _ = ConnectionToSchema diff --git a/pkg/schemas/gen/sdk_show_result_structs.go b/pkg/schemas/gen/sdk_show_result_structs.go index 3af846ccc7..8aa4a5bd9e 100644 --- a/pkg/schemas/gen/sdk_show_result_structs.go +++ b/pkg/schemas/gen/sdk_show_result_structs.go @@ -10,6 +10,7 @@ var SdkShowResultStructs = []any{ sdk.ApplicationRole{}, sdk.Application{}, sdk.AuthenticationPolicy{}, + sdk.Connection{}, sdk.DatabaseRole{}, sdk.Database{}, sdk.DynamicTable{}, diff --git a/pkg/sdk/connections_def.go b/pkg/sdk/connections_def.go index 39f583aff2..cad11042c9 100644 --- a/pkg/sdk/connections_def.go +++ b/pkg/sdk/connections_def.go @@ -20,7 +20,7 @@ var ConnectionDef = g.NewInterface( OptionalIdentifier( "AsReplicaOf", g.KindOfT[ExternalObjectIdentifier](), - g.IdentifierOptions().Required().SQL("AS REPLICA OF")). + g.IdentifierOptions().SQL("AS REPLICA OF")). OptionalComment(). WithValidation(g.ValidIdentifier, "name"). WithValidation(g.ValidIdentifierIfSet, "AsReplicaOf"), @@ -34,7 +34,8 @@ var ConnectionDef = g.NewInterface( OptionalQueryStructField( "EnableConnectionFailover", g.NewQueryStruct("EnableConnectionFailover"). - List("ToAccounts", "AccountIdentifier", g.ListOptions().NoParentheses()), + List("ToAccounts", "AccountIdentifier", g.ListOptions().NoParentheses().Required()). + WithValidation(g.AtLeastOneValueSet, "ToAccounts"), g.KeywordOptions().SQL("ENABLE FAILOVER TO ACCOUNTS"), ). OptionalQueryStructField( @@ -43,7 +44,7 @@ var ConnectionDef = g.NewInterface( OptionalQueryStructField( "ToAccounts", g.NewQueryStruct("ToAccounts"). - List("Accounts", "AccountIdentifier", g.ListOptions().NoParentheses()), + List("Accounts", "AccountIdentifier", g.ListOptions().NoParentheses().Required()), g.KeywordOptions().SQL("TO ACCOUNTS"), ), g.KeywordOptions().SQL("DISABLE FAILOVER"), @@ -51,14 +52,14 @@ var ConnectionDef = g.NewInterface( OptionalSQL("PRIMARY"). OptionalQueryStructField( "Set", - g.NewQueryStruct("Set"). + g.NewQueryStruct("ConnectionSet"). OptionalComment(). WithValidation(g.AtLeastOneValueSet, "Comment"), g.KeywordOptions().SQL("SET"), ). OptionalQueryStructField( "Unset", - g.NewQueryStruct("Unset"). + g.NewQueryStruct("ConnectionUnset"). OptionalSQL("COMMENT"). WithValidation(g.AtLeastOneValueSet, "Comment"), g.KeywordOptions().SQL("UNSET"), @@ -95,8 +96,8 @@ var ConnectionDef = g.NewInterface( Text("Name"). OptionalText("Comment"). Bool("IsPrimary"). - Text("Primary"). - Field("FailoverAllowedToAccounts", "[]string"). + Field("Primary", "ExternalObjectIdentifier"). + Field("FailoverAllowedToAccounts", "[]AccountIdentifier"). Text("ConnectionUrl"). Text("OrganizationName"). Text("AccountLocator"), diff --git a/pkg/sdk/connections_dto_builders_gen.go b/pkg/sdk/connections_dto_builders_gen.go index eb226b05a2..029d361d4b 100644 --- a/pkg/sdk/connections_dto_builders_gen.go +++ b/pkg/sdk/connections_dto_builders_gen.go @@ -2,6 +2,8 @@ package sdk +import () + func NewCreateConnectionRequest( name AccountObjectIdentifier, ) *CreateConnectionRequest { @@ -53,23 +55,22 @@ func (s *AlterConnectionRequest) WithPrimary(Primary bool) *AlterConnectionReque return s } -func (s *AlterConnectionRequest) WithSet(Set SetRequest) *AlterConnectionRequest { +func (s *AlterConnectionRequest) WithSet(Set ConnectionSetRequest) *AlterConnectionRequest { s.Set = &Set return s } -func (s *AlterConnectionRequest) WithUnset(Unset UnsetRequest) *AlterConnectionRequest { +func (s *AlterConnectionRequest) WithUnset(Unset ConnectionUnsetRequest) *AlterConnectionRequest { s.Unset = &Unset return s } -func NewEnableConnectionFailoverRequest() *EnableConnectionFailoverRequest { - return &EnableConnectionFailoverRequest{} -} - -func (s *EnableConnectionFailoverRequest) WithToAccounts(ToAccounts []AccountIdentifier) *EnableConnectionFailoverRequest { +func NewEnableConnectionFailoverRequest( + ToAccounts []AccountIdentifier, +) *EnableConnectionFailoverRequest { + s := EnableConnectionFailoverRequest{} s.ToAccounts = ToAccounts - return s + return &s } func NewDisableConnectionFailoverRequest() *DisableConnectionFailoverRequest { @@ -81,29 +82,28 @@ func (s *DisableConnectionFailoverRequest) WithToAccounts(ToAccounts ToAccountsR return s } -func NewToAccountsRequest() *ToAccountsRequest { - return &ToAccountsRequest{} -} - -func (s *ToAccountsRequest) WithAccounts(Accounts []AccountIdentifier) *ToAccountsRequest { +func NewToAccountsRequest( + Accounts []AccountIdentifier, +) *ToAccountsRequest { + s := ToAccountsRequest{} s.Accounts = Accounts - return s + return &s } -func NewSetRequest() *SetRequest { - return &SetRequest{} +func NewConnectionSetRequest() *ConnectionSetRequest { + return &ConnectionSetRequest{} } -func (s *SetRequest) WithComment(Comment string) *SetRequest { +func (s *ConnectionSetRequest) WithComment(Comment string) *ConnectionSetRequest { s.Comment = &Comment return s } -func NewUnsetRequest() *UnsetRequest { - return &UnsetRequest{} +func NewConnectionUnsetRequest() *ConnectionUnsetRequest { + return &ConnectionUnsetRequest{} } -func (s *UnsetRequest) WithComment(Comment bool) *UnsetRequest { +func (s *ConnectionUnsetRequest) WithComment(Comment bool) *ConnectionUnsetRequest { s.Comment = &Comment return s } diff --git a/pkg/sdk/connections_dto_gen.go b/pkg/sdk/connections_dto_gen.go index 1323bf0af3..c3daa0a107 100644 --- a/pkg/sdk/connections_dto_gen.go +++ b/pkg/sdk/connections_dto_gen.go @@ -22,12 +22,12 @@ type AlterConnectionRequest struct { EnableConnectionFailover *EnableConnectionFailoverRequest DisableConnectionFailover *DisableConnectionFailoverRequest Primary *bool - Set *SetRequest - Unset *UnsetRequest + Set *ConnectionSetRequest + Unset *ConnectionUnsetRequest } type EnableConnectionFailoverRequest struct { - ToAccounts []AccountIdentifier + ToAccounts []AccountIdentifier // required } type DisableConnectionFailoverRequest struct { @@ -35,14 +35,14 @@ type DisableConnectionFailoverRequest struct { } type ToAccountsRequest struct { - Accounts []AccountIdentifier + Accounts []AccountIdentifier // required } -type SetRequest struct { +type ConnectionSetRequest struct { Comment *string } -type UnsetRequest struct { +type ConnectionUnsetRequest struct { Comment *bool } diff --git a/pkg/sdk/connections_gen.go b/pkg/sdk/connections_gen.go index 80885df87b..692f38f2f5 100644 --- a/pkg/sdk/connections_gen.go +++ b/pkg/sdk/connections_gen.go @@ -33,8 +33,8 @@ type AlterConnectionOptions struct { EnableConnectionFailover *EnableConnectionFailover `ddl:"keyword" sql:"ENABLE FAILOVER TO ACCOUNTS"` DisableConnectionFailover *DisableConnectionFailover `ddl:"keyword" sql:"DISABLE FAILOVER"` Primary *bool `ddl:"keyword" sql:"PRIMARY"` - Set *Set `ddl:"keyword" sql:"SET"` - Unset *Unset `ddl:"keyword" sql:"UNSET"` + Set *ConnectionSet `ddl:"keyword" sql:"SET"` + Unset *ConnectionUnset `ddl:"keyword" sql:"UNSET"` } type EnableConnectionFailover struct { ToAccounts []AccountIdentifier `ddl:"list,no_parentheses"` @@ -45,10 +45,10 @@ type DisableConnectionFailover struct { type ToAccounts struct { Accounts []AccountIdentifier `ddl:"list,no_parentheses"` } -type Set struct { +type ConnectionSet struct { Comment *string `ddl:"parameter,single_quotes" sql:"COMMENT"` } -type Unset struct { +type ConnectionUnset struct { Comment *bool `ddl:"keyword" sql:"COMMENT"` } @@ -88,8 +88,8 @@ type Connection struct { Name string Comment *string IsPrimary bool - Primary string - FailoverAllowedToAccounts []string + Primary ExternalObjectIdentifier + FailoverAllowedToAccounts []AccountIdentifier ConnectionUrl string OrganizationName string AccountLocator string diff --git a/pkg/sdk/connections_gen_test.go b/pkg/sdk/connections_gen_test.go index 8ee6f7441a..b0b48b8934 100644 --- a/pkg/sdk/connections_gen_test.go +++ b/pkg/sdk/connections_gen_test.go @@ -1,6 +1,8 @@ package sdk -import "testing" +import ( + "testing" +) func TestConnections_Create(t *testing.T) { id := randomAccountObjectIdentifier() @@ -78,20 +80,26 @@ func TestConnections_Alter(t *testing.T) { opts.EnableConnectionFailover = &EnableConnectionFailover{} opts.DisableConnectionFailover = &DisableConnectionFailover{} opts.Primary = Bool(true) - opts.Set = &Set{} - opts.Unset = &Unset{} + opts.Set = &ConnectionSet{} + opts.Unset = &ConnectionUnset{} assertOptsInvalidJoinedErrors(t, opts, errExactlyOneOf("AlterConnectionOptions", "EnableConnectionFailover", "DisableConnectionFailover", "Primary", "Set", "Unset")) }) + t.Run("validation: at least one of the fields [opts.EnableConnectionFailover.ToAccounts] should be set", func(t *testing.T) { + opts := defaultOpts() + opts.EnableConnectionFailover = &EnableConnectionFailover{} + assertOptsInvalidJoinedErrors(t, opts, errAtLeastOneOf("AlterConnectionOptions.EnableConnectionFailover", "ToAccounts")) + }) + t.Run("validation: at least one of the fields [opts.Set.Comment] should be set", func(t *testing.T) { opts := defaultOpts() - opts.Set = &Set{} + opts.Set = &ConnectionSet{} assertOptsInvalidJoinedErrors(t, opts, errAtLeastOneOf("AlterConnectionOptions.Set", "Comment")) }) t.Run("validation: at least one of the fields [opts.Unset.Comment] should be set", func(t *testing.T) { opts := defaultOpts() - opts.Unset = &Unset{} + opts.Unset = &ConnectionUnset{} assertOptsInvalidJoinedErrors(t, opts, errAtLeastOneOf("AlterConnectionOptions.Unset", "Comment")) }) @@ -128,13 +136,13 @@ func TestConnections_Alter(t *testing.T) { t.Run("set comment", func(t *testing.T) { opts := defaultOpts() - opts.Set = &Set{Comment: String("test comment")} + opts.Set = &ConnectionSet{Comment: String("test comment")} assertOptsValidAndSQLEquals(t, opts, "ALTER CONNECTION %s SET COMMENT = 'test comment'", id.FullyQualifiedName()) }) t.Run("unset comment", func(t *testing.T) { opts := defaultOpts() - opts.Unset = &Unset{Comment: Bool(true)} + opts.Unset = &ConnectionUnset{Comment: Bool(true)} assertOptsValidAndSQLEquals(t, opts, "ALTER CONNECTION %s UNSET COMMENT", id.FullyQualifiedName()) }) } diff --git a/pkg/sdk/connections_impl_gen.go b/pkg/sdk/connections_impl_gen.go index 325141116e..a1fb403c4c 100644 --- a/pkg/sdk/connections_impl_gen.go +++ b/pkg/sdk/connections_impl_gen.go @@ -2,6 +2,7 @@ package sdk import ( "context" + "log" "strconv" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/collections" @@ -53,14 +54,9 @@ func (r *CreateConnectionRequest) toOpts() *CreateConnectionOptions { opts := &CreateConnectionOptions{ IfNotExists: r.IfNotExists, name: r.name, - - Comment: r.Comment, - } - - if r.AsReplicaOf != nil { - opts.AsReplicaOf = r.AsReplicaOf + AsReplicaOf: r.AsReplicaOf, + Comment: r.Comment, } - return opts } @@ -88,13 +84,13 @@ func (r *AlterConnectionRequest) toOpts() *AlterConnectionOptions { } if r.Set != nil { - opts.Set = &Set{ + opts.Set = &ConnectionSet{ Comment: r.Set.Comment, } } if r.Unset != nil { - opts.Unset = &Unset{ + opts.Unset = &ConnectionUnset{ Comment: r.Unset.Comment, } } @@ -119,18 +115,35 @@ func (r *ShowConnectionRequest) toOpts() *ShowConnectionOptions { func (r connectionRow) convert() *Connection { c := &Connection{ - SnowflakeRegion: r.SnowflakeRegion, - CreatedOn: r.CreatedOn, - AccountName: r.AccountName, - Name: r.Name, - Primary: r.Primary, - FailoverAllowedToAccounts: ParseCommaSeparatedStringArray(r.FailoverAllowedToAccounts, false), - ConnectionUrl: r.ConnectionUrl, - OrganizationName: r.OrganizationName, - AccountLocator: r.AccountLocator, + SnowflakeRegion: r.SnowflakeRegion, + CreatedOn: r.CreatedOn, + AccountName: r.AccountName, + Name: r.Name, + ConnectionUrl: r.ConnectionUrl, + OrganizationName: r.OrganizationName, + AccountLocator: r.AccountLocator, + } + + parsedIsPrimary, err := strconv.ParseBool(r.IsPrimary) + if err != nil { + log.Printf("unable to parse bool is_primary for connection: %v, err = %s", r.IsPrimary, err) + } else { + c.IsPrimary = parsedIsPrimary } - b, _ := strconv.ParseBool(r.IsPrimary) - c.IsPrimary = b + + primaryExternalId, err := ParseExternalObjectIdentifier(r.Primary) + if err != nil { + log.Printf("unable to parse primary connection external identifier: %v, err = %s", r.Primary, err) + } else { + c.Primary = primaryExternalId + } + + if allowedToAccounts, err := ParseCommaSeparatedAccountIdentifierArray(r.FailoverAllowedToAccounts); err != nil { + log.Printf("unable to parse account identifier list for 'enable failover to accounts': %s, err = %v", r.FailoverAllowedToAccounts, err) + } else { + c.FailoverAllowedToAccounts = allowedToAccounts + } + if r.Comment.Valid { c.Comment = String(r.Comment.String) } diff --git a/pkg/sdk/connections_validations_gen.go b/pkg/sdk/connections_validations_gen.go index df6b4e1d0f..f96dad1dd0 100644 --- a/pkg/sdk/connections_validations_gen.go +++ b/pkg/sdk/connections_validations_gen.go @@ -29,6 +29,11 @@ func (opts *AlterConnectionOptions) validate() error { if !exactlyOneValueSet(opts.EnableConnectionFailover, opts.DisableConnectionFailover, opts.Primary, opts.Set, opts.Unset) { errs = append(errs, errExactlyOneOf("AlterConnectionOptions", "EnableConnectionFailover", "DisableConnectionFailover", "Primary", "Set", "Unset")) } + if valueSet(opts.EnableConnectionFailover) { + if !anyValueSet(opts.EnableConnectionFailover.ToAccounts) { + errs = append(errs, errAtLeastOneOf("AlterConnectionOptions.EnableConnectionFailover", "ToAccounts")) + } + } if valueSet(opts.Set) { if !anyValueSet(opts.Set.Comment) { errs = append(errs, errAtLeastOneOf("AlterConnectionOptions.Set", "Comment")) diff --git a/pkg/sdk/parsers.go b/pkg/sdk/parsers.go index 09952d9381..9495058b02 100644 --- a/pkg/sdk/parsers.go +++ b/pkg/sdk/parsers.go @@ -52,3 +52,18 @@ func ParseCommaSeparatedSchemaObjectIdentifierArray(value string) ([]SchemaObjec } return ids, nil } + +// ParseCommaSeparatedAccountIdentifierArray can be used to parse Snowflake output containing a list of account identifiers +// in the format of ["organization1.account1", "organization2.account2", ...], +func ParseCommaSeparatedAccountIdentifierArray(value string) ([]AccountIdentifier, error) { + idsRaw := ParseCommaSeparatedStringArray(value, false) + ids := make([]AccountIdentifier, len(idsRaw)) + for i := range idsRaw { + id, err := ParseAccountIdentifier(idsRaw[i]) + if err != nil { + return nil, err + } + ids[i] = id + } + return ids, nil +} diff --git a/pkg/sdk/testint/connections_gen_integration_test.go b/pkg/sdk/testint/connections_gen_integration_test.go index 79573d1b6d..ff658dd622 100644 --- a/pkg/sdk/testint/connections_gen_integration_test.go +++ b/pkg/sdk/testint/connections_gen_integration_test.go @@ -16,6 +16,9 @@ import ( const ConnectionFailoverToAccountInSameRegionErrorMessage = "The connection cannot be failed over to an account in the same region" func TestInt_Connections(t *testing.T) { + // TODO: [SNOW-1002023]: Unskip; Business Critical Snowflake Edition needed + _ = testenvs.GetOrSkipTest(t, testenvs.TestFailoverGroups) + client := testClient(t) secondaryClient := testSecondaryClient(t) ctx := testContext(t) @@ -25,9 +28,6 @@ func TestInt_Connections(t *testing.T) { accountId := testClientHelper().Account.GetAccountIdentifier(t) t.Run("Create minimal", func(t *testing.T) { - // TODO: [SNOW-1002023]: Unskip; Business Critical Snowflake Edition needed - _ = testenvs.GetOrSkipTest(t, testenvs.TestFailoverGroups) - id := testClientHelper().Ids.RandomAccountObjectIdentifier() require.NoError(t, err) @@ -43,11 +43,7 @@ func TestInt_Connections(t *testing.T) { HasNoComment(). HasIsPrimary(true). HasPrimaryIdentifier(externalObjectIdentifier). - HasFailoverAllowedToAccounts( - []string{ - accountId.Name(), - }, - ). + HasFailoverAllowedToAccounts(accountId). HasOrganizationName(sessionDetails.OrganizationName). HasAccountLocator(client.GetAccountLocator()). HasConnectionUrl( @@ -59,9 +55,6 @@ func TestInt_Connections(t *testing.T) { }) t.Run("Create all options", func(t *testing.T) { - // TODO: [SNOW-1002023]: Unskip; Business Critical Snowflake Edition needed - _ = testenvs.GetOrSkipTest(t, testenvs.TestFailoverGroups) - id := testClientHelper().Ids.RandomAccountObjectIdentifier() err := client.Connections.Create(ctx, sdk.NewCreateConnectionRequest(id). WithIfNotExists(true). @@ -77,11 +70,7 @@ func TestInt_Connections(t *testing.T) { HasComment("test comment for connection"). HasIsPrimary(true). HasPrimaryIdentifier(externalObjectIdentifier). - HasFailoverAllowedToAccounts( - []string{ - accountId.Name(), - }, - ). + HasFailoverAllowedToAccounts(accountId). HasOrganizationName(sessionDetails.OrganizationName). HasAccountLocator(client.GetAccountLocator()). HasConnectionUrl( @@ -93,58 +82,45 @@ func TestInt_Connections(t *testing.T) { }) t.Run("Alter enable failover", func(t *testing.T) { - // TODO: [SNOW-1002023]: Unskip; Business Critical Snowflake Edition needed - _ = testenvs.GetOrSkipTest(t, testenvs.TestFailoverGroups) - id := testClientHelper().Ids.RandomAccountObjectIdentifier() - secondaryAccountId := secondaryTestClientHelper().Ids.AccountIdentifierWithLocator() + + secondaryAccountId := secondaryTestClientHelper().Account.GetAccountIdentifier(t) _, connectionCleanup := testClientHelper().Connection.Create(t, id) t.Cleanup(connectionCleanup) err := client.Connections.Alter(ctx, sdk.NewAlterConnectionRequest(id). WithEnableConnectionFailover( - *sdk.NewEnableConnectionFailoverRequest().WithToAccounts( - []sdk.AccountIdentifier{ - secondaryAccountId, - }, - ), - ), + *sdk.NewEnableConnectionFailoverRequest([]sdk.AccountIdentifier{secondaryAccountId})), ) + // TODO: [SNOW-1763442] + // require.NoError(t, err) require.ErrorContains(t, err, ConnectionFailoverToAccountInSameRegionErrorMessage) - // TODO: [SNOW-1763442] - /* - require.NoError(t, err) - externalObjectIdentifier := sdk.NewExternalObjectIdentifier(accountId, id) - assertions.AssertThatObject(t, objectassert.Connection(t, id). - HasSnowflakeRegion(sessionDetails.Region). - HasAccountName(sessionDetails.AccountName). - HasName(id.Name()). - HasNoComment(). - HasIsPrimary(true). - HasPrimaryIdentifier(externalObjectIdentifier). - HasFailoverAllowedToAccounts( - []string{ - accountId.Name(), - secondaryAccountId.Name(), - }, - ). - HasOrganizationName(sessionDetails.OrganizationName). - HasAccountLocator(client.GetAccountLocator()), - HasConnectionUrl( - strings.ToLower( - fmt.Sprintf("%s-%s.snowflakecomputing.com", sessionDetails.OrganizationName, id.Name()), - ), - ), - ) - */ + externalObjectIdentifier := sdk.NewExternalObjectIdentifier(accountId, id) + assertions.AssertThatObject(t, objectassert.Connection(t, id). + HasSnowflakeRegion(sessionDetails.Region). + HasAccountName(sessionDetails.AccountName). + HasName(id.Name()). + HasNoComment(). + HasIsPrimary(true). + HasPrimaryIdentifier(externalObjectIdentifier). + HasFailoverAllowedToAccounts( + accountId, + secondaryAccountId, + ). + HasOrganizationName(sessionDetails.OrganizationName). + HasAccountLocator(client.GetAccountLocator()). + HasConnectionUrl( + strings.ToLower( + fmt.Sprintf("%s-%s.snowflakecomputing.com", sessionDetails.OrganizationName, id.Name()), + ), + ), + ) }) t.Run("Create as replica of", func(t *testing.T) { - // TODO: [SNOW-1002023]: Unskip; Business Critical Snowflake Edition needed - _ = testenvs.GetOrSkipTest(t, testenvs.TestFailoverGroups) - + id := testClientHelper().Ids.RandomAccountObjectIdentifier() secondaryAccountId := secondaryTestClientHelper().Ids.AccountIdentifierWithLocator() primaryConn, connectionCleanup := testClientHelper().Connection.Create(t, testClientHelper().Ids.RandomAccountObjectIdentifier()) @@ -152,12 +128,7 @@ func TestInt_Connections(t *testing.T) { err := client.Connections.Alter(ctx, sdk.NewAlterConnectionRequest(primaryConn.ID()). WithEnableConnectionFailover( - *sdk.NewEnableConnectionFailoverRequest().WithToAccounts( - []sdk.AccountIdentifier{ - secondaryAccountId, - }, - ), - ), + *sdk.NewEnableConnectionFailoverRequest([]sdk.AccountIdentifier{secondaryAccountId})), ) require.ErrorContains(t, err, ConnectionFailoverToAccountInSameRegionErrorMessage) // TODO: [SNOW-1763442] @@ -165,43 +136,33 @@ func TestInt_Connections(t *testing.T) { // require.NoError(t, err) // create replica on secondary account - /* - externalObjectIdentifier := sdk.NewExternalObjectIdentifier(accountId, id) - err = secondaryClient.Connections.Create(ctx, sdk.NewCreateConnectionRequest(id). - WithAsReplicaOf(sdk.AsReplicaOfRequest{ - AsReplicaOf: externalObjectIdentifier, - })) - t.Cleanup(testClientHelper().Connection.DropFunc(t, id)) - require.NoError(t, err) - - assertions.AssertThatObject(t, objectassert.Connection(t, id). - HasSnowflakeRegion(sessionDetails.Region). - HasAccountName(sessionDetails.AccountName). - HasName(id.Name()). - HasNoComment(). - HasIsPrimary(false). - HasPrimaryIdentifier(externalObjectIdentifier). - HasFailoverAllowedToAccounts( - []string{ - accountId.Name(), - secondaryAccountId.Name(), - }, - ). - HasOrganizationName(sessionDetails.OrganizationName). - HasAccountLocator(client.GetAccountLocator()). - HasConnectionUrl( - strings.ToLower( - fmt.Sprintf("%s-%s.snowflakecomputing.com", sessionDetails.OrganizationName, id.Name()), - ), + externalObjectIdentifier := sdk.NewExternalObjectIdentifier(accountId, id) + err = secondaryClient.Connections.Create(ctx, sdk.NewCreateConnectionRequest(id).WithAsReplicaOf(externalObjectIdentifier)) + t.Cleanup(testClientHelper().Connection.DropFunc(t, id)) + require.NoError(t, err) + + assertions.AssertThatObject(t, objectassert.Connection(t, id). + HasSnowflakeRegion(sessionDetails.Region). + HasAccountName(sessionDetails.AccountName). + HasName(id.Name()). + HasNoComment(). + HasIsPrimary(false). + HasPrimaryIdentifier(externalObjectIdentifier). + HasFailoverAllowedToAccounts( + accountId, + secondaryAccountId, + ). + HasOrganizationName(sessionDetails.OrganizationName). + HasAccountLocator(client.GetAccountLocator()). + HasConnectionUrl( + strings.ToLower( + fmt.Sprintf("%s-%s.snowflakecomputing.com", sessionDetails.OrganizationName, id.Name()), ), - ) - */ + ), + ) }) t.Run("Alter disable failover", func(t *testing.T) { - // TODO: [SNOW-1763442]: Unskip; Business Critical Snowflake Edition needed - _ = testenvs.GetOrSkipTest(t, testenvs.TestFailoverGroups) - id := testClientHelper().Ids.RandomAccountObjectIdentifier() secondaryAccountId := secondaryTestClientHelper().Account.GetAccountIdentifier(t) @@ -211,12 +172,7 @@ func TestInt_Connections(t *testing.T) { // Add secondary account to failover list err := client.Connections.Alter(ctx, sdk.NewAlterConnectionRequest(id). WithEnableConnectionFailover( - *sdk.NewEnableConnectionFailoverRequest().WithToAccounts( - []sdk.AccountIdentifier{ - secondaryAccountId, - }, - ), - ), + *sdk.NewEnableConnectionFailoverRequest([]sdk.AccountIdentifier{secondaryAccountId})), ) require.ErrorContains(t, err, ConnectionFailoverToAccountInSameRegionErrorMessage) // TODO: [SNOW-1763442] @@ -232,11 +188,7 @@ func TestInt_Connections(t *testing.T) { externalObjectIdentifier := sdk.NewExternalObjectIdentifier(accountId, id) assertions.AssertThatObject(t, objectassert.Connection(t, primaryConn.ID()). HasPrimaryIdentifier(externalObjectIdentifier). - HasFailoverAllowedToAccounts( - []string{ - accountId.Name(), - }, - ), + HasFailoverAllowedToAccounts(accountId), ) // Try to create repllication on secondary account @@ -245,16 +197,13 @@ func TestInt_Connections(t *testing.T) { }) t.Run("Alter comment", func(t *testing.T) { - // TODO: [SNOW-1002023]: Unskip; Business Critical Snowflake Edition needed - _ = testenvs.GetOrSkipTest(t, testenvs.TestFailoverGroups) - id := testClientHelper().Ids.RandomAccountObjectIdentifier() _, connectionCleanup := testClientHelper().Connection.Create(t, id) t.Cleanup(connectionCleanup) // Set err := client.Connections.Alter(ctx, sdk.NewAlterConnectionRequest(id). - WithSet(*sdk.NewSetRequest(). + WithSet(*sdk.NewConnectionSetRequest(). WithComment("new integration test comment"))) require.NoError(t, err) @@ -265,7 +214,7 @@ func TestInt_Connections(t *testing.T) { // Unset err = client.Connections.Alter(ctx, sdk.NewAlterConnectionRequest(id). - WithUnset(*sdk.NewUnsetRequest(). + WithUnset(*sdk.NewConnectionUnsetRequest(). WithComment(true))) require.NoError(t, err) @@ -276,9 +225,6 @@ func TestInt_Connections(t *testing.T) { }) t.Run("Drop", func(t *testing.T) { - // TODO: [SNOW-1002023]: Unskip; Business Critical Snowflake Edition needed - _ = testenvs.GetOrSkipTest(t, testenvs.TestFailoverGroups) - id := testClientHelper().Ids.RandomAccountObjectIdentifier() _, connectionCleanup := testClientHelper().Connection.Create(t, id) t.Cleanup(connectionCleanup) @@ -296,9 +242,6 @@ func TestInt_Connections(t *testing.T) { }) t.Run("Drop with if exists", func(t *testing.T) { - // TODO: [SNOW-1002023]: Unskip; Business Critical Snowflake Edition needed - _ = testenvs.GetOrSkipTest(t, testenvs.TestFailoverGroups) - err = client.Connections.Drop(ctx, sdk.NewDropConnectionRequest(NonExistingAccountObjectIdentifier)) require.ErrorIs(t, err, sdk.ErrObjectNotExistOrAuthorized) @@ -307,9 +250,6 @@ func TestInt_Connections(t *testing.T) { }) t.Run("Show", func(t *testing.T) { - // TODO: [SNOW-1002023]: Unskip; Business Critical Snowflake Edition needed - _ = testenvs.GetOrSkipTest(t, testenvs.TestFailoverGroups) - id1 := testClientHelper().Ids.RandomAccountObjectIdentifier() id2 := testClientHelper().Ids.RandomAccountObjectIdentifier() @@ -326,9 +266,6 @@ func TestInt_Connections(t *testing.T) { }) t.Run("Show with Like", func(t *testing.T) { - // TODO: [SNOW-1002023]: Unskip; Business Critical Snowflake Edition needed - _ = testenvs.GetOrSkipTest(t, testenvs.TestFailoverGroups) - id1 := testClientHelper().Ids.RandomAccountObjectIdentifier() id2 := testClientHelper().Ids.RandomAccountObjectIdentifier() @@ -348,9 +285,6 @@ func TestInt_Connections(t *testing.T) { }) t.Run("ShowByID", func(t *testing.T) { - // TODO: [SNOW-1002023]: Unskip; Business Critical Snowflake Edition needed - _ = testenvs.GetOrSkipTest(t, testenvs.TestFailoverGroups) - id := testClientHelper().Ids.RandomAccountObjectIdentifier() _, connectionCleanup := testClientHelper().Connection.Create(t, id) diff --git a/templates/resources/primary_connection.md.tmpl b/templates/resources/primary_connection.md.tmpl new file mode 100644 index 0000000000..bbc5e20afb --- /dev/null +++ b/templates/resources/primary_connection.md.tmpl @@ -0,0 +1,35 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ if gt (len (split .Description "")) 1 -}} +{{ index (split .Description "") 1 | plainmarkdown | trimspace | prefixlines " " }} +{{- else -}} +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +{{- end }} +--- + +!> **V1 release candidate** This resource is a release candidate for the V1. It is on the list of remaining GA objects for V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0970--v0980) to use it. + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ tffile (printf "examples/resources/%s/resource.tf" .Name)}} +-> **Note** Instead of using fully_qualified_name, you can reference objects managed outside Terraform by constructing a correct ID, consult [identifiers guide](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/guides/identifiers#new-computed-fully-qualified-name-field-in-resources). + + +{{- end }} + +{{ .SchemaMarkdown | trimspace }} +{{- if .HasImport }} + +## Import + +Import is supported using the following syntax: + +{{ codefile "shell" (printf "examples/resources/%s/import.sh" .Name)}} +{{- end }} diff --git a/templates/resources/secondary_connection.md.tmpl b/templates/resources/secondary_connection.md.tmpl new file mode 100644 index 0000000000..bbc5e20afb --- /dev/null +++ b/templates/resources/secondary_connection.md.tmpl @@ -0,0 +1,35 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ if gt (len (split .Description "")) 1 -}} +{{ index (split .Description "") 1 | plainmarkdown | trimspace | prefixlines " " }} +{{- else -}} +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +{{- end }} +--- + +!> **V1 release candidate** This resource is a release candidate for the V1. It is on the list of remaining GA objects for V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the resource if needed. Any errors reported will be resolved with a higher priority. We encourage checking this resource out before the V1 release. Please follow the [migration guide](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/MIGRATION_GUIDE.md#v0970--v0980) to use it. + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Example Usage + +{{ tffile (printf "examples/resources/%s/resource.tf" .Name)}} +-> **Note** Instead of using fully_qualified_name, you can reference objects managed outside Terraform by constructing a correct ID, consult [identifiers guide](https://registry.terraform.io/providers/Snowflake-Labs/snowflake/latest/docs/guides/identifiers#new-computed-fully-qualified-name-field-in-resources). + + +{{- end }} + +{{ .SchemaMarkdown | trimspace }} +{{- if .HasImport }} + +## Import + +Import is supported using the following syntax: + +{{ codefile "shell" (printf "examples/resources/%s/import.sh" .Name)}} +{{- end }}