From 811013887cdf1a6624c93481f18e12a183865463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Budzy=C5=84ski?= Date: Fri, 18 Oct 2024 18:11:00 +0200 Subject: [PATCH] feat: Secrets datasource (#3131) ## Changes - added datasource for secrets - generated documentation for secrets - ComposeCheckDestroy function ## Test Plan * [X] acceptance tests ## References https://docs.snowflake.com/en/sql-reference/sql/create-secret ## TODO Handling secret type changed externally --- docs/data-sources/secrets.md | 131 +++++++ .../snowflake_secrets/data-source.tf | 46 +++ .../secrets_datasource_show_output_ext.go | 18 + pkg/acceptance/check_destroy.go | 13 + pkg/datasources/secrets.go | 142 +++++++ pkg/datasources/secrets_acceptance_test.go | 367 ++++++++++++++++++ pkg/provider/provider.go | 1 + templates/data-sources/secrets.md.tmpl | 24 ++ 8 files changed, 742 insertions(+) create mode 100644 docs/data-sources/secrets.md create mode 100644 examples/data-sources/snowflake_secrets/data-source.tf create mode 100644 pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/secrets_datasource_show_output_ext.go create mode 100644 pkg/datasources/secrets.go create mode 100644 pkg/datasources/secrets_acceptance_test.go create mode 100644 templates/data-sources/secrets.md.tmpl diff --git a/docs/data-sources/secrets.md b/docs/data-sources/secrets.md new file mode 100644 index 0000000000..7271fd6090 --- /dev/null +++ b/docs/data-sources/secrets.md @@ -0,0 +1,131 @@ +--- +page_title: "snowflake_secrets Data Source - terraform-provider-snowflake" +subcategory: "" +description: |- + Datasource used to get details of filtered secrets. Filtering is aligned with the current possibilities for SHOW SECRETS https://docs.snowflake.com/en/sql-reference/sql/show-secrets query. The results of SHOW and DESCRIBE are encapsulated in one output collection secrets. +--- + +!> **V1 release candidate** This data source is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the data source if needed. Any errors reported will be resolved with a higher priority. We encourage checking this data source 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_secrets (Data Source) + +Datasource used to get details of filtered secrets. Filtering is aligned with the current possibilities for [SHOW SECRETS](https://docs.snowflake.com/en/sql-reference/sql/show-secrets) query. The results of SHOW and DESCRIBE are encapsulated in one output collection `secrets`. + +## Example Usage + +```terraform +# Simple usage +data "snowflake_secrets" "simple" { +} + +output "simple_output" { + value = data.snowflake_secrets.simple.secrets +} + +# Filtering (like) +data "snowflake_secrets" "like" { + like = "secret-name" +} + +output "like_output" { + value = data.snowflake_secrets.like.secrets +} + +# Filtering by prefix (like) +data "snowflake_secrets" "like_prefix" { + like = "prefix%" +} + +output "like_prefix_output" { + value = data.snowflake_secrets.like_prefix.secrets +} + +# Filtering (in) +data "snowflake_secrets" "in" { + in { + schema = snowflake_schema.test.fully_qualified_name + } +} + +output "in_output" { + value = data.snowflake_secrets.in.secrets +} + +# Without additional data (to limit the number of calls make for every found secret) +data "snowflake_secrets" "only_show" { + # with_describe is turned on by default and it calls DESCRIBE SECRET for every secret found and attaches its output to secrets.*.describe_output field + with_describe = false +} + +output "only_show_output" { + value = data.snowflake_secrets.only_show.secrets +} +``` + + +## Schema + +### Optional + +- `in` (Block List, Max: 1) IN clause to filter the list of secrets (see [below for nested schema](#nestedblock--in)) +- `like` (String) Filters the output with **case-insensitive** pattern, with support for SQL wildcard characters (`%` and `_`). +- `with_describe` (Boolean) Runs DESC SECRET for each secret returned by SHOW SECRETS. The output of describe is saved to the description field. By default this value is set to true. + +### Read-Only + +- `id` (String) The ID of this resource. +- `secrets` (List of Object) Holds the aggregated output of all secrets details queries. (see [below for nested schema](#nestedatt--secrets)) + + +### Nested Schema for `in` + +Optional: + +- `account` (Boolean) Returns records for the entire account. +- `application` (String) Returns records for the specified application. +- `application_package` (String) Returns records for the specified application package. +- `database` (String) Returns records for the current database in use or for a specified database. +- `schema` (String) Returns records for the current schema in use or a specified schema. Use fully qualified name. + + + +### Nested Schema for `secrets` + +Read-Only: + +- `describe_output` (List of Object) (see [below for nested schema](#nestedobjatt--secrets--describe_output)) +- `show_output` (List of Object) (see [below for nested schema](#nestedobjatt--secrets--show_output)) + + +### Nested Schema for `secrets.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 `secrets.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) diff --git a/examples/data-sources/snowflake_secrets/data-source.tf b/examples/data-sources/snowflake_secrets/data-source.tf new file mode 100644 index 0000000000..16c23adcf9 --- /dev/null +++ b/examples/data-sources/snowflake_secrets/data-source.tf @@ -0,0 +1,46 @@ +# Simple usage +data "snowflake_secrets" "simple" { +} + +output "simple_output" { + value = data.snowflake_secrets.simple.secrets +} + +# Filtering (like) +data "snowflake_secrets" "like" { + like = "secret-name" +} + +output "like_output" { + value = data.snowflake_secrets.like.secrets +} + +# Filtering by prefix (like) +data "snowflake_secrets" "like_prefix" { + like = "prefix%" +} + +output "like_prefix_output" { + value = data.snowflake_secrets.like_prefix.secrets +} + +# Filtering (in) +data "snowflake_secrets" "in" { + in { + schema = snowflake_schema.test.fully_qualified_name + } +} + +output "in_output" { + value = data.snowflake_secrets.in.secrets +} + +# Without additional data (to limit the number of calls make for every found secret) +data "snowflake_secrets" "only_show" { + # with_describe is turned on by default and it calls DESCRIBE SECRET for every secret found and attaches its output to secrets.*.describe_output field + with_describe = false +} + +output "only_show_output" { + value = data.snowflake_secrets.only_show.secrets +} diff --git a/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/secrets_datasource_show_output_ext.go b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/secrets_datasource_show_output_ext.go new file mode 100644 index 0000000000..636e143135 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/secrets_datasource_show_output_ext.go @@ -0,0 +1,18 @@ +package resourceshowoutputassert + +import ( + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" +) + +// SecretsDatasourceShowOutput is a temporary workaround to have better show output assertions in data source acceptance tests. +func SecretsDatasourceShowOutput(t *testing.T, name string) *SecretShowOutputAssert { + t.Helper() + + s := SecretShowOutputAssert{ + ResourceAssert: assert.NewDatasourceAssert("data."+name, "show_output", "secrets.0."), + } + s.AddAssertion(assert.ValueSet("show_output.#", "1")) + return &s +} diff --git a/pkg/acceptance/check_destroy.go b/pkg/acceptance/check_destroy.go index c39bd71174..74e4195b74 100644 --- a/pkg/acceptance/check_destroy.go +++ b/pkg/acceptance/check_destroy.go @@ -15,6 +15,19 @@ import ( "github.com/hashicorp/terraform-plugin-testing/terraform" ) +func ComposeCheckDestroy(t *testing.T, resources ...resources.Resource) func(*terraform.State) error { + t.Helper() + + return func(s *terraform.State) error { + errs := make([]error, 0) + for _, resource := range resources { + checkFunc := CheckDestroy(t, resource) + errs = append(errs, checkFunc(s)) + } + return errors.Join(errs...) + } +} + func CheckDestroy(t *testing.T, resource resources.Resource) func(*terraform.State) error { t.Helper() // TODO [SNOW-1653619]: use TestClient() here diff --git a/pkg/datasources/secrets.go b/pkg/datasources/secrets.go new file mode 100644 index 0000000000..41101c1ae4 --- /dev/null +++ b/pkg/datasources/secrets.go @@ -0,0 +1,142 @@ +package datasources + +import ( + "context" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/resources" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/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 secretsSchema = map[string]*schema.Schema{ + "with_describe": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Runs DESC SECRET for each secret returned by SHOW SECRETS. The output of describe is saved to the description field. By default this value is set to true.", + }, + "in": { + Type: schema.TypeList, + Optional: true, + Description: "IN clause to filter the list of secrets", + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "account": { + Type: schema.TypeBool, + Optional: true, + Description: "Returns records for the entire account.", + ExactlyOneOf: []string{"in.0.account", "in.0.database", "in.0.schema", "in.0.application", "in.0.application_package"}, + }, + "database": { + Type: schema.TypeString, + Optional: true, + Description: "Returns records for the current database in use or for a specified database.", + ExactlyOneOf: []string{"in.0.account", "in.0.database", "in.0.schema", "in.0.application", "in.0.application_package"}, + ValidateDiagFunc: resources.IsValidIdentifier[sdk.AccountObjectIdentifier](), + }, + "schema": { + Type: schema.TypeString, + Optional: true, + Description: "Returns records for the current schema in use or a specified schema. Use fully qualified name.", + ExactlyOneOf: []string{"in.0.account", "in.0.database", "in.0.schema", "in.0.application", "in.0.application_package"}, + ValidateDiagFunc: resources.IsValidIdentifier[sdk.DatabaseObjectIdentifier](), + }, + "application": { + Type: schema.TypeString, + Optional: true, + Description: "Returns records for the specified application.", + ExactlyOneOf: []string{"in.0.account", "in.0.database", "in.0.schema", "in.0.application", "in.0.application_package"}, + ValidateDiagFunc: resources.IsValidIdentifier[sdk.AccountObjectIdentifier](), + }, + "application_package": { + Type: schema.TypeString, + Optional: true, + Description: "Returns records for the specified application package.", + ExactlyOneOf: []string{"in.0.account", "in.0.database", "in.0.schema", "in.0.application", "in.0.application_package"}, + ValidateDiagFunc: resources.IsValidIdentifier[sdk.AccountObjectIdentifier](), + }, + }, + }, + }, + "like": { + Type: schema.TypeString, + Optional: true, + Description: "Filters the output with **case-insensitive** pattern, with support for SQL wildcard characters (`%` and `_`).", + }, + "secrets": { + Type: schema.TypeList, + Computed: true, + Description: "Holds the aggregated output of all secrets details queries.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + resources.ShowOutputAttributeName: { + Type: schema.TypeList, + Computed: true, + Description: "Holds the output of SHOW SECRETS.", + Elem: &schema.Resource{ + Schema: schemas.ShowSecretSchema, + }, + }, + resources.DescribeOutputAttributeName: { + Type: schema.TypeList, + Computed: true, + Description: "Holds the output of DESCRIBE SECRET.", + Elem: &schema.Resource{ + Schema: schemas.DescribeSecretSchema, + }, + }, + }, + }, + }, +} + +func Secrets() *schema.Resource { + return &schema.Resource{ + ReadContext: ReadSecrets, + Schema: secretsSchema, + Description: "Datasource used to get details of filtered secrets. Filtering is aligned with the current possibilities for [SHOW SECRETS](https://docs.snowflake.com/en/sql-reference/sql/show-secrets) query. The results of SHOW and DESCRIBE are encapsulated in one output collection `secrets`.", + } +} + +func ReadSecrets(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + req := sdk.NewShowSecretRequest() + + handleLike(d, &req.Like) + err := handleExtendedIn(d, &req.In) + if err != nil { + return diag.FromErr(err) + } + + secrets, err := client.Secrets.Show(ctx, req) + if err != nil { + return diag.FromErr(err) + } + d.SetId("secrets_read") + + flattenedSecrets := make([]map[string]any, len(secrets)) + for i, secret := range secrets { + secret := secret + var secretDescriptions []map[string]any + if d.Get("with_describe").(bool) { + describeOutput, err := client.Secrets.Describe(ctx, secret.ID()) + if err != nil { + return diag.FromErr(err) + } + secretDescriptions = []map[string]any{schemas.SecretDescriptionToSchema(*describeOutput)} + } + + flattenedSecrets[i] = map[string]any{ + resources.ShowOutputAttributeName: []map[string]any{schemas.SecretToSchema(&secret)}, + resources.DescribeOutputAttributeName: secretDescriptions, + } + } + if err := d.Set("secrets", flattenedSecrets); err != nil { + return diag.FromErr(err) + } + return nil +} diff --git a/pkg/datasources/secrets_acceptance_test.go b/pkg/datasources/secrets_acceptance_test.go new file mode 100644 index 0000000000..28627337b2 --- /dev/null +++ b/pkg/datasources/secrets_acceptance_test.go @@ -0,0 +1,367 @@ +package datasources_test + +import ( + "fmt" + "regexp" + "strings" + "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/resourceshowoutputassert" + accConfig "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/testenvs" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +const ( + secretWithClientCredentials = "snowflake_secret_with_client_credentials" + secretWithAuthorizationCodeGrant = "snowflake_secret_with_authorization_code_grant" + secretWithBasicAuthentication = "snowflake_secret_with_basic_authentication" + secretWithGenericString = "snowflake_secret_with_generic_string" +) + +func TestAcc_Secrets_WithClientCredentials(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + + integrationId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + _, apiIntegrationCleanup := acc.TestClient().SecurityIntegration.CreateApiAuthenticationClientCredentialsWithRequest(t, + sdk.NewCreateApiAuthenticationWithClientCredentialsFlowSecurityIntegrationRequest(integrationId, true, "test_oauth_client_id", "test_oauth_client_secret"). + WithOauthAllowedScopes([]sdk.AllowedScope{{Scope: "username"}, {Scope: "test_scope"}}), + ) + t.Cleanup(apiIntegrationCleanup) + + secretModel := model.SecretWithClientCredentials("test", integrationId.Name(), id.DatabaseName(), id.SchemaName(), id.Name(), []string{"username", "test_scope"}) + + dataSecretsClientCredentials := accConfig.FromModel(t, secretModel) + secretsData(secretWithClientCredentials) + + dsName := "data.snowflake_secrets.test" + 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: dataSecretsClientCredentials, + Check: assert.AssertThat(t, + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.#", "1")), + resourceshowoutputassert.SecretsDatasourceShowOutput(t, "snowflake_secrets.test"). + HasName(id.Name()). + HasDatabaseName(id.DatabaseName()). + HasSchemaName(id.SchemaName()). + HasComment(""). + HasSecretType(sdk.SecretTypeOAuth2), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.show_output.0.oauth_scopes.#", "2")), + assert.Check(resource.TestCheckTypeSetElemAttr(dsName, "secrets.0.show_output.0.oauth_scopes.*", "username")), + assert.Check(resource.TestCheckTypeSetElemAttr(dsName, "secrets.0.show_output.0.oauth_scopes.*", "test_scope")), + + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.name", id.Name())), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.database_name", id.DatabaseName())), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.schema_name", id.SchemaName())), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.secret_type", sdk.SecretTypeOAuth2)), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.username", "")), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.comment", "")), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.oauth_scopes.#", "2")), + assert.Check(resource.TestCheckTypeSetElemAttr(dsName, "secrets.0.describe_output.0.oauth_scopes.*", "username")), + assert.Check(resource.TestCheckTypeSetElemAttr(dsName, "secrets.0.describe_output.0.oauth_scopes.*", "test_scope")), + ), + }, + }, + }) +} + +func TestAcc_Secrets_WithAuthorizationCodeGrant(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + + integrationId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + _, apiIntegrationCleanup := acc.TestClient().SecurityIntegration.CreateApiAuthenticationClientCredentialsWithRequest(t, + sdk.NewCreateApiAuthenticationWithClientCredentialsFlowSecurityIntegrationRequest(integrationId, true, "test_oauth_client_id", "test_oauth_client_secret"). + WithOauthAllowedScopes([]sdk.AllowedScope{{Scope: "username"}, {Scope: "test_scope"}}), + ) + t.Cleanup(apiIntegrationCleanup) + + secretModel := model.SecretWithAuthorizationCodeGrant("test", integrationId.Name(), id.DatabaseName(), id.SchemaName(), id.Name(), "test_token", time.Now().Add(24*time.Hour).Format(time.DateTime)).WithComment("test_comment") + + dataSecretsAuthorizationCode := accConfig.FromModel(t, secretModel) + secretsData(secretWithAuthorizationCodeGrant) + + dsName := "data.snowflake_secrets.test" + 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{ + { + Config: dataSecretsAuthorizationCode, + Check: assert.AssertThat(t, + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.#", "1")), + resourceshowoutputassert.SecretsDatasourceShowOutput(t, "snowflake_secrets.test"). + HasName(id.Name()). + HasDatabaseName(id.DatabaseName()). + HasSchemaName(id.SchemaName()). + HasComment("test_comment"). + HasSecretType(sdk.SecretTypeOAuth2), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.show_output.0.oauth_scopes.#", "0")), + + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.name", id.Name())), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.database_name", id.DatabaseName())), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.schema_name", id.SchemaName())), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.secret_type", sdk.SecretTypeOAuth2)), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.username", "")), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.comment", "test_comment")), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.oauth_scopes.#", "0")), + assert.Check(resource.TestCheckResourceAttrSet(dsName, "secrets.0.describe_output.0.oauth_refresh_token_expiry_time")), + ), + }, + }, + }) +} + +func TestAcc_Secrets_WithBasicAuthentication(t *testing.T) { + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + + secretModel := model.SecretWithBasicAuthentication("test", id.DatabaseName(), id.Name(), "test_passwd", id.SchemaName(), "test_username") + dataSecretsAuthorizationCode := accConfig.FromModel(t, secretModel) + secretsData(secretWithBasicAuthentication) + + dsName := "data.snowflake_secrets.test" + 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: dataSecretsAuthorizationCode, + Check: assert.AssertThat(t, + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.#", "1")), + resourceshowoutputassert.SecretsDatasourceShowOutput(t, "snowflake_secrets.test"). + HasName(id.Name()). + HasDatabaseName(id.DatabaseName()). + HasSchemaName(id.SchemaName()). + HasComment(""). + HasSecretType(sdk.SecretTypePassword), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.show_output.0.oauth_scopes.#", "0")), + + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.name", id.Name())), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.database_name", id.DatabaseName())), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.schema_name", id.SchemaName())), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.secret_type", sdk.SecretTypePassword)), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.username", "test_username")), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.comment", "")), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.oauth_scopes.#", "0")), + ), + }, + }, + }) +} + +func TestAcc_Secrets_WithGenericString(t *testing.T) { + id := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + + secretModel := model.SecretWithGenericString("test", id.DatabaseName(), id.Name(), id.SchemaName(), "test_secret_string") + + dataSecretsAuthorizationCode := accConfig.FromModel(t, secretModel) + secretsData(secretWithGenericString) + + dsName := "data.snowflake_secrets.test" + 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{ + { + Config: dataSecretsAuthorizationCode, + Check: assert.AssertThat(t, + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.#", "1")), + resourceshowoutputassert.SecretsDatasourceShowOutput(t, "snowflake_secrets.test"). + HasName(id.Name()). + HasDatabaseName(id.DatabaseName()). + HasSchemaName(id.SchemaName()). + HasComment(""). + HasSecretType(sdk.SecretTypeGenericString), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.show_output.0.oauth_scopes.#", "0")), + + assert.Check(resource.TestCheckResourceAttrSet(dsName, "secrets.0.describe_output.0.created_on")), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.name", id.Name())), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.database_name", id.DatabaseName())), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.schema_name", id.SchemaName())), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.secret_type", sdk.SecretTypeGenericString)), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.username", "")), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.comment", "")), + assert.Check(resource.TestCheckResourceAttr(dsName, "secrets.0.describe_output.0.oauth_scopes.#", "0")), + ), + }, + }, + }) +} + +func secretsData(secretResourceName string) string { + return fmt.Sprintf(` + data "snowflake_secrets" "test" { + depends_on = [%s.test] + }`, secretResourceName) +} + +func TestAcc_Secrets_Filtering(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + acc.TestAccPreCheck(t) + + prefix := random.AlphaN(4) + idOne := acc.TestClient().Ids.RandomSchemaObjectIdentifierWithPrefix(prefix) + idTwo := acc.TestClient().Ids.RandomSchemaObjectIdentifierWithPrefix(prefix) + idThree := acc.TestClient().Ids.RandomSchemaObjectIdentifierWithPrefix(prefix) + idFour := acc.TestClient().Ids.RandomSchemaObjectIdentifier() + + integrationId := acc.TestClient().Ids.RandomAccountObjectIdentifier() + _, apiIntegrationCleanup := acc.TestClient().SecurityIntegration.CreateApiAuthenticationClientCredentialsWithRequest(t, + sdk.NewCreateApiAuthenticationWithClientCredentialsFlowSecurityIntegrationRequest(integrationId, true, "test_oauth_client_id", "test_oauth_client_secret"). + WithOauthAllowedScopes([]sdk.AllowedScope{{Scope: "first_scope"}, {Scope: "second_scope"}}), + ) + t.Cleanup(apiIntegrationCleanup) + + schema, schemaCleanup := acc.TestClient().Schema.CreateSchemaInDatabase(t, acc.TestClient().Ids.DatabaseId()) + t.Cleanup(schemaCleanup) + + idFive := acc.TestClient().Ids.RandomSchemaObjectIdentifierInSchema(schema.ID()) + + secretModelBasicAuth := model.SecretWithBasicAuthentication("s", idOne.DatabaseName(), idOne.Name(), "test_passwd", idOne.SchemaName(), "test_username") + secretModelGenericString := model.SecretWithGenericString("s2", idTwo.DatabaseName(), idTwo.Name(), idTwo.SchemaName(), "foo") + secretModelClientCredentials := model.SecretWithClientCredentials("s3", integrationId.Name(), idThree.DatabaseName(), idThree.SchemaName(), idThree.Name(), []string{"first_scope", "second_scope"}) + secretModelAuthorizationCodeGrant := model.SecretWithAuthorizationCodeGrant("s4", integrationId.Name(), idFour.DatabaseName(), idFour.SchemaName(), idFour.Name(), "test_token", time.Now().Add(24*time.Hour).Format(time.DateTime)) + secretModelInDifferentSchema := model.SecretWithBasicAuthentication("s5", idFive.DatabaseName(), idFive.Name(), "test_passwd", idFive.SchemaName(), "test_username") + + multipleSecretModels := accConfig.FromModel(t, secretModelBasicAuth) + + accConfig.FromModel(t, secretModelGenericString) + + accConfig.FromModel(t, secretModelClientCredentials) + + accConfig.FromModel(t, secretModelAuthorizationCodeGrant) + + accConfig.FromModel(t, secretModelInDifferentSchema) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + CheckDestroy: acc.ComposeCheckDestroy(t, + resources.SecretWithClientCredentials, + resources.SecretWithAuthorizationCodeGrant, + resources.SecretWithBasicAuthentication, + resources.SecretWithGenericString, + ), + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + Steps: []resource.TestStep{ + // like with one type + { + Config: multipleSecretModels + datasourceWithLikeMultipleSecretTypes("snowflake_secret_with_basic_authentication.s.name"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.snowflake_secrets.test", "secrets.#", "1"), + ), + }, + // like with prefix + { + Config: multipleSecretModels + datasourceWithLikeMultipleSecretTypes("\""+prefix+"%"+"\""), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.snowflake_secrets.test", "secrets.#", "3"), + ), + }, + // In schema + { + Config: multipleSecretModels + secretDatasourceWithIn("schema", idFive.SchemaId().FullyQualifiedName()), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.snowflake_secrets.test", "secrets.#", "1"), + ), + }, + // In Database + { + Config: multipleSecretModels + secretDatasourceWithIn("database", idFive.DatabaseId().FullyQualifiedName()), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.snowflake_secrets.test", "secrets.#", "5"), + ), + }, + // In Account + { + Config: multipleSecretModels + secretDatasourceInAccountWithLike(prefix+"%"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.snowflake_secrets.test", "secrets.#", "3"), + ), + }, + }, + }) +} + +func datasourceWithLikeMultipleSecretTypes(like string) string { + return fmt.Sprintf(` + data "snowflake_secrets" "test" { + depends_on = [snowflake_secret_with_basic_authentication.s, snowflake_secret_with_generic_string.s2, snowflake_secret_with_client_credentials.s3, snowflake_secret_with_authorization_code_grant.s4] + like = %s + } +`, like) +} + +func secretDatasourceWithIn(objectName, objectFullyQualifiedName string) string { + return fmt.Sprintf(` + data "snowflake_secrets" "test" { + in { + %s = "%s" + } + } +`, objectName, strings.ReplaceAll(objectFullyQualifiedName, `"`, "")) +} + +func secretDatasourceInAccountWithLike(prefix string) string { + return fmt.Sprintf(` + data "snowflake_secrets" "test" { + depends_on = [snowflake_secret_with_basic_authentication.s, snowflake_secret_with_generic_string.s2, snowflake_secret_with_client_credentials.s3, snowflake_secret_with_authorization_code_grant.s4] + in { + account = true + } + like = "%s" + } +`, prefix) +} + +func TestAcc_Secrets_EmptyIn(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { acc.TestAccPreCheck(t) }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: nil, + Steps: []resource.TestStep{ + { + Config: secretDatasourceEmptyIn(), + ExpectError: regexp.MustCompile("Invalid combination of arguments"), + }, + }, + }) +} + +func secretDatasourceEmptyIn() string { + return ` + data "snowflake_secrets" "test" { + in { + } + } +` +} diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index cb28e3eba6..30cdccfe38 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -532,6 +532,7 @@ func getDataSources() map[string]*schema.Resource { "snowflake_roles": datasources.Roles(), "snowflake_row_access_policies": datasources.RowAccessPolicies(), "snowflake_schemas": datasources.Schemas(), + "snowflake_secrets": datasources.Secrets(), "snowflake_security_integrations": datasources.SecurityIntegrations(), "snowflake_sequences": datasources.Sequences(), "snowflake_shares": datasources.Shares(), diff --git a/templates/data-sources/secrets.md.tmpl b/templates/data-sources/secrets.md.tmpl new file mode 100644 index 0000000000..e974f2a33b --- /dev/null +++ b/templates/data-sources/secrets.md.tmpl @@ -0,0 +1,24 @@ +--- +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 data source is a release candidate for the V1. We do not expect significant changes in it before the V1. We will welcome any feedback and adjust the data source if needed. Any errors reported will be resolved with a higher priority. We encourage checking this data source 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/data-sources/%s/data-source.tf" .Name)}} +{{- end }} + +{{ .SchemaMarkdown | trimspace }}