From 886cad6f9e24d13dbdec558339984d994ece9ddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Cie=C5=9Blak?= Date: Thu, 5 Dec 2024 08:10:19 +0100 Subject: [PATCH] wip --- .../config/model/account_model_gen.go | 27 +++- .../helpers/random/random_helpers.go | 3 +- pkg/resources/account.go | 100 ++++++++++-- pkg/resources/account_acceptance_test.go | 153 ++++++++++++++---- pkg/resources/resource_helpers_read.go | 11 -- pkg/sdk/type_helpers.go | 11 -- 6 files changed, 234 insertions(+), 71 deletions(-) diff --git a/pkg/acceptance/bettertestspoc/config/model/account_model_gen.go b/pkg/acceptance/bettertestspoc/config/model/account_model_gen.go index 87ddf58d4e..8b616f90de 100644 --- a/pkg/acceptance/bettertestspoc/config/model/account_model_gen.go +++ b/pkg/acceptance/bettertestspoc/config/model/account_model_gen.go @@ -13,6 +13,7 @@ type AccountModel struct { AdminName tfconfig.Variable `json:"admin_name,omitempty"` AdminPassword tfconfig.Variable `json:"admin_password,omitempty"` AdminRsaPublicKey tfconfig.Variable `json:"admin_rsa_public_key,omitempty"` + AdminUserType tfconfig.Variable `json:"admin_user_type,omitempty"` Comment tfconfig.Variable `json:"comment,omitempty"` Edition tfconfig.Variable `json:"edition,omitempty"` Email tfconfig.Variable `json:"email,omitempty"` @@ -36,28 +37,36 @@ type AccountModel struct { func Account( resourceName string, adminName string, + adminUserType string, edition string, email string, + gracePeriodInDays int, name string, ) *AccountModel { a := &AccountModel{ResourceModelMeta: config.Meta(resourceName, resources.Account)} a.WithAdminName(adminName) + a.WithAdminUserType(adminUserType) a.WithEdition(edition) a.WithEmail(email) + a.WithGracePeriodInDays(gracePeriodInDays) a.WithName(name) return a } func AccountWithDefaultMeta( adminName string, + adminUserType string, edition string, email string, + gracePeriodInDays int, name string, ) *AccountModel { a := &AccountModel{ResourceModelMeta: config.DefaultMeta(resources.Account)} a.WithAdminName(adminName) + a.WithAdminUserType(adminUserType) a.WithEdition(edition) a.WithEmail(email) + a.WithGracePeriodInDays(gracePeriodInDays) a.WithName(name) return a } @@ -81,6 +90,11 @@ func (a *AccountModel) WithAdminRsaPublicKey(adminRsaPublicKey string) *AccountM return a } +func (a *AccountModel) WithAdminUserType(adminUserType string) *AccountModel { + a.AdminUserType = tfconfig.StringVariable(adminUserType) + return a +} + func (a *AccountModel) WithComment(comment string) *AccountModel { a.Comment = tfconfig.StringVariable(comment) return a @@ -111,8 +125,8 @@ func (a *AccountModel) WithGracePeriodInDays(gracePeriodInDays int) *AccountMode return a } -func (a *AccountModel) WithIsOrgAdmin(isOrgAdmin bool) *AccountModel { - a.IsOrgAdmin = tfconfig.BoolVariable(isOrgAdmin) +func (a *AccountModel) WithIsOrgAdmin(isOrgAdmin string) *AccountModel { + a.IsOrgAdmin = tfconfig.StringVariable(isOrgAdmin) return a } @@ -121,8 +135,8 @@ func (a *AccountModel) WithLastName(lastName string) *AccountModel { return a } -func (a *AccountModel) WithMustChangePassword(mustChangePassword bool) *AccountModel { - a.MustChangePassword = tfconfig.BoolVariable(mustChangePassword) +func (a *AccountModel) WithMustChangePassword(mustChangePassword string) *AccountModel { + a.MustChangePassword = tfconfig.StringVariable(mustChangePassword) return a } @@ -160,6 +174,11 @@ func (a *AccountModel) WithAdminRsaPublicKeyValue(value tfconfig.Variable) *Acco return a } +func (a *AccountModel) WithAdminUserTypeValue(value tfconfig.Variable) *AccountModel { + a.AdminUserType = value + return a +} + func (a *AccountModel) WithCommentValue(value tfconfig.Variable) *AccountModel { a.Comment = value return a diff --git a/pkg/acceptance/helpers/random/random_helpers.go b/pkg/acceptance/helpers/random/random_helpers.go index fcfb5b7208..6e11e7cd05 100644 --- a/pkg/acceptance/helpers/random/random_helpers.go +++ b/pkg/acceptance/helpers/random/random_helpers.go @@ -3,6 +3,7 @@ package random import ( "github.com/brianvoe/gofakeit/v6" "github.com/hashicorp/go-uuid" + "strings" ) func UUID() string { @@ -22,7 +23,7 @@ func Password() string { // 090088 (22000): ADMIN_NAME can only contain letters, numbers and underscores. // 090089 (22000): ADMIN_NAME must start with a letter. func AdminName() string { - return AlphaN(1) + AlphanumericN(11) + return strings.ToUpper(AlphaN(1) + AlphanumericN(11)) } func Bool() bool { diff --git a/pkg/resources/account.go b/pkg/resources/account.go index 47821954f8..96ec958e1d 100644 --- a/pkg/resources/account.go +++ b/pkg/resources/account.go @@ -3,6 +3,7 @@ package resources import ( "context" "errors" + "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" @@ -298,13 +299,20 @@ var accountSchema = map[string]*schema.Schema{ Optional: true, ForceNew: true, Description: "Specifies a comment for the account.", + DiffSuppressFunc: SuppressIfAny( + IgnoreChangeToCurrentSnowflakeValueInShow("comment"), + func(k, oldValue, newValue string, d *schema.ResourceData) bool { + return oldValue == "SNOWFLAKE" && newValue == "" + }, + ), }, "is_org_admin": { Type: schema.TypeString, Optional: true, Default: BooleanDefault, - Description: "TODO", + DiffSuppressFunc: IgnoreChangeToCurrentSnowflakeValueInShow("is_org_admin"), ValidateDiagFunc: validateBooleanString, + Description: "TODO", }, "grace_period_in_days": { Type: schema.TypeInt, @@ -348,6 +356,15 @@ func Account() *schema.Resource { func CreateAccount(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*provider.Context).Client + + isOrgAdmin, err := client.ContextFunctions.IsRoleInSession(ctx, snowflakeroles.Orgadmin) + if err != nil { + return diag.FromErr(err) + } + if !isOrgAdmin { + return diag.FromErr(errors.New("current user doesn't have the orgadmin role in session")) + } + id := sdk.NewAccountObjectIdentifier(d.Get("name").(string)) opts := &sdk.CreateAccountOptions{ @@ -408,12 +425,33 @@ func CreateAccount(ctx context.Context, d *schema.ResourceData, meta any) diag.D d.SetId(helpers.EncodeResourceIdentifier(sdk.NewAccountIdentifier(createResponse.OrganizationName, createResponse.AccountName))) + if v, ok := d.GetOk("is_org_admin"); ok && v == BooleanTrue { + err := client.Accounts.Alter(ctx, &sdk.AlterAccountOptions{ + SetIsOrgAdmin: &sdk.AccountSetIsOrgAdmin{ + Name: id, + OrgAdmin: true, + }, + }) + if err != nil { + return diag.FromErr(err) + } + } + return ReadAccount(false)(ctx, d, meta) } func ReadAccount(withExternalChangesMarking bool) schema.ReadContextFunc { return func(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*provider.Context).Client + + isOrgAdmin, err := client.ContextFunctions.IsRoleInSession(ctx, snowflakeroles.Orgadmin) + if err != nil { + return diag.FromErr(err) + } + if !isOrgAdmin { + return diag.FromErr(errors.New("current user doesn't have the orgadmin role in session")) + } + id, err := sdk.ParseAccountIdentifier(d.Id()) if err != nil { return diag.FromErr(err) @@ -425,16 +463,35 @@ func ReadAccount(withExternalChangesMarking bool) schema.ReadContextFunc { } if withExternalChangesMarking { + var regionGroup string + if account.RegionGroup != nil { + regionGroup = *account.RegionGroup + } if err = handleExternalChangesToObjectInShow(d, - outputMapping{"region_group", "region_group", account.RegionGroup, sdk.DerefIfNotNil(account.RegionGroup), sdk.DerefIfNotNil}, + 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}, - outputMapping{"comment", "comment", account.Comment, sdk.DerefIfNotNil(account.Comment), sdk.DerefIfNotNil}, + outputMapping{"comment", "comment", *account.Comment, *account.Comment, nil}, ); err != nil { return diag.FromErr(err) } } else { if err = setStateToValuesFromConfig(d, taskSchema, []string{ - "allow_overlapping_execution", + "name", + "admin_name", + "admin_password", + "admin_rsa_public_key", + "admin_user_type", + "first_name", + "last_name", + "email", + "must_change_password", + "edition", + "region_group", + "region", + "comment", + "is_org_admin", + "grace_period_in_days", }); err != nil { return diag.FromErr(err) } @@ -463,12 +520,12 @@ func ReadAccount(withExternalChangesMarking bool) schema.ReadContextFunc { // } // return "", nil //}), - attributeMappedValueReadOrNil(d, "is_org_admin", account.IsOrgAdmin, func(isOrgAdmin *bool) (string, error) { - if isOrgAdmin != nil { - return booleanStringFromBool(*isOrgAdmin), nil - } - return BooleanDefault, 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 { @@ -480,6 +537,16 @@ func ReadAccount(withExternalChangesMarking bool) schema.ReadContextFunc { } func UpdateAccount(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*provider.Context).Client + + isOrgAdmin, err := client.ContextFunctions.IsRoleInSession(ctx, snowflakeroles.Orgadmin) + if err != nil { + return diag.FromErr(err) + } + if !isOrgAdmin { + 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 @@ -506,12 +573,21 @@ func UpdateAccount(ctx context.Context, d *schema.ResourceData, meta any) diag.D func DeleteAccount(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*provider.Context).Client - id, err := sdk.ParseAccountObjectIdentifier(d.Id()) + + isOrgAdmin, err := client.ContextFunctions.IsRoleInSession(ctx, snowflakeroles.Orgadmin) + if err != nil { + return diag.FromErr(err) + } + if !isOrgAdmin { + return diag.FromErr(errors.New("current user doesn't have the orgadmin role in session")) + } + + id, err := sdk.ParseAccountIdentifier(d.Id()) if err != nil { return diag.FromErr(err) } - err = client.Accounts.Drop(ctx, id, d.Get("grace_period_in_days").(int), &sdk.DropAccountOptions{ + err = client.Accounts.Drop(ctx, sdk.NewAccountObjectIdentifier(id.AccountName()), 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 af7ff0de21..f945da6db4 100644 --- a/pkg/resources/account_acceptance_test.go +++ b/pkg/resources/account_acceptance_test.go @@ -3,6 +3,7 @@ package resources_test import ( "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" @@ -18,11 +19,86 @@ import ( "github.com/hashicorp/terraform-plugin-testing/tfversion" ) +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() + email := random.Email() + name := random.AdminName() + key, _ := random.GenerateRSAPublicKey(t) + region := acc.TestClient().Context.CurrentRegion(t) + + 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), + Check: assert.AssertThat(t, + resourceassert.AccountResource(t, configModel.ResourceReference()). + HasNameString(id). + HasFullyQualifiedNameString(sdk.NewAccountIdentifier(organizationName, id).FullyQualifiedName()). + HasAdminNameString(name). + HasAdminRsaPublicKeyString(key). + HasEmailString(email). + HasNoFirstName(). + HasNoLastName(). + HasMustChangePasswordString(r.BooleanDefault). + HasNoRegionGroup(). + HasNoRegion(). + HasNoComment(). + HasIsOrgAdminString(r.BooleanDefault). + HasGracePeriodInDaysString("3"), + resourceshowoutputassert.AccountShowOutput(t, configModel.ResourceReference()). + HasOrganizationName(organizationName). + HasAccountName(id). + HasSnowflakeRegion(region). + HasRegionGroup(""). + HasEdition(sdk.EditionStandard). + //HasAccountURL(). + //HasCreatedOn(). + HasComment("SNOWFLAKE"). + //HasAccountLocator(). + //HasAccountLocatorURL(). + HasManagedAccounts(0). + //HasConsumptionBillingEntityName(). + //HasMarketplaceConsumerBillingEntityName(). + //HasMarketplaceProviderBillingEntityName(). + //HasOldAccountURL(). + HasIsOrgAdmin(false). + //HasAccountOldUrlSavedOn(). + //HasAccountOldUrlLastUsed(). + //HasOrganizationOldUrl(). + //HasOrganizationOldUrlSavedOn(). + //HasOrganizationOldUrlLastUsed(). + HasIsEventsAccount(false). + HasIsOrganizationAccount(false), + //HasDroppedOn(). + //HasScheduledDeletionTime(). + //HasRestoredOn(). + //HasMovedToOrganization(). + //HasMovedOn(). + //HasOrganizationUrlExpirationOn(), + ), + }, + }, + }) +} + func TestAcc_Account_complete(t *testing.T) { _ = testenvs.GetOrSkipTest(t, testenvs.EnableAcceptance) _ = testenvs.GetOrSkipTest(t, testenvs.TestAccountCreate) - id := acc.TestClient().Ids.RandomAccountObjectIdentifier() + organizationName := acc.TestClient().Context.CurrentAccountId(t).OrganizationName() + id := random.AdminName() firstName := acc.TestClient().Ids.Alpha() lastName := acc.TestClient().Ids.Alpha() email := random.Email() @@ -31,21 +107,18 @@ func TestAcc_Account_complete(t *testing.T) { region := acc.TestClient().Context.CurrentRegion(t) comment := random.Comment() - configModel := model.Account("test", id.Name(), string(sdk.EditionStandard), email, name). - // TODO: WithAdminUserType() + configModel := model.Account("test", name, string(sdk.UserTypePerson), string(sdk.EditionStandard), email, 3, id). WithAdminRsaPublicKey(key). WithFirstName(firstName). WithLastName(lastName). - WithMustChangePassword(true). - WithRegionGroup("PUBLIC"). + WithMustChangePassword(r.BooleanTrue). + //WithRegionGroup("PUBLIC"). WithRegion(region). WithComment(comment). - WithIsOrgAdmin(true). - WithGracePeriodInDays(3) + WithIsOrgAdmin(r.BooleanFalse) resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: acc.TestAccProtoV6ProviderFactories, - PreCheck: func() { acc.TestAccPreCheck(t) }, TerraformVersionChecks: []tfversion.TerraformVersionCheck{ tfversion.RequireAbove(tfversion.Version1_5_0), }, @@ -55,42 +128,58 @@ func TestAcc_Account_complete(t *testing.T) { Config: config.FromModel(t, configModel), Check: assert.AssertThat(t, resourceassert.AccountResource(t, configModel.ResourceReference()). - HasNameString(id.Name()). - HasFullyQualifiedNameString(id.FullyQualifiedName()). + HasNameString(id). + HasFullyQualifiedNameString(sdk.NewAccountIdentifier(organizationName, id).FullyQualifiedName()). HasAdminNameString(name). HasAdminRsaPublicKeyString(key). HasEmailString(email). HasFirstNameString(firstName). HasLastNameString(lastName). HasMustChangePasswordString(r.BooleanTrue). - HasRegionGroupString("PUBLIC"). + HasNoRegionGroup(). // TODO HasRegionString(region). HasCommentString(comment). - HasIsOrgAdminString(r.BooleanTrue). + HasIsOrgAdminString(r.BooleanFalse). HasGracePeriodInDaysString("3"), - // TODO: Show output + resourceshowoutputassert.AccountShowOutput(t, configModel.ResourceReference()). + HasOrganizationName(organizationName). + HasAccountName(id). + HasSnowflakeRegion(region). + HasRegionGroup(""). + HasEdition(sdk.EditionStandard). + //HasAccountURL(). + //HasCreatedOn(). + HasComment(comment). + //HasAccountLocator(). + //HasAccountLocatorURL(). + HasManagedAccounts(0). + //HasConsumptionBillingEntityName(). + //HasMarketplaceConsumerBillingEntityName(). + //HasMarketplaceProviderBillingEntityName(). + //HasOldAccountURL(). + HasIsOrgAdmin(false). + //HasAccountOldUrlSavedOn(). + //HasAccountOldUrlLastUsed(). + //HasOrganizationOldUrl(). + //HasOrganizationOldUrlSavedOn(). + //HasOrganizationOldUrlLastUsed(). + HasIsEventsAccount(false). + HasIsOrganizationAccount(false), + //HasDroppedOn(). + //HasScheduledDeletionTime(). + //HasRestoredOn(). + //HasMovedToOrganization(). + //HasMovedOn(). + //HasOrganizationUrlExpirationOn(), ), }, }, }) } -//func accountConfig(name string, password string, comment string, gracePeriodInDays int) string { -// return fmt.Sprintf(` -//data "snowflake_current_account" "current" {} -// -//resource "snowflake_account" "test" { -// name = "%s" -// admin_name = "someadmin" -// admin_password = "%s" -// first_name = "Ad" -// last_name = "Min" -// email = "admin@example.com" -// must_change_password = false -// edition = "BUSINESS_CRITICAL" -// comment = "%s" -// region = data.snowflake_current_account.current.region -// grace_period_in_days = %d -//} -//`, name, password, comment, gracePeriodInDays) -//} +// 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/resource_helpers_read.go b/pkg/resources/resource_helpers_read.go index 72ee47daa6..b3dcfcebf1 100644 --- a/pkg/resources/resource_helpers_read.go +++ b/pkg/resources/resource_helpers_read.go @@ -50,17 +50,6 @@ func setBooleanStringFromBoolProperty(d *schema.ResourceData, key string, proper return nil } -func attributeMappedValueReadOrNil[T, R any](d *schema.ResourceData, key string, value *T, mapper func(*T) (R, error)) error { - if value != nil { - mappedValue, err := mapper(value) - if err != nil { - return err - } - return d.Set(key, mappedValue) - } - return d.Set(key, nil) -} - func attributeMappedValueReadOrDefault[T, R any](d *schema.ResourceData, key string, value *T, mapper func(*T) (R, error), defaultValue *R) error { if value != nil { mappedValue, err := mapper(value) diff --git a/pkg/sdk/type_helpers.go b/pkg/sdk/type_helpers.go index d1a13a57a3..e1c7034b9c 100644 --- a/pkg/sdk/type_helpers.go +++ b/pkg/sdk/type_helpers.go @@ -55,14 +55,3 @@ func ToFloat64(s string) float64 { func Pointer[K any](v K) *K { return &v } - -// TODO: Test -// DerefIfNotNil is a generic function that returns a dereferenced value if a given value is not nil -func DerefIfNotNil(v any) any { - if v != nil { - if pointerValue, ok := v.(*any); ok { - return *pointerValue - } - } - return nil -}