From 16a812dae20d59f31457790dcd99db03db697051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Budzy=C5=84ski?= Date: Thu, 17 Oct 2024 09:23:31 +0200 Subject: [PATCH] feat: Secret resource (#3110) ## Changes - add `snowflake_secret_with_client_credentials` resource - add `snowflake_secret_with_authorization_code_grant` resource - add `snowflake_secret_with_basic_authentication` resource - add `snowflake_secret_with_generic_string` resource - fix parsing oauth_scopes list with `ParseCommaSeparatedStringArray()` ## Test Plan * [x] acceptance tests ## References https://docs.snowflake.com/en/sql-reference/sql/create-secret ## TODO - datasource - tests for externally changed secret type --- MIGRATION_GUIDE.md | 14 + .../secret_with_authorization_code_grant.md | 104 ++++++ .../secret_with_basic_authentication.md | 101 ++++++ .../secret_with_client_credentials.md | 101 ++++++ docs/resources/secret_with_generic_string.md | 98 ++++++ .../import.sh | 1 + .../resource.tf | 20 ++ .../import.sh | 1 + .../resource.tf | 18 + .../import.sh | 1 + .../resource.tf | 18 + .../import.sh | 1 + .../resource.tf | 16 + pkg/acceptance/bettertestspoc/README.md | 1 + ...h_authorization_code_grant_resource_ext.go | 10 + ...h_authorization_code_grant_resource_gen.go | 117 +++++++ ..._with_basic_authentication_resource_gen.go | 107 ++++++ ...et_with_client_credentials_resource_ext.go | 12 + ...et_with_client_credentials_resource_gen.go | 107 ++++++ ...secret_with_generic_string_resource_gen.go | 97 ++++++ .../secret_show_output_gen.go | 82 +++++ ...ret_with_basic_authentication_model_gen.go | 137 ++++++++ .../secret_with_generic_string_model_gen.go | 122 +++++++ ...auth_authorization_code_grant_model_gen.go | 152 ++++++++ ...with_oauth_client_credentials_model_gen.go | 143 ++++++++ pkg/acceptance/check_destroy.go | 12 + pkg/acceptance/helpers/secret_client.go | 7 + pkg/provider/provider.go | 4 + pkg/provider/resources/resources.go | 4 + pkg/resources/secret_common.go | 99 ++++++ .../secret_with_basic_authentication.go | 183 ++++++++++ ...th_basic_authentication_acceptance_test.go | 227 ++++++++++++ pkg/resources/secret_with_generic_string.go | 160 +++++++++ ...ret_with_generic_string_acceptance_test.go | 187 ++++++++++ ...ret_with_oauth_authorization_code_grant.go | 211 ++++++++++++ ...uthorization_code_grant_acceptance_test.go | 325 ++++++++++++++++++ .../secret_with_oauth_client_credentials.go | 205 +++++++++++ ...auth_client_credentials_acceptance_test.go | 299 ++++++++++++++++ pkg/resources/show_and_describe_handlers.go | 22 ++ pkg/schemas/gen/sdk_show_result_structs.go | 1 + pkg/schemas/secret.go | 81 +++++ pkg/schemas/secret_gen.go | 69 ++++ pkg/sdk/secrets_def.go | 19 +- .../testint/secrets_gen_integration_test.go | 31 +- ...cret_with_authorization_code_grant.md.tmpl | 35 ++ .../secret_with_basic_authentication.md.tmpl | 35 ++ .../secret_with_client_credentials.md.tmpl | 35 ++ .../secret_with_generic_string.md.tmpl | 35 ++ 48 files changed, 3846 insertions(+), 21 deletions(-) create mode 100644 docs/resources/secret_with_authorization_code_grant.md create mode 100644 docs/resources/secret_with_basic_authentication.md create mode 100644 docs/resources/secret_with_client_credentials.md create mode 100644 docs/resources/secret_with_generic_string.md create mode 100644 examples/resources/snowflake_secret_with_authorization_code_grant/import.sh create mode 100644 examples/resources/snowflake_secret_with_authorization_code_grant/resource.tf create mode 100644 examples/resources/snowflake_secret_with_basic_authentication/import.sh create mode 100644 examples/resources/snowflake_secret_with_basic_authentication/resource.tf create mode 100644 examples/resources/snowflake_secret_with_client_credentials/import.sh create mode 100644 examples/resources/snowflake_secret_with_client_credentials/resource.tf create mode 100644 examples/resources/snowflake_secret_with_generic_string/import.sh create mode 100644 examples/resources/snowflake_secret_with_generic_string/resource.tf create mode 100644 pkg/acceptance/bettertestspoc/assert/resourceassert/secret_with_authorization_code_grant_resource_ext.go create mode 100644 pkg/acceptance/bettertestspoc/assert/resourceassert/secret_with_authorization_code_grant_resource_gen.go create mode 100644 pkg/acceptance/bettertestspoc/assert/resourceassert/secret_with_basic_authentication_resource_gen.go create mode 100644 pkg/acceptance/bettertestspoc/assert/resourceassert/secret_with_client_credentials_resource_ext.go create mode 100644 pkg/acceptance/bettertestspoc/assert/resourceassert/secret_with_client_credentials_resource_gen.go create mode 100644 pkg/acceptance/bettertestspoc/assert/resourceassert/secret_with_generic_string_resource_gen.go create mode 100644 pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/secret_show_output_gen.go create mode 100644 pkg/acceptance/bettertestspoc/config/model/secret_with_basic_authentication_model_gen.go create mode 100644 pkg/acceptance/bettertestspoc/config/model/secret_with_generic_string_model_gen.go create mode 100644 pkg/acceptance/bettertestspoc/config/model/secret_with_oauth_authorization_code_grant_model_gen.go create mode 100644 pkg/acceptance/bettertestspoc/config/model/secret_with_oauth_client_credentials_model_gen.go create mode 100644 pkg/resources/secret_common.go create mode 100644 pkg/resources/secret_with_basic_authentication.go create mode 100644 pkg/resources/secret_with_basic_authentication_acceptance_test.go create mode 100644 pkg/resources/secret_with_generic_string.go create mode 100644 pkg/resources/secret_with_generic_string_acceptance_test.go create mode 100644 pkg/resources/secret_with_oauth_authorization_code_grant.go create mode 100644 pkg/resources/secret_with_oauth_authorization_code_grant_acceptance_test.go create mode 100644 pkg/resources/secret_with_oauth_client_credentials.go create mode 100644 pkg/resources/secret_with_oauth_client_credentials_acceptance_test.go create mode 100644 pkg/schemas/secret.go create mode 100644 pkg/schemas/secret_gen.go create mode 100644 templates/resources/secret_with_authorization_code_grant.md.tmpl create mode 100644 templates/resources/secret_with_basic_authentication.md.tmpl create mode 100644 templates/resources/secret_with_client_credentials.md.tmpl create mode 100644 templates/resources/secret_with_generic_string.md.tmpl diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 48b7946ad3..697e7e47c6 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -7,6 +7,20 @@ across different versions. > [!TIP] > We highly recommend upgrading the versions one by one instead of bulk upgrades. +## v0.97.0 ➞ v0.98.0 + +### *(new feature)* Secret resources +Added a new secrets resources for managing secrets. +We decided to split each secret flow into individual resources. +This segregation was based on the secret flows in CREATE SECRET. i.e.: +- `snowflake_secret_with_client_credentials` +- `snowflake_secret_with_authorization_code_grant` +- `snowflake_secret_with_basic_authentication` +- `snowflake_secret_with_generic_string` + + +See reference [docs](https://docs.snowflake.com/en/sql-reference/sql/create-secret). + ## v0.96.0 ➞ v0.97.0 ### *(new feature)* snowflake_stream_on_table, snowflake_stream_on_external_table resource diff --git a/docs/resources/secret_with_authorization_code_grant.md b/docs/resources/secret_with_authorization_code_grant.md new file mode 100644 index 0000000000..5042b86916 --- /dev/null +++ b/docs/resources/secret_with_authorization_code_grant.md @@ -0,0 +1,104 @@ +--- +page_title: "snowflake_secret_with_authorization_code_grant Resource - terraform-provider-snowflake" +subcategory: "" +description: |- + Resource used to manage secret objects with OAuth Authorization Code Grant. For more information, check secret documentation https://docs.snowflake.com/en/sql-reference/sql/create-secret. +--- + +!> **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_secret_with_authorization_code_grant (Resource) + +Resource used to manage secret objects with OAuth Authorization Code Grant. For more information, check [secret documentation](https://docs.snowflake.com/en/sql-reference/sql/create-secret). + +## Example Usage + +```terraform +# basic resource +resource "snowflake_secret_with_authorization_code_grant" "test" { + name = "EXAMPLE_SECRET" + database = "EXAMPLE_DB" + schema = "EXAMPLE_SCHEMA" + api_authentication = "EXAMPLE_SECURITY_INTEGRATION_NAME" + oauth_refresh_token = "EXAMPLE_TOKEN" + oauth_refresh_token_expiry_time = "2025-01-02 15:04:01" +} + +# resource with all fields set +resource "snowflake_secret_with_authorization_code_grant" "test" { + name = "EXAMPLE_SECRET" + database = "EXAMPLE_DB" + schema = "EXAMPLE_SCHEMA" + api_authentication = "EXAMPLE_SECURITY_INTEGRATION_NAME" + oauth_refresh_token = "EXAMPLE_TOKEN" + oauth_refresh_token_expiry_time = "2025-01-02 15:04:01" + comment = "EXAMPLE_COMMENT" +} +``` +-> **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 + +- `api_authentication` (String) Specifies the name value of the Snowflake security integration that connects Snowflake to an external service. +- `database` (String) The database in which to create the secret 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: `|`, `.`, `(`, `)`, `"` +- `name` (String) String that specifies the identifier (i.e. name) for the secret, must be unique in your schema. 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: `|`, `.`, `(`, `)`, `"` +- `oauth_refresh_token` (String, Sensitive) Specifies the token as a string that is used to obtain a new access token from the OAuth authorization server when the access token expires. External changes for this field won't be detected. In case you want to apply external changes, you can re-create the resource manually using "terraform taint". +- `oauth_refresh_token_expiry_time` (String) Specifies the timestamp as a string when the OAuth refresh token expires. Accepted string formats: YYYY-MM-DD, YYYY-MM-DD HH:MI, YYYY-MM-DD HH:MI:SS, YYYY-MM-DD HH:MI +- `schema` (String) The schema in which to create the secret. 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 secret. + +### Read-Only + +- `describe_output` (List of Object) Outputs the result of `DESCRIBE SECRET` for the given secret. (see [below for nested schema](#nestedatt--describe_output)) +- `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 SECRETS` for the given secret. (see [below for nested schema](#nestedatt--show_output)) + + +### Nested Schema for `describe_output` + +Read-Only: + +- `comment` (String) +- `created_on` (String) +- `database_name` (String) +- `integration_name` (String) +- `name` (String) +- `oauth_access_token_expiry_time` (String) +- `oauth_refresh_token_expiry_time` (String) +- `oauth_scopes` (Set of String) +- `owner` (String) +- `schema_name` (String) +- `secret_type` (String) +- `username` (String) + + + +### Nested Schema for `show_output` + +Read-Only: + +- `comment` (String) +- `created_on` (String) +- `database_name` (String) +- `name` (String) +- `oauth_scopes` (Set of String) +- `owner` (String) +- `owner_role_type` (String) +- `schema_name` (String) +- `secret_type` (String) + +## Import + +Import is supported using the following syntax: + +```shell +terraform import snowflake_secret_with_authorization_code_grant.example '""."".""' +``` diff --git a/docs/resources/secret_with_basic_authentication.md b/docs/resources/secret_with_basic_authentication.md new file mode 100644 index 0000000000..6192ae72be --- /dev/null +++ b/docs/resources/secret_with_basic_authentication.md @@ -0,0 +1,101 @@ +--- +page_title: "snowflake_secret_with_basic_authentication Resource - terraform-provider-snowflake" +subcategory: "" +description: |- + Resource used to manage secret objects with Basic Authentication. For more information, check secret documentation https://docs.snowflake.com/en/sql-reference/sql/create-secret. +--- + +!> **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_secret_with_basic_authentication (Resource) + +Resource used to manage secret objects with Basic Authentication. For more information, check [secret documentation](https://docs.snowflake.com/en/sql-reference/sql/create-secret). + +## Example Usage + +```terraform +# basic resource +resource "snowflake_secret_with_basic_authentication" "test" { + name = "EXAMPLE_SECRET" + database = "EXAMPLE_DB" + schema = "EXAMPLE_SCHEMA" + username = "EXAMPLE_USERNAME" + password = "EXAMPLE_PASSWORD" +} + +# resource with all fields set +resource "snowflake_secret_with_basic_authentication" "test" { + name = "EXAMPLE_SECRET" + database = "EXAMPLE_DB" + schema = "EXAMPLE_SCHEMA" + username = "EXAMPLE_USERNAME" + password = "EXAMPLE_PASSWORD" + comment = "EXAMPLE_COMMENT" +} +``` +-> **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 + +- `database` (String) The database in which to create the secret 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: `|`, `.`, `(`, `)`, `"` +- `name` (String) String that specifies the identifier (i.e. name) for the secret, must be unique in your schema. 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: `|`, `.`, `(`, `)`, `"` +- `password` (String, Sensitive) Specifies the password value to store in the secret. External changes for this field won't be detected. In case you want to apply external changes, you can re-create the resource manually using "terraform taint". +- `schema` (String) The schema in which to create the secret. 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: `|`, `.`, `(`, `)`, `"` +- `username` (String, Sensitive) Specifies the username value to store in the secret. + +### Optional + +- `comment` (String) Specifies a comment for the secret. + +### Read-Only + +- `describe_output` (List of Object) Outputs the result of `DESCRIBE SECRET` for the given secret. (see [below for nested schema](#nestedatt--describe_output)) +- `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 SECRETS` for the given secret. (see [below for nested schema](#nestedatt--show_output)) + + +### Nested Schema for `describe_output` + +Read-Only: + +- `comment` (String) +- `created_on` (String) +- `database_name` (String) +- `integration_name` (String) +- `name` (String) +- `oauth_access_token_expiry_time` (String) +- `oauth_refresh_token_expiry_time` (String) +- `oauth_scopes` (Set of String) +- `owner` (String) +- `schema_name` (String) +- `secret_type` (String) +- `username` (String) + + + +### Nested Schema for `show_output` + +Read-Only: + +- `comment` (String) +- `created_on` (String) +- `database_name` (String) +- `name` (String) +- `oauth_scopes` (Set of String) +- `owner` (String) +- `owner_role_type` (String) +- `schema_name` (String) +- `secret_type` (String) + +## Import + +Import is supported using the following syntax: + +```shell +terraform import snowflake_secret_with_basic_authentication.example '""."".""' +``` diff --git a/docs/resources/secret_with_client_credentials.md b/docs/resources/secret_with_client_credentials.md new file mode 100644 index 0000000000..8d2c2e5895 --- /dev/null +++ b/docs/resources/secret_with_client_credentials.md @@ -0,0 +1,101 @@ +--- +page_title: "snowflake_secret_with_client_credentials Resource - terraform-provider-snowflake" +subcategory: "" +description: |- + Resource used to manage secret objects with OAuth Client Credentials. For more information, check secret documentation https://docs.snowflake.com/en/sql-reference/sql/create-secret. +--- + +!> **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_secret_with_client_credentials (Resource) + +Resource used to manage secret objects with OAuth Client Credentials. For more information, check [secret documentation](https://docs.snowflake.com/en/sql-reference/sql/create-secret). + +## Example Usage + +```terraform +# basic resource +resource "snowflake_secret_with_client_credentials" "test" { + name = "EXAMPLE_SECRET" + database = "EXAMPLE_DB" + schema = "EXAMPLE_SCHEMA" + api_authentication = "EXAMPLE_SECURITY_INTEGRATION_NAME" + oauth_scopes = ["useraccount", "testscope"] +} + +# resource with all fields set +resource "snowflake_secret_with_client_credentials" "test" { + name = "EXAMPLE_SECRET" + database = "EXAMPLE_DB" + schema = "EXAMPLE_SCHEMA" + api_authentication = "EXAMPLE_SECURITY_INTEGRATION_NAME" + oauth_scopes = ["useraccount", "testscope"] + comment = "EXAMPLE_COMMENT" +} +``` +-> **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 + +- `api_authentication` (String) Specifies the name value of the Snowflake security integration that connects Snowflake to an external service. +- `database` (String) The database in which to create the secret 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: `|`, `.`, `(`, `)`, `"` +- `name` (String) String that specifies the identifier (i.e. name) for the secret, must be unique in your schema. 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: `|`, `.`, `(`, `)`, `"` +- `oauth_scopes` (Set of String) Specifies a list of scopes to use when making a request from the OAuth server by a role with USAGE on the integration during the OAuth client credentials flow. +- `schema` (String) The schema in which to create the secret. 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 secret. + +### Read-Only + +- `describe_output` (List of Object) Outputs the result of `DESCRIBE SECRET` for the given secret. (see [below for nested schema](#nestedatt--describe_output)) +- `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 SECRETS` for the given secret. (see [below for nested schema](#nestedatt--show_output)) + + +### Nested Schema for `describe_output` + +Read-Only: + +- `comment` (String) +- `created_on` (String) +- `database_name` (String) +- `integration_name` (String) +- `name` (String) +- `oauth_access_token_expiry_time` (String) +- `oauth_refresh_token_expiry_time` (String) +- `oauth_scopes` (Set of String) +- `owner` (String) +- `schema_name` (String) +- `secret_type` (String) +- `username` (String) + + + +### Nested Schema for `show_output` + +Read-Only: + +- `comment` (String) +- `created_on` (String) +- `database_name` (String) +- `name` (String) +- `oauth_scopes` (Set of String) +- `owner` (String) +- `owner_role_type` (String) +- `schema_name` (String) +- `secret_type` (String) + +## Import + +Import is supported using the following syntax: + +```shell +terraform import snowflake_secret_with_client_credentials.example '""."".""' +``` diff --git a/docs/resources/secret_with_generic_string.md b/docs/resources/secret_with_generic_string.md new file mode 100644 index 0000000000..2a2783a71e --- /dev/null +++ b/docs/resources/secret_with_generic_string.md @@ -0,0 +1,98 @@ +--- +page_title: "snowflake_secret_with_generic_string Resource - terraform-provider-snowflake" +subcategory: "" +description: |- + Resource used to manage secret objects with Generic String. For more information, check secret documentation https://docs.snowflake.com/en/sql-reference/sql/create-secret. +--- + +!> **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_secret_with_generic_string (Resource) + +Resource used to manage secret objects with Generic String. For more information, check [secret documentation](https://docs.snowflake.com/en/sql-reference/sql/create-secret). + +## Example Usage + +```terraform +# basic resource +resource "snowflake_secret_with_generic_string" "test" { + name = "EXAMPLE_SECRET" + database = "EXAMPLE_DB" + schema = "EXAMPLE_SCHEMA" + secret_string = "EXAMPLE_SECRET_STRING" +} + +# resource with all fields set +resource "snowflake_secret_with_generic_string" "test" { + name = "EXAMPLE_SECRET" + database = "EXAMPLE_DB" + schema = "EXAMPLE_SCHEMA" + secret_string = "EXAMPLE_SECRET_STRING" + comment = "EXAMPLE_COMMENT" +} +``` +-> **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 + +- `database` (String) The database in which to create the secret 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: `|`, `.`, `(`, `)`, `"` +- `name` (String) String that specifies the identifier (i.e. name) for the secret, must be unique in your schema. 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: `|`, `.`, `(`, `)`, `"` +- `schema` (String) The schema in which to create the secret. 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: `|`, `.`, `(`, `)`, `"` +- `secret_string` (String, Sensitive) Specifies the string to store in the secret. The string can be an API token or a string of sensitive value that can be used in the handler code of a UDF or stored procedure. For details, see [Creating and using an external access integration](https://docs.snowflake.com/en/developer-guide/external-network-access/creating-using-external-network-access). You should not use this property to store any kind of OAuth token; use one of the other secret types for your OAuth use cases. External changes for this field won't be detected. In case you want to apply external changes, you can re-create the resource manually using "terraform taint". + +### Optional + +- `comment` (String) Specifies a comment for the secret. + +### Read-Only + +- `describe_output` (List of Object) Outputs the result of `DESCRIBE SECRET` for the given secret. (see [below for nested schema](#nestedatt--describe_output)) +- `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 SECRETS` for the given secret. (see [below for nested schema](#nestedatt--show_output)) + + +### Nested Schema for `describe_output` + +Read-Only: + +- `comment` (String) +- `created_on` (String) +- `database_name` (String) +- `integration_name` (String) +- `name` (String) +- `oauth_access_token_expiry_time` (String) +- `oauth_refresh_token_expiry_time` (String) +- `oauth_scopes` (Set of String) +- `owner` (String) +- `schema_name` (String) +- `secret_type` (String) +- `username` (String) + + + +### Nested Schema for `show_output` + +Read-Only: + +- `comment` (String) +- `created_on` (String) +- `database_name` (String) +- `name` (String) +- `oauth_scopes` (Set of String) +- `owner` (String) +- `owner_role_type` (String) +- `schema_name` (String) +- `secret_type` (String) + +## Import + +Import is supported using the following syntax: + +```shell +terraform import snowflake_secret_with_generic_string.example '""."".""' +``` diff --git a/examples/resources/snowflake_secret_with_authorization_code_grant/import.sh b/examples/resources/snowflake_secret_with_authorization_code_grant/import.sh new file mode 100644 index 0000000000..eca025de9e --- /dev/null +++ b/examples/resources/snowflake_secret_with_authorization_code_grant/import.sh @@ -0,0 +1 @@ +terraform import snowflake_secret_with_authorization_code_grant.example '""."".""' diff --git a/examples/resources/snowflake_secret_with_authorization_code_grant/resource.tf b/examples/resources/snowflake_secret_with_authorization_code_grant/resource.tf new file mode 100644 index 0000000000..bb45a36e87 --- /dev/null +++ b/examples/resources/snowflake_secret_with_authorization_code_grant/resource.tf @@ -0,0 +1,20 @@ +# basic resource +resource "snowflake_secret_with_authorization_code_grant" "test" { + name = "EXAMPLE_SECRET" + database = "EXAMPLE_DB" + schema = "EXAMPLE_SCHEMA" + api_authentication = "EXAMPLE_SECURITY_INTEGRATION_NAME" + oauth_refresh_token = "EXAMPLE_TOKEN" + oauth_refresh_token_expiry_time = "2025-01-02 15:04:01" +} + +# resource with all fields set +resource "snowflake_secret_with_authorization_code_grant" "test" { + name = "EXAMPLE_SECRET" + database = "EXAMPLE_DB" + schema = "EXAMPLE_SCHEMA" + api_authentication = "EXAMPLE_SECURITY_INTEGRATION_NAME" + oauth_refresh_token = "EXAMPLE_TOKEN" + oauth_refresh_token_expiry_time = "2025-01-02 15:04:01" + comment = "EXAMPLE_COMMENT" +} diff --git a/examples/resources/snowflake_secret_with_basic_authentication/import.sh b/examples/resources/snowflake_secret_with_basic_authentication/import.sh new file mode 100644 index 0000000000..caffffdfe8 --- /dev/null +++ b/examples/resources/snowflake_secret_with_basic_authentication/import.sh @@ -0,0 +1 @@ +terraform import snowflake_secret_with_basic_authentication.example '""."".""' diff --git a/examples/resources/snowflake_secret_with_basic_authentication/resource.tf b/examples/resources/snowflake_secret_with_basic_authentication/resource.tf new file mode 100644 index 0000000000..df60aa83b5 --- /dev/null +++ b/examples/resources/snowflake_secret_with_basic_authentication/resource.tf @@ -0,0 +1,18 @@ +# basic resource +resource "snowflake_secret_with_basic_authentication" "test" { + name = "EXAMPLE_SECRET" + database = "EXAMPLE_DB" + schema = "EXAMPLE_SCHEMA" + username = "EXAMPLE_USERNAME" + password = "EXAMPLE_PASSWORD" +} + +# resource with all fields set +resource "snowflake_secret_with_basic_authentication" "test" { + name = "EXAMPLE_SECRET" + database = "EXAMPLE_DB" + schema = "EXAMPLE_SCHEMA" + username = "EXAMPLE_USERNAME" + password = "EXAMPLE_PASSWORD" + comment = "EXAMPLE_COMMENT" +} diff --git a/examples/resources/snowflake_secret_with_client_credentials/import.sh b/examples/resources/snowflake_secret_with_client_credentials/import.sh new file mode 100644 index 0000000000..61dc5191ca --- /dev/null +++ b/examples/resources/snowflake_secret_with_client_credentials/import.sh @@ -0,0 +1 @@ +terraform import snowflake_secret_with_client_credentials.example '""."".""' diff --git a/examples/resources/snowflake_secret_with_client_credentials/resource.tf b/examples/resources/snowflake_secret_with_client_credentials/resource.tf new file mode 100644 index 0000000000..baaf605e67 --- /dev/null +++ b/examples/resources/snowflake_secret_with_client_credentials/resource.tf @@ -0,0 +1,18 @@ +# basic resource +resource "snowflake_secret_with_client_credentials" "test" { + name = "EXAMPLE_SECRET" + database = "EXAMPLE_DB" + schema = "EXAMPLE_SCHEMA" + api_authentication = "EXAMPLE_SECURITY_INTEGRATION_NAME" + oauth_scopes = ["useraccount", "testscope"] +} + +# resource with all fields set +resource "snowflake_secret_with_client_credentials" "test" { + name = "EXAMPLE_SECRET" + database = "EXAMPLE_DB" + schema = "EXAMPLE_SCHEMA" + api_authentication = "EXAMPLE_SECURITY_INTEGRATION_NAME" + oauth_scopes = ["useraccount", "testscope"] + comment = "EXAMPLE_COMMENT" +} diff --git a/examples/resources/snowflake_secret_with_generic_string/import.sh b/examples/resources/snowflake_secret_with_generic_string/import.sh new file mode 100644 index 0000000000..10b0f75023 --- /dev/null +++ b/examples/resources/snowflake_secret_with_generic_string/import.sh @@ -0,0 +1 @@ +terraform import snowflake_secret_with_generic_string.example '""."".""' diff --git a/examples/resources/snowflake_secret_with_generic_string/resource.tf b/examples/resources/snowflake_secret_with_generic_string/resource.tf new file mode 100644 index 0000000000..fab00a4340 --- /dev/null +++ b/examples/resources/snowflake_secret_with_generic_string/resource.tf @@ -0,0 +1,16 @@ +# basic resource +resource "snowflake_secret_with_generic_string" "test" { + name = "EXAMPLE_SECRET" + database = "EXAMPLE_DB" + schema = "EXAMPLE_SCHEMA" + secret_string = "EXAMPLE_SECRET_STRING" +} + +# resource with all fields set +resource "snowflake_secret_with_generic_string" "test" { + name = "EXAMPLE_SECRET" + database = "EXAMPLE_DB" + schema = "EXAMPLE_SCHEMA" + secret_string = "EXAMPLE_SECRET_STRING" + comment = "EXAMPLE_COMMENT" +} diff --git a/pkg/acceptance/bettertestspoc/README.md b/pkg/acceptance/bettertestspoc/README.md index 82b203f4d3..fcc4179cf2 100644 --- a/pkg/acceptance/bettertestspoc/README.md +++ b/pkg/acceptance/bettertestspoc/README.md @@ -325,6 +325,7 @@ it will result in: - Add support for datasource tests (assertions and config builders). - Consider overriding the assertions when invoking same check multiple times with different params (e.g. `Warehouse(...).HasType(X).HasType(Y)`; it could use the last-check-wins approach, to more easily reuse complex checks between the test steps). - Consider not adding the check for `show_output` presence on creation (same with `parameters`). The majority of the use cases need it to be present but there are a few others (like conditional presence in the datasources). Currently, it seems that they should be always present in the resources, so no change is made. Later, with adding the support for the datasource tests, consider simple destructive implementation like: +- Add support for `set` so that assertions like e.g. `oauth_scopes.*` could be done. ```go func (w *WarehouseDatasourceShowOutputAssert) IsEmpty() { w.assertions = make([]resourceAssertion, 0) diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/secret_with_authorization_code_grant_resource_ext.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/secret_with_authorization_code_grant_resource_ext.go new file mode 100644 index 0000000000..0044880efd --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/secret_with_authorization_code_grant_resource_ext.go @@ -0,0 +1,10 @@ +package resourceassert + +import ( + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" +) + +func (s *SecretWithAuthorizationCodeResourceAssert) HasOauthRefreshTokenExpiryTimeNotEmpty() *SecretWithAuthorizationCodeResourceAssert { + s.AddAssertion(assert.ValuePresent("oauth_refresh_token_expiry_time")) + return s +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/secret_with_authorization_code_grant_resource_gen.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/secret_with_authorization_code_grant_resource_gen.go new file mode 100644 index 0000000000..610faae14e --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/secret_with_authorization_code_grant_resource_gen.go @@ -0,0 +1,117 @@ +// Code generated by assertions generator; DO NOT EDIT. + +package resourceassert + +import ( + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" +) + +type SecretWithAuthorizationCodeResourceAssert struct { + *assert.ResourceAssert +} + +func SecretWithAuthorizationCodeResource(t *testing.T, name string) *SecretWithAuthorizationCodeResourceAssert { + t.Helper() + + return &SecretWithAuthorizationCodeResourceAssert{ + ResourceAssert: assert.NewResourceAssert(name, "resource"), + } +} + +func ImportedSecretWithAuthorizationCodeResource(t *testing.T, id string) *SecretWithAuthorizationCodeResourceAssert { + t.Helper() + + return &SecretWithAuthorizationCodeResourceAssert{ + ResourceAssert: assert.NewImportedResourceAssert(id, "imported resource"), + } +} + +/////////////////////////////////// +// Attribute value string checks // +/////////////////////////////////// + +func (s *SecretWithAuthorizationCodeResourceAssert) HasApiAuthenticationString(expected string) *SecretWithAuthorizationCodeResourceAssert { + s.AddAssertion(assert.ValueSet("api_authentication", expected)) + return s +} + +func (s *SecretWithAuthorizationCodeResourceAssert) HasCommentString(expected string) *SecretWithAuthorizationCodeResourceAssert { + s.AddAssertion(assert.ValueSet("comment", expected)) + return s +} + +func (s *SecretWithAuthorizationCodeResourceAssert) HasDatabaseString(expected string) *SecretWithAuthorizationCodeResourceAssert { + s.AddAssertion(assert.ValueSet("database", expected)) + return s +} + +func (s *SecretWithAuthorizationCodeResourceAssert) HasFullyQualifiedNameString(expected string) *SecretWithAuthorizationCodeResourceAssert { + s.AddAssertion(assert.ValueSet("fully_qualified_name", expected)) + return s +} + +func (s *SecretWithAuthorizationCodeResourceAssert) HasNameString(expected string) *SecretWithAuthorizationCodeResourceAssert { + s.AddAssertion(assert.ValueSet("name", expected)) + return s +} + +func (s *SecretWithAuthorizationCodeResourceAssert) HasOauthRefreshTokenString(expected string) *SecretWithAuthorizationCodeResourceAssert { + s.AddAssertion(assert.ValueSet("oauth_refresh_token", expected)) + return s +} + +func (s *SecretWithAuthorizationCodeResourceAssert) HasOauthRefreshTokenExpiryTimeString(expected string) *SecretWithAuthorizationCodeResourceAssert { + s.AddAssertion(assert.ValueSet("oauth_refresh_token_expiry_time", expected)) + return s +} + +func (s *SecretWithAuthorizationCodeResourceAssert) HasSchemaString(expected string) *SecretWithAuthorizationCodeResourceAssert { + s.AddAssertion(assert.ValueSet("schema", expected)) + return s +} + +//////////////////////////// +// Attribute empty checks // +//////////////////////////// + +func (s *SecretWithAuthorizationCodeResourceAssert) HasNoApiAuthentication() *SecretWithAuthorizationCodeResourceAssert { + s.AddAssertion(assert.ValueNotSet("api_authentication")) + return s +} + +func (s *SecretWithAuthorizationCodeResourceAssert) HasNoComment() *SecretWithAuthorizationCodeResourceAssert { + s.AddAssertion(assert.ValueNotSet("comment")) + return s +} + +func (s *SecretWithAuthorizationCodeResourceAssert) HasNoDatabase() *SecretWithAuthorizationCodeResourceAssert { + s.AddAssertion(assert.ValueNotSet("database")) + return s +} + +func (s *SecretWithAuthorizationCodeResourceAssert) HasNoFullyQualifiedName() *SecretWithAuthorizationCodeResourceAssert { + s.AddAssertion(assert.ValueNotSet("fully_qualified_name")) + return s +} + +func (s *SecretWithAuthorizationCodeResourceAssert) HasNoName() *SecretWithAuthorizationCodeResourceAssert { + s.AddAssertion(assert.ValueNotSet("name")) + return s +} + +func (s *SecretWithAuthorizationCodeResourceAssert) HasNoOauthRefreshToken() *SecretWithAuthorizationCodeResourceAssert { + s.AddAssertion(assert.ValueNotSet("oauth_refresh_token")) + return s +} + +func (s *SecretWithAuthorizationCodeResourceAssert) HasNoOauthRefreshTokenExpiryTime() *SecretWithAuthorizationCodeResourceAssert { + s.AddAssertion(assert.ValueNotSet("oauth_refresh_token_expiry_time")) + return s +} + +func (s *SecretWithAuthorizationCodeResourceAssert) HasNoSchema() *SecretWithAuthorizationCodeResourceAssert { + s.AddAssertion(assert.ValueNotSet("schema")) + return s +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/secret_with_basic_authentication_resource_gen.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/secret_with_basic_authentication_resource_gen.go new file mode 100644 index 0000000000..186aed6035 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/secret_with_basic_authentication_resource_gen.go @@ -0,0 +1,107 @@ +// Code generated by assertions generator; DO NOT EDIT. + +package resourceassert + +import ( + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" +) + +type SecretWithBasicAuthenticationResourceAssert struct { + *assert.ResourceAssert +} + +func SecretWithBasicAuthenticationResource(t *testing.T, name string) *SecretWithBasicAuthenticationResourceAssert { + t.Helper() + + return &SecretWithBasicAuthenticationResourceAssert{ + ResourceAssert: assert.NewResourceAssert(name, "resource"), + } +} + +func ImportedSecretWithBasicAuthenticationResource(t *testing.T, id string) *SecretWithBasicAuthenticationResourceAssert { + t.Helper() + + return &SecretWithBasicAuthenticationResourceAssert{ + ResourceAssert: assert.NewImportedResourceAssert(id, "imported resource"), + } +} + +/////////////////////////////////// +// Attribute value string checks // +/////////////////////////////////// + +func (s *SecretWithBasicAuthenticationResourceAssert) HasCommentString(expected string) *SecretWithBasicAuthenticationResourceAssert { + s.AddAssertion(assert.ValueSet("comment", expected)) + return s +} + +func (s *SecretWithBasicAuthenticationResourceAssert) HasDatabaseString(expected string) *SecretWithBasicAuthenticationResourceAssert { + s.AddAssertion(assert.ValueSet("database", expected)) + return s +} + +func (s *SecretWithBasicAuthenticationResourceAssert) HasFullyQualifiedNameString(expected string) *SecretWithBasicAuthenticationResourceAssert { + s.AddAssertion(assert.ValueSet("fully_qualified_name", expected)) + return s +} + +func (s *SecretWithBasicAuthenticationResourceAssert) HasNameString(expected string) *SecretWithBasicAuthenticationResourceAssert { + s.AddAssertion(assert.ValueSet("name", expected)) + return s +} + +func (s *SecretWithBasicAuthenticationResourceAssert) HasPasswordString(expected string) *SecretWithBasicAuthenticationResourceAssert { + s.AddAssertion(assert.ValueSet("password", expected)) + return s +} + +func (s *SecretWithBasicAuthenticationResourceAssert) HasSchemaString(expected string) *SecretWithBasicAuthenticationResourceAssert { + s.AddAssertion(assert.ValueSet("schema", expected)) + return s +} + +func (s *SecretWithBasicAuthenticationResourceAssert) HasUsernameString(expected string) *SecretWithBasicAuthenticationResourceAssert { + s.AddAssertion(assert.ValueSet("username", expected)) + return s +} + +//////////////////////////// +// Attribute empty checks // +//////////////////////////// + +func (s *SecretWithBasicAuthenticationResourceAssert) HasNoComment() *SecretWithBasicAuthenticationResourceAssert { + s.AddAssertion(assert.ValueNotSet("comment")) + return s +} + +func (s *SecretWithBasicAuthenticationResourceAssert) HasNoDatabase() *SecretWithBasicAuthenticationResourceAssert { + s.AddAssertion(assert.ValueNotSet("database")) + return s +} + +func (s *SecretWithBasicAuthenticationResourceAssert) HasNoFullyQualifiedName() *SecretWithBasicAuthenticationResourceAssert { + s.AddAssertion(assert.ValueNotSet("fully_qualified_name")) + return s +} + +func (s *SecretWithBasicAuthenticationResourceAssert) HasNoName() *SecretWithBasicAuthenticationResourceAssert { + s.AddAssertion(assert.ValueNotSet("name")) + return s +} + +func (s *SecretWithBasicAuthenticationResourceAssert) HasNoPassword() *SecretWithBasicAuthenticationResourceAssert { + s.AddAssertion(assert.ValueNotSet("password")) + return s +} + +func (s *SecretWithBasicAuthenticationResourceAssert) HasNoSchema() *SecretWithBasicAuthenticationResourceAssert { + s.AddAssertion(assert.ValueNotSet("schema")) + return s +} + +func (s *SecretWithBasicAuthenticationResourceAssert) HasNoUsername() *SecretWithBasicAuthenticationResourceAssert { + s.AddAssertion(assert.ValueNotSet("username")) + return s +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/secret_with_client_credentials_resource_ext.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/secret_with_client_credentials_resource_ext.go new file mode 100644 index 0000000000..27db6d5790 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/secret_with_client_credentials_resource_ext.go @@ -0,0 +1,12 @@ +package resourceassert + +import ( + "fmt" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" +) + +func (s *SecretWithClientCredentialsResourceAssert) HasOauthScopesLength(len int) *SecretWithClientCredentialsResourceAssert { + s.AddAssertion(assert.ValueSet("oauth_scopes.#", fmt.Sprintf("%d", len))) + return s +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/secret_with_client_credentials_resource_gen.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/secret_with_client_credentials_resource_gen.go new file mode 100644 index 0000000000..12a36fa46d --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/secret_with_client_credentials_resource_gen.go @@ -0,0 +1,107 @@ +// Code generated by assertions generator; DO NOT EDIT. + +package resourceassert + +import ( + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" +) + +type SecretWithClientCredentialsResourceAssert struct { + *assert.ResourceAssert +} + +func SecretWithClientCredentialsResource(t *testing.T, name string) *SecretWithClientCredentialsResourceAssert { + t.Helper() + + return &SecretWithClientCredentialsResourceAssert{ + ResourceAssert: assert.NewResourceAssert(name, "resource"), + } +} + +func ImportedSecretWithClientCredentialsResource(t *testing.T, id string) *SecretWithClientCredentialsResourceAssert { + t.Helper() + + return &SecretWithClientCredentialsResourceAssert{ + ResourceAssert: assert.NewImportedResourceAssert(id, "imported resource"), + } +} + +/////////////////////////////////// +// Attribute value string checks // +/////////////////////////////////// + +func (s *SecretWithClientCredentialsResourceAssert) HasApiAuthenticationString(expected string) *SecretWithClientCredentialsResourceAssert { + s.AddAssertion(assert.ValueSet("api_authentication", expected)) + return s +} + +func (s *SecretWithClientCredentialsResourceAssert) HasCommentString(expected string) *SecretWithClientCredentialsResourceAssert { + s.AddAssertion(assert.ValueSet("comment", expected)) + return s +} + +func (s *SecretWithClientCredentialsResourceAssert) HasDatabaseString(expected string) *SecretWithClientCredentialsResourceAssert { + s.AddAssertion(assert.ValueSet("database", expected)) + return s +} + +func (s *SecretWithClientCredentialsResourceAssert) HasFullyQualifiedNameString(expected string) *SecretWithClientCredentialsResourceAssert { + s.AddAssertion(assert.ValueSet("fully_qualified_name", expected)) + return s +} + +func (s *SecretWithClientCredentialsResourceAssert) HasNameString(expected string) *SecretWithClientCredentialsResourceAssert { + s.AddAssertion(assert.ValueSet("name", expected)) + return s +} + +func (s *SecretWithClientCredentialsResourceAssert) HasOauthScopesString(expected string) *SecretWithClientCredentialsResourceAssert { + s.AddAssertion(assert.ValueSet("oauth_scopes", expected)) + return s +} + +func (s *SecretWithClientCredentialsResourceAssert) HasSchemaString(expected string) *SecretWithClientCredentialsResourceAssert { + s.AddAssertion(assert.ValueSet("schema", expected)) + return s +} + +//////////////////////////// +// Attribute empty checks // +//////////////////////////// + +func (s *SecretWithClientCredentialsResourceAssert) HasNoApiAuthentication() *SecretWithClientCredentialsResourceAssert { + s.AddAssertion(assert.ValueNotSet("api_authentication")) + return s +} + +func (s *SecretWithClientCredentialsResourceAssert) HasNoComment() *SecretWithClientCredentialsResourceAssert { + s.AddAssertion(assert.ValueNotSet("comment")) + return s +} + +func (s *SecretWithClientCredentialsResourceAssert) HasNoDatabase() *SecretWithClientCredentialsResourceAssert { + s.AddAssertion(assert.ValueNotSet("database")) + return s +} + +func (s *SecretWithClientCredentialsResourceAssert) HasNoFullyQualifiedName() *SecretWithClientCredentialsResourceAssert { + s.AddAssertion(assert.ValueNotSet("fully_qualified_name")) + return s +} + +func (s *SecretWithClientCredentialsResourceAssert) HasNoName() *SecretWithClientCredentialsResourceAssert { + s.AddAssertion(assert.ValueNotSet("name")) + return s +} + +func (s *SecretWithClientCredentialsResourceAssert) HasNoOauthScopes() *SecretWithClientCredentialsResourceAssert { + s.AddAssertion(assert.ValueNotSet("oauth_scopes")) + return s +} + +func (s *SecretWithClientCredentialsResourceAssert) HasNoSchema() *SecretWithClientCredentialsResourceAssert { + s.AddAssertion(assert.ValueNotSet("schema")) + return s +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceassert/secret_with_generic_string_resource_gen.go b/pkg/acceptance/bettertestspoc/assert/resourceassert/secret_with_generic_string_resource_gen.go new file mode 100644 index 0000000000..d2d682aa1f --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceassert/secret_with_generic_string_resource_gen.go @@ -0,0 +1,97 @@ +// Code generated by assertions generator; DO NOT EDIT. + +package resourceassert + +import ( + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" +) + +type SecretWithGenericStringResourceAssert struct { + *assert.ResourceAssert +} + +func SecretWithGenericStringResource(t *testing.T, name string) *SecretWithGenericStringResourceAssert { + t.Helper() + + return &SecretWithGenericStringResourceAssert{ + ResourceAssert: assert.NewResourceAssert(name, "resource"), + } +} + +func ImportedSecretWithGenericStringResource(t *testing.T, id string) *SecretWithGenericStringResourceAssert { + t.Helper() + + return &SecretWithGenericStringResourceAssert{ + ResourceAssert: assert.NewImportedResourceAssert(id, "imported resource"), + } +} + +/////////////////////////////////// +// Attribute value string checks // +/////////////////////////////////// + +func (s *SecretWithGenericStringResourceAssert) HasCommentString(expected string) *SecretWithGenericStringResourceAssert { + s.AddAssertion(assert.ValueSet("comment", expected)) + return s +} + +func (s *SecretWithGenericStringResourceAssert) HasDatabaseString(expected string) *SecretWithGenericStringResourceAssert { + s.AddAssertion(assert.ValueSet("database", expected)) + return s +} + +func (s *SecretWithGenericStringResourceAssert) HasFullyQualifiedNameString(expected string) *SecretWithGenericStringResourceAssert { + s.AddAssertion(assert.ValueSet("fully_qualified_name", expected)) + return s +} + +func (s *SecretWithGenericStringResourceAssert) HasNameString(expected string) *SecretWithGenericStringResourceAssert { + s.AddAssertion(assert.ValueSet("name", expected)) + return s +} + +func (s *SecretWithGenericStringResourceAssert) HasSchemaString(expected string) *SecretWithGenericStringResourceAssert { + s.AddAssertion(assert.ValueSet("schema", expected)) + return s +} + +func (s *SecretWithGenericStringResourceAssert) HasSecretStringString(expected string) *SecretWithGenericStringResourceAssert { + s.AddAssertion(assert.ValueSet("secret_string", expected)) + return s +} + +//////////////////////////// +// Attribute empty checks // +//////////////////////////// + +func (s *SecretWithGenericStringResourceAssert) HasNoComment() *SecretWithGenericStringResourceAssert { + s.AddAssertion(assert.ValueNotSet("comment")) + return s +} + +func (s *SecretWithGenericStringResourceAssert) HasNoDatabase() *SecretWithGenericStringResourceAssert { + s.AddAssertion(assert.ValueNotSet("database")) + return s +} + +func (s *SecretWithGenericStringResourceAssert) HasNoFullyQualifiedName() *SecretWithGenericStringResourceAssert { + s.AddAssertion(assert.ValueNotSet("fully_qualified_name")) + return s +} + +func (s *SecretWithGenericStringResourceAssert) HasNoName() *SecretWithGenericStringResourceAssert { + s.AddAssertion(assert.ValueNotSet("name")) + return s +} + +func (s *SecretWithGenericStringResourceAssert) HasNoSchema() *SecretWithGenericStringResourceAssert { + s.AddAssertion(assert.ValueNotSet("schema")) + return s +} + +func (s *SecretWithGenericStringResourceAssert) HasNoSecretString() *SecretWithGenericStringResourceAssert { + s.AddAssertion(assert.ValueNotSet("secret_string")) + return s +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/secret_show_output_gen.go b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/secret_show_output_gen.go new file mode 100644 index 0000000000..665693da07 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/secret_show_output_gen.go @@ -0,0 +1,82 @@ +// 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 SecretShowOutputAssert struct { + *assert.ResourceAssert +} + +func SecretShowOutput(t *testing.T, name string) *SecretShowOutputAssert { + t.Helper() + + s := SecretShowOutputAssert{ + ResourceAssert: assert.NewResourceAssert(name, "show_output"), + } + s.AddAssertion(assert.ValueSet("show_output.#", "1")) + return &s +} + +func ImportedSecretShowOutput(t *testing.T, id string) *SecretShowOutputAssert { + t.Helper() + + s := SecretShowOutputAssert{ + ResourceAssert: assert.NewImportedResourceAssert(id, "show_output"), + } + s.AddAssertion(assert.ValueSet("show_output.#", "1")) + return &s +} + +//////////////////////////// +// Attribute value checks // +//////////////////////////// + +func (s *SecretShowOutputAssert) HasCreatedOn(expected time.Time) *SecretShowOutputAssert { + s.AddAssertion(assert.ResourceShowOutputValueSet("created_on", expected.String())) + return s +} + +func (s *SecretShowOutputAssert) HasName(expected string) *SecretShowOutputAssert { + s.AddAssertion(assert.ResourceShowOutputValueSet("name", expected)) + return s +} + +func (s *SecretShowOutputAssert) HasSchemaName(expected string) *SecretShowOutputAssert { + s.AddAssertion(assert.ResourceShowOutputValueSet("schema_name", expected)) + return s +} + +func (s *SecretShowOutputAssert) HasDatabaseName(expected string) *SecretShowOutputAssert { + s.AddAssertion(assert.ResourceShowOutputValueSet("database_name", expected)) + return s +} + +func (s *SecretShowOutputAssert) HasOwner(expected string) *SecretShowOutputAssert { + s.AddAssertion(assert.ResourceShowOutputValueSet("owner", expected)) + return s +} + +func (s *SecretShowOutputAssert) HasComment(expected string) *SecretShowOutputAssert { + s.AddAssertion(assert.ResourceShowOutputValueSet("comment", expected)) + return s +} + +func (s *SecretShowOutputAssert) HasSecretType(expected string) *SecretShowOutputAssert { + s.AddAssertion(assert.ResourceShowOutputValueSet("secret_type", expected)) + return s +} + +func (s *SecretShowOutputAssert) HasOwnerRoleType(expected string) *SecretShowOutputAssert { + s.AddAssertion(assert.ResourceShowOutputValueSet("owner_role_type", expected)) + return s +} diff --git a/pkg/acceptance/bettertestspoc/config/model/secret_with_basic_authentication_model_gen.go b/pkg/acceptance/bettertestspoc/config/model/secret_with_basic_authentication_model_gen.go new file mode 100644 index 0000000000..975b3c7ff7 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/model/secret_with_basic_authentication_model_gen.go @@ -0,0 +1,137 @@ +// 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 SecretWithBasicAuthenticationModel struct { + Comment tfconfig.Variable `json:"comment,omitempty"` + Database tfconfig.Variable `json:"database,omitempty"` + FullyQualifiedName tfconfig.Variable `json:"fully_qualified_name,omitempty"` + Name tfconfig.Variable `json:"name,omitempty"` + Password tfconfig.Variable `json:"password,omitempty"` + Schema tfconfig.Variable `json:"schema,omitempty"` + Username tfconfig.Variable `json:"username,omitempty"` + + *config.ResourceModelMeta +} + +///////////////////////////////////////////////// +// Basic builders (resource name and required) // +///////////////////////////////////////////////// + +func SecretWithBasicAuthentication( + resourceName string, + database string, + name string, + password string, + schema string, + username string, +) *SecretWithBasicAuthenticationModel { + s := &SecretWithBasicAuthenticationModel{ResourceModelMeta: config.Meta(resourceName, resources.SecretWithBasicAuthentication)} + s.WithDatabase(database) + s.WithName(name) + s.WithPassword(password) + s.WithSchema(schema) + s.WithUsername(username) + return s +} + +func SecretWithBasicAuthenticationWithDefaultMeta( + database string, + name string, + password string, + schema string, + username string, +) *SecretWithBasicAuthenticationModel { + s := &SecretWithBasicAuthenticationModel{ResourceModelMeta: config.DefaultMeta(resources.SecretWithBasicAuthentication)} + s.WithDatabase(database) + s.WithName(name) + s.WithPassword(password) + s.WithSchema(schema) + s.WithUsername(username) + return s +} + +///////////////////////////////// +// below all the proper values // +///////////////////////////////// + +func (s *SecretWithBasicAuthenticationModel) WithComment(comment string) *SecretWithBasicAuthenticationModel { + s.Comment = tfconfig.StringVariable(comment) + return s +} + +func (s *SecretWithBasicAuthenticationModel) WithDatabase(database string) *SecretWithBasicAuthenticationModel { + s.Database = tfconfig.StringVariable(database) + return s +} + +func (s *SecretWithBasicAuthenticationModel) WithFullyQualifiedName(fullyQualifiedName string) *SecretWithBasicAuthenticationModel { + s.FullyQualifiedName = tfconfig.StringVariable(fullyQualifiedName) + return s +} + +func (s *SecretWithBasicAuthenticationModel) WithName(name string) *SecretWithBasicAuthenticationModel { + s.Name = tfconfig.StringVariable(name) + return s +} + +func (s *SecretWithBasicAuthenticationModel) WithPassword(password string) *SecretWithBasicAuthenticationModel { + s.Password = tfconfig.StringVariable(password) + return s +} + +func (s *SecretWithBasicAuthenticationModel) WithSchema(schema string) *SecretWithBasicAuthenticationModel { + s.Schema = tfconfig.StringVariable(schema) + return s +} + +func (s *SecretWithBasicAuthenticationModel) WithUsername(username string) *SecretWithBasicAuthenticationModel { + s.Username = tfconfig.StringVariable(username) + return s +} + +////////////////////////////////////////// +// below it's possible to set any value // +////////////////////////////////////////// + +func (s *SecretWithBasicAuthenticationModel) WithCommentValue(value tfconfig.Variable) *SecretWithBasicAuthenticationModel { + s.Comment = value + return s +} + +func (s *SecretWithBasicAuthenticationModel) WithDatabaseValue(value tfconfig.Variable) *SecretWithBasicAuthenticationModel { + s.Database = value + return s +} + +func (s *SecretWithBasicAuthenticationModel) WithFullyQualifiedNameValue(value tfconfig.Variable) *SecretWithBasicAuthenticationModel { + s.FullyQualifiedName = value + return s +} + +func (s *SecretWithBasicAuthenticationModel) WithNameValue(value tfconfig.Variable) *SecretWithBasicAuthenticationModel { + s.Name = value + return s +} + +func (s *SecretWithBasicAuthenticationModel) WithPasswordValue(value tfconfig.Variable) *SecretWithBasicAuthenticationModel { + s.Password = value + return s +} + +func (s *SecretWithBasicAuthenticationModel) WithSchemaValue(value tfconfig.Variable) *SecretWithBasicAuthenticationModel { + s.Schema = value + return s +} + +func (s *SecretWithBasicAuthenticationModel) WithUsernameValue(value tfconfig.Variable) *SecretWithBasicAuthenticationModel { + s.Username = value + return s +} diff --git a/pkg/acceptance/bettertestspoc/config/model/secret_with_generic_string_model_gen.go b/pkg/acceptance/bettertestspoc/config/model/secret_with_generic_string_model_gen.go new file mode 100644 index 0000000000..e2f441ef3f --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/model/secret_with_generic_string_model_gen.go @@ -0,0 +1,122 @@ +// 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 SecretWithGenericStringModel struct { + Comment tfconfig.Variable `json:"comment,omitempty"` + Database tfconfig.Variable `json:"database,omitempty"` + FullyQualifiedName tfconfig.Variable `json:"fully_qualified_name,omitempty"` + Name tfconfig.Variable `json:"name,omitempty"` + Schema tfconfig.Variable `json:"schema,omitempty"` + SecretString tfconfig.Variable `json:"secret_string,omitempty"` + + *config.ResourceModelMeta +} + +///////////////////////////////////////////////// +// Basic builders (resource name and required) // +///////////////////////////////////////////////// + +func SecretWithGenericString( + resourceName string, + database string, + name string, + schema string, + secretString string, +) *SecretWithGenericStringModel { + s := &SecretWithGenericStringModel{ResourceModelMeta: config.Meta(resourceName, resources.SecretWithGenericString)} + s.WithDatabase(database) + s.WithName(name) + s.WithSchema(schema) + s.WithSecretString(secretString) + return s +} + +func SecretWithGenericStringWithDefaultMeta( + database string, + name string, + schema string, + secretString string, +) *SecretWithGenericStringModel { + s := &SecretWithGenericStringModel{ResourceModelMeta: config.DefaultMeta(resources.SecretWithGenericString)} + s.WithDatabase(database) + s.WithName(name) + s.WithSchema(schema) + s.WithSecretString(secretString) + return s +} + +///////////////////////////////// +// below all the proper values // +///////////////////////////////// + +func (s *SecretWithGenericStringModel) WithComment(comment string) *SecretWithGenericStringModel { + s.Comment = tfconfig.StringVariable(comment) + return s +} + +func (s *SecretWithGenericStringModel) WithDatabase(database string) *SecretWithGenericStringModel { + s.Database = tfconfig.StringVariable(database) + return s +} + +func (s *SecretWithGenericStringModel) WithFullyQualifiedName(fullyQualifiedName string) *SecretWithGenericStringModel { + s.FullyQualifiedName = tfconfig.StringVariable(fullyQualifiedName) + return s +} + +func (s *SecretWithGenericStringModel) WithName(name string) *SecretWithGenericStringModel { + s.Name = tfconfig.StringVariable(name) + return s +} + +func (s *SecretWithGenericStringModel) WithSchema(schema string) *SecretWithGenericStringModel { + s.Schema = tfconfig.StringVariable(schema) + return s +} + +func (s *SecretWithGenericStringModel) WithSecretString(secretString string) *SecretWithGenericStringModel { + s.SecretString = tfconfig.StringVariable(secretString) + return s +} + +////////////////////////////////////////// +// below it's possible to set any value // +////////////////////////////////////////// + +func (s *SecretWithGenericStringModel) WithCommentValue(value tfconfig.Variable) *SecretWithGenericStringModel { + s.Comment = value + return s +} + +func (s *SecretWithGenericStringModel) WithDatabaseValue(value tfconfig.Variable) *SecretWithGenericStringModel { + s.Database = value + return s +} + +func (s *SecretWithGenericStringModel) WithFullyQualifiedNameValue(value tfconfig.Variable) *SecretWithGenericStringModel { + s.FullyQualifiedName = value + return s +} + +func (s *SecretWithGenericStringModel) WithNameValue(value tfconfig.Variable) *SecretWithGenericStringModel { + s.Name = value + return s +} + +func (s *SecretWithGenericStringModel) WithSchemaValue(value tfconfig.Variable) *SecretWithGenericStringModel { + s.Schema = value + return s +} + +func (s *SecretWithGenericStringModel) WithSecretStringValue(value tfconfig.Variable) *SecretWithGenericStringModel { + s.SecretString = value + return s +} diff --git a/pkg/acceptance/bettertestspoc/config/model/secret_with_oauth_authorization_code_grant_model_gen.go b/pkg/acceptance/bettertestspoc/config/model/secret_with_oauth_authorization_code_grant_model_gen.go new file mode 100644 index 0000000000..212740cc5d --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/model/secret_with_oauth_authorization_code_grant_model_gen.go @@ -0,0 +1,152 @@ +// 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 SecretWithAuthorizationCodeGrantModel struct { + ApiAuthentication tfconfig.Variable `json:"api_authentication,omitempty"` + Comment tfconfig.Variable `json:"comment,omitempty"` + Database tfconfig.Variable `json:"database,omitempty"` + FullyQualifiedName tfconfig.Variable `json:"fully_qualified_name,omitempty"` + Name tfconfig.Variable `json:"name,omitempty"` + OauthRefreshToken tfconfig.Variable `json:"oauth_refresh_token,omitempty"` + OauthRefreshTokenExpiryTime tfconfig.Variable `json:"oauth_refresh_token_expiry_time,omitempty"` + Schema tfconfig.Variable `json:"schema,omitempty"` + + *config.ResourceModelMeta +} + +///////////////////////////////////////////////// +// Basic builders (resource name and required) // +///////////////////////////////////////////////// + +func SecretWithAuthorizationCodeGrant( + resourceName string, + apiAuthentication string, + database string, + schema string, + name string, + oauthRefreshToken string, + oauthRefreshTokenExpiryTime string, +) *SecretWithAuthorizationCodeGrantModel { + s := &SecretWithAuthorizationCodeGrantModel{ResourceModelMeta: config.Meta(resourceName, resources.SecretWithAuthorizationCodeGrant)} + s.WithApiAuthentication(apiAuthentication) + s.WithDatabase(database) + s.WithName(name) + s.WithOauthRefreshToken(oauthRefreshToken) + s.WithOauthRefreshTokenExpiryTime(oauthRefreshTokenExpiryTime) + s.WithSchema(schema) + return s +} + +func SecretWithAuthorizationCodeGrantWithDefaultMeta( + apiAuthentication string, + database string, + name string, + oauthRefreshToken string, + oauthRefreshTokenExpiryTime string, + schema string, +) *SecretWithAuthorizationCodeGrantModel { + s := &SecretWithAuthorizationCodeGrantModel{ResourceModelMeta: config.DefaultMeta(resources.SecretWithAuthorizationCodeGrant)} + s.WithApiAuthentication(apiAuthentication) + s.WithDatabase(database) + s.WithName(name) + s.WithOauthRefreshToken(oauthRefreshToken) + s.WithOauthRefreshTokenExpiryTime(oauthRefreshTokenExpiryTime) + s.WithSchema(schema) + return s +} + +///////////////////////////////// +// below all the proper values // +///////////////////////////////// + +func (s *SecretWithAuthorizationCodeGrantModel) WithApiAuthentication(apiAuthentication string) *SecretWithAuthorizationCodeGrantModel { + s.ApiAuthentication = tfconfig.StringVariable(apiAuthentication) + return s +} + +func (s *SecretWithAuthorizationCodeGrantModel) WithComment(comment string) *SecretWithAuthorizationCodeGrantModel { + s.Comment = tfconfig.StringVariable(comment) + return s +} + +func (s *SecretWithAuthorizationCodeGrantModel) WithDatabase(database string) *SecretWithAuthorizationCodeGrantModel { + s.Database = tfconfig.StringVariable(database) + return s +} + +func (s *SecretWithAuthorizationCodeGrantModel) WithFullyQualifiedName(fullyQualifiedName string) *SecretWithAuthorizationCodeGrantModel { + s.FullyQualifiedName = tfconfig.StringVariable(fullyQualifiedName) + return s +} + +func (s *SecretWithAuthorizationCodeGrantModel) WithName(name string) *SecretWithAuthorizationCodeGrantModel { + s.Name = tfconfig.StringVariable(name) + return s +} + +func (s *SecretWithAuthorizationCodeGrantModel) WithOauthRefreshToken(oauthRefreshToken string) *SecretWithAuthorizationCodeGrantModel { + s.OauthRefreshToken = tfconfig.StringVariable(oauthRefreshToken) + return s +} + +func (s *SecretWithAuthorizationCodeGrantModel) WithOauthRefreshTokenExpiryTime(oauthRefreshTokenExpiryTime string) *SecretWithAuthorizationCodeGrantModel { + s.OauthRefreshTokenExpiryTime = tfconfig.StringVariable(oauthRefreshTokenExpiryTime) + return s +} + +func (s *SecretWithAuthorizationCodeGrantModel) WithSchema(schema string) *SecretWithAuthorizationCodeGrantModel { + s.Schema = tfconfig.StringVariable(schema) + return s +} + +////////////////////////////////////////// +// below it's possible to set any value // +////////////////////////////////////////// + +func (s *SecretWithAuthorizationCodeGrantModel) WithApiAuthenticationValue(value tfconfig.Variable) *SecretWithAuthorizationCodeGrantModel { + s.ApiAuthentication = value + return s +} + +func (s *SecretWithAuthorizationCodeGrantModel) WithCommentValue(value tfconfig.Variable) *SecretWithAuthorizationCodeGrantModel { + s.Comment = value + return s +} + +func (s *SecretWithAuthorizationCodeGrantModel) WithDatabaseValue(value tfconfig.Variable) *SecretWithAuthorizationCodeGrantModel { + s.Database = value + return s +} + +func (s *SecretWithAuthorizationCodeGrantModel) WithFullyQualifiedNameValue(value tfconfig.Variable) *SecretWithAuthorizationCodeGrantModel { + s.FullyQualifiedName = value + return s +} + +func (s *SecretWithAuthorizationCodeGrantModel) WithNameValue(value tfconfig.Variable) *SecretWithAuthorizationCodeGrantModel { + s.Name = value + return s +} + +func (s *SecretWithAuthorizationCodeGrantModel) WithOauthRefreshTokenValue(value tfconfig.Variable) *SecretWithAuthorizationCodeGrantModel { + s.OauthRefreshToken = value + return s +} + +func (s *SecretWithAuthorizationCodeGrantModel) WithOauthRefreshTokenExpiryTimeValue(value tfconfig.Variable) *SecretWithAuthorizationCodeGrantModel { + s.OauthRefreshTokenExpiryTime = value + return s +} + +func (s *SecretWithAuthorizationCodeGrantModel) WithSchemaValue(value tfconfig.Variable) *SecretWithAuthorizationCodeGrantModel { + s.Schema = value + return s +} diff --git a/pkg/acceptance/bettertestspoc/config/model/secret_with_oauth_client_credentials_model_gen.go b/pkg/acceptance/bettertestspoc/config/model/secret_with_oauth_client_credentials_model_gen.go new file mode 100644 index 0000000000..9e093b48d0 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/model/secret_with_oauth_client_credentials_model_gen.go @@ -0,0 +1,143 @@ +// 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 SecretWithClientCredentialsModel struct { + ApiAuthentication tfconfig.Variable `json:"api_authentication,omitempty"` + Comment tfconfig.Variable `json:"comment,omitempty"` + Database tfconfig.Variable `json:"database,omitempty"` + FullyQualifiedName tfconfig.Variable `json:"fully_qualified_name,omitempty"` + Name tfconfig.Variable `json:"name,omitempty"` + OauthScopes tfconfig.Variable `json:"oauth_scopes,omitempty"` + Schema tfconfig.Variable `json:"schema,omitempty"` + + *config.ResourceModelMeta +} + +///////////////////////////////////////////////// +// Basic builders (resource name and required) // +///////////////////////////////////////////////// + +func SecretWithClientCredentials( + resourceName string, + apiAuthentication string, + database string, + schema string, + name string, + oauthScopes []string, +) *SecretWithClientCredentialsModel { + s := &SecretWithClientCredentialsModel{ResourceModelMeta: config.Meta(resourceName, resources.SecretWithClientCredentials)} + s.WithApiAuthentication(apiAuthentication) + s.WithDatabase(database) + s.WithName(name) + s.WithOauthScopes(oauthScopes) + s.WithSchema(schema) + return s +} + +func SecretWithClientCredentialsWithDefaultMeta( + apiAuthentication string, + database string, + name string, + oauthScopes []string, + schema string, +) *SecretWithClientCredentialsModel { + s := &SecretWithClientCredentialsModel{ResourceModelMeta: config.DefaultMeta(resources.SecretWithClientCredentials)} + s.WithApiAuthentication(apiAuthentication) + s.WithDatabase(database) + s.WithName(name) + s.WithOauthScopes(oauthScopes) + s.WithSchema(schema) + return s +} + +///////////////////////////////// +// below all the proper values // +///////////////////////////////// + +func (s *SecretWithClientCredentialsModel) WithApiAuthentication(apiAuthentication string) *SecretWithClientCredentialsModel { + s.ApiAuthentication = tfconfig.StringVariable(apiAuthentication) + return s +} + +func (s *SecretWithClientCredentialsModel) WithComment(comment string) *SecretWithClientCredentialsModel { + s.Comment = tfconfig.StringVariable(comment) + return s +} + +func (s *SecretWithClientCredentialsModel) WithDatabase(database string) *SecretWithClientCredentialsModel { + s.Database = tfconfig.StringVariable(database) + return s +} + +func (s *SecretWithClientCredentialsModel) WithFullyQualifiedName(fullyQualifiedName string) *SecretWithClientCredentialsModel { + s.FullyQualifiedName = tfconfig.StringVariable(fullyQualifiedName) + return s +} + +func (s *SecretWithClientCredentialsModel) WithName(name string) *SecretWithClientCredentialsModel { + s.Name = tfconfig.StringVariable(name) + return s +} + +// oauth_scopes attribute type is not yet supported, so WithOauthScopes can't be generated + +func (s *SecretWithClientCredentialsModel) WithSchema(schema string) *SecretWithClientCredentialsModel { + s.Schema = tfconfig.StringVariable(schema) + return s +} + +////////////////////////////////////////// +// below it's possible to set any value // +////////////////////////////////////////// + +func (s *SecretWithClientCredentialsModel) WithApiAuthenticationValue(value tfconfig.Variable) *SecretWithClientCredentialsModel { + s.ApiAuthentication = value + return s +} + +func (s *SecretWithClientCredentialsModel) WithCommentValue(value tfconfig.Variable) *SecretWithClientCredentialsModel { + s.Comment = value + return s +} + +func (s *SecretWithClientCredentialsModel) WithDatabaseValue(value tfconfig.Variable) *SecretWithClientCredentialsModel { + s.Database = value + return s +} + +func (s *SecretWithClientCredentialsModel) WithFullyQualifiedNameValue(value tfconfig.Variable) *SecretWithClientCredentialsModel { + s.FullyQualifiedName = value + return s +} + +func (s *SecretWithClientCredentialsModel) WithNameValue(value tfconfig.Variable) *SecretWithClientCredentialsModel { + s.Name = value + return s +} + +func (s *SecretWithClientCredentialsModel) WithOauthScopesValue(value tfconfig.Variable) *SecretWithClientCredentialsModel { + s.OauthScopes = value + return s +} + +func (s *SecretWithClientCredentialsModel) WithSchemaValue(value tfconfig.Variable) *SecretWithClientCredentialsModel { + s.Schema = value + return s +} +func (s *SecretWithClientCredentialsModel) WithOauthScopes(oauthScopes []string) *SecretWithClientCredentialsModel { + oauthScopesStringVariables := make([]tfconfig.Variable, len(oauthScopes)) + for i, v := range oauthScopes { + oauthScopesStringVariables[i] = tfconfig.StringVariable(v) + } + + s.OauthScopes = tfconfig.SetVariable(oauthScopesStringVariables...) + return s +} diff --git a/pkg/acceptance/check_destroy.go b/pkg/acceptance/check_destroy.go index 2ee96796d1..c39bd71174 100644 --- a/pkg/acceptance/check_destroy.go +++ b/pkg/acceptance/check_destroy.go @@ -179,6 +179,18 @@ var showByIdFunctions = map[resources.Resource]showByIdFunc{ resources.SecondaryDatabase: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error { return runShowById(ctx, id, client.Databases.ShowByID) }, + resources.SecretWithAuthorizationCodeGrant: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error { + return runShowById(ctx, id, client.Secrets.ShowByID) + }, + resources.SecretWithBasicAuthentication: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error { + return runShowById(ctx, id, client.Secrets.ShowByID) + }, + resources.SecretWithClientCredentials: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error { + return runShowById(ctx, id, client.Secrets.ShowByID) + }, + resources.SecretWithGenericString: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error { + return runShowById(ctx, id, client.Secrets.ShowByID) + }, resources.Sequence: func(ctx context.Context, client *sdk.Client, id sdk.ObjectIdentifier) error { return runShowById(ctx, id, client.Sequences.ShowByID) }, diff --git a/pkg/acceptance/helpers/secret_client.go b/pkg/acceptance/helpers/secret_client.go index e3eb87c8f1..184498784b 100644 --- a/pkg/acceptance/helpers/secret_client.go +++ b/pkg/acceptance/helpers/secret_client.go @@ -82,6 +82,13 @@ func (c *SecretClient) CreateWithGenericString(t *testing.T, id sdk.SchemaObject return secret, c.DropFunc(t, id) } +func (c *SecretClient) Alter(t *testing.T, req *sdk.AlterSecretRequest) { + t.Helper() + ctx := context.Background() + err := c.client().Alter(ctx, req) + require.NoError(t, err) +} + func (c *SecretClient) DropFunc(t *testing.T, id sdk.SchemaObjectIdentifier) func() { t.Helper() ctx := context.Background() diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 6e96b66904..cb28e3eba6 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -472,6 +472,10 @@ func getResources() map[string]*schema.Resource { "snowflake_schema": resources.Schema(), "snowflake_scim_integration": resources.SCIMIntegration(), "snowflake_secondary_database": resources.SecondaryDatabase(), + "snowflake_secret_with_authorization_code_grant": resources.SecretWithAuthorizationCodeGrant(), + "snowflake_secret_with_basic_authentication": resources.SecretWithBasicAuthentication(), + "snowflake_secret_with_client_credentials": resources.SecretWithClientCredentials(), + "snowflake_secret_with_generic_string": resources.SecretWithGenericString(), "snowflake_sequence": resources.Sequence(), "snowflake_service_user": resources.ServiceUser(), "snowflake_session_parameter": resources.SessionParameter(), diff --git a/pkg/provider/resources/resources.go b/pkg/provider/resources/resources.go index 59f0bea661..6730843d38 100644 --- a/pkg/provider/resources/resources.go +++ b/pkg/provider/resources/resources.go @@ -41,6 +41,10 @@ const ( Schema resource = "snowflake_schema" ScimSecurityIntegration resource = "snowflake_scim_integration" SecondaryDatabase resource = "snowflake_secondary_database" + SecretWithAuthorizationCodeGrant resource = "snowflake_secret_with_authorization_code_grant" + SecretWithBasicAuthentication resource = "snowflake_secret_with_basic_authentication" + SecretWithClientCredentials resource = "snowflake_secret_with_client_credentials" + SecretWithGenericString resource = "snowflake_secret_with_generic_string" Sequence resource = "snowflake_sequence" ServiceUser resource = "snowflake_service_user" Share resource = "snowflake_share" diff --git a/pkg/resources/secret_common.go b/pkg/resources/secret_common.go new file mode 100644 index 0000000000..95ceebc949 --- /dev/null +++ b/pkg/resources/secret_common.go @@ -0,0 +1,99 @@ +package resources + +import ( + "context" + "errors" + + "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/schema" +) + +var secretCommonSchema = map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: blocklistedCharactersFieldDescription("String that specifies the identifier (i.e. name) for the secret, must be unique in your schema."), + DiffSuppressFunc: suppressIdentifierQuoting, + }, + "database": { + Type: schema.TypeString, + Required: true, + Description: blocklistedCharactersFieldDescription("The database in which to create the secret"), + ForceNew: true, + DiffSuppressFunc: suppressIdentifierQuoting, + }, + "schema": { + Type: schema.TypeString, + Required: true, + Description: blocklistedCharactersFieldDescription("The schema in which to create the secret."), + ForceNew: true, + DiffSuppressFunc: suppressIdentifierQuoting, + }, + "comment": { + Type: schema.TypeString, + Optional: true, + Description: "Specifies a comment for the secret.", + }, + ShowOutputAttributeName: { + Type: schema.TypeList, + Computed: true, + Description: "Outputs the result of `SHOW SECRETS` for the given secret.", + Elem: &schema.Resource{ + Schema: schemas.ShowSecretSchema, + }, + }, + DescribeOutputAttributeName: { + Type: schema.TypeList, + Computed: true, + Description: "Outputs the result of `DESCRIBE SECRET` for the given secret.", + Elem: &schema.Resource{ + Schema: schemas.DescribeSecretSchema, + }, + }, + FullyQualifiedNameAttributeName: schemas.FullyQualifiedNameSchema, +} + +func handleSecretImport(d *schema.ResourceData) error { + if _, err := ImportName[sdk.SchemaObjectIdentifier](context.Background(), d, nil); err != nil { + return err + } + return nil +} + +func handleSecretRead(d *schema.ResourceData, id sdk.SchemaObjectIdentifier, secret *sdk.Secret, secretDescription *sdk.SecretDetails) error { + return errors.Join( + d.Set(FullyQualifiedNameAttributeName, id.FullyQualifiedName()), + d.Set("comment", secret.Comment), + d.Set(ShowOutputAttributeName, []map[string]any{schemas.SecretToSchema(secret)}), + d.Set(DescribeOutputAttributeName, []map[string]any{schemas.SecretDescriptionToSchema(*secretDescription)}), + ) +} + +func handleSecretUpdate(d *schema.ResourceData, set *sdk.SecretSetRequest, unset *sdk.SecretUnsetRequest) { + if d.HasChange("comment") { + if v, ok := d.GetOk("comment"); ok { + set.WithComment(v.(string)) + } else { + unset.WithComment(true) + } + } +} + +func DeleteContextSecret(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + if err := client.Secrets.Drop(ctx, sdk.NewDropSecretRequest(id).WithIfExists(true)); err != nil { + return diag.FromErr(err) + } + + d.SetId("") + return nil +} diff --git a/pkg/resources/secret_with_basic_authentication.go b/pkg/resources/secret_with_basic_authentication.go new file mode 100644 index 0000000000..e4ec6f42fe --- /dev/null +++ b/pkg/resources/secret_with_basic_authentication.go @@ -0,0 +1,183 @@ +package resources + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/logging" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +var secretBasicAuthenticationSchema = func() map[string]*schema.Schema { + secretBasicAuthentication := map[string]*schema.Schema{ + "username": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + Description: "Specifies the username value to store in the secret.", + }, + "password": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + Description: externalChangesNotDetectedFieldDescription("Specifies the password value to store in the secret."), + }, + } + return helpers.MergeMaps(secretCommonSchema, secretBasicAuthentication) +}() + +func SecretWithBasicAuthentication() *schema.Resource { + return &schema.Resource{ + CreateContext: CreateContextSecretWithBasicAuthentication, + ReadContext: ReadContextSecretWithBasicAuthentication, + UpdateContext: UpdateContextSecretWithBasicAuthentication, + DeleteContext: DeleteContextSecret, + Description: "Resource used to manage secret objects with Basic Authentication. For more information, check [secret documentation](https://docs.snowflake.com/en/sql-reference/sql/create-secret).", + + CustomizeDiff: customdiff.All( + ComputedIfAnyAttributeChanged(secretBasicAuthenticationSchema, ShowOutputAttributeName, "name", "comment"), + ComputedIfAnyAttributeChanged(secretBasicAuthenticationSchema, DescribeOutputAttributeName, "name", "username"), + ComputedIfAnyAttributeChanged(secretBasicAuthenticationSchema, FullyQualifiedNameAttributeName, "name"), + ), + + Schema: secretBasicAuthenticationSchema, + Importer: &schema.ResourceImporter{ + StateContext: ImportSecretWithBasicAuthentication, + }, + } +} + +func ImportSecretWithBasicAuthentication(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + logging.DebugLogger.Printf("[DEBUG] Starting secret with basic authentication import") + client := meta.(*provider.Context).Client + + id, err := sdk.ParseSchemaObjectIdentifier(d.Id()) + if err != nil { + return nil, err + } + + if err := handleSecretImport(d); err != nil { + return nil, err + } + secretDescription, err := client.Secrets.Describe(ctx, id) + if err != nil { + return nil, err + } + + if err := d.Set("username", secretDescription.Username); err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} + +func CreateContextSecretWithBasicAuthentication(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + databaseName, schemaName, name := d.Get("database").(string), d.Get("schema").(string), d.Get("name").(string) + id := sdk.NewSchemaObjectIdentifier(databaseName, schemaName, name) + + usernameString := d.Get("username").(string) + passwordString := d.Get("password").(string) + + request := sdk.NewCreateWithBasicAuthenticationSecretRequest(id, usernameString, passwordString) + if v, ok := d.GetOk("comment"); ok { + request.WithComment(v.(string)) + } + + err := client.Secrets.CreateWithBasicAuthentication(ctx, request) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(helpers.EncodeResourceIdentifier(id)) + + return ReadContextSecretWithBasicAuthentication(ctx, d, meta) +} + +func ReadContextSecretWithBasicAuthentication(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + secret, err := client.Secrets.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 secret with basic authentication. Target object not found. Marking the resource as removed.", + Detail: fmt.Sprintf("Secret with basic authentication name: %s, Err: %s", id.FullyQualifiedName(), err), + }, + } + } + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to retrieve secret.", + Detail: fmt.Sprintf("Secret with basic authentication name: %s, Err: %s", id.FullyQualifiedName(), err), + }, + } + } + secretDescription, err := client.Secrets.Describe(ctx, id) + if err != nil { + return diag.FromErr(err) + } + if err = handleSecretRead(d, id, secret, secretDescription); err != nil { + return diag.FromErr(err) + } + if err = d.Set("username", secretDescription.Username); err != nil { + return diag.FromErr(err) + } + return nil +} + +func UpdateContextSecretWithBasicAuthentication(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + set := &sdk.SecretSetRequest{} + unset := &sdk.SecretUnsetRequest{} + handleSecretUpdate(d, set, unset) + setForBasicAuthentication := &sdk.SetForBasicAuthenticationRequest{} + + if d.HasChange("username") { + username := d.Get("username").(string) + setForBasicAuthentication.WithUsername(username) + } + + if d.HasChange("password") { + password := d.Get("password").(string) + setForBasicAuthentication.WithPassword(password) + } + + if !reflect.DeepEqual(*setForBasicAuthentication, sdk.SetForBasicAuthenticationRequest{}) { + set.WithSetForFlow(sdk.SetForFlowRequest{SetForBasicAuthentication: setForBasicAuthentication}) + } + + if !reflect.DeepEqual(*set, sdk.SecretSetRequest{}) { + if err := client.Secrets.Alter(ctx, sdk.NewAlterSecretRequest(id).WithSet(*set)); err != nil { + return diag.FromErr(err) + } + } + + if !reflect.DeepEqual(*unset, sdk.SecretUnsetRequest{}) { + if err := client.Secrets.Alter(ctx, sdk.NewAlterSecretRequest(id).WithUnset(*unset)); err != nil { + return diag.FromErr(err) + } + } + + return ReadContextSecretWithBasicAuthentication(ctx, d, meta) +} diff --git a/pkg/resources/secret_with_basic_authentication_acceptance_test.go b/pkg/resources/secret_with_basic_authentication_acceptance_test.go new file mode 100644 index 0000000000..5c0aa401cb --- /dev/null +++ b/pkg/resources/secret_with_basic_authentication_acceptance_test.go @@ -0,0 +1,227 @@ +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/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/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_SecretWithBasicAuthentication_BasicFlow(t *testing.T) { + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + name := id.Name() + comment := random.Comment() + + secretModel := model.SecretWithBasicAuthentication("s", id.DatabaseName(), name, "foo", id.SchemaName(), "foo") + secretModelDifferentCredentialsWithComment := model.SecretWithBasicAuthentication("s", id.DatabaseName(), name, "bar", id.SchemaName(), "bar").WithComment(comment) + secretModelWithoutComment := model.SecretWithBasicAuthentication("s", id.DatabaseName(), name, "bar", id.SchemaName(), "bar") + secretModelEmptyCredentials := model.SecretWithBasicAuthentication("s", id.DatabaseName(), name, "", id.SchemaName(), "") + + secretName := secretModel.ResourceReference() + + 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.SecretWithBasicAuthentication), + Steps: []resource.TestStep{ + // create + { + Config: config.FromModel(t, secretModel), + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.SecretWithBasicAuthenticationResource(t, secretName). + HasNameString(name). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasUsernameString("foo"). + HasPasswordString("foo"). + HasCommentString(""), + + resourceshowoutputassert.SecretShowOutput(t, secretName). + HasName(name). + HasDatabaseName(id.DatabaseName()). + HasSecretType(sdk.SecretTypePassword). + HasSchemaName(id.SchemaName()). + HasComment(""), + ), + + resource.TestCheckResourceAttr(secretName, "fully_qualified_name", id.FullyQualifiedName()), + resource.TestCheckResourceAttrSet(secretName, "describe_output.0.created_on"), + resource.TestCheckResourceAttr(secretName, "describe_output.0.name", name), + resource.TestCheckResourceAttr(secretName, "describe_output.0.database_name", id.DatabaseName()), + resource.TestCheckResourceAttr(secretName, "describe_output.0.schema_name", id.SchemaName()), + resource.TestCheckResourceAttr(secretName, "describe_output.0.secret_type", sdk.SecretTypePassword), + resource.TestCheckResourceAttr(secretName, "describe_output.0.username", "foo"), + resource.TestCheckResourceAttr(secretName, "describe_output.0.comment", ""), + resource.TestCheckResourceAttr(secretName, "describe_output.0.oauth_access_token_expiry_time", ""), + resource.TestCheckResourceAttr(secretName, "describe_output.0.oauth_refresh_token_expiry_time", ""), + resource.TestCheckResourceAttr(secretName, "describe_output.0.integration_name", ""), + resource.TestCheckResourceAttr(secretName, "describe_output.0.oauth_scopes.#", "0"), + ), + }, + // set username, password and comment + { + Config: config.FromModel(t, secretModelDifferentCredentialsWithComment), + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + + resourceassert.SecretWithBasicAuthenticationResource(t, secretName). + HasNameString(name). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasUsernameString("bar"). + HasPasswordString("bar"). + HasCommentString(comment), + + resourceshowoutputassert.SecretShowOutput(t, secretName). + HasSecretType(sdk.SecretTypePassword). + HasComment(comment), + ), + + resource.TestCheckResourceAttr(secretName, "describe_output.0.username", "bar"), + resource.TestCheckResourceAttr(secretName, "describe_output.0.comment", comment), + ), + }, + // set username and comment externally + { + PreConfig: func() { + acc.TestClient().Secret.Alter(t, sdk.NewAlterSecretRequest(id). + WithSet(*sdk.NewSecretSetRequest(). + WithComment("test_comment"). + WithSetForFlow(*sdk.NewSetForFlowRequest(). + WithSetForBasicAuthentication(*sdk.NewSetForBasicAuthenticationRequest(). + WithUsername("test_username"), + ), + ), + ), + ) + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(secretName, plancheck.ResourceActionUpdate), + planchecks.ExpectDrift(secretName, "comment", sdk.String(comment), sdk.String("test_comment")), + planchecks.ExpectDrift(secretName, "username", sdk.String("bar"), sdk.String("test_username")), + + planchecks.ExpectChange(secretName, "comment", tfjson.ActionUpdate, sdk.String("test_comment"), sdk.String(comment)), + planchecks.ExpectChange(secretName, "username", tfjson.ActionUpdate, sdk.String("test_username"), sdk.String("bar")), + }, + }, + Config: config.FromModel(t, secretModelDifferentCredentialsWithComment), + Check: assert.AssertThat(t, + resourceassert.SecretWithBasicAuthenticationResource(t, secretName). + HasNameString(name). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasUsernameString("bar"). + HasPasswordString("bar"). + HasCommentString(comment), + ), + }, + // import + { + ResourceName: secretName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"password"}, + ImportStateCheck: importchecks.ComposeImportStateCheck( + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "name", id.Name()), + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "database", id.DatabaseId().Name()), + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "schema", id.SchemaId().Name()), + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "username", "bar"), + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "comment", comment), + ), + }, + // unset comment + { + Config: config.FromModel(t, secretModelWithoutComment), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(secretName, plancheck.ResourceActionUpdate), + planchecks.ExpectChange(secretName, "comment", tfjson.ActionUpdate, sdk.String(comment), nil), + }, + }, + Check: assert.AssertThat(t, + resourceassert.SecretWithClientCredentialsResource(t, secretName). + HasCommentString(""), + ), + }, + // import with no fields set + { + ResourceName: secretName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"password"}, + ImportStateCheck: importchecks.ComposeImportStateCheck( + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "name", id.Name()), + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "database", id.DatabaseId().Name()), + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "schema", id.SchemaId().Name()), + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "username", "bar"), + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "comment", ""), + ), + }, + // set empty username and password + { + Config: config.FromModel(t, secretModelEmptyCredentials), + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.SecretWithBasicAuthenticationResource(t, secretName). + HasNameString(name). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasUsernameString(""). + HasPasswordString(""). + HasCommentString(""), + ), + ), + }, + }, + }) +} + +func TestAcc_SecretWithBasicAuthentication_CreateWithEmptyCredentials(t *testing.T) { + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + name := id.Name() + secretModelEmptyCredentials := model.SecretWithBasicAuthentication("s", id.DatabaseName(), name, "", id.SchemaName(), "") + + 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.SecretWithBasicAuthentication), + Steps: []resource.TestStep{ + { + Config: config.FromModel(t, secretModelEmptyCredentials), + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.SecretWithBasicAuthenticationResource(t, secretModelEmptyCredentials.ResourceReference()). + HasNameString(name). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasUsernameString(""). + HasPasswordString(""). + HasCommentString(""), + ), + ), + }, + }, + }) +} diff --git a/pkg/resources/secret_with_generic_string.go b/pkg/resources/secret_with_generic_string.go new file mode 100644 index 0000000000..42c2fc6f92 --- /dev/null +++ b/pkg/resources/secret_with_generic_string.go @@ -0,0 +1,160 @@ +package resources + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/logging" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +var secretGenericStringSchema = func() map[string]*schema.Schema { + secretGenericString := map[string]*schema.Schema{ + "secret_string": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + Description: externalChangesNotDetectedFieldDescription("Specifies the string to store in the secret. The string can be an API token or a string of sensitive value that can be used in the handler code of a UDF or stored procedure. For details, see [Creating and using an external access integration](https://docs.snowflake.com/en/developer-guide/external-network-access/creating-using-external-network-access). You should not use this property to store any kind of OAuth token; use one of the other secret types for your OAuth use cases."), + }, + } + return helpers.MergeMaps(secretCommonSchema, secretGenericString) +}() + +func SecretWithGenericString() *schema.Resource { + return &schema.Resource{ + CreateContext: CreateContextSecretWithGenericString, + ReadContext: ReadContextSecretWithGenericString, + UpdateContext: UpdateContextSecretWithGenericString, + DeleteContext: DeleteContextSecret, + Description: "Resource used to manage secret objects with Generic String. For more information, check [secret documentation](https://docs.snowflake.com/en/sql-reference/sql/create-secret).", + + CustomizeDiff: customdiff.All( + ComputedIfAnyAttributeChanged(secretGenericStringSchema, DescribeOutputAttributeName, "name"), + ComputedIfAnyAttributeChanged(secretGenericStringSchema, ShowOutputAttributeName, "name", "comment"), + ComputedIfAnyAttributeChanged(secretGenericStringSchema, FullyQualifiedNameAttributeName, "name"), + ), + + Schema: secretGenericStringSchema, + Importer: &schema.ResourceImporter{ + StateContext: ImportSecretWithGenericString, + }, + } +} + +func ImportSecretWithGenericString(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + logging.DebugLogger.Printf("[DEBUG] Starting secret with generic string import") + + _, err := sdk.ParseSchemaObjectIdentifier(d.Id()) + if err != nil { + return nil, err + } + + if err := handleSecretImport(d); err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} + +func CreateContextSecretWithGenericString(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + databaseName, schemaName, name := d.Get("database").(string), d.Get("schema").(string), d.Get("name").(string) + id := sdk.NewSchemaObjectIdentifier(databaseName, schemaName, name) + + secretSting := d.Get("secret_string").(string) + + request := sdk.NewCreateWithGenericStringSecretRequest(id, secretSting) + if v, ok := d.GetOk("comment"); ok { + request.WithComment(v.(string)) + } + + err := client.Secrets.CreateWithGenericString(ctx, request) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(helpers.EncodeResourceIdentifier(id)) + + return ReadContextSecretWithGenericString(ctx, d, meta) +} + +func ReadContextSecretWithGenericString(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + secret, err := client.Secrets.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 secret with generic string. Target object not found. Marking the resource as removed.", + Detail: fmt.Sprintf("Secret with generic string name: %s, Err: %s", id.FullyQualifiedName(), err), + }, + } + } + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to retrieve secret generic string.", + Detail: fmt.Sprintf("Secret with generic string name: %s, Err: %s", id.FullyQualifiedName(), err), + }, + } + } + + secretDescription, err := client.Secrets.Describe(ctx, id) + if err != nil { + return diag.FromErr(err) + } + if err := handleSecretRead(d, id, secret, secretDescription); err != nil { + return diag.FromErr(err) + } + return nil +} + +func UpdateContextSecretWithGenericString(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + set := &sdk.SecretSetRequest{} + unset := &sdk.SecretUnsetRequest{} + handleSecretUpdate(d, set, unset) + setForGenericString := &sdk.SetForGenericStringRequest{} + + if d.HasChange("secret_string") { + secretString := d.Get("secret_string").(string) + setForGenericString.WithSecretString(secretString) + } + + if !reflect.DeepEqual(setForGenericString, sdk.SetForGenericStringRequest{}) { + set.WithSetForFlow(sdk.SetForFlowRequest{SetForGenericString: setForGenericString}) + } + + if !reflect.DeepEqual(*set, sdk.SecretSetRequest{}) { + if err := client.Secrets.Alter(ctx, sdk.NewAlterSecretRequest(id).WithSet(*set)); err != nil { + return diag.FromErr(err) + } + } + + if !reflect.DeepEqual(*unset, sdk.SecretUnsetRequest{}) { + if err := client.Secrets.Alter(ctx, sdk.NewAlterSecretRequest(id).WithUnset(*unset)); err != nil { + return diag.FromErr(err) + } + } + + return ReadContextSecretWithGenericString(ctx, d, meta) +} diff --git a/pkg/resources/secret_with_generic_string_acceptance_test.go b/pkg/resources/secret_with_generic_string_acceptance_test.go new file mode 100644 index 0000000000..d2eb8a54cf --- /dev/null +++ b/pkg/resources/secret_with_generic_string_acceptance_test.go @@ -0,0 +1,187 @@ +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/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/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_SecretWithGenericString_BasicFlow(t *testing.T) { + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + name := id.Name() + comment := random.Comment() + + secretModel := model.SecretWithGenericString("s", id.DatabaseName(), name, id.SchemaName(), "foo") + secretModelEmptySecretString := model.SecretWithGenericString("s", id.DatabaseName(), name, id.SchemaName(), "") + + secretName := secretModel.ResourceReference() + + 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.SecretWithGenericString), + Steps: []resource.TestStep{ + // create + { + Config: config.FromModel(t, secretModel), + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.SecretWithGenericStringResource(t, secretModel.ResourceReference()). + HasNameString(name). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasSecretStringString("foo"). + HasCommentString(""), + + resourceshowoutputassert.SecretShowOutput(t, secretModel.ResourceReference()). + HasName(name). + HasDatabaseName(id.DatabaseName()). + HasSecretType(sdk.SecretTypeGenericString). + HasSchemaName(id.SchemaName()). + HasComment(""), + ), + + resource.TestCheckResourceAttr(secretName, "fully_qualified_name", id.FullyQualifiedName()), + resource.TestCheckResourceAttrSet(secretName, "describe_output.0.created_on"), + resource.TestCheckResourceAttr(secretName, "describe_output.0.name", name), + resource.TestCheckResourceAttr(secretName, "describe_output.0.database_name", id.DatabaseName()), + resource.TestCheckResourceAttr(secretName, "describe_output.0.schema_name", id.SchemaName()), + resource.TestCheckResourceAttr(secretName, "describe_output.0.secret_type", sdk.SecretTypeGenericString), + resource.TestCheckResourceAttr(secretName, "describe_output.0.username", ""), + resource.TestCheckResourceAttr(secretName, "describe_output.0.comment", ""), + resource.TestCheckResourceAttr(secretName, "describe_output.0.oauth_access_token_expiry_time", ""), + resource.TestCheckResourceAttr(secretName, "describe_output.0.oauth_refresh_token_expiry_time", ""), + resource.TestCheckResourceAttr(secretName, "describe_output.0.integration_name", ""), + resource.TestCheckResourceAttr(secretName, "describe_output.0.oauth_scopes.#", "0"), + ), + }, + // set secret_string and comment + { + Config: config.FromModel(t, secretModel. + WithSecretString("bar"). + WithComment(comment), + ), + + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.SecretWithGenericStringResource(t, secretName). + HasNameString(name). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasSecretStringString("bar"). + HasCommentString(comment), + + resourceshowoutputassert.SecretShowOutput(t, secretModel.ResourceReference()). + HasSecretType(sdk.SecretTypeGenericString). + HasComment(comment), + ), + + resource.TestCheckResourceAttr(secretName, "describe_output.0.comment", comment), + ), + }, + // set comment externally, external changes for secret_string are not being detected + { + PreConfig: func() { + acc.TestClient().Secret.Alter(t, sdk.NewAlterSecretRequest(id). + WithSet(*sdk.NewSecretSetRequest(). + WithComment("test_comment"), + ), + ) + }, + Config: config.FromModel(t, secretModel), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(secretName, plancheck.ResourceActionUpdate), + planchecks.ExpectDrift(secretName, "comment", sdk.String(comment), sdk.String("test_comment")), + planchecks.ExpectChange(secretName, "comment", tfjson.ActionUpdate, sdk.String("test_comment"), sdk.String(comment)), + }, + }, + Check: assert.AssertThat(t, + resourceassert.SecretWithGenericStringResource(t, secretName). + HasNameString(name). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasSecretStringString("bar"). + HasCommentString(comment), + ), + }, + // import + { + ResourceName: secretName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"secret_string"}, + ImportStateCheck: importchecks.ComposeImportStateCheck( + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "name", id.Name()), + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "database", id.DatabaseId().Name()), + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "schema", id.SchemaId().Name()), + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "comment", comment), + ), + }, + // unset comment + { + Config: config.FromModel(t, secretModelEmptySecretString), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(secretModel.ResourceReference(), plancheck.ResourceActionUpdate), + planchecks.ExpectChange(secretModel.ResourceReference(), "comment", tfjson.ActionUpdate, sdk.String(comment), nil), + }, + }, + Check: assert.AssertThat(t, + resourceassert.SecretWithClientCredentialsResource(t, secretModelEmptySecretString.ResourceReference()). + HasCommentString(""), + ), + }, + // import with no fields set + { + ResourceName: secretModel.ResourceReference(), + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"secret_string"}, + ImportStateCheck: importchecks.ComposeImportStateCheck( + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "name", id.Name()), + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "database", id.DatabaseId().Name()), + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "schema", id.SchemaId().Name()), + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "comment", ""), + ), + }, + // destroy + { + Config: config.FromModel(t, secretModel), + Destroy: true, + }, + // create with empty secret_string + { + Config: config.FromModel(t, secretModelEmptySecretString), + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.SecretWithGenericStringResource(t, secretModelEmptySecretString.ResourceReference()). + HasNameString(name). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasSecretStringString(""). + HasCommentString(""), + ), + ), + }, + }, + }) +} diff --git a/pkg/resources/secret_with_oauth_authorization_code_grant.go b/pkg/resources/secret_with_oauth_authorization_code_grant.go new file mode 100644 index 0000000000..97025a1bc8 --- /dev/null +++ b/pkg/resources/secret_with_oauth_authorization_code_grant.go @@ -0,0 +1,211 @@ +package resources + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/logging" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +var secretAuthorizationCodeGrantSchema = func() map[string]*schema.Schema { + secretAuthorizationCodeGrant := map[string]*schema.Schema{ + "oauth_refresh_token": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + Description: externalChangesNotDetectedFieldDescription("Specifies the token as a string that is used to obtain a new access token from the OAuth authorization server when the access token expires."), + }, + "oauth_refresh_token_expiry_time": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInDescribe("oauth_refresh_token_expiry_time"), + Description: "Specifies the timestamp as a string when the OAuth refresh token expires. Accepted string formats: YYYY-MM-DD, YYYY-MM-DD HH:MI, YYYY-MM-DD HH:MI:SS, YYYY-MM-DD HH:MI ", + }, + "api_authentication": { + Type: schema.TypeString, + ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](), + Required: true, + Description: "Specifies the name value of the Snowflake security integration that connects Snowflake to an external service.", + DiffSuppressFunc: suppressIdentifierQuoting, + }, + } + return helpers.MergeMaps(secretCommonSchema, secretAuthorizationCodeGrant) +}() + +func SecretWithAuthorizationCodeGrant() *schema.Resource { + return &schema.Resource{ + CreateContext: CreateContextSecretWithAuthorizationCodeGrant, + ReadContext: ReadContextSecretWithAuthorizationCodeGrant(true), + UpdateContext: UpdateContextSecretWithAuthorizationCodeGrant, + DeleteContext: DeleteContextSecret, + Description: "Resource used to manage secret objects with OAuth Authorization Code Grant. For more information, check [secret documentation](https://docs.snowflake.com/en/sql-reference/sql/create-secret).", + + Schema: secretAuthorizationCodeGrantSchema, + Importer: &schema.ResourceImporter{ + StateContext: ImportSecretWithAuthorizationCodeGrant, + }, + + CustomizeDiff: customdiff.All( + ComputedIfAnyAttributeChanged(secretAuthorizationCodeGrantSchema, DescribeOutputAttributeName, "name", "oauth_refresh_token_expiry_time", "api_authentication"), + ComputedIfAnyAttributeChanged(secretAuthorizationCodeGrantSchema, ShowOutputAttributeName, "name", "comment"), + ComputedIfAnyAttributeChanged(secretAuthorizationCodeGrantSchema, FullyQualifiedNameAttributeName, "name"), + ), + } +} + +func ImportSecretWithAuthorizationCodeGrant(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + logging.DebugLogger.Printf("[DEBUG] Starting secret with authorization code import") + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifier(d.Id()) + if err != nil { + return nil, err + } + if err = handleSecretImport(d); err != nil { + return nil, err + } + + secretDescription, err := client.Secrets.Describe(ctx, id) + if err != nil { + return nil, err + } + + if err = d.Set("oauth_refresh_token_expiry_time", secretDescription.OauthRefreshTokenExpiryTime.String()); err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} + +func CreateContextSecretWithAuthorizationCodeGrant(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + databaseName, schemaName, name := d.Get("database").(string), d.Get("schema").(string), d.Get("name").(string) + id := sdk.NewSchemaObjectIdentifier(databaseName, schemaName, name) + + apiIntegrationString := d.Get("api_authentication").(string) + apiIntegration, err := sdk.ParseAccountObjectIdentifier(apiIntegrationString) + if err != nil { + return diag.FromErr(err) + } + + refreshToken := d.Get("oauth_refresh_token").(string) + refreshTokenExpiryTime := d.Get("oauth_refresh_token_expiry_time").(string) + + request := sdk.NewCreateWithOAuthAuthorizationCodeFlowSecretRequest(id, refreshToken, refreshTokenExpiryTime, apiIntegration) + if v, ok := d.GetOk("comment"); ok { + request.WithComment(v.(string)) + } + + err = client.Secrets.CreateWithOAuthAuthorizationCodeFlow(ctx, request) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(helpers.EncodeResourceIdentifier(id)) + + return ReadContextSecretWithAuthorizationCodeGrant(false)(ctx, d, meta) +} + +func ReadContextSecretWithAuthorizationCodeGrant(withExternalChangesMarking bool) schema.ReadContextFunc { + return func(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + secret, err := client.Secrets.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 secret with authorization code grant. Target object not found. Marking the resource as removed.", + Detail: fmt.Sprintf("Secret with authorization code grant name: %s, Err: %s", id.FullyQualifiedName(), err), + }, + } + } + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to retrieve secret with authorization code grant.", + Detail: fmt.Sprintf("Secret with authorization code grant name: %s, Err: %s", id.FullyQualifiedName(), err), + }, + } + } + secretDescription, err := client.Secrets.Describe(ctx, id) + if err != nil { + return diag.FromErr(err) + } + + if withExternalChangesMarking { + if err = handleExternalValueChangesToObjectInDescribe(d, + describeMapping{"oauth_refresh_token_expiry_time", "oauth_refresh_token_expiry_time", secretDescription.OauthRefreshTokenExpiryTime.String(), secretDescription.OauthRefreshTokenExpiryTime.String(), nil}, + ); err != nil { + return diag.FromErr(err) + } + } + + if err = setStateToValuesFromConfig(d, secretAuthorizationCodeGrantSchema, []string{"oauth_refresh_token_expiry_time"}); err != nil { + return diag.FromErr(err) + } + + if err = d.Set("api_authentication", secretDescription.IntegrationName); err != nil { + return diag.FromErr(err) + } + + if err := handleSecretRead(d, id, secret, secretDescription); err != nil { + return diag.FromErr(err) + } + + return nil + } +} + +func UpdateContextSecretWithAuthorizationCodeGrant(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + set := &sdk.SecretSetRequest{} + unset := &sdk.SecretUnsetRequest{} + handleSecretUpdate(d, set, unset) + setForOAuthAuthorization := &sdk.SetForOAuthAuthorizationRequest{} + + if d.HasChange("oauth_refresh_token") { + refreshToken := d.Get("oauth_refresh_token").(string) + setForOAuthAuthorization.WithOauthRefreshToken(refreshToken) + } + + if d.HasChange("oauth_refresh_token_expiry_time") { + refreshTokenExpiryTime := d.Get("oauth_refresh_token_expiry_time").(string) + setForOAuthAuthorization.WithOauthRefreshTokenExpiryTime(refreshTokenExpiryTime) + } + if !reflect.DeepEqual(setForOAuthAuthorization, sdk.SetForOAuthAuthorizationRequest{}) { + set.WithSetForFlow(sdk.SetForFlowRequest{SetForOAuthAuthorization: setForOAuthAuthorization}) + } + + if !reflect.DeepEqual(*set, sdk.SecretSetRequest{}) { + if err := client.Secrets.Alter(ctx, sdk.NewAlterSecretRequest(id).WithSet(*set)); err != nil { + return diag.FromErr(err) + } + } + + if !reflect.DeepEqual(*unset, sdk.SecretUnsetRequest{}) { + if err := client.Secrets.Alter(ctx, sdk.NewAlterSecretRequest(id).WithUnset(*unset)); err != nil { + return diag.FromErr(err) + } + } + + return ReadContextSecretWithAuthorizationCodeGrant(false)(ctx, d, meta) +} diff --git a/pkg/resources/secret_with_oauth_authorization_code_grant_acceptance_test.go b/pkg/resources/secret_with_oauth_authorization_code_grant_acceptance_test.go new file mode 100644 index 0000000000..b8f6373a46 --- /dev/null +++ b/pkg/resources/secret_with_oauth_authorization_code_grant_acceptance_test.go @@ -0,0 +1,325 @@ +package resources_test + +import ( + "fmt" + "testing" + "time" + + 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/planchecks" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" + r "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/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_SecretWithAuthorizationCodeGrant_BasicFlow(t *testing.T) { + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + name := id.Name() + comment := random.Comment() + refreshTokenExpiryDateTime := time.Now().Add(24 * time.Hour).Format(time.DateTime) + newRefreshTokenExpiryDateOnly := time.Now().Add(4 * 24 * time.Hour).Format(time.DateOnly) + refreshToken := "test_token" + newRefreshToken := "new_test_token" + + integrationId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + _, apiIntegrationCleanup := acc.TestClient().SecurityIntegration.CreateApiAuthenticationClientCredentialsWithRequest(t, + sdk.NewCreateApiAuthenticationWithClientCredentialsFlowSecurityIntegrationRequest(integrationId, true, "foo", "foo"), + ) + t.Cleanup(apiIntegrationCleanup) + + secretModel := model.SecretWithAuthorizationCodeGrant("s", integrationId.Name(), id.DatabaseName(), id.SchemaName(), name, refreshToken, refreshTokenExpiryDateTime) + secretModelAllSet := model.SecretWithAuthorizationCodeGrant("s", integrationId.Name(), id.DatabaseName(), id.SchemaName(), name, newRefreshToken, newRefreshTokenExpiryDateOnly).WithComment(comment) + + secretName := secretModel.ResourceReference() + + 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.SecretWithAuthorizationCodeGrant), + Steps: []resource.TestStep{ + // create + { + Config: config.FromModel(t, secretModel), + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.SecretWithAuthorizationCodeResource(t, secretModel.ResourceReference()). + HasNameString(name). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasApiAuthenticationString(integrationId.Name()). + HasOauthRefreshTokenString(refreshToken). + HasOauthRefreshTokenExpiryTimeString(refreshTokenExpiryDateTime). + HasCommentString(""), + + resourceshowoutputassert.SecretShowOutput(t, secretModel.ResourceReference()). + HasName(name). + HasDatabaseName(id.DatabaseName()). + HasSecretType(sdk.SecretTypeOAuth2). + HasSchemaName(id.SchemaName()). + HasComment(""), + ), + + resource.TestCheckResourceAttr(secretName, "fully_qualified_name", id.FullyQualifiedName()), + resource.TestCheckResourceAttrSet(secretName, "describe_output.0.created_on"), + resource.TestCheckResourceAttr(secretName, "describe_output.0.name", name), + resource.TestCheckResourceAttr(secretName, "describe_output.0.database_name", id.DatabaseName()), + resource.TestCheckResourceAttr(secretName, "describe_output.0.schema_name", id.SchemaName()), + resource.TestCheckResourceAttr(secretName, "describe_output.0.secret_type", sdk.SecretTypeOAuth2), + resource.TestCheckResourceAttr(secretName, "describe_output.0.integration_name", integrationId.Name()), + resource.TestCheckResourceAttr(secretName, "describe_output.0.username", ""), + resource.TestCheckResourceAttr(secretName, "describe_output.0.oauth_access_token_expiry_time", ""), + resource.TestCheckResourceAttrSet(secretName, "describe_output.0.oauth_refresh_token_expiry_time"), + resource.TestCheckResourceAttr(secretName, "describe_output.0.comment", ""), + resource.TestCheckResourceAttr(secretName, "describe_output.0.oauth_scopes.#", "0"), + ), + }, + // set all + { + Config: config.FromModel(t, secretModelAllSet), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(secretName, plancheck.ResourceActionUpdate), + planchecks.ExpectChange(secretName, "comment", tfjson.ActionUpdate, nil, sdk.String(comment)), + planchecks.ExpectChange(secretName, "oauth_refresh_token", tfjson.ActionUpdate, sdk.String(refreshToken), sdk.String(newRefreshToken)), + }, + }, + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.SecretWithAuthorizationCodeResource(t, secretName). + HasNameString(name). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasApiAuthenticationString(integrationId.Name()). + HasOauthRefreshTokenString(newRefreshToken). + HasOauthRefreshTokenExpiryTimeString(newRefreshTokenExpiryDateOnly). + HasCommentString(comment), + + resourceshowoutputassert.SecretShowOutput(t, secretModel.ResourceReference()). + HasSecretType(sdk.SecretTypeOAuth2). + HasComment(comment), + ), + resource.TestCheckResourceAttrSet(secretName, "describe_output.0.oauth_refresh_token_expiry_time"), + resource.TestCheckResourceAttr(secretName, "describe_output.0.comment", comment), + ), + }, + // set comment and refresh_token_expiry_time externally + { + PreConfig: func() { + acc.TestClient().Secret.Alter(t, sdk.NewAlterSecretRequest(id).WithSet(*sdk.NewSecretSetRequest(). + WithComment("secret resource - changed comment"). + WithSetForFlow(*sdk.NewSetForFlowRequest(). + WithSetForOAuthAuthorization(*sdk.NewSetForOAuthAuthorizationRequest(). + WithOauthRefreshTokenExpiryTime(time.Now().Add(24 * time.Hour).Format(time.DateOnly)), + ), + ), + )) + }, + Config: config.FromModel(t, secretModelAllSet), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(secretName, plancheck.ResourceActionUpdate), + planchecks.ExpectChange(secretName, "comment", tfjson.ActionUpdate, sdk.String("secret resource - changed comment"), sdk.String(comment)), + planchecks.ExpectComputed(secretName, r.DescribeOutputAttributeName, true), + }, + }, + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.SecretWithAuthorizationCodeResource(t, secretName). + HasNameString(name). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasApiAuthenticationString(integrationId.Name()). + HasOauthRefreshTokenString(newRefreshToken). + HasOauthRefreshTokenExpiryTimeString(newRefreshTokenExpiryDateOnly). + HasCommentString(comment), + assert.Check(resource.TestCheckResourceAttrSet(secretName, "describe_output.0.oauth_refresh_token_expiry_time")), + ), + ), + }, + // import + { + ResourceName: secretName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"oauth_refresh_token"}, + ImportStateCheck: assert.AssertThatImport(t, + resourceassert.ImportedSecretWithAuthorizationCodeResource(t, helpers.EncodeResourceIdentifier(id)). + HasNameString(id.Name()). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasApiAuthenticationString(integrationId.Name()). + HasCommentString(comment). + HasOauthRefreshTokenExpiryTimeNotEmpty(), + ), + }, + }, + }) +} + +func TestAcc_SecretWithAuthorizationCodeGrant_DifferentTimeFormats(t *testing.T) { + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + name := id.Name() + + integrationId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + _, apiIntegrationCleanup := acc.TestClient().SecurityIntegration.CreateApiAuthenticationClientCredentialsWithRequest(t, + sdk.NewCreateApiAuthenticationWithClientCredentialsFlowSecurityIntegrationRequest(integrationId, true, "foo", "foo"), + ) + t.Cleanup(apiIntegrationCleanup) + + refreshTokenExpiryDateOnly := time.Now().Add(4 * 24 * time.Hour).Format(time.DateOnly) + refreshTokenExpiryWithoutSeconds := time.Now().Add(4 * 24 * time.Hour).Format("2006-01-02 15:04") + refreshTokenExpiryDateTime := time.Now().Add(4 * 24 * time.Hour).Format(time.DateTime) + refreshTokenExpiryWithPDT := fmt.Sprintf("%s %s", time.Now().Add(4*24*time.Hour).Format("2006-01-02 15:04"), "PDT") + + secretModelDateOnly := model.SecretWithAuthorizationCodeGrant("s", integrationId.Name(), id.DatabaseName(), id.SchemaName(), name, "test_token", refreshTokenExpiryDateOnly) + secretModelWithoutSeconds := model.SecretWithAuthorizationCodeGrant("s", integrationId.Name(), id.DatabaseName(), id.SchemaName(), name, "test_token", refreshTokenExpiryWithoutSeconds) + secretModelDateTime := model.SecretWithAuthorizationCodeGrant("s", integrationId.Name(), id.DatabaseName(), id.SchemaName(), name, "test_token", refreshTokenExpiryDateTime) + secretModelWithPDT := model.SecretWithAuthorizationCodeGrant("s", integrationId.Name(), id.DatabaseName(), id.SchemaName(), name, "test_token", refreshTokenExpiryWithPDT) + + 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.SecretWithAuthorizationCodeGrant), + Steps: []resource.TestStep{ + // create with DateOnly + { + Config: config.FromModel(t, secretModelDateOnly), + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.SecretWithAuthorizationCodeResource(t, secretModelDateOnly.ResourceReference()). + HasOauthRefreshTokenExpiryTimeString(refreshTokenExpiryDateOnly), + assert.Check(resource.TestCheckResourceAttrSet(secretModelDateOnly.ResourceReference(), "describe_output.0.oauth_refresh_token_expiry_time")), + ), + ), + }, + // update with DateTime without seconds + { + Config: config.FromModel(t, secretModelWithoutSeconds), + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.SecretWithAuthorizationCodeResource(t, secretModelWithoutSeconds.ResourceReference()). + HasOauthRefreshTokenExpiryTimeString(refreshTokenExpiryWithoutSeconds), + assert.Check(resource.TestCheckResourceAttrSet(secretModelWithoutSeconds.ResourceReference(), "describe_output.0.oauth_refresh_token_expiry_time")), + ), + ), + }, + // update with DateTime + { + Config: config.FromModel(t, secretModelDateTime), + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.SecretWithAuthorizationCodeResource(t, secretModelDateTime.ResourceReference()). + HasOauthRefreshTokenExpiryTimeString(refreshTokenExpiryDateTime), + assert.Check(resource.TestCheckResourceAttrSet(secretModelDateTime.ResourceReference(), "describe_output.0.oauth_refresh_token_expiry_time")), + ), + ), + }, + // update with DateTime with PDT timezone + { + Config: config.FromModel(t, secretModelWithPDT), + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.SecretWithAuthorizationCodeResource(t, secretModelWithPDT.ResourceReference()). + HasOauthRefreshTokenExpiryTimeString(refreshTokenExpiryWithPDT), + assert.Check(resource.TestCheckResourceAttrSet(secretModelWithPDT.ResourceReference(), "describe_output.0.oauth_refresh_token_expiry_time")), + ), + ), + }, + }, + }) +} + +func TestAcc_SecretWithAuthorizationCodeGrant_ExternalChange(t *testing.T) { + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + name := id.Name() + comment := random.Comment() + refreshTokenExpiryDateTime := time.Now().Add(24 * time.Hour).Format(time.DateTime) + externalRefreshTokenExpiryTime := time.Now().Add(10 * 24 * time.Hour) + refreshToken := "test_token" + + integrationId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + _, apiIntegrationCleanup := acc.TestClient().SecurityIntegration.CreateApiAuthenticationClientCredentialsWithRequest(t, + sdk.NewCreateApiAuthenticationWithClientCredentialsFlowSecurityIntegrationRequest(integrationId, true, "foo", "foo"), + ) + t.Cleanup(apiIntegrationCleanup) + + secretModel := model.SecretWithAuthorizationCodeGrant("s", integrationId.Name(), id.DatabaseName(), id.SchemaName(), name, refreshToken, refreshTokenExpiryDateTime).WithComment(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.SecretWithAuthorizationCodeGrant), + Steps: []resource.TestStep{ + // create + { + Config: config.FromModel(t, secretModel), + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.SecretWithAuthorizationCodeResource(t, secretModel.ResourceReference()). + HasNameString(name). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasApiAuthenticationString(integrationId.Name()). + HasOauthRefreshTokenString(refreshToken). + HasOauthRefreshTokenExpiryTimeString(refreshTokenExpiryDateTime). + HasCommentString(comment), + + resourceshowoutputassert.SecretShowOutput(t, secretModel.ResourceReference()). + HasName(name). + HasDatabaseName(id.DatabaseName()). + HasSecretType(sdk.SecretTypeOAuth2). + HasSchemaName(id.SchemaName()). + HasComment(comment), + ), + ), + }, + { + PreConfig: func() { + acc.TestClient().Secret.Alter(t, sdk.NewAlterSecretRequest(id). + WithSet(*sdk.NewSecretSetRequest(). + WithSetForFlow(*sdk.NewSetForFlowRequest(). + WithSetForOAuthAuthorization(*sdk.NewSetForOAuthAuthorizationRequest(). + WithOauthRefreshTokenExpiryTime(externalRefreshTokenExpiryTime.Format(time.DateOnly)), + ), + ), + ), + ) + }, + Config: config.FromModel(t, secretModel), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(secretModel.ResourceReference(), plancheck.ResourceActionUpdate), + // cannot check before value due to snowflake timestamp format + }, + }, + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.SecretWithAuthorizationCodeResource(t, secretModel.ResourceReference()). + HasOauthRefreshTokenExpiryTimeString(refreshTokenExpiryDateTime), + assert.Check(resource.TestCheckResourceAttrSet(secretModel.ResourceReference(), "describe_output.0.oauth_refresh_token_expiry_time")), + ), + ), + }, + }, + }) +} diff --git a/pkg/resources/secret_with_oauth_client_credentials.go b/pkg/resources/secret_with_oauth_client_credentials.go new file mode 100644 index 0000000000..1a387efe0e --- /dev/null +++ b/pkg/resources/secret_with_oauth_client_credentials.go @@ -0,0 +1,205 @@ +package resources + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/logging" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +var secretClientCredentialsSchema = func() map[string]*schema.Schema { + secretClientCredentials := map[string]*schema.Schema{ + "api_authentication": { + Type: schema.TypeString, + ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](), + Required: true, + Description: "Specifies the name value of the Snowflake security integration that connects Snowflake to an external service.", + DiffSuppressFunc: suppressIdentifierQuoting, + }, + "oauth_scopes": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "Specifies a list of scopes to use when making a request from the OAuth server by a role with USAGE on the integration during the OAuth client credentials flow.", + }, + } + return helpers.MergeMaps(secretCommonSchema, secretClientCredentials) +}() + +func SecretWithClientCredentials() *schema.Resource { + return &schema.Resource{ + CreateContext: CreateContextSecretWithClientCredentials, + ReadContext: ReadContextSecretWithClientCredentials, + UpdateContext: UpdateContextSecretWithClientCredentials, + DeleteContext: DeleteContextSecret, + Description: "Resource used to manage secret objects with OAuth Client Credentials. For more information, check [secret documentation](https://docs.snowflake.com/en/sql-reference/sql/create-secret).", + + CustomizeDiff: customdiff.All( + ComputedIfAnyAttributeChanged(secretClientCredentialsSchema, DescribeOutputAttributeName, "name", "oauth_scopes", "api_authentication"), + ComputedIfAnyAttributeChanged(secretClientCredentialsSchema, ShowOutputAttributeName, "name", "comment"), + ComputedIfAnyAttributeChanged(secretClientCredentialsSchema, FullyQualifiedNameAttributeName, "name"), + ), + + Schema: secretClientCredentialsSchema, + Importer: &schema.ResourceImporter{ + StateContext: ImportSecretWithClientCredentials, + }, + } +} + +func ImportSecretWithClientCredentials(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + logging.DebugLogger.Printf("[DEBUG] Starting secret with client credentials import") + client := meta.(*provider.Context).Client + + id, err := sdk.ParseSchemaObjectIdentifier(d.Id()) + if err != nil { + return nil, err + } + + if err := handleSecretImport(d); err != nil { + return nil, err + } + secretDescription, err := client.Secrets.Describe(ctx, id) + if err != nil { + return nil, err + } + + if err := d.Set("api_authentication", secretDescription.IntegrationName); err != nil { + return nil, err + } + + if err := d.Set("oauth_scopes", secretDescription.OauthScopes); err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} + +func CreateContextSecretWithClientCredentials(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + databaseName, schemaName, name := d.Get("database").(string), d.Get("schema").(string), d.Get("name").(string) + id := sdk.NewSchemaObjectIdentifier(databaseName, schemaName, name) + + apiIntegrationString := d.Get("api_authentication").(string) + apiIntegration, err := sdk.ParseAccountObjectIdentifier(apiIntegrationString) + if err != nil { + return diag.FromErr(err) + } + + request := sdk.NewCreateWithOAuthClientCredentialsFlowSecretRequest(id, apiIntegration) + + stringScopes := expandStringList(d.Get("oauth_scopes").(*schema.Set).List()) + oauthScopes := make([]sdk.ApiIntegrationScope, len(stringScopes)) + for i, scope := range stringScopes { + oauthScopes[i] = sdk.ApiIntegrationScope{Scope: scope} + } + request.WithOauthScopes(sdk.OauthScopesListRequest{OauthScopesList: oauthScopes}) + + if v, ok := d.GetOk("comment"); ok { + request.WithComment(v.(string)) + } + + err = client.Secrets.CreateWithOAuthClientCredentialsFlow(ctx, request) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(helpers.EncodeResourceIdentifier(id)) + + return ReadContextSecretWithClientCredentials(ctx, d, meta) +} + +func ReadContextSecretWithClientCredentials(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + secret, err := client.Secrets.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 secret with client credentials. Target object not found. Marking the resource as removed.", + Detail: fmt.Sprintf("Secret with client credentials name: %s, Err: %s", id.FullyQualifiedName(), err), + }, + } + } + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to retrieve secret with client credentials.", + Detail: fmt.Sprintf("Id: %s\nError: %s", d.Id(), err), + }, + } + } + secretDescription, err := client.Secrets.Describe(ctx, id) + if err != nil { + return diag.FromErr(err) + } + + if err := handleSecretRead(d, id, secret, secretDescription); err != nil { + return diag.FromErr(err) + } + + if err = d.Set("api_authentication", secretDescription.IntegrationName); err != nil { + return diag.FromErr(err) + } + + if err = d.Set("oauth_scopes", secretDescription.OauthScopes); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func UpdateContextSecretWithClientCredentials(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + id, err := sdk.ParseSchemaObjectIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + set := &sdk.SecretSetRequest{} + unset := &sdk.SecretUnsetRequest{} + handleSecretUpdate(d, set, unset) + setForClientCredentials := &sdk.SetForOAuthClientCredentialsRequest{} + + if d.HasChange("oauth_scopes") { + stringScopes := expandStringList(d.Get("oauth_scopes").(*schema.Set).List()) + oauthScopes := make([]sdk.ApiIntegrationScope, len(stringScopes)) + for i, scope := range stringScopes { + oauthScopes[i] = sdk.ApiIntegrationScope{Scope: scope} + } + setForClientCredentials.WithOauthScopes(sdk.OauthScopesListRequest{OauthScopesList: oauthScopes}) + } + + if !reflect.DeepEqual(*setForClientCredentials, sdk.SetForOAuthClientCredentialsRequest{}) { + set.WithSetForFlow(sdk.SetForFlowRequest{SetForOAuthClientCredentials: setForClientCredentials}) + } + + if !reflect.DeepEqual(*set, sdk.SecretSetRequest{}) { + if err := client.Secrets.Alter(ctx, sdk.NewAlterSecretRequest(id).WithSet(*set)); err != nil { + return diag.FromErr(err) + } + } + + if !reflect.DeepEqual(*unset, sdk.SecretUnsetRequest{}) { + if err := client.Secrets.Alter(ctx, sdk.NewAlterSecretRequest(id).WithUnset(*unset)); err != nil { + return diag.FromErr(err) + } + } + + return ReadContextSecretWithClientCredentials(ctx, d, meta) +} diff --git a/pkg/resources/secret_with_oauth_client_credentials_acceptance_test.go b/pkg/resources/secret_with_oauth_client_credentials_acceptance_test.go new file mode 100644 index 0000000000..265f479d1c --- /dev/null +++ b/pkg/resources/secret_with_oauth_client_credentials_acceptance_test.go @@ -0,0 +1,299 @@ +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/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/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_SecretWithClientCredentials_BasicFlow(t *testing.T) { + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + name := id.Name() + comment := random.Comment() + newComment := random.Comment() + + integrationId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + _, apiIntegrationCleanup := acc.TestClient().SecurityIntegration.CreateApiAuthenticationClientCredentialsWithRequest(t, + sdk.NewCreateApiAuthenticationWithClientCredentialsFlowSecurityIntegrationRequest(integrationId, true, "test_client_id", "test_client_secret"). + WithOauthAllowedScopes([]sdk.AllowedScope{{Scope: "foo"}, {Scope: "bar"}, {Scope: "test"}}), + ) + t.Cleanup(apiIntegrationCleanup) + + secretModel := model.SecretWithClientCredentials("s", integrationId.Name(), id.DatabaseName(), id.SchemaName(), name, []string{"foo", "bar"}).WithComment(comment) + secretModelTestInScopes := model.SecretWithClientCredentials("s", integrationId.Name(), id.DatabaseName(), id.SchemaName(), name, []string{"test"}).WithComment(newComment) + secretModelFooInScopesWithComment := model.SecretWithClientCredentials("s", integrationId.Name(), id.DatabaseName(), id.SchemaName(), name, []string{"foo"}).WithComment(newComment) + secretModelFooInScopes := model.SecretWithClientCredentials("s", integrationId.Name(), id.DatabaseName(), id.SchemaName(), name, []string{"foo"}) + secretModelWithoutComment := model.SecretWithClientCredentials("s", integrationId.Name(), id.DatabaseName(), id.SchemaName(), name, []string{"foo", "bar"}) + secretName := secretModel.ResourceReference() + + 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.SecretWithClientCredentials), + Steps: []resource.TestStep{ + { + Config: config.FromModel(t, secretModel), + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.SecretWithClientCredentialsResource(t, secretModel.ResourceReference()). + HasNameString(name). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasApiAuthenticationString(integrationId.Name()). + HasOauthScopesLength(len([]string{"foo", "bar"})). + HasCommentString(comment), + + resourceshowoutputassert.SecretShowOutput(t, secretModel.ResourceReference()). + HasName(name). + HasDatabaseName(id.DatabaseName()). + HasSecretType(sdk.SecretTypeOAuth2). + HasSchemaName(id.SchemaName()), + ), + resource.TestCheckResourceAttr(secretName, "oauth_scopes.#", "2"), + resource.TestCheckTypeSetElemAttr(secretName, "oauth_scopes.*", "foo"), + resource.TestCheckTypeSetElemAttr(secretName, "oauth_scopes.*", "bar"), + + resource.TestCheckResourceAttr(secretName, "fully_qualified_name", id.FullyQualifiedName()), + resource.TestCheckResourceAttrSet(secretName, "describe_output.0.created_on"), + resource.TestCheckResourceAttr(secretName, "describe_output.0.name", name), + resource.TestCheckResourceAttr(secretName, "describe_output.0.database_name", id.DatabaseName()), + resource.TestCheckResourceAttr(secretName, "describe_output.0.schema_name", id.SchemaName()), + resource.TestCheckResourceAttr(secretName, "describe_output.0.username", ""), + resource.TestCheckResourceAttr(secretName, "describe_output.0.oauth_access_token_expiry_time", ""), + resource.TestCheckResourceAttr(secretName, "describe_output.0.oauth_refresh_token_expiry_time", ""), + resource.TestCheckResourceAttr(secretName, "describe_output.0.integration_name", integrationId.Name()), + resource.TestCheckResourceAttr(secretName, "describe_output.0.oauth_scopes.#", "2"), + resource.TestCheckTypeSetElemAttr(secretName, "describe_output.0.oauth_scopes.*", "foo"), + resource.TestCheckTypeSetElemAttr(secretName, "describe_output.0.oauth_scopes.*", "bar"), + ), + }, + // set oauth_scopes and comment in config + { + Config: config.FromModel(t, secretModelTestInScopes), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(secretName, plancheck.ResourceActionUpdate), + planchecks.ExpectChange(secretName, "oauth_scopes", tfjson.ActionUpdate, sdk.String("[bar foo]"), sdk.String("[test]")), + }, + }, + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.SecretWithClientCredentialsResource(t, "snowflake_secret_with_client_credentials.s"). + HasNameString(name). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasApiAuthenticationString(integrationId.Name()). + HasOauthScopesLength(len([]string{"test"})). + HasCommentString(newComment), + assert.Check(resource.TestCheckResourceAttr(secretName, "oauth_scopes.#", "1")), + assert.Check(resource.TestCheckTypeSetElemAttr(secretName, "oauth_scopes.*", "test")), + + resourceshowoutputassert.SecretShowOutput(t, secretModel.ResourceReference()). + HasSecretType(sdk.SecretTypeOAuth2). + HasComment(newComment), + ), + + resource.TestCheckResourceAttr(secretName, "describe_output.0.comment", newComment), + resource.TestCheckResourceAttr(secretName, "describe_output.0.oauth_scopes.#", "1"), + resource.TestCheckTypeSetElemAttr(secretName, "describe_output.0.oauth_scopes.*", "test"), + ), + }, + // set oauth_scopes and comment externally + { + PreConfig: func() { + req := sdk.NewAlterSecretRequest(id).WithSet(*sdk.NewSecretSetRequest(). + WithSetForFlow(*sdk.NewSetForFlowRequest(). + WithSetForOAuthClientCredentials( + *sdk.NewSetForOAuthClientCredentialsRequest().WithOauthScopes( + *sdk.NewOauthScopesListRequest([]sdk.ApiIntegrationScope{{Scope: "bar"}}), + ), + ), + ), + ) + acc.TestClient().Secret.Alter(t, req) + }, + Config: config.FromModel(t, secretModelFooInScopesWithComment), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(secretModel.ResourceReference(), plancheck.ResourceActionUpdate), + planchecks.ExpectDrift(secretModel.ResourceReference(), "oauth_scopes", sdk.String("[test]"), sdk.String("[bar]")), + planchecks.ExpectChange(secretModel.ResourceReference(), "oauth_scopes", tfjson.ActionUpdate, sdk.String("[bar]"), sdk.String("[foo]")), + }, + }, + Check: assert.AssertThat(t, + resourceassert.SecretWithClientCredentialsResource(t, "snowflake_secret_with_client_credentials.s"). + HasNameString(name). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasApiAuthenticationString(integrationId.Name()). + HasOauthScopesLength(len([]string{"foo"})). + HasCommentString(newComment), + assert.Check(resource.TestCheckResourceAttr(secretName, "oauth_scopes.#", "1")), + assert.Check(resource.TestCheckTypeSetElemAttr(secretName, "oauth_scopes.*", "foo")), + ), + }, + // unset comment + { + Config: config.FromModel(t, secretModelFooInScopes), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(secretModelFooInScopes.ResourceReference(), plancheck.ResourceActionUpdate), + planchecks.ExpectChange(secretModelFooInScopes.ResourceReference(), "comment", tfjson.ActionUpdate, sdk.String(newComment), nil), + }, + }, + Check: assert.AssertThat(t, + resourceassert.SecretWithClientCredentialsResource(t, secretModelFooInScopes.ResourceReference()). + HasCommentString(""), + ), + }, + // set comment externally + { + PreConfig: func() { + req := sdk.NewAlterSecretRequest(id).WithSet(*sdk.NewSecretSetRequest().WithComment(comment)) + acc.TestClient().Secret.Alter(t, req) + }, + Config: config.FromModel(t, secretModelWithoutComment), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(secretModel.ResourceReference(), plancheck.ResourceActionUpdate), + planchecks.ExpectChange(secretModelWithoutComment.ResourceReference(), "comment", tfjson.ActionUpdate, sdk.String(comment), nil), + }, + }, + Check: assert.AssertThat(t, + resourceassert.SecretWithClientCredentialsResource(t, secretModelWithoutComment.ResourceReference()). + HasCommentString(""), + ), + }, + // create without comment + { + Config: config.FromModel(t, secretModelWithoutComment.WithOauthScopes([]string{"foo", "bar"})), + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.SecretWithClientCredentialsResource(t, "snowflake_secret_with_client_credentials.s"). + HasNameString(name). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasApiAuthenticationString(integrationId.Name()). + HasOauthScopesLength(len([]string{"foo", "bar"})). + HasCommentString(""), + ), + resource.TestCheckResourceAttr(secretName, "oauth_scopes.#", "2"), + resource.TestCheckTypeSetElemAttr(secretName, "oauth_scopes.*", "foo"), + resource.TestCheckTypeSetElemAttr(secretName, "oauth_scopes.*", "bar"), + ), + }, + // import + { + ResourceName: secretModelWithoutComment.ResourceReference(), + ImportState: true, + ImportStateVerify: true, + ImportStateCheck: importchecks.ComposeImportStateCheck( + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "name", id.Name()), + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "database", id.DatabaseId().Name()), + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "schema", id.SchemaId().Name()), + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "api_authentication", integrationId.Name()), + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "oauth_scopes.#", "2"), + importchecks.TestCheckResourceAttrInstanceState(helpers.EncodeResourceIdentifier(id), "comment", ""), + ), + }, + }, + }) +} + +func TestAcc_SecretWithClientCredentials_EmptyScopesList(t *testing.T) { + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + name := id.Name() + + integrationId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + _, apiIntegrationCleanup := acc.TestClient().SecurityIntegration.CreateApiAuthenticationClientCredentialsWithRequest(t, + sdk.NewCreateApiAuthenticationWithClientCredentialsFlowSecurityIntegrationRequest(integrationId, true, "foo", "foo"). + WithOauthAllowedScopes([]sdk.AllowedScope{{Scope: "foo"}, {Scope: "bar"}}), + ) + t.Cleanup(apiIntegrationCleanup) + + secretModel := model.SecretWithClientCredentials("s", integrationId.Name(), id.DatabaseName(), id.SchemaName(), name, []string{}) + secretModelEmptyScopes := model.SecretWithClientCredentials("s", integrationId.Name(), id.DatabaseName(), id.SchemaName(), name, []string{}) + + 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.SecretWithClientCredentials), + Steps: []resource.TestStep{ + // create secret without providing oauth_scopes value + { + Config: config.FromModel(t, secretModel), + Check: resource.ComposeTestCheckFunc( + assert.AssertThat(t, + resourceassert.SecretWithClientCredentialsResource(t, secretModel.ResourceReference()). + HasNameString(name). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasApiAuthenticationString(integrationId.Name()). + HasCommentString(""), + ), + resource.TestCheckResourceAttr(secretModel.ResourceReference(), "oauth_scopes.#", "0"), + ), + }, + // Set oauth_scopes + { + Config: config.FromModel(t, secretModel. + WithOauthScopes([]string{"foo"}), + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(secretModel.ResourceReference(), plancheck.ResourceActionUpdate), + planchecks.ExpectChange(secretModel.ResourceReference(), "oauth_scopes", tfjson.ActionUpdate, sdk.String("[]"), sdk.String("[foo]")), + }, + }, + Check: assert.AssertThat(t, + resourceassert.SecretWithClientCredentialsResource(t, secretModel.ResourceReference()). + HasNameString(name). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasApiAuthenticationString(integrationId.Name()), + assert.Check(resource.TestCheckResourceAttr(secretModel.ResourceReference(), "oauth_scopes.#", "1")), + assert.Check(resource.TestCheckTypeSetElemAttr(secretModel.ResourceReference(), "oauth_scopes.*", "foo")), + ), + }, + // Set empty oauth_scopes + { + Config: config.FromModel(t, secretModelEmptyScopes), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(secretModel.ResourceReference(), plancheck.ResourceActionUpdate), + planchecks.ExpectChange(secretModel.ResourceReference(), "oauth_scopes", tfjson.ActionUpdate, sdk.String("[foo]"), sdk.String("[]")), + }, + }, + Check: assert.AssertThat(t, + resourceassert.SecretWithClientCredentialsResource(t, secretModel.ResourceReference()). + HasNameString(name). + HasDatabaseString(id.DatabaseName()). + HasSchemaString(id.SchemaName()). + HasApiAuthenticationString(integrationId.Name()), + assert.Check(resource.TestCheckResourceAttr(secretModel.ResourceReference(), "oauth_scopes.#", "0")), + ), + }, + }, + }) +} diff --git a/pkg/resources/show_and_describe_handlers.go b/pkg/resources/show_and_describe_handlers.go index fcf546ee69..b915b82345 100644 --- a/pkg/resources/show_and_describe_handlers.go +++ b/pkg/resources/show_and_describe_handlers.go @@ -43,6 +43,28 @@ type showMapping struct { normalizeFunc func(any) any } +// handleExternalValueChangesToObjectInDescribe assumes that describe output is kept in DescribeOutputAttributeName attribute +func handleExternalValueChangesToObjectInDescribe(d *schema.ResourceData, mappings ...describeMapping) error { + if descOutput, ok := d.GetOk(DescribeOutputAttributeName); ok { + descOutputList := descOutput.([]any) + if len(descOutputList) == 1 { + result := descOutputList[0].(map[string]any) + for _, mapping := range mappings { + valueToCompareFrom := result[mapping.nameInDescribe] + if mapping.normalizeFunc != nil { + valueToCompareFrom = mapping.normalizeFunc(valueToCompareFrom) + } + if valueToCompareFrom != mapping.valueToCompare { + if err := d.Set(mapping.nameInConfig, mapping.valueToSet); err != nil { + return err + } + } + } + } + } + return nil +} + // handleExternalChangesToObjectInDescribe assumes that show output is kept in DescribeOutputAttributeName attribute func handleExternalChangesToObjectInDescribe(d *schema.ResourceData, mappings ...describeMapping) error { if describeOutput, ok := d.GetOk(DescribeOutputAttributeName); ok { diff --git a/pkg/schemas/gen/sdk_show_result_structs.go b/pkg/schemas/gen/sdk_show_result_structs.go index ea2e977160..337367a112 100644 --- a/pkg/schemas/gen/sdk_show_result_structs.go +++ b/pkg/schemas/gen/sdk_show_result_structs.go @@ -37,6 +37,7 @@ var SdkShowResultStructs = []any{ sdk.Role{}, sdk.RowAccessPolicy{}, sdk.Schema{}, + sdk.Secret{}, sdk.SecurityIntegration{}, sdk.Sequence{}, sdk.SessionPolicy{}, diff --git a/pkg/schemas/secret.go b/pkg/schemas/secret.go new file mode 100644 index 0000000000..2ba91ef9fa --- /dev/null +++ b/pkg/schemas/secret.go @@ -0,0 +1,81 @@ +package schemas + +import ( + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +// DescribeSecretSchema represents output of DESCRIBE query for the single secret. +var DescribeSecretSchema = map[string]*schema.Schema{ + "created_on": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "database_name": { + Type: schema.TypeString, + Computed: true, + }, + "schema_name": { + Type: schema.TypeString, + Computed: true, + }, + "owner": { + Type: schema.TypeString, + Computed: true, + }, + "comment": { + Type: schema.TypeString, + Computed: true, + }, + "secret_type": { + Type: schema.TypeString, + Computed: true, + }, + "username": { + Type: schema.TypeString, + Computed: true, + }, + "oauth_access_token_expiry_time": { + Type: schema.TypeString, + Computed: true, + }, + "oauth_refresh_token_expiry_time": { + Type: schema.TypeString, + Computed: true, + }, + "oauth_scopes": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + }, + "integration_name": { + Type: schema.TypeString, + Computed: true, + }, +} + +func SecretDescriptionToSchema(details sdk.SecretDetails) map[string]any { + s := map[string]any{ + "created_on": details.CreatedOn.String(), + "name": details.Name, + "database_name": details.DatabaseName, + "schema_name": details.SchemaName, + "owner": details.Owner, + "comment": details.Comment, + "secret_type": details.SecretType, + "username": details.Username, + "oauth_scopes": details.OauthScopes, + "integration_name": details.IntegrationName, + } + if details.OauthAccessTokenExpiryTime != nil { + s["oauth_access_token_expiry_time"] = details.OauthAccessTokenExpiryTime.String() + } + if details.OauthRefreshTokenExpiryTime != nil { + s["oauth_refresh_token_expiry_time"] = details.OauthRefreshTokenExpiryTime.String() + } + return s +} diff --git a/pkg/schemas/secret_gen.go b/pkg/schemas/secret_gen.go new file mode 100644 index 0000000000..e76a1777d1 --- /dev/null +++ b/pkg/schemas/secret_gen.go @@ -0,0 +1,69 @@ +// 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" +) + +// ShowSecretSchema represents output of SHOW query for the single Secret. +var ShowSecretSchema = map[string]*schema.Schema{ + "created_on": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "schema_name": { + Type: schema.TypeString, + Computed: true, + }, + "database_name": { + Type: schema.TypeString, + Computed: true, + }, + "owner": { + Type: schema.TypeString, + Computed: true, + }, + "comment": { + Type: schema.TypeString, + Computed: true, + }, + "secret_type": { + Type: schema.TypeString, + Computed: true, + }, + "oauth_scopes": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + }, + "owner_role_type": { + Type: schema.TypeString, + Computed: true, + }, +} + +var _ = ShowSecretSchema + +func SecretToSchema(secret *sdk.Secret) map[string]any { + secretSchema := make(map[string]any) + secretSchema["created_on"] = secret.CreatedOn.String() + secretSchema["name"] = secret.Name + secretSchema["schema_name"] = secret.SchemaName + secretSchema["database_name"] = secret.DatabaseName + secretSchema["owner"] = secret.Owner + if secret.Comment != nil { + secretSchema["comment"] = secret.Comment + } + secretSchema["secret_type"] = secret.SecretType + secretSchema["oauth_scopes"] = secret.OauthScopes + secretSchema["owner_role_type"] = secret.OwnerRoleType + return secretSchema +} + +var _ = SecretToSchema diff --git a/pkg/sdk/secrets_def.go b/pkg/sdk/secrets_def.go index a1c2ec1a2f..44c80475c6 100644 --- a/pkg/sdk/secrets_def.go +++ b/pkg/sdk/secrets_def.go @@ -1,8 +1,17 @@ package sdk -import g "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/poc/generator" +import ( + "fmt" + + g "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk/poc/generator" +) //go:generate go run ./poc/main.go +const ( + SecretTypePassword = "PASSWORD" + SecretTypeOAuth2 = "OAUTH2" + SecretTypeGenericString = "GENERIC_STRING" +) var secretDbRow = g.DbStruct("secretDBRow"). Field("created_on", "time.Time"). @@ -112,7 +121,7 @@ var SecretsDef = g.NewInterface( SQL("SECRET"). IfNotExists(). Name(). - PredefinedQueryStructField("secretType", "string", g.StaticOptions().SQL("TYPE = OAUTH2")). + PredefinedQueryStructField("secretType", "string", g.StaticOptions().SQL(fmt.Sprintf("TYPE = %s", SecretTypeOAuth2))). Identifier("ApiIntegration", g.KindOfT[AccountObjectIdentifier](), g.IdentifierOptions().Required().Equals().SQL("API_AUTHENTICATION")). OptionalQueryStructField("OauthScopes", oauthScopesListDef, g.ParameterOptions().SQL("OAUTH_SCOPES").Parentheses()). OptionalComment(). @@ -128,7 +137,7 @@ var SecretsDef = g.NewInterface( SQL("SECRET"). IfNotExists(). Name(). - PredefinedQueryStructField("secretType", "string", g.StaticOptions().SQL("TYPE = OAUTH2")). + PredefinedQueryStructField("secretType", "string", g.StaticOptions().SQL(fmt.Sprintf("TYPE = %s", SecretTypeOAuth2))). TextAssignment("OAUTH_REFRESH_TOKEN", g.ParameterOptions().NoParentheses().SingleQuotes().Required()). TextAssignment("OAUTH_REFRESH_TOKEN_EXPIRY_TIME", g.ParameterOptions().NoParentheses().SingleQuotes().Required()). Identifier("ApiIntegration", g.KindOfT[AccountObjectIdentifier](), g.IdentifierOptions().Required().Equals().SQL("API_AUTHENTICATION")). @@ -144,7 +153,7 @@ var SecretsDef = g.NewInterface( SQL("SECRET"). IfNotExists(). Name(). - PredefinedQueryStructField("secretType", "string", g.StaticOptions().SQL("TYPE = PASSWORD")). + PredefinedQueryStructField("secretType", "string", g.StaticOptions().SQL(fmt.Sprintf("TYPE = %s", SecretTypePassword))). TextAssignment("USERNAME", g.ParameterOptions().NoParentheses().SingleQuotes().Required()). TextAssignment("PASSWORD", g.ParameterOptions().NoParentheses().SingleQuotes().Required()). OptionalComment(). @@ -159,7 +168,7 @@ var SecretsDef = g.NewInterface( SQL("SECRET"). IfNotExists(). Name(). - PredefinedQueryStructField("secretType", "string", g.StaticOptions().SQL("TYPE = GENERIC_STRING")). + PredefinedQueryStructField("secretType", "string", g.StaticOptions().SQL(fmt.Sprintf("TYPE = %s", SecretTypeGenericString))). TextAssignment("SECRET_STRING", g.ParameterOptions().SingleQuotes().Required()). OptionalComment(). WithValidation(g.ValidIdentifier, "name"). diff --git a/pkg/sdk/testint/secrets_gen_integration_test.go b/pkg/sdk/testint/secrets_gen_integration_test.go index fef05740ba..fa8042701c 100644 --- a/pkg/sdk/testint/secrets_gen_integration_test.go +++ b/pkg/sdk/testint/secrets_gen_integration_test.go @@ -19,7 +19,6 @@ func TestInt_Secrets(t *testing.T) { integrationId := testClientHelper().Ids.RandomAccountObjectIdentifier() - // "YYYY-MM-DD" or "YYYY-MM-DD HH:MI-SS" format has to be used, otherwise Snowflake returns error: "Invalid date/time format" refreshTokenExpiryTime := time.Now().Add(24 * time.Hour).Format(time.DateOnly) _, apiIntegrationCleanup := testClientHelper().SecurityIntegration.CreateApiAuthenticationClientCredentialsWithRequest(t, @@ -76,7 +75,7 @@ func TestInt_Secrets(t *testing.T) { objectassert.Secret(t, id). HasName(id.Name()). HasComment("a"). - HasSecretType("OAUTH2"). + HasSecretType(sdk.SecretTypeOAuth2). HasOauthScopes([]string{"foo", "bar"}). HasDatabaseName(id.DatabaseName()). HasSchemaName(id.SchemaName()), @@ -88,7 +87,7 @@ func TestInt_Secrets(t *testing.T) { assertSecretDetails(details, secretDetails{ Name: id.Name(), Comment: sdk.String("a"), - SecretType: "OAUTH2", + SecretType: sdk.SecretTypeOAuth2, OauthScopes: []string{"foo", "bar"}, IntegrationName: sdk.String(integrationId.Name()), }) @@ -115,7 +114,7 @@ func TestInt_Secrets(t *testing.T) { assertSecretDetails(details, secretDetails{ Name: id.Name(), - SecretType: "OAUTH2", + SecretType: sdk.SecretTypeOAuth2, IntegrationName: sdk.String(integrationId.Name()), }) }) @@ -165,7 +164,7 @@ func TestInt_Secrets(t *testing.T) { objectassert.Secret(t, id). HasName(id.Name()). HasComment("a"). - HasSecretType("OAUTH2"). + HasSecretType(sdk.SecretTypeOAuth2). HasDatabaseName(id.DatabaseName()). HasSchemaName(id.SchemaName()), ) @@ -175,7 +174,7 @@ func TestInt_Secrets(t *testing.T) { assertSecretDetails(details, secretDetails{ Name: id.Name(), - SecretType: "OAUTH2", + SecretType: sdk.SecretTypeOAuth2, Comment: sdk.String("a"), OauthRefreshTokenExpiryTime: stringDateToSnowflakeTimeFormat(time.DateOnly, refreshTokenExpiryTime), IntegrationName: sdk.String(integrationId.Name()), @@ -197,7 +196,7 @@ func TestInt_Secrets(t *testing.T) { assertSecretDetails(details, secretDetails{ Name: id.Name(), - SecretType: "OAUTH2", + SecretType: sdk.SecretTypeOAuth2, OauthRefreshTokenExpiryTime: stringDateToSnowflakeTimeFormat(time.DateTime, refreshTokenWithTime), IntegrationName: sdk.String(integrationId.Name()), }) @@ -221,7 +220,7 @@ func TestInt_Secrets(t *testing.T) { objectassert.SecretFromObject(t, secret). HasName(id.Name()). HasComment(comment). - HasSecretType("PASSWORD"). + HasSecretType(sdk.SecretTypePassword). HasDatabaseName(id.DatabaseName()). HasSchemaName(id.SchemaName()), ) @@ -232,7 +231,7 @@ func TestInt_Secrets(t *testing.T) { assertSecretDetails(details, secretDetails{ Name: id.Name(), Comment: sdk.String(comment), - SecretType: "PASSWORD", + SecretType: sdk.SecretTypePassword, Username: sdk.String("foo"), }) }) @@ -251,7 +250,7 @@ func TestInt_Secrets(t *testing.T) { assertSecretDetails(details, secretDetails{ Name: id.Name(), - SecretType: "PASSWORD", + SecretType: sdk.SecretTypePassword, Username: sdk.String(""), }) }) @@ -274,7 +273,7 @@ func TestInt_Secrets(t *testing.T) { objectassert.Secret(t, id). HasName(id.Name()). HasComment(comment). - HasSecretType("GENERIC_STRING"). + HasSecretType(sdk.SecretTypeGenericString). HasDatabaseName(id.DatabaseName()). HasSchemaName(id.SchemaName()), ) @@ -291,7 +290,7 @@ func TestInt_Secrets(t *testing.T) { assertions.AssertThat(t, objectassert.Secret(t, id). HasName(id.Name()). - HasSecretType("GENERIC_STRING"). + HasSecretType(sdk.SecretTypeGenericString). HasDatabaseName(id.DatabaseName()). HasSchemaName(id.SchemaName()), ) @@ -321,7 +320,7 @@ func TestInt_Secrets(t *testing.T) { assertSecretDetails(details, secretDetails{ Name: id.Name(), - SecretType: "OAUTH2", + SecretType: sdk.SecretTypeOAuth2, Comment: sdk.String(comment), OauthScopes: []string{"foo", "bar"}, IntegrationName: sdk.String(integrationId.Name()), @@ -368,7 +367,7 @@ func TestInt_Secrets(t *testing.T) { assertSecretDetails(details, secretDetails{ Name: id.Name(), - SecretType: "OAUTH2", + SecretType: sdk.SecretTypeOAuth2, Comment: sdk.String(comment), OauthRefreshTokenExpiryTime: stringDateToSnowflakeTimeFormat(time.DateOnly, alteredRefreshTokenExpiryTime), IntegrationName: sdk.String(integrationId.Name()), @@ -415,7 +414,7 @@ func TestInt_Secrets(t *testing.T) { // Cannot check password property since show and describe on secret do not have access to it assertSecretDetails(details, secretDetails{ Name: id.Name(), - SecretType: "PASSWORD", + SecretType: sdk.SecretTypePassword, Comment: sdk.String(comment), Username: sdk.String("bar"), }) @@ -713,7 +712,7 @@ func TestInt_Secrets(t *testing.T) { assertSecretDetails(details, secretDetails{ Name: id.Name(), - SecretType: "GENERIC_STRING", + SecretType: sdk.SecretTypeGenericString, }) }) } diff --git a/templates/resources/secret_with_authorization_code_grant.md.tmpl b/templates/resources/secret_with_authorization_code_grant.md.tmpl new file mode 100644 index 0000000000..bbc5e20afb --- /dev/null +++ b/templates/resources/secret_with_authorization_code_grant.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/secret_with_basic_authentication.md.tmpl b/templates/resources/secret_with_basic_authentication.md.tmpl new file mode 100644 index 0000000000..bbc5e20afb --- /dev/null +++ b/templates/resources/secret_with_basic_authentication.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/secret_with_client_credentials.md.tmpl b/templates/resources/secret_with_client_credentials.md.tmpl new file mode 100644 index 0000000000..bbc5e20afb --- /dev/null +++ b/templates/resources/secret_with_client_credentials.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/secret_with_generic_string.md.tmpl b/templates/resources/secret_with_generic_string.md.tmpl new file mode 100644 index 0000000000..bbc5e20afb --- /dev/null +++ b/templates/resources/secret_with_generic_string.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 }}