From fd03f33f850de52968056ee24b215f827109a2b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Cie=C5=9Blak?= Date: Thu, 5 Dec 2024 12:59:15 +0100 Subject: [PATCH] wip --- .../account_show_output_ext.go | 80 +++ pkg/resources/account.go | 341 ++++--------- pkg/resources/account_acceptance_test.go | 467 ++++++++++++++++-- pkg/resources/common.go | 11 +- pkg/sdk/identifier_helpers.go | 4 + 5 files changed, 607 insertions(+), 296 deletions(-) create mode 100644 pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/account_show_output_ext.go diff --git a/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/account_show_output_ext.go b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/account_show_output_ext.go new file mode 100644 index 0000000000..66a7a98a42 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert/account_show_output_ext.go @@ -0,0 +1,80 @@ +package resourceshowoutputassert + +import ( + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" +) + +func (a *AccountShowOutputAssert) HasAccountUrlNotEmpty() *AccountShowOutputAssert { + a.AddAssertion(assert.ResourceShowOutputValuePresent("account_url")) + return a +} + +func (a *AccountShowOutputAssert) HasCreatedOnNotEmpty() *AccountShowOutputAssert { + a.AddAssertion(assert.ResourceShowOutputValuePresent("created_on")) + return a +} + +func (a *AccountShowOutputAssert) HasAccountLocatorNotEmpty() *AccountShowOutputAssert { + a.AddAssertion(assert.ResourceShowOutputValuePresent("account_locator")) + return a +} + +func (a *AccountShowOutputAssert) HasAccountLocatorUrlNotEmpty() *AccountShowOutputAssert { + a.AddAssertion(assert.ResourceShowOutputValuePresent("account_locator_url")) + return a +} + +func (a *AccountShowOutputAssert) HasConsumptionBillingEntityNameNotEmpty() *AccountShowOutputAssert { + a.AddAssertion(assert.ResourceShowOutputValuePresent("consumption_billing_entity_name")) + return a +} + +func (a *AccountShowOutputAssert) HasMarketplaceProviderBillingEntityNameNotEmpty() *AccountShowOutputAssert { + a.AddAssertion(assert.ResourceShowOutputValuePresent("marketplace_provider_billing_entity_name")) + return a +} + +func (a *AccountShowOutputAssert) HasAccountOldUrlSavedOnEmpty() *AccountShowOutputAssert { + a.AddAssertion(assert.ResourceShowOutputValueSet("account_old_url_saved_on", "")) + return a +} + +func (a *AccountShowOutputAssert) HasAccountOldUrlLastUsedEmpty() *AccountShowOutputAssert { + a.AddAssertion(assert.ResourceShowOutputValueSet("account_old_url_last_used", "")) + return a +} + +func (a *AccountShowOutputAssert) HasOrganizationOldUrlSavedOnEmpty() *AccountShowOutputAssert { + a.AddAssertion(assert.ResourceShowOutputValueSet("organization_old_url_saved_on", "")) + return a +} + +func (a *AccountShowOutputAssert) HasOrganizationOldUrlLastUsedEmpty() *AccountShowOutputAssert { + a.AddAssertion(assert.ResourceShowOutputValueSet("organization_old_url_last_used", "")) + return a +} + +func (a *AccountShowOutputAssert) HasDroppedOnEmpty() *AccountShowOutputAssert { + a.AddAssertion(assert.ResourceShowOutputValueSet("dropped_on", "")) + return a +} + +func (a *AccountShowOutputAssert) HasScheduledDeletionTimeEmpty() *AccountShowOutputAssert { + a.AddAssertion(assert.ResourceShowOutputValueSet("scheduled_deletion_time", "")) + return a +} + +func (a *AccountShowOutputAssert) HasRestoredOnEmpty() *AccountShowOutputAssert { + a.AddAssertion(assert.ResourceShowOutputValueSet("restored_on", "")) + return a +} + +func (a *AccountShowOutputAssert) HasMovedOnEmpty() *AccountShowOutputAssert { + a.AddAssertion(assert.ResourceShowOutputValueSet("moved_on", "")) + return a +} + +func (a *AccountShowOutputAssert) HasOrganizationUrlExpirationOnEmpty() *AccountShowOutputAssert { + a.AddAssertion(assert.ResourceShowOutputValueSet("organization_url_expiration_on", "")) + return a +} diff --git a/pkg/resources/account.go b/pkg/resources/account.go index 96ec958e1d..86c1058d40 100644 --- a/pkg/resources/account.go +++ b/pkg/resources/account.go @@ -6,6 +6,7 @@ import ( "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/snowflakeroles" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "strings" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/provider" @@ -16,197 +17,6 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) -// Note: no test case was created for account since we cannot actually delete them after creation, which is a critical part of the test suite. Instead, this resource -// was manually tested - -//var accountSchemaOld = map[string]*schema.Schema{ -// "name": { -// Type: schema.TypeString, -// Required: true, -// Description: "Specifies the identifier (i.e. name) for the account; must be unique within an organization, regardless of which Snowflake Region the account is in. In addition, the identifier must start with an alphabetic character and cannot contain spaces or special characters except for underscores (_). Note that if the account name includes underscores, features that do not accept account names with underscores (e.g. Okta SSO or SCIM) can reference a version of the account name that substitutes hyphens (-) for the underscores.", -// // Name is automatically uppercase by Snowflake -// StateFunc: func(val interface{}) string { -// return strings.ToUpper(val.(string)) -// }, -// ValidateDiagFunc: IsValidIdentifier[sdk.AccountObjectIdentifier](), -// }, -// "admin_name": { -// Type: schema.TypeString, -// Required: true, -// Description: "Login name of the initial administrative user of the account. A new user is created in the new account with this name and password and granted the ACCOUNTADMIN role in the account. A login name can be any string consisting of letters, numbers, and underscores. Login names are always case-insensitive.", -// // We have no way of assuming a role into this account to change the admin user name so this has to be ForceNew even though it's not ideal -// ForceNew: true, -// DiffSuppressOnRefresh: true, -// DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { -// // For new resources always show the diff -// if d.Id() == "" { -// return false -// } -// // This suppresses the diff if the old value is empty. This would happen in the event of importing existing accounts since we have no way of reading this value -// return old == "" -// }, -// }, -// "admin_password": { -// Type: schema.TypeString, -// Optional: true, -// Sensitive: true, -// Description: "Password for the initial administrative user of the account. Optional if the `ADMIN_RSA_PUBLIC_KEY` parameter is specified. For more information about passwords in Snowflake, see [Snowflake-provided Password Policy](https://docs.snowflake.com/en/sql-reference/sql/create-account.html#:~:text=Snowflake%2Dprovided%20Password%20Policy).", -// AtLeastOneOf: []string{"admin_password", "admin_rsa_public_key"}, -// // We have no way of assuming a role into this account to change the password so this has to be ForceNew even though it's not ideal -// ForceNew: true, -// DiffSuppressOnRefresh: true, -// DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { -// // For new resources always show the diff -// if d.Id() == "" { -// return false -// } -// // This suppresses the diff if the old value is empty. This would happen in the event of importing existing accounts since we have no way of reading this value -// return old == "" -// }, -// }, -// "admin_rsa_public_key": { -// Type: schema.TypeString, -// Optional: true, -// Sensitive: true, -// Description: "Assigns a public key to the initial administrative user of the account in order to implement [key pair authentication](https://docs.snowflake.com/en/sql-reference/sql/create-account.html#:~:text=key%20pair%20authentication) for the user. Optional if the `ADMIN_PASSWORD` parameter is specified.", -// AtLeastOneOf: []string{"admin_password", "admin_rsa_public_key"}, -// // We have no way of assuming a role into this account to change the admin rsa public key so this has to be ForceNew even though it's not ideal -// ForceNew: true, -// DiffSuppressOnRefresh: true, -// DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { -// // For new resources always show the diff -// if d.Id() == "" { -// return false -// } -// // This suppresses the diff if the old value is empty. This would happen in the event of importing existing accounts since we have no way of reading this value -// return old == "" -// }, -// }, -// "email": { -// Type: schema.TypeString, -// Required: true, -// Sensitive: true, -// Description: "Email address of the initial administrative user of the account. This email address is used to send any notifications about the account.", -// // We have no way of assuming a role into this account to change the admin email so this has to be ForceNew even though it's not ideal -// ForceNew: true, -// DiffSuppressOnRefresh: true, -// DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { -// // For new resources always show the diff -// if d.Id() == "" { -// return false -// } -// // This suppresses the diff if the old value is empty. This would happen in the event of importing existing accounts since we have no way of reading this value -// return old == "" -// }, -// }, -// "edition": { -// Type: schema.TypeString, -// Required: true, -// ForceNew: true, -// Description: "[Snowflake Edition](https://docs.snowflake.com/en/user-guide/intro-editions.html) of the account. Valid values are: STANDARD | ENTERPRISE | BUSINESS_CRITICAL", -// ValidateFunc: validation.StringInSlice([]string{string(sdk.EditionStandard), string(sdk.EditionEnterprise), string(sdk.EditionBusinessCritical)}, false), -// }, -// "first_name": { -// Type: schema.TypeString, -// Optional: true, -// Sensitive: true, -// Description: "First name of the initial administrative user of the account", -// // We have no way of assuming a role into this account to change the admin first name so this has to be ForceNew even though it's not ideal -// ForceNew: true, -// DiffSuppressOnRefresh: true, -// DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { -// // For new resources always show the diff -// if d.Id() == "" { -// return false -// } -// // This suppresses the diff if the old value is empty. This would happen in the event of importing existing accounts since we have no way of reading this value -// return old == "" -// }, -// }, -// "last_name": { -// Type: schema.TypeString, -// Optional: true, -// Sensitive: true, -// Description: "Last name of the initial administrative user of the account", -// // We have no way of assuming a role into this account to change the admin last name so this has to be ForceNew even though it's not ideal -// ForceNew: true, -// DiffSuppressOnRefresh: true, -// DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { -// // For new resources always show the diff -// if d.Id() == "" { -// return false -// } -// // This suppresses the diff if the old value is empty. This would happen in the event of importing existing accounts since we have no way of reading this value -// return old == "" -// }, -// }, -// "must_change_password": { -// Type: schema.TypeBool, -// Optional: true, -// Default: false, -// Description: "Specifies whether the new user created to administer the account is forced to change their password upon first login into the account.", -// // We have no way of assuming a role into this account to change the admin password policy so this has to be ForceNew even though it's not ideal -// ForceNew: true, -// DiffSuppressOnRefresh: true, -// DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { -// // For new resources always show the diff -// if d.Id() == "" { -// return false -// } -// // This suppresses the diff if the old value is empty. This would happen in the event of importing existing accounts since we have no way of reading this value -// return old == "" -// }, -// }, -// "region_group": { -// Type: schema.TypeString, -// Optional: true, -// Description: "ID of the Snowflake Region where the account is created. If no value is provided, Snowflake creates the account in the same Snowflake Region as the current account (i.e. the account in which the CREATE ACCOUNT statement is executed.)", -// ForceNew: true, -// DiffSuppressOnRefresh: true, -// DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { -// // For new resources always show the diff -// if d.Id() == "" { -// return false -// } -// // This suppresses the diff if the old value is empty. This would happen in the event of importing existing accounts since we have no way of reading this value -// return new == "" -// }, -// }, -// "region": { -// Type: schema.TypeString, -// Optional: true, -// Description: "ID of the Snowflake Region where the account is created. If no value is provided, Snowflake creates the account in the same Snowflake Region as the current account (i.e. the account in which the CREATE ACCOUNT statement is executed.)", -// ForceNew: true, -// DiffSuppressOnRefresh: true, -// DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { -// // For new resources always show the diff -// if d.Id() == "" { -// return false -// } -// // This suppresses the diff if the old value is empty. This would happen in the event of importing existing accounts since we have no way of reading this value -// return new == "" -// }, -// }, -// "comment": { -// Type: schema.TypeString, -// Optional: true, -// Description: "Specifies a comment for the account.", -// ForceNew: true, -// }, -// "is_org_admin": { -// Type: schema.TypeBool, -// Computed: true, -// Description: "Indicates whether the ORGADMIN role is enabled in an account. If TRUE, the role is enabled.", -// }, -// "grace_period_in_days": { -// Type: schema.TypeInt, -// Optional: true, -// Default: 3, -// Description: "Specifies the number of days to wait before dropping the account. The default is 3 days.", -// }, -// FullyQualifiedNameAttributeName: schemas.FullyQualifiedNameSchema, -//} - var accountSchema = map[string]*schema.Schema{ "name": { Type: schema.TypeString, @@ -347,11 +157,55 @@ func Account() *schema.Resource { Schema: accountSchema, Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, + StateContext: TrackingImportWrapper(resources.Account, ImportAccount), }, - // TODO: State upgrader and import + // TODO: State upgrader + } +} + +func ImportAccount(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + client := meta.(*provider.Context).Client + + isOrgAdmin, err := client.ContextFunctions.IsRoleInSession(ctx, snowflakeroles.Orgadmin) + if err != nil { + return nil, err + } + if !isOrgAdmin { + // TODO: + return nil, errors.New("current user doesn't have the orgadmin role in session") + } + + id, err := sdk.ParseAccountIdentifier(d.Id()) + if err != nil { + return nil, err + } + + account, err := client.Accounts.ShowByID(ctx, id.AccountId()) + if err != nil { + return nil, err } + + if _, err := ImportName[sdk.AccountIdentifier](context.Background(), d, nil); err != nil { + return nil, err + } + + if account.RegionGroup != nil { + if err = d.Set("region_group", *account.RegionGroup); err != nil { + return nil, err + } + } + + if err := errors.Join( + d.Set("edition", string(*account.Edition)), + d.Set("region", account.SnowflakeRegion), + d.Set("comment", *account.Comment), + d.Set("is_org_admin", booleanStringFromBool(*account.IsOrgAdmin)), + ); err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil } func CreateAccount(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { @@ -457,7 +311,7 @@ func ReadAccount(withExternalChangesMarking bool) schema.ReadContextFunc { return diag.FromErr(err) } - account, err := client.Accounts.ShowByID(ctx, sdk.NewAccountObjectIdentifier(id.AccountName())) + account, err := client.Accounts.ShowByID(ctx, id.AccountId()) if err != nil { return diag.FromErr(err) } @@ -468,6 +322,7 @@ func ReadAccount(withExternalChangesMarking bool) schema.ReadContextFunc { regionGroup = *account.RegionGroup } if err = handleExternalChangesToObjectInShow(d, + outputMapping{"edition", "edition", *account.Edition, *account.Edition, nil}, outputMapping{"is_org_admin", "is_org_admin", *account.IsOrgAdmin, booleanStringFromBool(*account.IsOrgAdmin), nil}, outputMapping{"region_group", "region_group", regionGroup, regionGroup, nil}, outputMapping{"snowflake_region", "region", account.SnowflakeRegion, account.SnowflakeRegion, nil}, @@ -476,7 +331,7 @@ func ReadAccount(withExternalChangesMarking bool) schema.ReadContextFunc { return diag.FromErr(err) } } else { - if err = setStateToValuesFromConfig(d, taskSchema, []string{ + if err = setStateToValuesFromConfig(d, accountSchema, []string{ "name", "admin_name", "admin_password", @@ -498,34 +353,6 @@ func ReadAccount(withExternalChangesMarking bool) schema.ReadContextFunc { } if errs := errors.Join( - attributeMappedValueReadOrDefault(d, "edition", account.Edition, func(edition *sdk.AccountEdition) (string, error) { - if edition != nil { - return string(*edition), nil - } - return "", nil - }, nil), - // TODO: Region group is only returned when org is span on multiple region groups, but you can explicitly set it (e.g. PUBLIC) - //attributeMappedValueReadOrNil(d, "region_group", account.RegionGroup, func(regionGroup *string) (string, error) { - // if regionGroup != nil { - // return *regionGroup, nil - // } - // return "", nil - //}), - // TODO: Can be left empty and it will be populated with current account's region - //d.Set("region", account.SnowflakeRegion), - // TODO: Default comment is "SNOWFLAKE" - //attributeMappedValueReadOrNil(d, "comment", account.Comment, func(comment *string) (string, error) { - // if comment != nil { - // return *comment, nil - // } - // return "", nil - //}), - //attributeMappedValueReadOrNil(d, "is_org_admin", account.IsOrgAdmin, func(isOrgAdmin *bool) (string, error) { - // if isOrgAdmin != nil { - // return booleanStringFromBool(*isOrgAdmin), nil - // } - // return BooleanDefault, nil - //}), d.Set(FullyQualifiedNameAttributeName, id.FullyQualifiedName()), d.Set(ShowOutputAttributeName, []map[string]any{schemas.AccountToSchema(account)}), ); errs != nil { @@ -547,28 +374,56 @@ func UpdateAccount(ctx context.Context, d *schema.ResourceData, meta any) diag.D return diag.FromErr(errors.New("current user doesn't have the orgadmin role in session")) } - /* - todo: comments may eventually work again for accounts, so this can be uncommented when that happens - client := meta.(*provider.Context).Client - client := sdk.NewClientFromDB(db) - ctx := context.Background() - - id := helpers.DecodeSnowflakeID(d.Id()).(sdk.AccountObjectIdentifier) - - // Change comment - if d.HasChange("comment") { - // changing comment isn't supported for accounts - err := client.Comments.Set(ctx, &sdk.SetCommentOptions{ - ObjectType: sdk.ObjectTypeAccount, - ObjectName: sdk.NewAccountObjectIdentifier(d.Get("name").(string)), - Value: sdk.String(d.Get("comment").(string)), - }) + id, err := sdk.ParseAccountIdentifier(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + if d.HasChange("name") { + newId := sdk.NewAccountIdentifier(id.OrganizationName(), d.Get("name").(string)) + + err = client.Accounts.Alter(ctx, &sdk.AlterAccountOptions{ + Rename: &sdk.AccountRename{ + Name: id.AccountId(), + NewName: newId.AccountId(), + }, + }) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(helpers.EncodeResourceIdentifier(newId)) + id = newId + } + + if d.HasChange("is_org_admin") { + if v := d.Get("is_org_admin").(string); v != BooleanDefault { + parsed, err := booleanStringToBool(v) if err != nil { - return err + return diag.FromErr(err) + } + if err := client.Accounts.Alter(ctx, &sdk.AlterAccountOptions{ + SetIsOrgAdmin: &sdk.AccountSetIsOrgAdmin{ + Name: id.AccountId(), + OrgAdmin: parsed, + }, + }); err != nil { + return diag.FromErr(err) + } + } else { + // No unset available for this field (setting Snowflake default) + if err := client.Accounts.Alter(ctx, &sdk.AlterAccountOptions{ + SetIsOrgAdmin: &sdk.AccountSetIsOrgAdmin{ + Name: id.AccountId(), + OrgAdmin: false, + }, + }); err != nil && !strings.Contains(err.Error(), "already has ORGADMIN disabled") { // TODO: What to do about this error? + return diag.FromErr(err) } } - */ - return nil + } + + return ReadAccount(false)(ctx, d, meta) } func DeleteAccount(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { @@ -587,7 +442,7 @@ func DeleteAccount(ctx context.Context, d *schema.ResourceData, meta any) diag.D return diag.FromErr(err) } - err = client.Accounts.Drop(ctx, sdk.NewAccountObjectIdentifier(id.AccountName()), d.Get("grace_period_in_days").(int), &sdk.DropAccountOptions{ + err = client.Accounts.Drop(ctx, id.AccountId(), d.Get("grace_period_in_days").(int), &sdk.DropAccountOptions{ IfExists: sdk.Bool(true), }) if err != nil { diff --git a/pkg/resources/account_acceptance_test.go b/pkg/resources/account_acceptance_test.go index f945da6db4..cc07fc8a17 100644 --- a/pkg/resources/account_acceptance_test.go +++ b/pkg/resources/account_acceptance_test.go @@ -1,30 +1,35 @@ package resources_test import ( + acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert/resourceassert" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/assert/resourceshowoutputassert" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config/model" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/helpers/random" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/snowflakeenvs" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/snowflakeroles" r "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/resources" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "regexp" "testing" - acc "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance" - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/testenvs" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/tfversion" ) -func TestAcc_Account_minimal(t *testing.T) { +func TestAcc_Account_Minimal(t *testing.T) { _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) _ = testenvs.GetOrSkipTest(t, testenvs.TestAccountCreate) organizationName := acc.TestClient().Context.CurrentAccountId(t).OrganizationName() id := random.AdminName() + accountId := sdk.NewAccountIdentifier(organizationName, id) email := random.Email() name := random.AdminName() key, _ := random.GenerateRSAPublicKey(t) @@ -45,7 +50,7 @@ func TestAcc_Account_minimal(t *testing.T) { Check: assert.AssertThat(t, resourceassert.AccountResource(t, configModel.ResourceReference()). HasNameString(id). - HasFullyQualifiedNameString(sdk.NewAccountIdentifier(organizationName, id).FullyQualifiedName()). + HasFullyQualifiedNameString(accountId.FullyQualifiedName()). HasAdminNameString(name). HasAdminRsaPublicKeyString(key). HasEmailString(email). @@ -63,42 +68,65 @@ func TestAcc_Account_minimal(t *testing.T) { HasSnowflakeRegion(region). HasRegionGroup(""). HasEdition(sdk.EditionStandard). - //HasAccountURL(). - //HasCreatedOn(). + HasAccountUrlNotEmpty(). + HasCreatedOnNotEmpty(). HasComment("SNOWFLAKE"). - //HasAccountLocator(). - //HasAccountLocatorURL(). + HasAccountLocatorNotEmpty(). + HasAccountLocatorUrlNotEmpty(). HasManagedAccounts(0). - //HasConsumptionBillingEntityName(). - //HasMarketplaceConsumerBillingEntityName(). - //HasMarketplaceProviderBillingEntityName(). - //HasOldAccountURL(). + HasConsumptionBillingEntityNameNotEmpty(). + HasMarketplaceConsumerBillingEntityName(""). + HasMarketplaceProviderBillingEntityNameNotEmpty(). + HasOldAccountURL(""). HasIsOrgAdmin(false). - //HasAccountOldUrlSavedOn(). - //HasAccountOldUrlLastUsed(). - //HasOrganizationOldUrl(). - //HasOrganizationOldUrlSavedOn(). - //HasOrganizationOldUrlLastUsed(). + HasAccountOldUrlSavedOnEmpty(). + HasAccountOldUrlLastUsedEmpty(). + HasOrganizationOldUrl(""). + HasOrganizationOldUrlSavedOnEmpty(). + HasOrganizationOldUrlLastUsedEmpty(). HasIsEventsAccount(false). - HasIsOrganizationAccount(false), - //HasDroppedOn(). - //HasScheduledDeletionTime(). - //HasRestoredOn(). - //HasMovedToOrganization(). - //HasMovedOn(). - //HasOrganizationUrlExpirationOn(), + HasIsOrganizationAccount(false). + HasDroppedOnEmpty(). + HasScheduledDeletionTimeEmpty(). + HasRestoredOnEmpty(). + HasMovedToOrganization(""). + HasMovedOn(""). + HasOrganizationUrlExpirationOnEmpty(), + ), + }, + { + ResourceName: configModel.ResourceReference(), + Config: config.FromModel(t, configModel), + ImportState: true, + ImportStateCheck: assert.AssertThatImport(t, + resourceassert.ImportedAccountResource(t, helpers.EncodeResourceIdentifier(accountId)). + HasNameString(id). + HasFullyQualifiedNameString(accountId.FullyQualifiedName()). + HasNoAdminName(). + HasNoAdminRsaPublicKey(). + HasNoEmail(). + HasNoFirstName(). + HasNoLastName(). + HasNoMustChangePassword(). + HasEditionString(string(sdk.EditionStandard)). + HasNoRegionGroup(). + HasRegionString(region). + HasCommentString("SNOWFLAKE"). + HasIsOrgAdminString(r.BooleanFalse). + HasNoGracePeriodInDays(), ), }, }, }) } -func TestAcc_Account_complete(t *testing.T) { +func TestAcc_Account_Complete(t *testing.T) { _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) _ = testenvs.GetOrSkipTest(t, testenvs.TestAccountCreate) organizationName := acc.TestClient().Context.CurrentAccountId(t).OrganizationName() id := random.AdminName() + accountId := sdk.NewAccountIdentifier(organizationName, id) firstName := acc.TestClient().Ids.Alpha() lastName := acc.TestClient().Ids.Alpha() email := random.Email() @@ -112,7 +140,7 @@ func TestAcc_Account_complete(t *testing.T) { WithFirstName(firstName). WithLastName(lastName). WithMustChangePassword(r.BooleanTrue). - //WithRegionGroup("PUBLIC"). + WithRegionGroup("PUBLIC"). WithRegion(region). WithComment(comment). WithIsOrgAdmin(r.BooleanFalse) @@ -136,7 +164,7 @@ func TestAcc_Account_complete(t *testing.T) { HasFirstNameString(firstName). HasLastNameString(lastName). HasMustChangePasswordString(r.BooleanTrue). - HasNoRegionGroup(). // TODO + HasRegionGroupString("PUBLIC"). HasRegionString(region). HasCommentString(comment). HasIsOrgAdminString(r.BooleanFalse). @@ -147,39 +175,374 @@ func TestAcc_Account_complete(t *testing.T) { HasSnowflakeRegion(region). HasRegionGroup(""). HasEdition(sdk.EditionStandard). - //HasAccountURL(). - //HasCreatedOn(). + HasAccountUrlNotEmpty(). + HasCreatedOnNotEmpty(). HasComment(comment). - //HasAccountLocator(). - //HasAccountLocatorURL(). + HasAccountLocatorNotEmpty(). + HasAccountLocatorUrlNotEmpty(). HasManagedAccounts(0). - //HasConsumptionBillingEntityName(). - //HasMarketplaceConsumerBillingEntityName(). - //HasMarketplaceProviderBillingEntityName(). - //HasOldAccountURL(). + HasConsumptionBillingEntityNameNotEmpty(). + HasMarketplaceConsumerBillingEntityName(""). + HasMarketplaceProviderBillingEntityNameNotEmpty(). + HasOldAccountURL(""). HasIsOrgAdmin(false). - //HasAccountOldUrlSavedOn(). - //HasAccountOldUrlLastUsed(). - //HasOrganizationOldUrl(). - //HasOrganizationOldUrlSavedOn(). - //HasOrganizationOldUrlLastUsed(). + HasAccountOldUrlSavedOnEmpty(). + HasAccountOldUrlLastUsedEmpty(). + HasOrganizationOldUrl(""). + HasOrganizationOldUrlSavedOnEmpty(). + HasOrganizationOldUrlLastUsedEmpty(). HasIsEventsAccount(false). - HasIsOrganizationAccount(false), - //HasDroppedOn(). - //HasScheduledDeletionTime(). - //HasRestoredOn(). - //HasMovedToOrganization(). - //HasMovedOn(). - //HasOrganizationUrlExpirationOn(), + HasIsOrganizationAccount(false). + HasDroppedOnEmpty(). + HasScheduledDeletionTimeEmpty(). + HasRestoredOnEmpty(). + HasMovedToOrganization(""). + HasMovedOn(""). + HasOrganizationUrlExpirationOnEmpty(), + ), + }, + { + ResourceName: configModel.ResourceReference(), + Config: config.FromModel(t, configModel), + ImportState: true, + ImportStateCheck: assert.AssertThatImport(t, + resourceassert.ImportedAccountResource(t, helpers.EncodeResourceIdentifier(accountId)). + HasNameString(id). + HasFullyQualifiedNameString(sdk.NewAccountIdentifier(organizationName, id).FullyQualifiedName()). + HasNoAdminName(). + HasNoAdminRsaPublicKey(). + HasNoEmail(). + HasNoFirstName(). + HasNoLastName(). + HasNoMustChangePassword(). + HasEditionString(string(sdk.EditionStandard)). + HasNoRegionGroup(). + HasRegionString(region). + HasCommentString(comment). + HasIsOrgAdminString(r.BooleanFalse). + HasNoGracePeriodInDays(), + ), + }, + }, + }) +} + +func TestAcc_Account_Rename(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + _ = testenvs.GetOrSkipTest(t, testenvs.TestAccountCreate) + + organizationName := acc.TestClient().Context.CurrentAccountId(t).OrganizationName() + id := random.AdminName() + accountId := sdk.NewAccountIdentifier(organizationName, id) + + newId := random.AdminName() + newAccountId := sdk.NewAccountIdentifier(organizationName, newId) + + email := random.Email() + name := random.AdminName() + key, _ := random.GenerateRSAPublicKey(t) + + configModel := model.Account("test", name, string(sdk.UserTypeService), string(sdk.EditionStandard), email, 3, id). + WithAdminRsaPublicKey(key) + newConfigModel := model.Account("test", name, string(sdk.UserTypeService), string(sdk.EditionStandard), email, 3, newId). + WithAdminRsaPublicKey(key) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Account), + Steps: []resource.TestStep{ + { + Config: config.FromModel(t, configModel), + Check: assert.AssertThat(t, + resourceassert.AccountResource(t, configModel.ResourceReference()). + HasNameString(id). + HasFullyQualifiedNameString(accountId.FullyQualifiedName()), + resourceshowoutputassert.AccountShowOutput(t, configModel.ResourceReference()). + HasOrganizationName(organizationName). + HasAccountName(id), + ), + }, + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(newConfigModel.ResourceReference(), plancheck.ResourceActionUpdate), + }, + }, + Config: config.FromModel(t, newConfigModel), + Check: assert.AssertThat(t, + resourceassert.AccountResource(t, newConfigModel.ResourceReference()). + HasNameString(newId). + HasFullyQualifiedNameString(newAccountId.FullyQualifiedName()), + resourceshowoutputassert.AccountShowOutput(t, newConfigModel.ResourceReference()). + HasOrganizationName(organizationName). + HasAccountName(newId), + ), + }, + }, + }) +} + +func TestAcc_Account_IsOrgAdmin(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + _ = testenvs.GetOrSkipTest(t, testenvs.TestAccountCreate) + + organizationName := acc.TestClient().Context.CurrentAccountId(t).OrganizationName() + id := random.AdminName() + accountId := sdk.NewAccountIdentifier(organizationName, id) + + email := random.Email() + name := random.AdminName() + key, _ := random.GenerateRSAPublicKey(t) + + configModelWithOrgAdminTrue := model.Account("test", name, string(sdk.UserTypeService), string(sdk.EditionStandard), email, 3, id). + WithAdminRsaPublicKey(key). + WithIsOrgAdmin(r.BooleanTrue) + + configModelWithOrgAdminFalse := model.Account("test", name, string(sdk.UserTypeService), string(sdk.EditionStandard), email, 3, id). + WithAdminRsaPublicKey(key). + WithIsOrgAdmin(r.BooleanFalse) + + configModelWithoutOrgAdmin := model.Account("test", name, string(sdk.UserTypeService), string(sdk.EditionStandard), email, 3, id). + WithAdminRsaPublicKey(key) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Account), + Steps: []resource.TestStep{ + // Create with ORGADMIN enabled + { + Config: config.FromModel(t, configModelWithOrgAdminTrue), + Check: assert.AssertThat(t, + resourceassert.AccountResource(t, configModelWithOrgAdminTrue.ResourceReference()). + HasNameString(id). + HasFullyQualifiedNameString(accountId.FullyQualifiedName()). + HasIsOrgAdminString(r.BooleanTrue), + resourceshowoutputassert.AccountShowOutput(t, configModelWithOrgAdminTrue.ResourceReference()). + HasOrganizationName(organizationName). + HasAccountName(id). + HasIsOrgAdmin(true), + ), + }, + // Disable ORGADMIN + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(configModelWithOrgAdminFalse.ResourceReference(), plancheck.ResourceActionUpdate), + }, + }, + Config: config.FromModel(t, configModelWithOrgAdminFalse), + Check: assert.AssertThat(t, + resourceassert.AccountResource(t, configModelWithOrgAdminFalse.ResourceReference()). + HasNameString(id). + HasFullyQualifiedNameString(accountId.FullyQualifiedName()). + HasIsOrgAdminString(r.BooleanFalse), + resourceshowoutputassert.AccountShowOutput(t, configModelWithOrgAdminFalse.ResourceReference()). + HasOrganizationName(organizationName). + HasAccountName(id). + HasIsOrgAdmin(false), ), }, + // Remove is_org_admin from the config and go back to default (disabled) + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(configModelWithoutOrgAdmin.ResourceReference(), plancheck.ResourceActionUpdate), + }, + }, + Config: config.FromModel(t, configModelWithoutOrgAdmin), + Check: assert.AssertThat(t, + resourceassert.AccountResource(t, configModelWithoutOrgAdmin.ResourceReference()). + HasNameString(id). + HasFullyQualifiedNameString(accountId.FullyQualifiedName()). + HasIsOrgAdminString(r.BooleanDefault), + resourceshowoutputassert.AccountShowOutput(t, configModelWithoutOrgAdmin.ResourceReference()). + HasOrganizationName(organizationName). + HasAccountName(id). + HasIsOrgAdmin(false), + ), + }, + // External change (enable ORGADMIN) + { + PreConfig: func() { + acc.TestClient().Account.Alter(t, &sdk.AlterAccountOptions{ + SetIsOrgAdmin: &sdk.AccountSetIsOrgAdmin{ + Name: accountId.AccountId(), + OrgAdmin: true, + }, + }) + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(configModelWithoutOrgAdmin.ResourceReference(), plancheck.ResourceActionUpdate), + }, + }, + Config: config.FromModel(t, configModelWithoutOrgAdmin), + Check: assert.AssertThat(t, + resourceassert.AccountResource(t, configModelWithoutOrgAdmin.ResourceReference()). + HasNameString(id). + HasFullyQualifiedNameString(accountId.FullyQualifiedName()). + HasIsOrgAdminString(r.BooleanDefault), + resourceshowoutputassert.AccountShowOutput(t, configModelWithoutOrgAdmin.ResourceReference()). + HasOrganizationName(organizationName). + HasAccountName(id). + HasIsOrgAdmin(false), + ), + }, + }, + }) +} + +func TestAcc_Account_IgnoreUpdateAfterCreationOnCertainFields(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + _ = testenvs.GetOrSkipTest(t, testenvs.TestAccountCreate) + + organizationName := acc.TestClient().Context.CurrentAccountId(t).OrganizationName() + id := random.AdminName() + accountId := sdk.NewAccountIdentifier(organizationName, id) + + firstName := random.AdminName() + lastName := random.AdminName() + email := random.Email() + name := random.AdminName() + pass := random.Password() + + newFirstName := random.AdminName() + newLastName := random.AdminName() + newEmail := random.Email() + newName := random.AdminName() + newPass := random.Password() + + configModel := model.Account("test", name, string(sdk.UserTypePerson), string(sdk.EditionStandard), email, 3, id). + WithFirstName(firstName). + WithLastName(lastName). + WithMustChangePassword(r.BooleanTrue). + WithAdminPassword(pass) + + newConfigModel := model.Account("test", newName, string(sdk.UserTypeService), string(sdk.EditionStandard), newEmail, 3, id). + WithAdminPassword(newPass). + WithFirstName(newFirstName). + WithLastName(newLastName) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Account), + Steps: []resource.TestStep{ + { + Config: config.FromModel(t, configModel), + Check: assert.AssertThat(t, + resourceassert.AccountResource(t, configModel.ResourceReference()). + HasNameString(id). + HasFullyQualifiedNameString(accountId.FullyQualifiedName()). + HasAdminNameString(name). + HasAdminPasswordString(pass). + //HasAdminUserType(). TODO + HasEmailString(email). + HasFirstNameString(firstName). + HasLastNameString(lastName). + HasMustChangePasswordString(r.BooleanTrue), + ), + }, + { + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(newConfigModel.ResourceReference(), plancheck.ResourceActionNoop), + }, + }, + Config: config.FromModel(t, newConfigModel), + Check: assert.AssertThat(t, + resourceassert.AccountResource(t, newConfigModel.ResourceReference()). + HasNameString(id). + HasFullyQualifiedNameString(accountId.FullyQualifiedName()). + HasAdminNameString(name). + HasAdminPasswordString(pass). + HasEmailString(email). + HasFirstNameString(firstName). + HasLastNameString(lastName). + HasMustChangePasswordString(r.BooleanTrue), + ), + }, + }, + }) +} + +func TestAcc_Account_TryToCreateWithoutOrgadmin(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + _ = testenvs.GetOrSkipTest(t, testenvs.TestAccountCreate) + + id := random.AdminName() + email := random.Email() + name := random.AdminName() + key, _ := random.GenerateRSAPublicKey(t) + + t.Setenv(string(testenvs.ConfigureClientOnce), "") + t.Setenv(snowflakeenvs.Role, snowflakeroles.Accountadmin.Name()) + + configModel := model.Account("test", name, string(sdk.UserTypeService), string(sdk.EditionStandard), email, 3, id). + WithAdminRsaPublicKey(key) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Account), + Steps: []resource.TestStep{ + { + Config: config.FromModel(t, configModel), + ExpectError: regexp.MustCompile("Error: current user doesn't have the orgadmin role in session"), + }, + }, + }) +} + +func TestAcc_Account_InvalidValues(t *testing.T) { + _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) + _ = testenvs.GetOrSkipTest(t, testenvs.TestAccountCreate) + + id := random.AdminName() + email := random.Email() + name := random.AdminName() + key, _ := random.GenerateRSAPublicKey(t) + + configModelInvalidUserType := model.Account("test", name, "invalid_user_type", string(sdk.EditionStandard), email, 3, id). + WithAdminRsaPublicKey(key) + + configModelInvalidAccountEdition := model.Account("test", name, string(sdk.UserTypeService), "invalid_account_edition", email, 3, id). + WithAdminRsaPublicKey(key) + + configModelInvalidGracePeriodInDays := model.Account("test", name, string(sdk.UserTypeService), string(sdk.EditionStandard), email, 2, id). + WithAdminRsaPublicKey(key) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.RequireAbove(tfversion.Version1_5_0), + }, + CheckDestroy: acc.CheckDestroy(t, resources.Account), + Steps: []resource.TestStep{ + { + Config: config.FromModel(t, configModelInvalidUserType), + ExpectError: regexp.MustCompile("invalid user type: invalid_user_type"), + }, + { + Config: config.FromModel(t, configModelInvalidAccountEdition), + ExpectError: regexp.MustCompile("unknown account edition: invalid_account_edition"), + }, + { + Config: config.FromModel(t, configModelInvalidGracePeriodInDays), + ExpectError: regexp.MustCompile("Error: expected grace_period_in_days to be at least \\(3\\), got 2"), + }, }, }) } -// TODO: All show outputs in minimal and complete -// TODO: Imports -// TODO: Alters -// TODO: Not orgadmin role -// TODO: Invalid values // TODO: State upgrader diff --git a/pkg/resources/common.go b/pkg/resources/common.go index 643524f9d9..4c84ac1c4c 100644 --- a/pkg/resources/common.go +++ b/pkg/resources/common.go @@ -60,7 +60,7 @@ func ctyValToSliceString(valueElems []cty.Value) []string { return elems } -func ImportName[T sdk.AccountObjectIdentifier | sdk.DatabaseObjectIdentifier | sdk.SchemaObjectIdentifier](ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { +func ImportName[T sdk.AccountObjectIdentifier | sdk.DatabaseObjectIdentifier | sdk.SchemaObjectIdentifier | sdk.AccountIdentifier](ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { switch any(new(T)).(type) { case *sdk.AccountObjectIdentifier: id, err := sdk.ParseAccountObjectIdentifier(d.Id()) @@ -101,6 +101,15 @@ func ImportName[T sdk.AccountObjectIdentifier | sdk.DatabaseObjectIdentifier | s if err := d.Set("schema", id.SchemaName()); err != nil { return nil, err } + case *sdk.AccountIdentifier: + id, err := sdk.ParseAccountIdentifier(d.Id()) + if err != nil { + return nil, err + } + + if err := d.Set("name", id.AccountName()); err != nil { + return nil, err + } } return []*schema.ResourceData{d}, nil diff --git a/pkg/sdk/identifier_helpers.go b/pkg/sdk/identifier_helpers.go index 90d1acdf44..8ce7a23850 100644 --- a/pkg/sdk/identifier_helpers.go +++ b/pkg/sdk/identifier_helpers.go @@ -124,6 +124,10 @@ func (i AccountIdentifier) AccountName() string { return i.accountName } +func (i AccountIdentifier) AccountId() AccountObjectIdentifier { + return NewAccountObjectIdentifier(i.accountName) +} + func (i AccountIdentifier) Name() string { if i.organizationName != "" && i.accountName != "" { return fmt.Sprintf("%s.%s", i.organizationName, i.accountName)