diff --git a/pkg/resources/managed_account.go b/pkg/resources/managed_account.go index 294bbdf547..b5114f06cc 100644 --- a/pkg/resources/managed_account.go +++ b/pkg/resources/managed_account.go @@ -1,14 +1,15 @@ package resources import ( + "context" "database/sql" - "errors" "fmt" - "log" "time" - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/snowflake" snowflakeValidation "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/validation" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) @@ -17,13 +18,6 @@ const ( SnowflakeReaderAccountType = "READER" ) -var managedAccountProperties = []string{ - "admin_name", - "admin_password", - "type", - "comment", -} - var managedAccountSchema = map[string]*schema.Schema{ "name": { Type: schema.TypeString, @@ -102,69 +96,82 @@ func ManagedAccount() *schema.Resource { // CreateManagedAccount implements schema.CreateFunc. func CreateManagedAccount(d *schema.ResourceData, meta interface{}) error { - return CreateResource( - "this does not seem to be used", - managedAccountProperties, - managedAccountSchema, - snowflake.NewManagedAccountBuilder, - initialReadManagedAccount, - )(d, meta) -} + db := meta.(*sql.DB) + ctx := context.Background() + client := sdk.NewClientFromDB(db) + + name := d.Get("name").(string) + id := sdk.NewAccountObjectIdentifier(name) + + adminName := d.Get("admin_name").(string) + adminPassword := d.Get("admin_password").(string) + createParams := sdk.NewCreateManagedAccountParamsRequest(adminName, adminPassword) + + if v, ok := d.GetOk("comment"); ok { + createParams.WithComment(sdk.String(v.(string))) + } + + createRequest := sdk.NewCreateManagedAccountRequest(id, *createParams) + + err := client.ManagedAccounts.Create(ctx, createRequest) + if err != nil { + return err + } + + d.SetId(helpers.EncodeSnowflakeID(id)) -// initialReadManagedAccount is used for the first read, since the locator takes -// some time to appear. This is currently implemented as a sleep. @TODO actually -// wait until the locator is generated. -func initialReadManagedAccount(d *schema.ResourceData, meta interface{}) error { - log.Println("[INFO] sleeping to give the locator a chance to be generated") - // lintignore:R018 - time.Sleep(10 * time.Second) return ReadManagedAccount(d, meta) } // ReadManagedAccount implements schema.ReadFunc. func ReadManagedAccount(d *schema.ResourceData, meta interface{}) error { db := meta.(*sql.DB) - id := d.Id() - - stmt := snowflake.NewManagedAccountBuilder(id).Show() - row := snowflake.QueryRow(db, stmt) - a, err := snowflake.ScanManagedAccount(row) - - if errors.Is(err, sql.ErrNoRows) { - // If not found, remove resource from - log.Printf("[DEBUG] managed account (%s) not found", d.Id()) - d.SetId("") - return nil - } + client := sdk.NewClientFromDB(db) + + ctx := context.Background() + objectIdentifier := helpers.DecodeSnowflakeID(d.Id()).(sdk.AccountObjectIdentifier) + + // We have to wait during the first read, since the locator takes some time to appear. + // This approach has a downside of not handling correctly the situation where managed account was removed externally. + // TODO [SNOW-1003380]: discuss it as a provider-wide topic during resources redesign. + var managedAccount *sdk.ManagedAccount + var err error + err = helpers.Retry(5, 3*time.Second, func() (error, bool) { + managedAccount, err = client.ManagedAccounts.ShowByID(ctx, objectIdentifier) + if err != nil { + return nil, false + } + return nil, true + }) if err != nil { return err } - if err := d.Set("name", a.Name.String); err != nil { + if err := d.Set("name", managedAccount.Name); err != nil { return err } - if err := d.Set("cloud", a.Cloud.String); err != nil { + if err := d.Set("cloud", managedAccount.Cloud); err != nil { return err } - if err := d.Set("region", a.Region.String); err != nil { + if err := d.Set("region", managedAccount.Region); err != nil { return err } - if err := d.Set("locator", a.Locator.String); err != nil { + if err := d.Set("locator", managedAccount.Locator); err != nil { return err } - if err := d.Set("created_on", a.CreatedOn.String); err != nil { + if err := d.Set("created_on", managedAccount.CreatedOn); err != nil { return err } - if err := d.Set("url", a.URL.String); err != nil { + if err := d.Set("url", managedAccount.URL); err != nil { return err } - if a.IsReader { + if managedAccount.IsReader { if err := d.Set("type", "READER"); err != nil { return err } @@ -172,12 +179,26 @@ func ReadManagedAccount(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("unable to determine the account type") } - err = d.Set("comment", a.Comment.String) + if err := d.Set("comment", managedAccount.Comment); err != nil { + return err + } - return err + return nil } // DeleteManagedAccount implements schema.DeleteFunc. func DeleteManagedAccount(d *schema.ResourceData, meta interface{}) error { - return DeleteResource("this does not seem to be used", snowflake.NewManagedAccountBuilder)(d, meta) + db := meta.(*sql.DB) + client := sdk.NewClientFromDB(db) + ctx := context.Background() + + objectIdentifier := helpers.DecodeSnowflakeID(d.Id()).(sdk.AccountObjectIdentifier) + + err := client.ManagedAccounts.Drop(ctx, sdk.NewDropManagedAccountRequest(objectIdentifier)) + if err != nil { + return err + } + + d.SetId("") + return nil } diff --git a/pkg/resources/managed_account_test.go b/pkg/resources/managed_account_test.go deleted file mode 100644 index b888c964cb..0000000000 --- a/pkg/resources/managed_account_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package resources_test - -import ( - "database/sql" - "testing" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/stretchr/testify/require" - - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider" - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/resources" - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/snowflake" - . "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/testhelpers" -) - -func TestManagedAccount(t *testing.T) { - r := require.New(t) - err := resources.ManagedAccount().InternalValidate(provider.Provider().Schema, true) - r.NoError(err) -} - -func TestManagedAccountCreate(t *testing.T) { - r := require.New(t) - - in := map[string]interface{}{ - "name": "test-account", - "admin_name": "bob", - "admin_password": "abc123ABC", - "comment": "great comment", - } - d := schema.TestResourceDataRaw(t, resources.ManagedAccount().Schema, in) - r.NotNil(d) - - WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) { - mock.ExpectExec(`^CREATE MANAGED ACCOUNT "test-account" ADMIN_NAME='bob' ADMIN_PASSWORD='abc123ABC' COMMENT='great comment' TYPE='READER'$`).WillReturnResult(sqlmock.NewResult(1, 1)) - expectReadManagedAccount(mock) - err := resources.CreateManagedAccount(d, db) - r.NoError(err) - }) -} - -func TestManagedAccountRead(t *testing.T) { - r := require.New(t) - d := managedAccount(t, "test-account", map[string]interface{}{"name": "test-account"}) - - WithMockDb(t, func(db *sql.DB, mock sqlmock.Sqlmock) { - // Test when resource is not found, checking if state will be empty - r.NotEmpty(d.State()) - q := snowflake.NewManagedAccountBuilder(d.Id()).Show() - mock.ExpectQuery(q).WillReturnError(sql.ErrNoRows) - err := resources.ReadManagedAccount(d, db) - - r.Empty(d.State()) - r.Nil(err) - }) -} - -func expectReadManagedAccount(mock sqlmock.Sqlmock) { - rows := sqlmock.NewRows([]string{ - "name", "cloud", "region", "locator", "created_on", "url", "is_reader", "comment", - }).AddRow("test-account", "aws", "ap-southeast-2", "locatorstring", "2019-01-01", "www.test.com", true, "great comment") - mock.ExpectQuery(`^SHOW MANAGED ACCOUNTS LIKE 'test-account'$`).WillReturnRows(rows) -} diff --git a/pkg/resources/resource.go b/pkg/resources/resource.go index 187baf0890..e72a73f231 100644 --- a/pkg/resources/resource.go +++ b/pkg/resources/resource.go @@ -3,125 +3,11 @@ package resources import ( "database/sql" "fmt" - "log" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/snowflake" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -func CreateResource( - t string, - properties []string, - s map[string]*schema.Schema, - builder func(string) *snowflake.Builder, - read func(*schema.ResourceData, interface{}) error, -) func(*schema.ResourceData, interface{}) error { - return func(d *schema.ResourceData, meta interface{}) error { - db := meta.(*sql.DB) - name := d.Get("name").(string) - - qb := builder(name).Create() - - for _, field := range properties { - val, ok := d.GetOk(field) - if ok { - switch s[field].Type { - case schema.TypeString: - valStr := val.(string) - qb.SetString(field, valStr) - case schema.TypeBool: - valBool := val.(bool) - qb.SetBool(field, valBool) - case schema.TypeInt: - valInt := val.(int) - qb.SetInt(field, valInt) - case schema.TypeSet: - valList := expandStringList(val.(*schema.Set).List()) - qb.SetStringList(field, valList) - } - } - } - if v, ok := d.GetOk("tag"); ok { - tags := getTags(v) - qb.SetTags(tags.toSnowflakeTagValues()) - } - if err := snowflake.Exec(db, qb.Statement()); err != nil { - return fmt.Errorf("error creating %s err = %w", t, err) - } - - d.SetId(name) - - return read(d, meta) - } -} - -func UpdateResource( - t string, - properties []string, - s map[string]*schema.Schema, - builder func(string) *snowflake.Builder, - read func(*schema.ResourceData, interface{}) error, -) func(*schema.ResourceData, interface{}) error { - return func(d *schema.ResourceData, meta interface{}) error { - db := meta.(*sql.DB) - if d.HasChange("name") { - // I wish this could be done on one line. - oldNameI, newNameI := d.GetChange("name") - oldName := oldNameI.(string) - newName := newNameI.(string) - - stmt := builder(oldName).Rename(newName) - - err := snowflake.Exec(db, stmt) - if err != nil { - return fmt.Errorf("error renaming %s %s to %s err = %w", t, oldName, newName, err) - } - d.SetId(newName) - } - - changes := []string{} - for _, prop := range properties { - if d.HasChange(prop) { - changes = append(changes, prop) - } - } - if len(changes) > 0 { - name := d.Get("name").(string) - qb := builder(name).Alter() - - for _, field := range changes { - val := d.Get(field) - switch s[field].Type { - case schema.TypeString: - valStr := val.(string) - qb.SetString(field, valStr) - case schema.TypeBool: - valBool := val.(bool) - qb.SetBool(field, valBool) - case schema.TypeInt: - valInt := val.(int) - qb.SetInt(field, valInt) - case schema.TypeSet: - valList := expandStringList(val.(*schema.Set).List()) - qb.SetStringList(field, valList) - } - } - if d.HasChange("tag") { - log.Println("[DEBUG] updating tags") - v := d.Get("tag") - tags := getTags(v) - qb.SetTags(tags.toSnowflakeTagValues()) - } - - if err := snowflake.Exec(db, qb.Statement()); err != nil { - return fmt.Errorf("error altering %s err = %w", t, err) - } - } - log.Println("[DEBUG] performing read") - return read(d, meta) - } -} - func DeleteResource(t string, builder func(string) *snowflake.Builder) func(*schema.ResourceData, interface{}) error { return func(d *schema.ResourceData, meta interface{}) error { db := meta.(*sql.DB) diff --git a/pkg/snowflake/managed_account.go b/pkg/snowflake/managed_account.go deleted file mode 100644 index 017f2685f5..0000000000 --- a/pkg/snowflake/managed_account.go +++ /dev/null @@ -1,40 +0,0 @@ -package snowflake - -import ( - "database/sql" - - "github.com/jmoiron/sqlx" -) - -// NewManagedAccountBuilder returns a pointer to a Builder that abstracts the DDL -// operations for a reader account. -// -// Supported DDL operations are: -// - CREATE MANAGED ACCOUNT -// - DROP MANAGED ACCOUNT -// - SHOW MANAGED ACCOUNTS -// -// [Snowflake Reference](https://docs.snowflake.net/manuals/user-guide/data-sharing-reader-create.html) -func NewManagedAccountBuilder(name string) *Builder { - return &Builder{ - entityType: ManagedAccountType, - name: name, - } -} - -type ManagedAccount struct { - Name sql.NullString `db:"name"` - Cloud sql.NullString `db:"cloud"` - Region sql.NullString `db:"region"` - Locator sql.NullString `db:"locator"` - CreatedOn sql.NullString `db:"created_on"` - URL sql.NullString `db:"url"` - Comment sql.NullString `db:"comment"` - IsReader bool `db:"is_reader"` -} - -func ScanManagedAccount(row *sqlx.Row) (*ManagedAccount, error) { - a := &ManagedAccount{} - e := row.StructScan(a) - return a, e -} diff --git a/pkg/snowflake/managed_account_test.go b/pkg/snowflake/managed_account_test.go deleted file mode 100644 index 4213633c36..0000000000 --- a/pkg/snowflake/managed_account_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package snowflake_test - -import ( - "testing" - - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/snowflake" - "github.com/stretchr/testify/require" -) - -func TestManagedAccount(t *testing.T) { - r := require.New(t) - u := snowflake.NewManagedAccountBuilder("managedaccount1") - r.NotNil(u) - - q := u.Show() - r.Equal("SHOW MANAGED ACCOUNTS LIKE 'managedaccount1'", q) - - q = u.Drop() - r.Equal(`DROP MANAGED ACCOUNT "managedaccount1"`, q) - - c := u.Create() - c.SetString("foo", "bar") - c.SetBool("bam", false) - q = c.Statement() - r.Equal(`CREATE MANAGED ACCOUNT "managedaccount1" FOO='bar' BAM=false`, q) -}