diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index df8799b7d6..fac8add85f 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -9,6 +9,20 @@ across different versions. ## v0.97.0 ➞ v0.98.0 +### *(behavior change)* Provider configuration rework +On our road to v1, we have decided to rework configuration to address the most common issues (see a [roadmap entry](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/ROADMAP.md#providers-configuration-rework)). We have created a list of topics we wanted to address before v1. We will prepare an announcement soon. The following subsections describe the things addressed in the v0.98.0. + +#### *(behavior change)* changed behavior of some fields +For the fields that are not deprecated, we focused on improving validations and documentation. Also, we adjusted some fields to match our [driver's](https://github.com/snowflakedb/gosnowflake) defaults. Specifically: +- Relaxed validations for enum fields like `protocol` and `authenticator`. Now, the case on such fields is ignored. +- `user`, `warehouse`, `role` - added a validation for an account object identifier +- `validate_default_parameters`, `client_request_mfa_token`, `client_store_temporary_credential`, `ocsp_fail_open`, - to easily handle three-value logic (true, false, unknown) in provider's config, type of these fields was changed from boolean to string. For more details about default values, please refer to the [changes before v1](https://github.com/Snowflake-Labs/terraform-provider-snowflake/blob/main/v1-preparations/CHANGES_BEFORE_V1.md#default-values) document. +- `client_ip` - added a validation for an IP address +- `port` - added a validation for a port number +- `okta_url`, `token_accessor.token_endpoint`, `client_store_temporary_credential` - added a validation for a URL address +- `login_timeout`, `request_timeout`, `jwt_expire_timeout`, `client_timeout`, `jwt_client_timeout`, `external_browser_timeout` - added a validation for setting this value to at least `0` +- `authenticator` - added a possibility to configure JWT flow with `SNOWFLAKE_JWT` (formerly, this was upported with `JWT`); the previous value `JWT` was left for compatibility, but will be removed before v1 + ### *(behavior change)* handling copy_grants Currently, resources like `snowflake_view`, `snowflake_stream_on_table`, `snowflake_stream_on_external_table` and `snowflake_stream_on_directory_table` support `copy_grants` field corresponding with `COPY GRANTS` during `CREATE`. The current behavior is that, when a change leading for recreation is detected (meaning a change that can not be handled by ALTER, but only by `CREATE OR REPLACE`), `COPY GRANTS` are used during recreation when `copy_grants` is set to `true`. Changing this field without changes in other field results in a noop because in this case there is no need to recreate a resource. diff --git a/docs/index.md b/docs/index.md index 947b2d16ee..cc619bb20a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -65,49 +65,49 @@ provider "snowflake" { ### Optional -- `account` (String) Specifies your Snowflake account identifier assigned, by Snowflake. For information about account identifiers, see the [Snowflake documentation](https://docs.snowflake.com/en/user-guide/admin-account-identifier.html). Can also be sourced from the `SNOWFLAKE_ACCOUNT` environment variable. Required unless using `profile`. -- `authenticator` (String) Specifies the [authentication type](https://pkg.go.dev/github.com/snowflakedb/gosnowflake#AuthType) to use when connecting to Snowflake. Valid values include: Snowflake, OAuth, ExternalBrowser, Okta, JWT, TokenAccessor, UsernamePasswordMFA. Can also be sourced from the `SNOWFLAKE_AUTHENTICATOR` environment variable. It has to be set explicitly to JWT for private key authentication. +- `account` (String) Specifies your Snowflake account identifier assigned, by Snowflake. The [account locator](https://docs.snowflake.com/en/user-guide/admin-account-identifier#format-2-account-locator-in-a-region) format is not supported. For information about account identifiers, see the [Snowflake documentation](https://docs.snowflake.com/en/user-guide/admin-account-identifier.html). Required unless using `profile`. Can also be sourced from the `SNOWFLAKE_ACCOUNT` environment variable. +- `authenticator` (String) Specifies the [authentication type](https://pkg.go.dev/github.com/snowflakedb/gosnowflake#AuthType) to use when connecting to Snowflake. Valid values include: Snowflake, OAuth, ExternalBrowser, Okta, JWT, TokenAccessor, UsernamePasswordMFA. It has to be set explicitly to JWT for private key authentication. Can also be sourced from the `SNOWFLAKE_AUTHENTICATOR` environment variable. - `browser_auth` (Boolean, Deprecated) Required when `oauth_refresh_token` is used. Can also be sourced from `SNOWFLAKE_USE_BROWSER_AUTH` environment variable. - `client_ip` (String) IP address for network checks. Can also be sourced from the `SNOWFLAKE_CLIENT_IP` environment variable. -- `client_request_mfa_token` (Boolean) When true the MFA token is cached in the credential manager. True by default in Windows/OSX. False for Linux. Can also be sourced from the `SNOWFLAKE_CLIENT_REQUEST_MFA_TOKEN` environment variable. -- `client_store_temporary_credential` (Boolean) When true the ID token is cached in the credential manager. True by default in Windows/OSX. False for Linux. Can also be sourced from the `SNOWFLAKE_CLIENT_STORE_TEMPORARY_CREDENTIAL` environment variable. -- `client_timeout` (Number) The timeout in seconds for the client to complete the authentication. Default is 900 seconds. Can also be sourced from the `SNOWFLAKE_CLIENT_TIMEOUT` environment variable. -- `disable_query_context_cache` (Boolean) Should HTAP query context cache be disabled. Can also be sourced from the `SNOWFLAKE_DISABLE_QUERY_CONTEXT_CACHE` environment variable. -- `disable_telemetry` (Boolean) Indicates whether to disable telemetry. Can also be sourced from the `SNOWFLAKE_DISABLE_TELEMETRY` environment variable. -- `external_browser_timeout` (Number) The timeout in seconds for the external browser to complete the authentication. Default is 120 seconds. Can also be sourced from the `SNOWFLAKE_EXTERNAL_BROWSER_TIMEOUT` environment variable. -- `host` (String) Supports passing in a custom host value to the snowflake go driver for use with privatelink. Can also be sourced from the `SNOWFLAKE_HOST` environment variable. +- `client_request_mfa_token` (String) When true the MFA token is cached in the credential manager. True by default in Windows/OSX. False for Linux. Can also be sourced from the `SNOWFLAKE_CLIENT_REQUEST_MFA_TOKEN` environment variable. +- `client_store_temporary_credential` (String) When true the ID token is cached in the credential manager. True by default in Windows/OSX. False for Linux. Can also be sourced from the `SNOWFLAKE_CLIENT_STORE_TEMPORARY_CREDENTIAL` environment variable. +- `client_timeout` (Number) The timeout in seconds for the client to complete the authentication. Can also be sourced from the `SNOWFLAKE_CLIENT_TIMEOUT` environment variable. +- `disable_query_context_cache` (Boolean) Disables HTAP query context cache in the driver. Can also be sourced from the `SNOWFLAKE_DISABLE_QUERY_CONTEXT_CACHE` environment variable. +- `disable_telemetry` (Boolean) Disables telemetry in the driver. Can also be sourced from the `DISABLE_TELEMETRY` environment variable. +- `external_browser_timeout` (Number) The timeout in seconds for the external browser to complete the authentication. Can also be sourced from the `SNOWFLAKE_EXTERNAL_BROWSER_TIMEOUT` environment variable. +- `host` (String) Specifies a custom host value used by the driver for privatelink connections. Can also be sourced from the `SNOWFLAKE_HOST` environment variable. - `insecure_mode` (Boolean) If true, bypass the Online Certificate Status Protocol (OCSP) certificate revocation check. IMPORTANT: Change the default value for testing or emergency situations only. Can also be sourced from the `SNOWFLAKE_INSECURE_MODE` environment variable. -- `jwt_client_timeout` (Number) The timeout in seconds for the JWT client to complete the authentication. Default is 10 seconds. Can also be sourced from the `SNOWFLAKE_JWT_CLIENT_TIMEOUT` environment variable. +- `jwt_client_timeout` (Number) The timeout in seconds for the JWT client to complete the authentication. Can also be sourced from the `SNOWFLAKE_JWT_CLIENT_TIMEOUT` environment variable. - `jwt_expire_timeout` (Number) JWT expire after timeout in seconds. Can also be sourced from the `SNOWFLAKE_JWT_EXPIRE_TIMEOUT` environment variable. - `keep_session_alive` (Boolean) Enables the session to persist even after the connection is closed. Can also be sourced from the `SNOWFLAKE_KEEP_SESSION_ALIVE` environment variable. -- `login_timeout` (Number) Login retry timeout EXCLUDING network roundtrip and read out http response. Can also be sourced from the `SNOWFLAKE_LOGIN_TIMEOUT` environment variable. +- `login_timeout` (Number) Login retry timeout in seconds EXCLUDING network roundtrip and read out http response. Can also be sourced from the `SNOWFLAKE_LOGIN_TIMEOUT` environment variable. - `oauth_access_token` (String, Sensitive, Deprecated) Token for use with OAuth. Generating the token is left to other tools. Cannot be used with `browser_auth`, `private_key_path`, `oauth_refresh_token` or `password`. Can also be sourced from `SNOWFLAKE_OAUTH_ACCESS_TOKEN` environment variable. - `oauth_client_id` (String, Sensitive, Deprecated) Required when `oauth_refresh_token` is used. Can also be sourced from `SNOWFLAKE_OAUTH_CLIENT_ID` environment variable. - `oauth_client_secret` (String, Sensitive, Deprecated) Required when `oauth_refresh_token` is used. Can also be sourced from `SNOWFLAKE_OAUTH_CLIENT_SECRET` environment variable. - `oauth_endpoint` (String, Sensitive, Deprecated) Required when `oauth_refresh_token` is used. Can also be sourced from `SNOWFLAKE_OAUTH_ENDPOINT` environment variable. - `oauth_redirect_url` (String, Sensitive, Deprecated) Required when `oauth_refresh_token` is used. Can also be sourced from `SNOWFLAKE_OAUTH_REDIRECT_URL` environment variable. - `oauth_refresh_token` (String, Sensitive, Deprecated) Token for use with OAuth. Setup and generation of the token is left to other tools. Should be used in conjunction with `oauth_client_id`, `oauth_client_secret`, `oauth_endpoint`, `oauth_redirect_url`. Cannot be used with `browser_auth`, `private_key_path`, `oauth_access_token` or `password`. Can also be sourced from `SNOWFLAKE_OAUTH_REFRESH_TOKEN` environment variable. -- `ocsp_fail_open` (Boolean) True represents OCSP fail open mode. False represents OCSP fail closed mode. Fail open true by default. Can also be sourced from the `SNOWFLAKE_OCSP_FAIL_OPEN` environment variable. +- `ocsp_fail_open` (String) True represents OCSP fail open mode. False represents OCSP fail closed mode. Fail open true by default. Can also be sourced from the `SNOWFLAKE_OCSP_FAIL_OPEN` environment variable. - `okta_url` (String) The URL of the Okta server. e.g. https://example.okta.com. Can also be sourced from the `SNOWFLAKE_OKTA_URL` environment variable. - `params` (Map of String) Sets other connection (i.e. session) parameters. [Parameters](https://docs.snowflake.com/en/sql-reference/parameters) - `passcode` (String) Specifies the passcode provided by Duo when using multi-factor authentication (MFA) for login. Can also be sourced from the `SNOWFLAKE_PASSCODE` environment variable. -- `passcode_in_password` (Boolean) False by default. Set to true if the MFA passcode is embedded in the login password. Appends the MFA passcode to the end of the password. Can also be sourced from the `SNOWFLAKE_PASSCODE_IN_PASSWORD` environment variable. +- `passcode_in_password` (Boolean) False by default. Set to true if the MFA passcode is embedded to the configured password. Can also be sourced from the `SNOWFLAKE_PASSCODE_IN_PASSWORD` environment variable. - `password` (String, Sensitive) Password for username+password auth. Cannot be used with `browser_auth` or `private_key_path`. Can also be sourced from the `SNOWFLAKE_PASSWORD` environment variable. -- `port` (Number) Support custom port values to snowflake go driver for use with privatelink. Can also be sourced from the `SNOWFLAKE_PORT` environment variable. -- `private_key` (String, Sensitive) Private Key for username+private-key auth. Cannot be used with `browser_auth` or `password`. Can also be sourced from `SNOWFLAKE_PRIVATE_KEY` environment variable. -- `private_key_passphrase` (String, Sensitive) Supports the encryption ciphers aes-128-cbc, aes-128-gcm, aes-192-cbc, aes-192-gcm, aes-256-cbc, aes-256-gcm, and des-ede3-cbc. Can also be sourced from `SNOWFLAKE_PRIVATE_KEY_PASSPHRASE` environment variable. +- `port` (Number) Specifies a custom port value used by the driver for privatelink connections. Can also be sourced from the `SNOWFLAKE_PORT` environment variable. +- `private_key` (String, Sensitive) Private Key for username+private-key auth. Cannot be used with `browser_auth` or `password`. Can also be sourced from the `SNOWFLAKE_PRIVATE_KEY` environment variable. +- `private_key_passphrase` (String, Sensitive) Supports the encryption ciphers aes-128-cbc, aes-128-gcm, aes-192-cbc, aes-192-gcm, aes-256-cbc, aes-256-gcm, and des-ede3-cbc. Can also be sourced from the `SNOWFLAKE_PRIVATE_KEY_PASSPHRASE` environment variable. - `private_key_path` (String, Sensitive, Deprecated) Path to a private key for using keypair authentication. Cannot be used with `browser_auth`, `oauth_access_token` or `password`. Can also be sourced from `SNOWFLAKE_PRIVATE_KEY_PATH` environment variable. - `profile` (String) Sets the profile to read from ~/.snowflake/config file. Can also be sourced from the `SNOWFLAKE_PROFILE` environment variable. -- `protocol` (String) Either http or https, defaults to https. Can also be sourced from the `SNOWFLAKE_PROTOCOL` environment variable. +- `protocol` (String) A protocol used in the connection. Valid options are: `HTTP` | `HTTPS`. Can also be sourced from the `SNOWFLAKE_PROTOCOL` environment variable. - `region` (String, Deprecated) Snowflake region, such as "eu-central-1", with this parameter. However, since this parameter is deprecated, it is best to specify the region as part of the account parameter. For details, see the description of the account parameter. [Snowflake region](https://docs.snowflake.com/en/user-guide/intro-regions.html) to use. Required if using the [legacy format for the `account` identifier](https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#format-2-legacy-account-locator-in-a-region) in the form of `.`. Can also be sourced from the `SNOWFLAKE_REGION` environment variable. -- `request_timeout` (Number) request retry timeout EXCLUDING network roundtrip and read out http response. Can also be sourced from the `SNOWFLAKE_REQUEST_TIMEOUT` environment variable. -- `role` (String) Specifies the role to use by default for accessing Snowflake objects in the client session. Can also be sourced from the `SNOWFLAKE_ROLE` environment variable. . +- `request_timeout` (Number) request retry timeout in seconds EXCLUDING network roundtrip and read out http response. Can also be sourced from the `SNOWFLAKE_REQUEST_TIMEOUT` environment variable. +- `role` (String) Specifies the role to use by default for accessing Snowflake objects in the client session. Can also be sourced from the `SNOWFLAKE_ROLE` environment variable. - `session_params` (Map of String, Deprecated) Sets session parameters. [Parameters](https://docs.snowflake.com/en/sql-reference/parameters) - `token` (String, Sensitive) Token to use for OAuth and other forms of token based auth. Can also be sourced from the `SNOWFLAKE_TOKEN` environment variable. - `token_accessor` (Block List, Max: 1) (see [below for nested schema](#nestedblock--token_accessor)) -- `user` (String) Username. Can also be sourced from the `SNOWFLAKE_USER` environment variable. Required unless using `profile`. -- `username` (String, Deprecated) Username for username+password authentication. Can also be sourced from the `SNOWFLAKE_USERNAME` environment variable. Required unless using `profile`. -- `validate_default_parameters` (Boolean) True by default. If false, disables the validation checks for Database, Schema, Warehouse and Role at the time a connection is established. Can also be sourced from the `SNOWFLAKE_VALIDATE_DEFAULT_PARAMETERS` environment variable. +- `user` (String) Username. Required unless using `profile`. Can also be sourced from the `SNOWFLAKE_USER` environment variable. +- `username` (String, Deprecated) Username for username+password authentication. Required unless using `profile`. Can also be sourced from the `SNOWFLAKE_USERNAME` environment variable. +- `validate_default_parameters` (String) True by default. If false, disables the validation checks for Database, Schema, Warehouse and Role at the time a connection is established. Can also be sourced from the `SNOWFLAKE_VALIDATE_DEFAULT_PARAMETERS` environment variable. - `warehouse` (String) Specifies the virtual warehouse to use by default for queries, loading, etc. in the client session. Can also be sourced from the `SNOWFLAKE_WAREHOUSE` environment variable. diff --git a/pkg/acceptance/bettertestspoc/README.md b/pkg/acceptance/bettertestspoc/README.md index fcc4179cf2..f5aea01d27 100644 --- a/pkg/acceptance/bettertestspoc/README.md +++ b/pkg/acceptance/bettertestspoc/README.md @@ -351,3 +351,4 @@ func (w *WarehouseDatasourceShowOutputAssert) IsEmpty() { 1. Lists of objects are partially generated, and only parameter name is generated in some functions (the type has to be added manually). 2. `testing` is a package name that makes Go think that we want to have unnamed parameter there, but we just didn't generate the type for that field in the function argument. - generate assertions checking that time is not empty - we often do not compare time fields by value, but check if they are set +- support generating provider config and use generated configs in `pkg/provider/provider_acceptance_test.go` diff --git a/pkg/internal/provider/docs/doc_helpers.go b/pkg/internal/provider/docs/doc_helpers.go index 82877a8b8b..80d1339b85 100644 --- a/pkg/internal/provider/docs/doc_helpers.go +++ b/pkg/internal/provider/docs/doc_helpers.go @@ -3,6 +3,7 @@ package docs import ( "fmt" "regexp" + "strings" ) // deprecationMessageRegex is the message that should be used in resource/datasource DeprecationMessage to get a nice link in the documentation to the replacing resource. @@ -23,3 +24,11 @@ func GetDeprecatedResourceReplacement(deprecationMessage string) (replacement st func RelativeLink(title string, path string) string { return fmt.Sprintf(`[%s](./%s)`, title, path) } + +func PossibleValuesListed[T ~string | ~int](values []T) string { + valuesWrapped := make([]string, len(values)) + for i, value := range values { + valuesWrapped[i] = fmt.Sprintf("`%v`", value) + } + return strings.Join(valuesWrapped, " | ") +} diff --git a/pkg/resources/doc_helpers_test.go b/pkg/internal/provider/docs/doc_helpers_test.go similarity index 75% rename from pkg/resources/doc_helpers_test.go rename to pkg/internal/provider/docs/doc_helpers_test.go index 81ecbccd8f..e65b860073 100644 --- a/pkg/resources/doc_helpers_test.go +++ b/pkg/internal/provider/docs/doc_helpers_test.go @@ -1,4 +1,4 @@ -package resources +package docs import ( "testing" @@ -9,7 +9,7 @@ import ( func Test_PossibleValuesListedStrings(t *testing.T) { values := []string{"abc", "DEF"} - result := possibleValuesListed(values) + result := PossibleValuesListed(values) assert.Equal(t, "`abc` | `DEF`", result) } @@ -17,7 +17,7 @@ func Test_PossibleValuesListedStrings(t *testing.T) { func Test_PossibleValuesListedInts(t *testing.T) { values := []int{42, 21} - result := possibleValuesListed(values) + result := PossibleValuesListed(values) assert.Equal(t, "`42` | `21`", result) } @@ -25,7 +25,7 @@ func Test_PossibleValuesListedInts(t *testing.T) { func Test_PossibleValuesListed_empty(t *testing.T) { var values []string - result := possibleValuesListed(values) + result := PossibleValuesListed(values) assert.Empty(t, result) } diff --git a/pkg/internal/provider/special_values.go b/pkg/internal/provider/special_values.go new file mode 100644 index 0000000000..0348299950 --- /dev/null +++ b/pkg/internal/provider/special_values.go @@ -0,0 +1,33 @@ +package provider + +import ( + "fmt" +) + +const ( + BooleanTrue = "true" + BooleanFalse = "false" + BooleanDefault = "default" + + IntDefault = -1 + IntDefaultString = "-1" +) + +func booleanStringFromBool(value bool) string { + if value { + return BooleanTrue + } else { + return BooleanFalse + } +} + +func BooleanStringToBool(value string) (bool, error) { + switch value { + case BooleanTrue: + return true, nil + case BooleanFalse: + return false, nil + default: + return false, fmt.Errorf("cannot retrieve boolean value from %s", value) + } +} diff --git a/pkg/internal/provider/validators/validators.go b/pkg/internal/provider/validators/validators.go new file mode 100644 index 0000000000..edb2a93757 --- /dev/null +++ b/pkg/internal/provider/validators/validators.go @@ -0,0 +1,135 @@ +package validators + +import ( + "fmt" + "reflect" + "strings" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func NormalizeValidation[T any](normalize func(string) (T, error)) schema.SchemaValidateDiagFunc { + return func(val interface{}, _ cty.Path) diag.Diagnostics { + _, err := normalize(val.(string)) + if err != nil { + return diag.FromErr(err) + } + return nil + } +} + +// IsValidIdentifier is a validator that can be used for validating identifiers passed in resources and data sources. +// +// Typically, we expect passed identifiers to be a variation of sdk.ObjectIdentifier. +// For now, we're expecting implementations of sdk.ObjectIdentifier, because we won't support sdk.ExternalObjectIdentifiers. +// The reason behind it is that the functions that parse identifiers are not able to differentiate between +// sdk.ExternalObjectIdentifiers and sdk.DatabaseObjectIdentifier or sdk.SchemaObjectIdentifier. +// That's because sdk.ExternalObjectIdentifiers has varying parts count (2 or 3). +// +// To use this function, pass it as a validation function on identifier field with generic parameter set to the desired sdk.ObjectIdentifier implementation. +func IsValidIdentifier[T sdk.AccountObjectIdentifier | sdk.DatabaseObjectIdentifier | sdk.SchemaObjectIdentifier | sdk.TableColumnIdentifier]() schema.SchemaValidateDiagFunc { + return func(value any, path cty.Path) diag.Diagnostics { + if _, ok := value.(string); !ok { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Invalid schema identifier type", + Detail: fmt.Sprintf("Expected schema string type, but got: %T. This is a provider error please file a report: https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/new/choose", value), + AttributePath: path, + }, + } + } + + // TODO(SNOW-1495079): Right now we have to skip validation for AccountObjectIdentifier to handle a case where identifier contains dots + // TODO(SNOW-1495079): with sdk.AccountObjectIdentifier{} (or a new type of identifier) we should be able to validate individual part of the identifier field (e.g. "database" or "schema" field) + if _, ok := any(sdk.AccountObjectIdentifier{}).(T); ok { + return nil + } + + stringValue := value.(string) + id, err := helpers.DecodeSnowflakeParameterID(stringValue) + if err != nil { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Unable to parse the identifier", + Detail: fmt.Sprintf( + "Unable to parse the identifier: %s. Make sure you are using the correct form of the fully qualified name for this field: %s.\nOriginal Error: %s", + stringValue, + getExpectedIdentifierRepresentationFromGeneric[T](), + err.Error(), + ), + AttributePath: path, + }, + } + } + + if _, ok := id.(T); !ok { + return diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "Invalid identifier type", + Detail: fmt.Sprintf( + "Expected %s identifier type, but got: %T. The correct form of the fully qualified name for this field is: %s, but was %s", + reflect.TypeOf(new(T)).Elem().Name(), + id, + getExpectedIdentifierRepresentationFromGeneric[T](), + getExpectedIdentifierRepresentationFromParam(id), + ), + AttributePath: path, + }, + } + } + + return nil + } +} + +func getExpectedIdentifierRepresentationFromGeneric[T sdk.AccountObjectIdentifier | sdk.DatabaseObjectIdentifier | sdk.SchemaObjectIdentifier | sdk.TableColumnIdentifier]() string { + return getExpectedIdentifierForm(new(T)) +} + +func getExpectedIdentifierRepresentationFromParam(id sdk.ObjectIdentifier) string { + return getExpectedIdentifierForm(id) +} + +func getExpectedIdentifierForm(id any) string { + switch id.(type) { + case sdk.AccountObjectIdentifier, *sdk.AccountObjectIdentifier: + return "" + case sdk.DatabaseObjectIdentifier, *sdk.DatabaseObjectIdentifier: + return "." + case sdk.SchemaObjectIdentifier, *sdk.SchemaObjectIdentifier: + return ".." + case sdk.TableColumnIdentifier, *sdk.TableColumnIdentifier: + return "..." + } + return "" +} + +// StringInSlice has the same implementation as validation.StringInSlice, but adapted to schema.SchemaValidateDiagFunc +func StringInSlice(valid []string, ignoreCase bool) schema.SchemaValidateDiagFunc { + return func(i interface{}, path cty.Path) diag.Diagnostics { + v, ok := i.(string) + if !ok { + return diag.Errorf("expected type of %v to be string", path) + } + + for _, str := range valid { + if v == str || (ignoreCase && strings.EqualFold(v, str)) { + return nil + } + } + + return diag.Errorf("expected %v to be one of %q, got %s", path, valid, v) + } +} + +var ValidateBooleanString = StringInSlice([]string{provider.BooleanTrue, provider.BooleanFalse}, false) + +var ValidateBooleanStringWithDefault = StringInSlice([]string{provider.BooleanTrue, provider.BooleanFalse, provider.BooleanDefault}, false) diff --git a/pkg/internal/provider/validators/validators_test.go b/pkg/internal/provider/validators/validators_test.go new file mode 100644 index 0000000000..634fcc47b2 --- /dev/null +++ b/pkg/internal/provider/validators/validators_test.go @@ -0,0 +1,170 @@ +package validators + +import ( + "testing" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/stretchr/testify/assert" +) + +func TestIsValidIdentifier(t *testing.T) { + accountObjectIdentifierCheck := IsValidIdentifier[sdk.AccountObjectIdentifier]() + databaseObjectIdentifierCheck := IsValidIdentifier[sdk.DatabaseObjectIdentifier]() + schemaObjectIdentifierCheck := IsValidIdentifier[sdk.SchemaObjectIdentifier]() + tableColumnIdentifierCheck := IsValidIdentifier[sdk.TableColumnIdentifier]() + + testCases := []struct { + Name string + Value any + Error string + CheckingFn schema.SchemaValidateDiagFunc + }{ + { + Name: "validation: invalid value type", + Value: 123, + Error: "Expected schema string type, but got: int", + CheckingFn: accountObjectIdentifierCheck, + }, + { + Name: "validation: incorrect form for database object identifier", + Value: "a.b.c", + Error: "., but was ..", + CheckingFn: databaseObjectIdentifierCheck, + }, + { + Name: "validation: incorrect form for schema object identifier", + Value: "a.b.c.d", + Error: ".., but was ...", + CheckingFn: schemaObjectIdentifierCheck, + }, + { + Name: "validation: incorrect form for table column identifier", + Value: "a", + Error: "..., but was ", + CheckingFn: tableColumnIdentifierCheck, + }, + { + Name: "correct form for account object identifier", + Value: "a", + CheckingFn: accountObjectIdentifierCheck, + }, + { + Name: "correct form for account object identifier - multiple parts", + Value: "a.b", + CheckingFn: accountObjectIdentifierCheck, + }, + { + Name: "correct form for account object identifier - quoted", + Value: "\"a.b\"", + CheckingFn: accountObjectIdentifierCheck, + }, + { + Name: "correct form for database object identifier", + Value: "a.b", + CheckingFn: databaseObjectIdentifierCheck, + }, + { + Name: "correct form for schema object identifier", + Value: "a.b.c", + CheckingFn: schemaObjectIdentifierCheck, + }, + { + Name: "correct form for table column identifier", + Value: "a.b.c.d", + CheckingFn: tableColumnIdentifierCheck, + }, + } + + for _, tt := range testCases { + t.Run(tt.Name, func(t *testing.T) { + diag := tt.CheckingFn(tt.Value, cty.IndexStringPath("path")) + if tt.Error != "" { + assert.Len(t, diag, 1) + assert.Contains(t, diag[0].Detail, tt.Error) + } else { + assert.Len(t, diag, 0) + } + }) + } +} + +func TestGetExpectedIdentifierFormGeneric(t *testing.T) { + testCases := []struct { + Name string + Expected string + Actual string + }{ + { + Name: "correct account object identifier from generic parameter", + Expected: "", + Actual: getExpectedIdentifierRepresentationFromGeneric[sdk.AccountObjectIdentifier](), + }, + { + Name: "correct database object identifier from generic parameter", + Expected: ".", + Actual: getExpectedIdentifierRepresentationFromGeneric[sdk.DatabaseObjectIdentifier](), + }, + { + Name: "correct schema object identifier from generic parameter", + Expected: "..", + Actual: getExpectedIdentifierRepresentationFromGeneric[sdk.SchemaObjectIdentifier](), + }, + { + Name: "correct table column identifier from generic parameter", + Expected: "...", + Actual: getExpectedIdentifierRepresentationFromGeneric[sdk.TableColumnIdentifier](), + }, + } + + for _, tt := range testCases { + t.Run(tt.Name, func(t *testing.T) { + assert.Equal(t, tt.Expected, tt.Actual) + }) + } +} + +func TestGetExpectedIdentifierFormParam(t *testing.T) { + testCases := []struct { + Name string + Expected string + Identifier sdk.ObjectIdentifier + IdentifierPointer sdk.ObjectIdentifier + }{ + { + Name: "correct account object identifier from function argument", + Expected: "", + Identifier: sdk.AccountObjectIdentifier{}, + IdentifierPointer: &sdk.AccountObjectIdentifier{}, + }, + { + Name: "correct database object identifier from function argument", + Expected: ".", + Identifier: sdk.DatabaseObjectIdentifier{}, + IdentifierPointer: &sdk.DatabaseObjectIdentifier{}, + }, + { + Name: "correct schema object identifier from function argument", + Expected: "..", + Identifier: sdk.SchemaObjectIdentifier{}, + IdentifierPointer: &sdk.SchemaObjectIdentifier{}, + }, + { + Name: "correct table column identifier from function argument", + Expected: "...", + Identifier: sdk.TableColumnIdentifier{}, + IdentifierPointer: &sdk.TableColumnIdentifier{}, + }, + } + + for _, tt := range testCases { + t.Run(tt.Name+" - non-pointer", func(t *testing.T) { + assert.Equal(t, tt.Expected, getExpectedIdentifierRepresentationFromParam(tt.Identifier)) + }) + + t.Run(tt.Name+" - pointer", func(t *testing.T) { + assert.Equal(t, tt.Expected, getExpectedIdentifierRepresentationFromParam(tt.IdentifierPointer)) + }) + } +} diff --git a/pkg/internal/snowflakeenvs/snowflake_environment_variables.go b/pkg/internal/snowflakeenvs/snowflake_environment_variables.go index d3af7464f5..e8443e8fdd 100644 --- a/pkg/internal/snowflakeenvs/snowflake_environment_variables.go +++ b/pkg/internal/snowflakeenvs/snowflake_environment_variables.go @@ -1,12 +1,48 @@ package snowflakeenvs const ( - Account = "SNOWFLAKE_ACCOUNT" - User = "SNOWFLAKE_USER" - Password = "SNOWFLAKE_PASSWORD" - Role = "SNOWFLAKE_ROLE" + Account = "SNOWFLAKE_ACCOUNT" + User = "SNOWFLAKE_USER" + Username = "SNOWFLAKE_USERNAME" + Password = "SNOWFLAKE_PASSWORD" + Warehouse = "SNOWFLAKE_WAREHOUSE" + Role = "SNOWFLAKE_ROLE" + ValidateDefaultParameters = "SNOWFLAKE_VALIDATE_DEFAULT_PARAMETERS" + ClientIp = "SNOWFLAKE_CLIENT_IP" + Protocol = "SNOWFLAKE_PROTOCOL" + Host = "SNOWFLAKE_HOST" + Port = "SNOWFLAKE_PORT" + Authenticator = "SNOWFLAKE_AUTHENTICATOR" + Passcode = "SNOWFLAKE_PASSCODE" + PasscodeInPassword = "SNOWFLAKE_PASSCODE_IN_PASSWORD" + OktaUrl = "SNOWFLAKE_OKTA_URL" + LoginTimeout = "SNOWFLAKE_LOGIN_TIMEOUT" + RequestTimeout = "SNOWFLAKE_REQUEST_TIMEOUT" + JwtExpireTimeout = "SNOWFLAKE_JWT_EXPIRE_TIMEOUT" + ClientTimeout = "SNOWFLAKE_CLIENT_TIMEOUT" + JwtClientTimeout = "SNOWFLAKE_JWT_CLIENT_TIMEOUT" + ExternalBrowserTimeout = "SNOWFLAKE_EXTERNAL_BROWSER_TIMEOUT" + InsecureMode = "SNOWFLAKE_INSECURE_MODE" + OcspFailOpen = "SNOWFLAKE_OCSP_FAIL_OPEN" + + Token = "SNOWFLAKE_TOKEN" + TokenAccessorTokenEndpoint = "SNOWFLAKE_TOKEN_ACCESSOR_TOKEN_ENDPOINT" + TokenAccessorRefreshToken = "SNOWFLAKE_TOKEN_ACCESSOR_REFRESH_TOKEN" + TokenAccessorClientId = "SNOWFLAKE_TOKEN_ACCESSOR_CLIENT_ID" + TokenAccessorClientSecret = "SNOWFLAKE_TOKEN_ACCESSOR_CLIENT_SECRET" + TokenAccessorRedirectUri = "SNOWFLAKE_TOKEN_ACCESSOR_REDIRECT_URI" + + KeepSessionAlive = "SNOWFLAKE_KEEP_SESSION_ALIVE" + PrivateKey = "SNOWFLAKE_PRIVATE_KEY" + PrivateKeyPassphrase = "SNOWFLAKE_PRIVATE_KEY_PASSPHRASE" + DisableTelemetry = "DISABLE_TELEMETRY" + ClientRequestMfaToken = "SNOWFLAKE_CLIENT_REQUEST_MFA_TOKEN" + ClientStoreTemporaryCredential = "SNOWFLAKE_CLIENT_STORE_TEMPORARY_CREDENTIAL" + DisableQueryContextCache = "SNOWFLAKE_DISABLE_QUERY_CONTEXT_CACHE" + IncludeRetryReason = "SNOWFLAKE_INCLUDE_RETRY_REASON" + Profile = "SNOWFLAKE_PROFILE" + ConfigPath = "SNOWFLAKE_CONFIG_PATH" - Host = "SNOWFLAKE_HOST" NoInstrumentedSql = "SF_TF_NO_INSTRUMENTED_SQL" GosnowflakeLogLevel = "SF_TF_GOSNOWFLAKE_LOG_LEVEL" diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 9f339632d7..9369ca0451 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -1,6 +1,7 @@ package provider import ( + "context" "errors" "fmt" "net" @@ -12,9 +13,13 @@ import ( "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/datasources" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider/docs" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider/validators" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/snowflakeenvs" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/resources" "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" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/snowflakedb/gosnowflake" ) @@ -38,55 +43,51 @@ func init() { } } -// Provider returns a Terraform Provider using configuration from https://pkg.go.dev/github.com/snowflakedb/gosnowflake#Config +// Provider returns a Terraform Provider using configuration. It is based on https://pkg.go.dev/github.com/snowflakedb/gosnowflake#Config. func Provider() *schema.Provider { return &schema.Provider{ Schema: map[string]*schema.Schema{ "account": { Type: schema.TypeString, - Description: "Specifies your Snowflake account identifier assigned, by Snowflake. For information about account identifiers, see the [Snowflake documentation](https://docs.snowflake.com/en/user-guide/admin-account-identifier.html). Can also be sourced from the `SNOWFLAKE_ACCOUNT` environment variable. Required unless using `profile`.", + Description: envNameFieldDescription("Specifies your Snowflake account identifier assigned, by Snowflake. The [account locator](https://docs.snowflake.com/en/user-guide/admin-account-identifier#format-2-account-locator-in-a-region) format is not supported. For information about account identifiers, see the [Snowflake documentation](https://docs.snowflake.com/en/user-guide/admin-account-identifier.html). Required unless using `profile`.", snowflakeenvs.Account), Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_ACCOUNT", nil), + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.Account, nil), }, "user": { - Type: schema.TypeString, - Description: "Username. Can also be sourced from the `SNOWFLAKE_USER` environment variable. Required unless using `profile`.", - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_USER", nil), - }, - "username": { - Type: schema.TypeString, - Description: "Username for username+password authentication. Can also be sourced from the `SNOWFLAKE_USERNAME` environment variable. Required unless using `profile`.", - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_USERNAME", nil), - Deprecated: "Use `user` instead of `username`", + Type: schema.TypeString, + Description: envNameFieldDescription("Username. Required unless using `profile`.", snowflakeenvs.User), + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.User, nil), + ValidateDiagFunc: validators.IsValidIdentifier[sdk.AccountObjectIdentifier](), }, "password": { Type: schema.TypeString, - Description: "Password for username+password auth. Cannot be used with `browser_auth` or `private_key_path`. Can also be sourced from the `SNOWFLAKE_PASSWORD` environment variable.", + Description: envNameFieldDescription("Password for username+password auth. Cannot be used with `browser_auth` or `private_key_path`.", snowflakeenvs.Password), Optional: true, Sensitive: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_PASSWORD", nil), + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.Password, nil), ConflictsWith: []string{"browser_auth", "private_key_path", "private_key", "private_key_passphrase", "oauth_access_token", "oauth_refresh_token"}, }, - // todo: add database and schema once unqualified identifiers are supported "warehouse": { - Type: schema.TypeString, - Description: "Specifies the virtual warehouse to use by default for queries, loading, etc. in the client session. Can also be sourced from the `SNOWFLAKE_WAREHOUSE` environment variable.", - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_WAREHOUSE", nil), + Type: schema.TypeString, + Description: envNameFieldDescription("Specifies the virtual warehouse to use by default for queries, loading, etc. in the client session.", snowflakeenvs.Warehouse), + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.Warehouse, nil), + ValidateDiagFunc: validators.IsValidIdentifier[sdk.AccountObjectIdentifier](), }, "role": { - Type: schema.TypeString, - Description: "Specifies the role to use by default for accessing Snowflake objects in the client session. Can also be sourced from the `SNOWFLAKE_ROLE` environment variable. .", - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_ROLE", nil), + Type: schema.TypeString, + Description: envNameFieldDescription("Specifies the role to use by default for accessing Snowflake objects in the client session.", snowflakeenvs.Role), + Optional: true, + ValidateDiagFunc: validators.IsValidIdentifier[sdk.AccountObjectIdentifier](), + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.Role, nil), }, "validate_default_parameters": { - Type: schema.TypeBool, - Description: "True by default. If false, disables the validation checks for Database, Schema, Warehouse and Role at the time a connection is established. Can also be sourced from the `SNOWFLAKE_VALIDATE_DEFAULT_PARAMETERS` environment variable.", - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_VALIDATE_DEFAULT_PARAMETERS", nil), + Type: schema.TypeString, + Description: envNameFieldDescription("True by default. If false, disables the validation checks for Database, Schema, Warehouse and Role at the time a connection is established.", snowflakeenvs.ValidateDefaultParameters), + Optional: true, + ValidateDiagFunc: validators.ValidateBooleanStringWithDefault, + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.ValidateDefaultParameters, provider.BooleanDefault), }, "params": { Type: schema.TypeMap, @@ -94,127 +95,121 @@ func Provider() *schema.Provider { Optional: true, }, "client_ip": { - Type: schema.TypeString, - Description: "IP address for network checks. Can also be sourced from the `SNOWFLAKE_CLIENT_IP` environment variable.", - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_CLIENT_IP", nil), + Type: schema.TypeString, + Description: envNameFieldDescription("IP address for network checks.", snowflakeenvs.ClientIp), + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.ClientIp, nil), + ValidateDiagFunc: validation.ToDiagFunc(validation.IsIPAddress), }, "protocol": { - Type: schema.TypeString, - Description: "Either http or https, defaults to https. Can also be sourced from the `SNOWFLAKE_PROTOCOL` environment variable.", - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_PROTOCOL", nil), - ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { - switch val.(string) { - case "http", "https": - return nil, nil - default: - errs := append(errs, fmt.Errorf("%q must be one of http or https", key)) - return warns, errs - } - }, + Type: schema.TypeString, + Description: envNameFieldDescription(fmt.Sprintf("A protocol used in the connection. Valid options are: %v.", docs.PossibleValuesListed(allProtocols)), snowflakeenvs.Protocol), + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.Protocol, nil), + ValidateDiagFunc: validators.NormalizeValidation(toProtocol), }, "host": { Type: schema.TypeString, - Description: "Supports passing in a custom host value to the snowflake go driver for use with privatelink. Can also be sourced from the `SNOWFLAKE_HOST` environment variable. ", + Description: envNameFieldDescription("Specifies a custom host value used by the driver for privatelink connections.", snowflakeenvs.Host), Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_HOST", nil), + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.Host, nil), }, "port": { - Type: schema.TypeInt, - Description: "Support custom port values to snowflake go driver for use with privatelink. Can also be sourced from the `SNOWFLAKE_PORT` environment variable. ", - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_PORT", nil), + Type: schema.TypeInt, + Description: envNameFieldDescription("Specifies a custom port value used by the driver for privatelink connections.", snowflakeenvs.Port), + Optional: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.IsPortNumberOrZero), + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.Port, nil), }, "authenticator": { - Type: schema.TypeString, - Description: "Specifies the [authentication type](https://pkg.go.dev/github.com/snowflakedb/gosnowflake#AuthType) to use when connecting to Snowflake. Valid values include: Snowflake, OAuth, ExternalBrowser, Okta, JWT, TokenAccessor, UsernamePasswordMFA. Can also be sourced from the `SNOWFLAKE_AUTHENTICATOR` environment variable. It has to be set explicitly to JWT for private key authentication.", - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_AUTHENTICATOR", nil), - ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { - switch val.(string) { - case "Snowflake", "OAuth", "ExternalBrowser", "Okta", "JWT", "TokenAccessor", "UsernamePasswordMFA": - return nil, nil - default: - errs := append(errs, fmt.Errorf("%q must be one of Snowflake, OAuth, ExternalBrowser, Okta, JWT, TokenAccessor or UsernamePasswordMFA", key)) - return warns, errs - } - }, + Type: schema.TypeString, + Description: envNameFieldDescription("Specifies the [authentication type](https://pkg.go.dev/github.com/snowflakedb/gosnowflake#AuthType) to use when connecting to Snowflake. Valid values include: Snowflake, OAuth, ExternalBrowser, Okta, JWT, TokenAccessor, UsernamePasswordMFA. It has to be set explicitly to JWT for private key authentication.", snowflakeenvs.Authenticator), + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.Authenticator, string(authenticationTypeSnowflake)), + ValidateDiagFunc: validators.NormalizeValidation(toAuthenticatorType), }, "passcode": { Type: schema.TypeString, - Description: "Specifies the passcode provided by Duo when using multi-factor authentication (MFA) for login. Can also be sourced from the `SNOWFLAKE_PASSCODE` environment variable. ", + Description: envNameFieldDescription("Specifies the passcode provided by Duo when using multi-factor authentication (MFA) for login.", snowflakeenvs.Passcode), Optional: true, ConflictsWith: []string{"passcode_in_password"}, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_PASSCODE", nil), + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.Passcode, nil), }, "passcode_in_password": { Type: schema.TypeBool, - Description: "False by default. Set to true if the MFA passcode is embedded in the login password. Appends the MFA passcode to the end of the password. Can also be sourced from the `SNOWFLAKE_PASSCODE_IN_PASSWORD` environment variable. ", + Description: envNameFieldDescription("False by default. Set to true if the MFA passcode is embedded to the configured password.", snowflakeenvs.PasscodeInPassword), Optional: true, ConflictsWith: []string{"passcode"}, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_PASSCODE_IN_PASSWORD", nil), + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.PasscodeInPassword, nil), }, "okta_url": { - Type: schema.TypeString, - Description: "The URL of the Okta server. e.g. https://example.okta.com. Can also be sourced from the `SNOWFLAKE_OKTA_URL` environment variable.", - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_OKTA_URL", nil), + Type: schema.TypeString, + Description: envNameFieldDescription("The URL of the Okta server. e.g. https://example.okta.com.", snowflakeenvs.OktaUrl), + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.OktaUrl, nil), + ValidateDiagFunc: validation.ToDiagFunc(validation.IsURLWithHTTPorHTTPS), }, "login_timeout": { - Type: schema.TypeInt, - Description: "Login retry timeout EXCLUDING network roundtrip and read out http response. Can also be sourced from the `SNOWFLAKE_LOGIN_TIMEOUT` environment variable.", - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_LOGIN_TIMEOUT", nil), + Type: schema.TypeInt, + Description: envNameFieldDescription("Login retry timeout in seconds EXCLUDING network roundtrip and read out http response.", snowflakeenvs.LoginTimeout), + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.LoginTimeout, nil), + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)), }, "request_timeout": { - Type: schema.TypeInt, - Description: "request retry timeout EXCLUDING network roundtrip and read out http response. Can also be sourced from the `SNOWFLAKE_REQUEST_TIMEOUT` environment variable.", - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_REQUEST_TIMEOUT", nil), + Type: schema.TypeInt, + Description: envNameFieldDescription("request retry timeout in seconds EXCLUDING network roundtrip and read out http response.", snowflakeenvs.RequestTimeout), + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.RequestTimeout, nil), + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)), }, "jwt_expire_timeout": { - Type: schema.TypeInt, - Description: "JWT expire after timeout in seconds. Can also be sourced from the `SNOWFLAKE_JWT_EXPIRE_TIMEOUT` environment variable.", - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_JWT_EXPIRE_TIMEOUT", nil), + Type: schema.TypeInt, + Description: envNameFieldDescription("JWT expire after timeout in seconds.", snowflakeenvs.JwtExpireTimeout), + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.JwtExpireTimeout, nil), + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)), }, "client_timeout": { - Type: schema.TypeInt, - Description: "The timeout in seconds for the client to complete the authentication. Default is 900 seconds. Can also be sourced from the `SNOWFLAKE_CLIENT_TIMEOUT` environment variable.", - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_CLIENT_TIMEOUT", nil), + Type: schema.TypeInt, + Description: envNameFieldDescription("The timeout in seconds for the client to complete the authentication.", snowflakeenvs.ClientTimeout), + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.ClientTimeout, nil), + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)), }, "jwt_client_timeout": { - Type: schema.TypeInt, - Description: "The timeout in seconds for the JWT client to complete the authentication. Default is 10 seconds. Can also be sourced from the `SNOWFLAKE_JWT_CLIENT_TIMEOUT` environment variable.", - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_JWT_CLIENT_TIMEOUT", nil), + Type: schema.TypeInt, + Description: envNameFieldDescription("The timeout in seconds for the JWT client to complete the authentication.", snowflakeenvs.JwtClientTimeout), + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.JwtClientTimeout, nil), + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)), }, "external_browser_timeout": { - Type: schema.TypeInt, - Description: "The timeout in seconds for the external browser to complete the authentication. Default is 120 seconds. Can also be sourced from the `SNOWFLAKE_EXTERNAL_BROWSER_TIMEOUT` environment variable.", - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_EXTERNAL_BROWSER_TIMEOUT", nil), + Type: schema.TypeInt, + Description: envNameFieldDescription("The timeout in seconds for the external browser to complete the authentication.", snowflakeenvs.ExternalBrowserTimeout), + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.ExternalBrowserTimeout, nil), + ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(0)), }, "insecure_mode": { Type: schema.TypeBool, - Description: "If true, bypass the Online Certificate Status Protocol (OCSP) certificate revocation check. IMPORTANT: Change the default value for testing or emergency situations only. Can also be sourced from the `SNOWFLAKE_INSECURE_MODE` environment variable.", + Description: envNameFieldDescription("If true, bypass the Online Certificate Status Protocol (OCSP) certificate revocation check. IMPORTANT: Change the default value for testing or emergency situations only.", snowflakeenvs.InsecureMode), Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_INSECURE_MODE", nil), + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.InsecureMode, nil), }, "ocsp_fail_open": { - Type: schema.TypeBool, - Description: "True represents OCSP fail open mode. False represents OCSP fail closed mode. Fail open true by default. Can also be sourced from the `SNOWFLAKE_OCSP_FAIL_OPEN` environment variable.", - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_OCSP_FAIL_OPEN", nil), + Type: schema.TypeString, + Description: envNameFieldDescription("True represents OCSP fail open mode. False represents OCSP fail closed mode. Fail open true by default.", snowflakeenvs.OcspFailOpen), + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.OcspFailOpen, provider.BooleanDefault), + ValidateDiagFunc: validators.ValidateBooleanStringWithDefault, }, "token": { Type: schema.TypeString, - Description: "Token to use for OAuth and other forms of token based auth. Can also be sourced from the `SNOWFLAKE_TOKEN` environment variable.", + Description: envNameFieldDescription("Token to use for OAuth and other forms of token based auth.", snowflakeenvs.Token), Sensitive: true, Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_TOKEN", nil), + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.Token, nil), }, "token_accessor": { Type: schema.TypeList, @@ -223,105 +218,107 @@ func Provider() *schema.Provider { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "token_endpoint": { - Type: schema.TypeString, - Description: "The token endpoint for the OAuth provider e.g. https://{yourDomain}/oauth/token when using a refresh token to renew access token. Can also be sourced from the `SNOWFLAKE_TOKEN_ACCESSOR_TOKEN_ENDPOINT` environment variable.", - Required: true, - Sensitive: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_TOKEN_ACCESSOR_TOKEN_ENDPOINT", nil), + Type: schema.TypeString, + Description: envNameFieldDescription("The token endpoint for the OAuth provider e.g. https://{yourDomain}/oauth/token when using a refresh token to renew access token.", snowflakeenvs.TokenAccessorTokenEndpoint), + Required: true, + Sensitive: true, + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.TokenAccessorTokenEndpoint, nil), + ValidateDiagFunc: validation.ToDiagFunc(validation.IsURLWithHTTPorHTTPS), }, "refresh_token": { Type: schema.TypeString, - Description: "The refresh token for the OAuth provider when using a refresh token to renew access token. Can also be sourced from the `SNOWFLAKE_TOKEN_ACCESSOR_REFRESH_TOKEN` environment variable.", + Description: envNameFieldDescription("The refresh token for the OAuth provider when using a refresh token to renew access token.", snowflakeenvs.TokenAccessorRefreshToken), Required: true, Sensitive: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_TOKEN_ACCESSOR_REFRESH_TOKEN", nil), + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.TokenAccessorRefreshToken, nil), }, "client_id": { Type: schema.TypeString, - Description: "The client ID for the OAuth provider when using a refresh token to renew access token. Can also be sourced from the `SNOWFLAKE_TOKEN_ACCESSOR_CLIENT_ID` environment variable.", + Description: envNameFieldDescription("The client ID for the OAuth provider when using a refresh token to renew access token.", snowflakeenvs.TokenAccessorClientId), Required: true, Sensitive: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_TOKEN_ACCESSOR_CLIENT_ID", nil), + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.TokenAccessorClientId, nil), }, "client_secret": { Type: schema.TypeString, - Description: "The client secret for the OAuth provider when using a refresh token to renew access token. Can also be sourced from the `SNOWFLAKE_TOKEN_ACCESSOR_CLIENT_SECRET` environment variable.", + Description: envNameFieldDescription("The client secret for the OAuth provider when using a refresh token to renew access token.", snowflakeenvs.TokenAccessorClientSecret), Required: true, Sensitive: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_TOKEN_ACCESSOR_CLIENT_SECRET", nil), + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.TokenAccessorClientSecret, nil), }, "redirect_uri": { Type: schema.TypeString, - Description: "The redirect URI for the OAuth provider when using a refresh token to renew access token. Can also be sourced from the `SNOWFLAKE_TOKEN_ACCESSOR_REDIRECT_URI` environment variable.", + Description: envNameFieldDescription("The redirect URI for the OAuth provider when using a refresh token to renew access token.", snowflakeenvs.TokenAccessorRedirectUri), Required: true, Sensitive: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_TOKEN_ACCESSOR_REDIRECT_URI", nil), + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.TokenAccessorRedirectUri, nil), }, }, }, }, "keep_session_alive": { Type: schema.TypeBool, - Description: "Enables the session to persist even after the connection is closed. Can also be sourced from the `SNOWFLAKE_KEEP_SESSION_ALIVE` environment variable.", + Description: envNameFieldDescription("Enables the session to persist even after the connection is closed.", snowflakeenvs.KeepSessionAlive), Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_KEEP_SESSION_ALIVE", nil), + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.KeepSessionAlive, nil), }, "private_key": { Type: schema.TypeString, - Description: "Private Key for username+private-key auth. Cannot be used with `browser_auth` or `password`. Can also be sourced from `SNOWFLAKE_PRIVATE_KEY` environment variable.", + Description: envNameFieldDescription("Private Key for username+private-key auth. Cannot be used with `browser_auth` or `password`.", snowflakeenvs.PrivateKey), Optional: true, Sensitive: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_PRIVATE_KEY", nil), + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.PrivateKey, nil), ConflictsWith: []string{"browser_auth", "password", "oauth_access_token", "private_key_path", "oauth_refresh_token"}, }, "private_key_passphrase": { Type: schema.TypeString, - Description: "Supports the encryption ciphers aes-128-cbc, aes-128-gcm, aes-192-cbc, aes-192-gcm, aes-256-cbc, aes-256-gcm, and des-ede3-cbc. Can also be sourced from `SNOWFLAKE_PRIVATE_KEY_PASSPHRASE` environment variable.", + Description: envNameFieldDescription("Supports the encryption ciphers aes-128-cbc, aes-128-gcm, aes-192-cbc, aes-192-gcm, aes-256-cbc, aes-256-gcm, and des-ede3-cbc.", snowflakeenvs.PrivateKeyPassphrase), Optional: true, Sensitive: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_PRIVATE_KEY_PASSPHRASE", nil), + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.PrivateKeyPassphrase, nil), ConflictsWith: []string{"browser_auth", "password", "oauth_access_token", "oauth_refresh_token"}, }, "disable_telemetry": { Type: schema.TypeBool, - Description: "Indicates whether to disable telemetry. Can also be sourced from the `SNOWFLAKE_DISABLE_TELEMETRY` environment variable.", + Description: envNameFieldDescription("Disables telemetry in the driver.", snowflakeenvs.DisableTelemetry), Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_DISABLE_TELEMETRY", nil), + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.DisableTelemetry, nil), }, "client_request_mfa_token": { - Type: schema.TypeBool, - Description: "When true the MFA token is cached in the credential manager. True by default in Windows/OSX. False for Linux. Can also be sourced from the `SNOWFLAKE_CLIENT_REQUEST_MFA_TOKEN` environment variable.", - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_CLIENT_REQUEST_MFA_TOKEN", nil), + Type: schema.TypeString, + Description: envNameFieldDescription("When true the MFA token is cached in the credential manager. True by default in Windows/OSX. False for Linux.", snowflakeenvs.ClientRequestMfaToken), + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.ClientRequestMfaToken, provider.BooleanDefault), + ValidateDiagFunc: validators.ValidateBooleanStringWithDefault, }, "client_store_temporary_credential": { - Type: schema.TypeBool, - Description: "When true the ID token is cached in the credential manager. True by default in Windows/OSX. False for Linux. Can also be sourced from the `SNOWFLAKE_CLIENT_STORE_TEMPORARY_CREDENTIAL` environment variable.", - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_CLIENT_STORE_TEMPORARY_CREDENTIAL", nil), + Type: schema.TypeString, + Description: envNameFieldDescription("When true the ID token is cached in the credential manager. True by default in Windows/OSX. False for Linux.", snowflakeenvs.ClientStoreTemporaryCredential), + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.ClientStoreTemporaryCredential, provider.BooleanDefault), + ValidateDiagFunc: validators.ValidateBooleanStringWithDefault, }, "disable_query_context_cache": { Type: schema.TypeBool, - Description: "Should HTAP query context cache be disabled. Can also be sourced from the `SNOWFLAKE_DISABLE_QUERY_CONTEXT_CACHE` environment variable.", + Description: envNameFieldDescription("Disables HTAP query context cache in the driver.", snowflakeenvs.DisableQueryContextCache), Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_DISABLE_QUERY_CONTEXT_CACHE", nil), - }, - /* - Feature not yet released as of latest gosnowflake release - https://github.com/snowflakedb/gosnowflake/blob/master/dsn.go#L103 - "include_retry_reason": { - Type: schema.TypeBool, - Description: "Should retried request contain retry reason. Can also be sourced from the `SNOWFLAKE_INCLUDE_RETRY_REASON` environment variable.", - Optional: true, - }, - */ + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.DisableQueryContextCache, nil), + }, "profile": { - Type: schema.TypeString, - Description: "Sets the profile to read from ~/.snowflake/config file. Can also be sourced from the `SNOWFLAKE_PROFILE` environment variable.", + Type: schema.TypeString, + // TODO(SNOW-1754364): Note that a default file path is already filled on sdk side. + Description: envNameFieldDescription("Sets the profile to read from ~/.snowflake/config file.", snowflakeenvs.Profile), Optional: true, - DefaultFunc: schema.EnvDefaultFunc("SNOWFLAKE_PROFILE", "default"), + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.Profile, "default"), }, // Deprecated attributes + "username": { + Type: schema.TypeString, + Description: envNameFieldDescription("Username for username+password authentication. Required unless using `profile`.", snowflakeenvs.Username), + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(snowflakeenvs.Username, nil), + Deprecated: "Use `user` instead of `username`", + }, "region": { Type: schema.TypeString, Description: "Snowflake region, such as \"eu-central-1\", with this parameter. However, since this parameter is deprecated, it is best to specify the region as part of the account parameter. For details, see the description of the account parameter. [Snowflake region](https://docs.snowflake.com/en/user-guide/intro-regions.html) to use. Required if using the [legacy format for the `account` identifier](https://docs.snowflake.com/en/user-guide/admin-account-identifier.html#format-2-legacy-account-locator-in-a-region) in the form of `.`. Can also be sourced from the `SNOWFLAKE_REGION` environment variable. ", @@ -412,10 +409,10 @@ func Provider() *schema.Provider { Deprecated: "use the [file Function](https://developer.hashicorp.com/terraform/language/functions/file) instead", }, }, - ResourcesMap: getResources(), - DataSourcesMap: getDataSources(), - ConfigureFunc: ConfigureProvider, - ProviderMetaSchema: map[string]*schema.Schema{}, + ResourcesMap: getResources(), + DataSourcesMap: getDataSources(), + ConfigureContextFunc: ConfigureProvider, + ProviderMetaSchema: map[string]*schema.Schema{}, } } @@ -563,14 +560,14 @@ var ( configureClientError error //nolint:errname ) -func ConfigureProvider(s *schema.ResourceData) (interface{}, error) { +func ConfigureProvider(ctx context.Context, s *schema.ResourceData) (any, diag.Diagnostics) { // hacky way to speed up our acceptance tests if os.Getenv("TF_ACC") != "" && os.Getenv("SF_TF_ACC_TEST_CONFIGURE_CLIENT_ONCE") == "true" { if configuredClient != nil { return &provider.Context{Client: configuredClient}, nil } if configureClientError != nil { - return nil, configureClientError + return nil, diag.FromErr(configureClientError) } } @@ -581,10 +578,12 @@ func ConfigureProvider(s *schema.ResourceData) (interface{}, error) { if v, ok := s.GetOk("account"); ok && v.(string) != "" { config.Account = v.(string) } + // backwards compatibility until we can remove this if v, ok := s.GetOk("username"); ok && v.(string) != "" { config.User = v.(string) } + if v, ok := s.GetOk("user"); ok && v.(string) != "" { config.User = v.(string) } @@ -605,8 +604,16 @@ func ConfigureProvider(s *schema.ResourceData) (interface{}, error) { config.Region = v.(string) } - if v, ok := s.GetOk("validate_default_parameters"); ok && v.(bool) { - config.ValidateDefaultParameters = gosnowflake.ConfigBoolTrue + if v := s.Get("validate_default_parameters").(string); v != provider.BooleanDefault { + parsed, err := provider.BooleanStringToBool(v) + if err != nil { + return nil, diag.FromErr(err) + } + if parsed { + config.ValidateDefaultParameters = gosnowflake.ConfigBoolTrue + } else { + config.ValidateDefaultParameters = gosnowflake.ConfigBoolFalse + } } m := make(map[string]interface{}) @@ -648,7 +655,11 @@ func ConfigureProvider(s *schema.ResourceData) (interface{}, error) { } if v, ok := s.GetOk("authenticator"); ok && v.(string) != "" { - config.Authenticator = toAuthenticatorType(v.(string)) + authType, err := toAuthenticatorType(v.(string)) + if err != nil { + return "", diag.FromErr(err) + } + config.Authenticator = authType } if v, ok := s.GetOk("passcode"); ok && v.(string) != "" { @@ -661,7 +672,7 @@ func ConfigureProvider(s *schema.ResourceData) (interface{}, error) { if v, ok := s.GetOk("okta_url"); ok && v.(string) != "" { oktaURL, err := url.Parse(v.(string)) if err != nil { - return nil, fmt.Errorf("could not parse okta_url err = %w", err) + return nil, diag.FromErr(fmt.Errorf("could not parse okta_url err = %w", err)) } config.OktaURL = oktaURL } @@ -694,8 +705,16 @@ func ConfigureProvider(s *schema.ResourceData) (interface{}, error) { config.InsecureMode = v.(bool) } - if v, ok := s.GetOk("ocsp_fail_open"); ok && v.(bool) { - config.OCSPFailOpen = gosnowflake.OCSPFailOpenTrue + if v := s.Get("ocsp_fail_open").(string); v != provider.BooleanDefault { + parsed, err := provider.BooleanStringToBool(v) + if err != nil { + return nil, diag.FromErr(err) + } + if parsed { + config.OCSPFailOpen = gosnowflake.OCSPFailOpenTrue + } else { + config.OCSPFailOpen = gosnowflake.OCSPFailOpenFalse + } } if v, ok := s.GetOk("token"); ok && v.(string) != "" { @@ -713,7 +732,7 @@ func ConfigureProvider(s *schema.ResourceData) (interface{}, error) { redirectURI := tokenAccessor["redirect_uri"].(string) accessToken, err := GetAccessTokenWithRefreshToken(tokenEndpoint, clientID, clientSecret, refreshToken, redirectURI) if err != nil { - return nil, fmt.Errorf("could not retrieve access token from refresh token, err = %w", err) + return nil, diag.FromErr(fmt.Errorf("could not retrieve access token from refresh token, err = %w", err)) } config.Token = accessToken config.Authenticator = gosnowflake.AuthTypeOAuth @@ -729,7 +748,7 @@ func ConfigureProvider(s *schema.ResourceData) (interface{}, error) { privateKeyPassphrase := s.Get("private_key_passphrase").(string) v, err := getPrivateKey(privateKeyPath, privateKey, privateKeyPassphrase) if err != nil { - return nil, fmt.Errorf("could not retrieve private key: %w", err) + return nil, diag.FromErr(fmt.Errorf("could not retrieve private key: %w", err)) } if v != nil { config.PrivateKey = v @@ -739,25 +758,34 @@ func ConfigureProvider(s *schema.ResourceData) (interface{}, error) { config.DisableTelemetry = v.(bool) } - if v, ok := s.GetOk("client_request_mfa_token"); ok && v.(bool) { - config.ClientRequestMfaToken = gosnowflake.ConfigBoolTrue + if v := s.Get("client_request_mfa_token").(string); v != provider.BooleanDefault { + parsed, err := provider.BooleanStringToBool(v) + if err != nil { + return nil, diag.FromErr(err) + } + if parsed { + config.ClientRequestMfaToken = gosnowflake.ConfigBoolTrue + } else { + config.ClientRequestMfaToken = gosnowflake.ConfigBoolFalse + } } - if v, ok := s.GetOk("client_store_temporary_credential"); ok && v.(bool) { - config.ClientStoreTemporaryCredential = gosnowflake.ConfigBoolTrue + if v := s.Get("client_store_temporary_credential").(string); v != provider.BooleanDefault { + parsed, err := provider.BooleanStringToBool(v) + if err != nil { + return nil, diag.FromErr(err) + } + if parsed { + config.ClientStoreTemporaryCredential = gosnowflake.ConfigBoolTrue + } else { + config.ClientStoreTemporaryCredential = gosnowflake.ConfigBoolFalse + } } if v, ok := s.GetOk("disable_query_context_cache"); ok && v.(bool) { config.DisableQueryContextCache = v.(bool) } - /* - Feature not yet released as of latest gosnowflake release - https://github.com/snowflakedb/gosnowflake/blob/master/dsn.go#L103 - if v, ok := s.GetOk("include_retry_reason"); ok && v.(bool) { - config.IncludeRetryParameters = v.(bool) - } - */ if v, ok := s.GetOk("profile"); ok && v.(string) != "" { profile := v.(string) if profile == "default" { @@ -766,30 +794,30 @@ func ConfigureProvider(s *schema.ResourceData) (interface{}, error) { } else { profileConfig, err := sdk.ProfileConfig(profile) if err != nil { - return "", errors.New("could not retrieve profile config: " + err.Error()) + return "", diag.FromErr(errors.New("could not retrieve profile config: " + err.Error())) } if profileConfig == nil { - return "", errors.New("profile with name: " + profile + " not found in config file") + return "", diag.FromErr(errors.New("profile with name: " + profile + " not found in config file")) } // merge any credentials found in profile with config config = sdk.MergeConfig(config, profileConfig) } } - cl, clErr := sdk.NewClient(config) + client, clientErr := sdk.NewClient(config) // needed for tests verifying different provider setups if os.Getenv("TF_ACC") != "" && os.Getenv("SF_TF_ACC_TEST_CONFIGURE_CLIENT_ONCE") == "true" { - configuredClient = cl - configureClientError = clErr + configuredClient = client + configureClientError = clientErr } else { configuredClient = nil configureClientError = nil } - if clErr != nil { - return nil, clErr + if clientErr != nil { + return nil, diag.FromErr(clientErr) } - return &provider.Context{Client: cl}, nil + return &provider.Context{Client: client}, nil } diff --git a/pkg/provider/provider_acceptance_test.go b/pkg/provider/provider_acceptance_test.go index e9d05169c0..20f9b0cabe 100644 --- a/pkg/provider/provider_acceptance_test.go +++ b/pkg/provider/provider_acceptance_test.go @@ -154,6 +154,115 @@ func TestAcc_Provider_configureClientOnceSwitching(t *testing.T) { }) } +func TestAcc_Provider_useNonExistentDefaultParams(t *testing.T) { + t.Setenv(string(testenvs.ConfigureClientOnce), "") + + nonExisting := "NON-EXISTENT" + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + PreCheck: func() { + acc.TestAccPreCheck(t) + testenvs.AssertEnvNotSet(t, snowflakeenvs.User) + testenvs.AssertEnvNotSet(t, snowflakeenvs.Password) + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + Steps: []resource.TestStep{ + { + Config: providerConfigWithRole(testprofiles.Default, nonExisting), + ExpectError: regexp.MustCompile("Role 'NON-EXISTENT' specified in the connect string does not exist or not authorized."), + }, + { + Config: providerConfigWithWarehouse(testprofiles.Default, nonExisting), + ExpectError: regexp.MustCompile("The requested warehouse does not exist or not authorized."), + }, + // check that using a non-existing warehouse with disabled verification succeeds + { + Config: providerConfigWithWarehouseAndDisabledValidation(testprofiles.Default, nonExisting), + }, + }, + }) +} + +// prove we can use tri-value booleans, similarly to the ones in resources +func TestAcc_Provider_triValueBoolean(t *testing.T) { + t.Setenv(string(testenvs.ConfigureClientOnce), "") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acc.TestAccPreCheck(t) + testenvs.AssertEnvNotSet(t, snowflakeenvs.User) + testenvs.AssertEnvNotSet(t, snowflakeenvs.Password) + }, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "snowflake": { + VersionConstraint: "=0.97.0", + Source: "Snowflake-Labs/snowflake", + }, + }, + Config: providerConfigWithClientStoreTemporaryCredential(testprofiles.Default, `true`), + }, + { + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + Config: providerConfigWithClientStoreTemporaryCredential(testprofiles.Default, `true`), + }, + { + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + Config: providerConfigWithClientStoreTemporaryCredential(testprofiles.Default, `"true"`), + }, + }, + }) +} + +func TestAcc_Provider_invalidConfigurations(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + Steps: []resource.TestStep{ + { + Config: providerConfigWithClientIp(testprofiles.Default, "invalid"), + ExpectError: regexp.MustCompile("expected client_ip to contain a valid IP"), + }, + { + Config: providerConfigWithProtocol(testprofiles.Default, "invalid"), + ExpectError: regexp.MustCompile("invalid protocol: INVALID"), + }, + { + Config: providerConfigWithPort(testprofiles.Default, 123456789), + ExpectError: regexp.MustCompile(`expected "port" to be a valid port number or 0, got: 123456789`), + }, + { + Config: providerConfigWithAuthType(testprofiles.Default, "invalid"), + ExpectError: regexp.MustCompile("invalid authenticator type: INVALID"), + }, + { + Config: providerConfigWithOktaUrl(testprofiles.Default, "invalid"), + ExpectError: regexp.MustCompile(`expected "okta_url" to have a host, got invalid`), + }, + { + Config: providerConfigWithTimeout(testprofiles.Default, "login_timeout", -1), + ExpectError: regexp.MustCompile(`expected login_timeout to be at least \(0\), got -1`), + }, + { + Config: providerConfigWithTokenEndpoint(testprofiles.Default, "invalid"), + ExpectError: regexp.MustCompile(`expected "token_endpoint" to have a host, got invalid`), + }, + }, + }) +} + +// TODO(SNOW-1754319): for JWT auth flow, check setting authenticator value as `SNOWFLAKE_JWT`. +// This will ensure https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/2983 is solved. + func emptyProviderConfig() string { return ` provider "snowflake" { @@ -168,6 +277,112 @@ provider "snowflake" { `, profile) + datasourceConfig() } +func providerConfigWithRole(profile, role string) string { + return fmt.Sprintf(` +provider "snowflake" { + profile = "%[1]s" + role = "%[2]s" +} +`, profile, role) + datasourceConfig() +} + +func providerConfigWithWarehouse(profile, warehouse string) string { + return fmt.Sprintf(` +provider "snowflake" { + profile = "%[1]s" + warehouse = "%[2]s" +} +`, profile, warehouse) + datasourceConfig() +} + +func providerConfigWithClientStoreTemporaryCredential(profile, clientStoreTemporaryCredential string) string { + return fmt.Sprintf(` +provider "snowflake" { + profile = "%[1]s" + client_store_temporary_credential = %[2]s +} +`, profile, clientStoreTemporaryCredential) + datasourceConfig() +} + +func providerConfigWithWarehouseAndDisabledValidation(profile, warehouse string) string { + return fmt.Sprintf(` +provider "snowflake" { + profile = "%[1]s" + warehouse = "%[2]s" + validate_default_parameters = "false" +} +`, profile, warehouse) + datasourceConfig() +} + +func providerConfigWithProtocol(profile, protocol string) string { + return fmt.Sprintf(` +provider "snowflake" { + profile = "%[1]s" + protocol = "%[2]s" +} +`, profile, protocol) + datasourceConfig() +} + +func providerConfigWithPort(profile string, port int) string { + return fmt.Sprintf(` +provider "snowflake" { + profile = "%[1]s" + port = %[2]d +} +`, profile, port) + datasourceConfig() +} + +func providerConfigWithAuthType(profile, authType string) string { + return fmt.Sprintf(` +provider "snowflake" { + profile = "%[1]s" + authenticator = "%[2]s" +} +`, profile, authType) + datasourceConfig() +} + +func providerConfigWithOktaUrl(profile, oktaUrl string) string { + return fmt.Sprintf(` +provider "snowflake" { + profile = "%[1]s" + okta_url = "%[2]s" +} +`, profile, oktaUrl) + datasourceConfig() +} + +func providerConfigWithTimeout(profile, timeoutName string, timeoutSeconds int) string { + return fmt.Sprintf(` +provider "snowflake" { + profile = "%[1]s" + %[2]s = %[3]d +} +`, profile, timeoutName, timeoutSeconds) + datasourceConfig() +} + +func providerConfigWithTokenEndpoint(profile, tokenEndpoint string) string { + return fmt.Sprintf(` +provider "snowflake" { + profile = "%[1]s" + token_accessor { + token_endpoint = "%[2]s" + refresh_token = "refresh_token" + client_id = "client_id" + client_secret = "client_secret" + redirect_uri = "redirect_uri" + } +} +`, profile, tokenEndpoint) + datasourceConfig() +} + +func providerConfigWithClientIp(profile, clientIp string) string { + return fmt.Sprintf(` +provider "snowflake" { + profile = "%[1]s" + client_ip = "%[2]s" +} +`, profile, clientIp) + datasourceConfig() +} + func providerConfigWithUser(user string, profile string) string { return fmt.Sprintf(` provider "snowflake" { diff --git a/pkg/provider/provider_helpers.go b/pkg/provider/provider_helpers.go index dfa0494c44..0e8088cae4 100644 --- a/pkg/provider/provider_helpers.go +++ b/pkg/provider/provider_helpers.go @@ -13,21 +13,79 @@ import ( "strconv" "strings" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/mitchellh/go-homedir" "github.com/snowflakedb/gosnowflake" "github.com/youmark/pkcs8" "golang.org/x/crypto/ssh" ) -func mergeSchemas(schemaCollections ...map[string]*schema.Resource) map[string]*schema.Resource { - out := map[string]*schema.Resource{} - for _, schemaCollection := range schemaCollections { - for name, s := range schemaCollection { - out[name] = s - } +type authenticationType string + +const ( + authenticationTypeSnowflake authenticationType = "SNOWFLAKE" + authenticationTypeOauth authenticationType = "OAUTH" + authenticationTypeExternalBrowser authenticationType = "EXTERNALBROWSER" + authenticationTypeOkta authenticationType = "OKTA" + authenticationTypeJwtLegacy authenticationType = "JWT" + authenticationTypeJwt authenticationType = "SNOWFLAKE_JWT" + authenticationTypeTokenAccessor authenticationType = "TOKENACCESSOR" + authenticationTypeUsernamePasswordMfa authenticationType = "USERNAMEPASSWORDMFA" +) + +var allAuthenticationTypes = []authenticationType{ + authenticationTypeSnowflake, + authenticationTypeOauth, + authenticationTypeExternalBrowser, + authenticationTypeOkta, + authenticationTypeJwt, + authenticationTypeTokenAccessor, + authenticationTypeUsernamePasswordMfa, +} + +func toAuthenticatorType(s string) (gosnowflake.AuthType, error) { + s = strings.ToUpper(s) + switch s { + case string(authenticationTypeSnowflake): + return gosnowflake.AuthTypeSnowflake, nil + case string(authenticationTypeOauth): + return gosnowflake.AuthTypeOAuth, nil + case string(authenticationTypeExternalBrowser): + return gosnowflake.AuthTypeExternalBrowser, nil + case string(authenticationTypeOkta): + return gosnowflake.AuthTypeOkta, nil + case string(authenticationTypeJwt), string(authenticationTypeJwtLegacy): + return gosnowflake.AuthTypeJwt, nil + case string(authenticationTypeTokenAccessor): + return gosnowflake.AuthTypeTokenAccessor, nil + case string(authenticationTypeUsernamePasswordMfa): + return gosnowflake.AuthTypeUsernamePasswordMFA, nil + default: + return gosnowflake.AuthType(0), fmt.Errorf("invalid authenticator type: %s", s) + } +} + +type protocol string + +const ( + protocolHttp protocol = "HTTP" + protocolHttps protocol = "HTTPS" +) + +var allProtocols = []protocol{ + protocolHttp, + protocolHttps, +} + +func toProtocol(s string) (protocol, error) { + s = strings.ToUpper(s) + switch s { + case string(protocolHttp): + return protocolHttp, nil + case string(protocolHttps): + return protocolHttps, nil + default: + return "", fmt.Errorf("invalid protocol: %s", s) } - return out } func getPrivateKey(privateKeyPath, privateKeyString, privateKeyPassphrase string) (*rsa.PrivateKey, error) { @@ -45,54 +103,6 @@ func getPrivateKey(privateKeyPath, privateKeyString, privateKeyPassphrase string return parsePrivateKey(privateKeyBytes, []byte(privateKeyPassphrase)) } -func toAuthenticatorType(authenticator string) gosnowflake.AuthType { - switch authenticator { - case "Snowflake": - return gosnowflake.AuthTypeSnowflake - case "OAuth": - return gosnowflake.AuthTypeOAuth - case "ExternalBrowser": - return gosnowflake.AuthTypeExternalBrowser - case "Okta": - return gosnowflake.AuthTypeOkta - case "JWT": - return gosnowflake.AuthTypeJwt - case "TokenAccessor": - return gosnowflake.AuthTypeTokenAccessor - case "UsernamePasswordMFA": - return gosnowflake.AuthTypeUsernamePasswordMFA - default: - return gosnowflake.AuthTypeSnowflake - } -} - -func getInt64Env(key string, defaultValue int64) int64 { - s := os.Getenv(key) - if s == "" { - return defaultValue - } - i, err := strconv.Atoi(s) - if err != nil { - return defaultValue - } - return int64(i) -} - -func getBoolEnv(key string, defaultValue bool) bool { - s := strings.ToLower(os.Getenv(key)) - if s == "" { - return defaultValue - } - switch s { - case "true", "1": - return true - case "false", "0": - return false - default: - return defaultValue - } -} - func readFile(privateKeyPath string) ([]byte, error) { expandedPrivateKeyPath, err := homedir.Expand(privateKeyPath) if err != nil { @@ -187,3 +197,7 @@ func GetAccessTokenWithRefreshToken( } return result.AccessToken, nil } + +func envNameFieldDescription(description, envName string) string { + return fmt.Sprintf("%s Can also be sourced from the `%s` environment variable.", description, envName) +} diff --git a/pkg/provider/provider_helpers_test.go b/pkg/provider/provider_helpers_test.go new file mode 100644 index 0000000000..eb9e2a8caf --- /dev/null +++ b/pkg/provider/provider_helpers_test.go @@ -0,0 +1,86 @@ +package provider + +import ( + "testing" + + "github.com/snowflakedb/gosnowflake" + "github.com/stretchr/testify/require" +) + +func Test_Provider_toAuthenticationType(t *testing.T) { + type test struct { + input string + want gosnowflake.AuthType + } + + valid := []test{ + // Case insensitive. + {input: "snowflake", want: gosnowflake.AuthTypeSnowflake}, + + // Supported Values. + {input: "SNOWFLAKE", want: gosnowflake.AuthTypeSnowflake}, + {input: "OAUTH", want: gosnowflake.AuthTypeOAuth}, + {input: "EXTERNALBROWSER", want: gosnowflake.AuthTypeExternalBrowser}, + {input: "OKTA", want: gosnowflake.AuthTypeOkta}, + {input: "JWT", want: gosnowflake.AuthTypeJwt}, + {input: "SNOWFLAKE_JWT", want: gosnowflake.AuthTypeJwt}, + {input: "TOKENACCESSOR", want: gosnowflake.AuthTypeTokenAccessor}, + {input: "USERNAMEPASSWORDMFA", want: gosnowflake.AuthTypeUsernamePasswordMFA}, + } + + invalid := []test{ + {input: ""}, + {input: "foo"}, + } + + for _, tc := range valid { + t.Run(tc.input, func(t *testing.T) { + got, err := toAuthenticatorType(tc.input) + require.NoError(t, err) + require.Equal(t, tc.want, got) + }) + } + + for _, tc := range invalid { + t.Run(tc.input, func(t *testing.T) { + _, err := toAuthenticatorType(tc.input) + require.Error(t, err) + }) + } +} + +func Test_Provider_toProtocol(t *testing.T) { + type test struct { + input string + want protocol + } + + valid := []test{ + // Case insensitive. + {input: "http", want: protocolHttp}, + + // Supported Values. + {input: "HTTP", want: protocolHttp}, + {input: "HTTPS", want: protocolHttps}, + } + + invalid := []test{ + {input: ""}, + {input: "foo"}, + } + + for _, tc := range valid { + t.Run(tc.input, func(t *testing.T) { + got, err := toProtocol(tc.input) + require.NoError(t, err) + require.Equal(t, tc.want, got) + }) + } + + for _, tc := range invalid { + t.Run(tc.input, func(t *testing.T) { + _, err := toProtocol(tc.input) + require.Error(t, err) + }) + } +} diff --git a/pkg/resources/doc_helpers.go b/pkg/resources/doc_helpers.go index 33640deb4e..8476ed050b 100644 --- a/pkg/resources/doc_helpers.go +++ b/pkg/resources/doc_helpers.go @@ -3,14 +3,12 @@ package resources import ( "fmt" "strings" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider/docs" ) func possibleValuesListed[T ~string | ~int](values []T) string { - valuesWrapped := make([]string, len(values)) - for i, value := range values { - valuesWrapped[i] = fmt.Sprintf("`%v`", value) - } - return strings.Join(valuesWrapped, " | ") + return docs.PossibleValuesListed(values) } func characterList(values []rune) string { diff --git a/pkg/resources/validators.go b/pkg/resources/validators.go index f54965df35..071a73a33c 100644 --- a/pkg/resources/validators.go +++ b/pkg/resources/validators.go @@ -2,10 +2,10 @@ package resources import ( "fmt" - "reflect" "strings" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider/validators" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" @@ -29,93 +29,8 @@ func IsDataType() schema.SchemaValidateFunc { //nolint:staticcheck } } -// IsValidIdentifier is a validator that can be used for validating identifiers passed in resources and data sources. -// -// Typically, we expect passed identifiers to be a variation of sdk.ObjectIdentifier. -// For now, we're expecting implementations of sdk.ObjectIdentifier, because we won't support sdk.ExternalObjectIdentifiers. -// The reason behind it is that the functions that parse identifiers are not able to differentiate between -// sdk.ExternalObjectIdentifiers and sdk.DatabaseObjectIdentifier or sdk.SchemaObjectIdentifier. -// That's because sdk.ExternalObjectIdentifiers has varying parts count (2 or 3). -// -// To use this function, pass it as a validation function on identifier field with generic parameter set to the desired sdk.ObjectIdentifier implementation. func IsValidIdentifier[T sdk.AccountObjectIdentifier | sdk.DatabaseObjectIdentifier | sdk.SchemaObjectIdentifier | sdk.TableColumnIdentifier]() schema.SchemaValidateDiagFunc { - return func(value any, path cty.Path) diag.Diagnostics { - if _, ok := value.(string); !ok { - return diag.Diagnostics{ - diag.Diagnostic{ - Severity: diag.Error, - Summary: "Invalid schema identifier type", - Detail: fmt.Sprintf("Expected schema string type, but got: %T. This is a provider error please file a report: https://github.com/Snowflake-Labs/terraform-provider-snowflake/issues/new/choose", value), - AttributePath: path, - }, - } - } - - // TODO(SNOW-1495079): Right now we have to skip validation for AccountObjectIdentifier to handle a case where identifier contains dots - // TODO(SNOW-1495079): with sdk.AccountObjectIdentifier{} (or a new type of identifier) we should be able to validate individual part of the identifier field (e.g. "database" or "schema" field) - if _, ok := any(sdk.AccountObjectIdentifier{}).(T); ok { - return nil - } - - stringValue := value.(string) - id, err := helpers.DecodeSnowflakeParameterID(stringValue) - if err != nil { - return diag.Diagnostics{ - diag.Diagnostic{ - Severity: diag.Error, - Summary: "Unable to parse the identifier", - Detail: fmt.Sprintf( - "Unable to parse the identifier: %s. Make sure you are using the correct form of the fully qualified name for this field: %s.\nOriginal Error: %s", - stringValue, - getExpectedIdentifierRepresentationFromGeneric[T](), - err.Error(), - ), - AttributePath: path, - }, - } - } - - if _, ok := id.(T); !ok { - return diag.Diagnostics{ - diag.Diagnostic{ - Severity: diag.Error, - Summary: "Invalid identifier type", - Detail: fmt.Sprintf( - "Expected %s identifier type, but got: %T. The correct form of the fully qualified name for this field is: %s, but was %s", - reflect.TypeOf(new(T)).Elem().Name(), - id, - getExpectedIdentifierRepresentationFromGeneric[T](), - getExpectedIdentifierRepresentationFromParam(id), - ), - AttributePath: path, - }, - } - } - - return nil - } -} - -func getExpectedIdentifierRepresentationFromGeneric[T sdk.AccountObjectIdentifier | sdk.DatabaseObjectIdentifier | sdk.SchemaObjectIdentifier | sdk.TableColumnIdentifier]() string { - return getExpectedIdentifierForm(new(T)) -} - -func getExpectedIdentifierRepresentationFromParam(id sdk.ObjectIdentifier) string { - return getExpectedIdentifierForm(id) -} - -func getExpectedIdentifierForm(id any) string { - switch id.(type) { - case sdk.AccountObjectIdentifier, *sdk.AccountObjectIdentifier: - return "" - case sdk.DatabaseObjectIdentifier, *sdk.DatabaseObjectIdentifier: - return "." - case sdk.SchemaObjectIdentifier, *sdk.SchemaObjectIdentifier: - return ".." - case sdk.TableColumnIdentifier, *sdk.TableColumnIdentifier: - return "..." - } - return "" + return validators.IsValidIdentifier[T]() } // IsValidAccountIdentifier is a validator that can be used for validating account identifiers passed in resources and data sources. @@ -158,20 +73,7 @@ func IsValidAccountIdentifier() schema.SchemaValidateDiagFunc { // StringInSlice has the same implementation as validation.StringInSlice, but adapted to schema.SchemaValidateDiagFunc func StringInSlice(valid []string, ignoreCase bool) schema.SchemaValidateDiagFunc { - return func(i interface{}, path cty.Path) diag.Diagnostics { - v, ok := i.(string) - if !ok { - return diag.Errorf("expected type of %v to be string", path) - } - - for _, str := range valid { - if v == str || (ignoreCase && strings.EqualFold(v, str)) { - return nil - } - } - - return diag.Errorf("expected %v to be one of %q, got %s", path, valid, v) - } + return validators.StringInSlice(valid, ignoreCase) } // IntInSlice has the same implementation as validation.StringInSlice, but adapted to schema.SchemaValidateDiagFunc @@ -193,13 +95,7 @@ func IntInSlice(valid []int) schema.SchemaValidateDiagFunc { } func sdkValidation[T any](normalize func(string) (T, error)) schema.SchemaValidateDiagFunc { - return func(val interface{}, _ cty.Path) diag.Diagnostics { - _, err := normalize(val.(string)) - if err != nil { - return diag.FromErr(err) - } - return nil - } + return validators.NormalizeValidation(normalize) } func isNotEqualTo(notExpectedValue string, errorMessage string) schema.SchemaValidateDiagFunc { diff --git a/pkg/resources/validators_test.go b/pkg/resources/validators_test.go index 6146d3c603..9125bfd98c 100644 --- a/pkg/resources/validators_test.go +++ b/pkg/resources/validators_test.go @@ -6,7 +6,6 @@ import ( "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/hashicorp/go-cty/cty" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/stretchr/testify/assert" ) @@ -52,166 +51,6 @@ func TestIsDataType(t *testing.T) { } } -func TestIsValidIdentifier(t *testing.T) { - accountObjectIdentifierCheck := IsValidIdentifier[sdk.AccountObjectIdentifier]() - databaseObjectIdentifierCheck := IsValidIdentifier[sdk.DatabaseObjectIdentifier]() - schemaObjectIdentifierCheck := IsValidIdentifier[sdk.SchemaObjectIdentifier]() - tableColumnIdentifierCheck := IsValidIdentifier[sdk.TableColumnIdentifier]() - - testCases := []struct { - Name string - Value any - Error string - CheckingFn schema.SchemaValidateDiagFunc - }{ - { - Name: "validation: invalid value type", - Value: 123, - Error: "Expected schema string type, but got: int", - CheckingFn: accountObjectIdentifierCheck, - }, - { - Name: "validation: incorrect form for database object identifier", - Value: "a.b.c", - Error: "., but was ..", - CheckingFn: databaseObjectIdentifierCheck, - }, - { - Name: "validation: incorrect form for schema object identifier", - Value: "a.b.c.d", - Error: ".., but was ...", - CheckingFn: schemaObjectIdentifierCheck, - }, - { - Name: "validation: incorrect form for table column identifier", - Value: "a", - Error: "..., but was ", - CheckingFn: tableColumnIdentifierCheck, - }, - { - Name: "correct form for account object identifier", - Value: "a", - CheckingFn: accountObjectIdentifierCheck, - }, - { - Name: "correct form for account object identifier - multiple parts", - Value: "a.b", - CheckingFn: accountObjectIdentifierCheck, - }, - { - Name: "correct form for account object identifier - quoted", - Value: "\"a.b\"", - CheckingFn: accountObjectIdentifierCheck, - }, - { - Name: "correct form for database object identifier", - Value: "a.b", - CheckingFn: databaseObjectIdentifierCheck, - }, - { - Name: "correct form for schema object identifier", - Value: "a.b.c", - CheckingFn: schemaObjectIdentifierCheck, - }, - { - Name: "correct form for table column identifier", - Value: "a.b.c.d", - CheckingFn: tableColumnIdentifierCheck, - }, - } - - for _, tt := range testCases { - t.Run(tt.Name, func(t *testing.T) { - diag := tt.CheckingFn(tt.Value, cty.IndexStringPath("path")) - if tt.Error != "" { - assert.Len(t, diag, 1) - assert.Contains(t, diag[0].Detail, tt.Error) - } else { - assert.Len(t, diag, 0) - } - }) - } -} - -func TestGetExpectedIdentifierFormGeneric(t *testing.T) { - testCases := []struct { - Name string - Expected string - Actual string - }{ - { - Name: "correct account object identifier from generic parameter", - Expected: "", - Actual: getExpectedIdentifierRepresentationFromGeneric[sdk.AccountObjectIdentifier](), - }, - { - Name: "correct database object identifier from generic parameter", - Expected: ".", - Actual: getExpectedIdentifierRepresentationFromGeneric[sdk.DatabaseObjectIdentifier](), - }, - { - Name: "correct schema object identifier from generic parameter", - Expected: "..", - Actual: getExpectedIdentifierRepresentationFromGeneric[sdk.SchemaObjectIdentifier](), - }, - { - Name: "correct table column identifier from generic parameter", - Expected: "...", - Actual: getExpectedIdentifierRepresentationFromGeneric[sdk.TableColumnIdentifier](), - }, - } - - for _, tt := range testCases { - t.Run(tt.Name, func(t *testing.T) { - assert.Equal(t, tt.Expected, tt.Actual) - }) - } -} - -func TestGetExpectedIdentifierFormParam(t *testing.T) { - testCases := []struct { - Name string - Expected string - Identifier sdk.ObjectIdentifier - IdentifierPointer sdk.ObjectIdentifier - }{ - { - Name: "correct account object identifier from function argument", - Expected: "", - Identifier: sdk.AccountObjectIdentifier{}, - IdentifierPointer: &sdk.AccountObjectIdentifier{}, - }, - { - Name: "correct database object identifier from function argument", - Expected: ".", - Identifier: sdk.DatabaseObjectIdentifier{}, - IdentifierPointer: &sdk.DatabaseObjectIdentifier{}, - }, - { - Name: "correct schema object identifier from function argument", - Expected: "..", - Identifier: sdk.SchemaObjectIdentifier{}, - IdentifierPointer: &sdk.SchemaObjectIdentifier{}, - }, - { - Name: "correct table column identifier from function argument", - Expected: "...", - Identifier: sdk.TableColumnIdentifier{}, - IdentifierPointer: &sdk.TableColumnIdentifier{}, - }, - } - - for _, tt := range testCases { - t.Run(tt.Name+" - non-pointer", func(t *testing.T) { - assert.Equal(t, tt.Expected, getExpectedIdentifierRepresentationFromParam(tt.Identifier)) - }) - - t.Run(tt.Name+" - pointer", func(t *testing.T) { - assert.Equal(t, tt.Expected, getExpectedIdentifierRepresentationFromParam(tt.IdentifierPointer)) - }) - } -} - func Test_sdkValidation(t *testing.T) { genericNormalize := func(value string) (any, error) { if value == "ok" {