From fcf03800cf92e1acff7f89e59968914f1bcad4f4 Mon Sep 17 00:00:00 2001 From: Piotr Bosak Date: Thu, 6 Jul 2023 00:15:20 +0200 Subject: [PATCH] feat: alerts sdk v2 (#1871) * feat: alerts sdk v2 * fixes after CI/CD * fixes formatting * review fixes * fixes: formatting * fixes: rename * review fixes * ci fixes * format code * ci fixes * golang ci * fix failing tests * fix tests * fix tests * add test * fix tests * remove condition * rename alert state and alert action constants * fixes after rename * fixes compilation errors --------- Co-authored-by: Scott Winkler --- pkg/resources/alert.go | 300 ++++++++------------ pkg/sdk/alerts.go | 382 ++++++++++++++++++++++++++ pkg/sdk/alerts_integration_test.go | 423 +++++++++++++++++++++++++++++ pkg/sdk/alerts_test.go | 293 ++++++++++++++++++++ pkg/sdk/client.go | 2 + pkg/sdk/helper_test.go | 55 ++++ pkg/sdk/object_types.go | 1 + pkg/snowflake/alert.go | 39 ++- 8 files changed, 1283 insertions(+), 212 deletions(-) create mode 100644 pkg/sdk/alerts.go create mode 100644 pkg/sdk/alerts_integration_test.go create mode 100644 pkg/sdk/alerts_test.go diff --git a/pkg/resources/alert.go b/pkg/resources/alert.go index 83d5535872..e4060bb0cc 100644 --- a/pkg/resources/alert.go +++ b/pkg/resources/alert.go @@ -1,23 +1,19 @@ package resources import ( - "bytes" + "context" "database/sql" - "encoding/csv" - "errors" "fmt" "log" "strconv" "strings" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/helpers" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/snowflake" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -const ( - alertIDDelimiter = '|' -) - var alertSchema = map[string]*schema.Schema{ "name": { Type: schema.TypeString, @@ -107,51 +103,6 @@ var alertSchema = map[string]*schema.Schema{ }, } -type alertID struct { - DatabaseName string - SchemaName string - AlertName string -} - -// String() takes in a alertID object and returns a pipe-delimited string: -// DatabaseName|SchemaName|AlertName. -func (aId *alertID) String() (string, error) { - var buf bytes.Buffer - csvWriter := csv.NewWriter(&buf) - csvWriter.Comma = alertIDDelimiter - dataIdentifiers := [][]string{{aId.DatabaseName, aId.SchemaName, aId.AlertName}} - if err := csvWriter.WriteAll(dataIdentifiers); err != nil { - return "", err - } - strAlertID := strings.TrimSpace(buf.String()) - return strAlertID, nil -} - -// alertIDFromString() takes in a pipe-delimited string: DatabaseName|SchemaName|AlertName -// and returns a alertID object. -func alertIDFromString(stringID string) (*alertID, error) { - reader := csv.NewReader(strings.NewReader(stringID)) - reader.Comma = pipeIDDelimiter - lines, err := reader.ReadAll() - if err != nil { - return nil, fmt.Errorf("not CSV compatible") - } - - if len(lines) != 1 { - return nil, fmt.Errorf("1 line per alert") - } - if len(lines[0]) != 3 { - return nil, fmt.Errorf("3 fields allowed") - } - - alertResult := &alertID{ - DatabaseName: lines[0][0], - SchemaName: lines[0][1], - AlertName: lines[0][2], - } - return alertResult, nil -} - // Alert returns a pointer to the resource representing an alert. func Alert() *schema.Resource { return &schema.Resource{ @@ -170,20 +121,12 @@ func Alert() *schema.Resource { // ReadAlert implements schema.ReadFunc. func ReadAlert(d *schema.ResourceData, meta interface{}) error { db := meta.(*sql.DB) - alertID, err := alertIDFromString(d.Id()) - if err != nil { - return err - } + client := sdk.NewClientFromDB(db) + objectIdentifier := helpers.DecodeSnowflakeID(d.Id()).(sdk.SchemaObjectIdentifier) - database := alertID.DatabaseName - SchemaName := alertID.SchemaName - name := alertID.AlertName - - builder := snowflake.NewAlertBuilder(name, database, SchemaName) - qry := builder.Show() - row := snowflake.QueryRow(db, qry) - alert, err := snowflake.ScanAlert(row) - if errors.Is(err, sql.ErrNoRows) { + ctx := context.Background() + alert, err := client.Alerts.ShowByID(ctx, objectIdentifier) + if err != nil { // If not found, mark resource to be removed from state file during apply or refresh log.Printf("[DEBUG] alert (%s) not found", d.Id()) d.SetId("") @@ -193,7 +136,7 @@ func ReadAlert(d *schema.ResourceData, meta interface{}) error { return err } - if err := d.Set("enabled", alert.IsEnabled()); err != nil { + if err := d.Set("enabled", alert.State == sdk.AlertStateStarted); err != nil { return err } @@ -205,14 +148,6 @@ func ReadAlert(d *schema.ResourceData, meta interface{}) error { return err } - if err := d.Set("schema", alert.SchemaName); err != nil { - return err - } - - if err := d.Set("warehouse", alert.Warehouse); err != nil { - return err - } - alertSchedule := alert.Schedule if alertSchedule != "" { if strings.Contains(alertSchedule, "MINUTE") { @@ -248,6 +183,14 @@ func ReadAlert(d *schema.ResourceData, meta interface{}) error { } } + if err := d.Set("schema", alert.SchemaName); err != nil { + return err + } + + if err := d.Set("warehouse", alert.Warehouse); err != nil { + return err + } + if err := d.Set("comment", alert.Comment); err != nil { return err } @@ -264,169 +207,149 @@ func ReadAlert(d *schema.ResourceData, meta interface{}) error { // CreateAlert implements schema.CreateFunc. func CreateAlert(d *schema.ResourceData, meta interface{}) error { - var err error db := meta.(*sql.DB) + client := sdk.NewClientFromDB(db) - database := d.Get("database").(string) + databaseName := d.Get("database").(string) schemaName := d.Get("schema").(string) name := d.Get("name").(string) - builder := snowflake.NewAlertBuilder(name, database, schemaName) - - if v, ok := d.GetOk("alert_schedule"); ok { - alertSchedule := v.([]interface{})[0].(map[string]interface{}) - if v, ok := alertSchedule["cron"]; ok { - c := v.([]interface{}) - if len(c) > 0 { - cron := c[0].(map[string]interface{}) - cronExpression := cron["expression"].(string) - builder.WithAlertScheduleCronExpression(cronExpression) - if v, ok := cron["time_zone"]; ok { - timeZone := v.(string) - builder.WithAlertScheduleTimeZone(timeZone) - } - } - } - if v, ok := alertSchedule["interval"]; ok { - interval := v.(int) - if interval > 0 { - builder.WithAlertScheduleInterval(interval) - } - } - } + ctx := context.Background() + objectIdentifier := sdk.NewSchemaObjectIdentifier(databaseName, schemaName, name) - enabled := d.Get("enabled").(bool) + alertSchedule := getAlertSchedule(d.Get("alert_schedule")) - warehouse := d.Get("warehouse").(string) - builder.WithWarehouse(warehouse) + warehouseName := d.Get("warehouse").(string) + warehouse := sdk.NewAccountObjectIdentifier(warehouseName) + + opts := &sdk.CreateAlertOptions{} if v, ok := d.GetOk("comment"); ok { - builder.WithComment(v.(string)) + opts.Comment = sdk.String(v.(string)) } condition := d.Get("condition").(string) - builder.WithCondition(condition) action := d.Get("action").(string) - builder.WithAction(action) - - q := builder.Create() - if err := snowflake.Exec(db, q); err != nil { - return fmt.Errorf("error creating alert %v err = %w", name, err) - } - alertID := &alertID{ - DatabaseName: database, - SchemaName: schemaName, - AlertName: name, - } - dataIDInput, err := alertID.String() + err := client.Alerts.Create(ctx, objectIdentifier, warehouse, alertSchedule, condition, action, opts) if err != nil { return err } - d.SetId(dataIDInput) + + enabled := d.Get("enabled").(bool) if enabled { - if err := snowflake.WaitResumeAlert(db, name, database, schemaName); err != nil { - log.Printf("[WARN] failed to resume alert %s", name) + opts := sdk.AlterAlertOptions{Action: &sdk.AlertActionResume} + err := client.Alerts.Alter(ctx, objectIdentifier, &opts) + if err != nil { + return err } } + d.SetId(helpers.EncodeSnowflakeID(objectIdentifier)) + return ReadAlert(d, meta) } -// UpdateAlert implements schema.UpdateFunc. -func UpdateAlert(d *schema.ResourceData, meta interface{}) error { - alertID, err := alertIDFromString(d.Id()) - if err != nil { - return err +func getAlertSchedule(v interface{}) string { + var alertSchedule string + schedule := v.([]interface{})[0].(map[string]interface{}) + if v, ok := schedule["cron"]; ok { + c := v.([]interface{}) + if len(c) > 0 { + cron := c[0].(map[string]interface{}) + cronExpression := cron["expression"].(string) + timeZone := cron["time_zone"].(string) + alertSchedule = fmt.Sprintf("USING CRON %s %s", cronExpression, timeZone) + } + } + if v, ok := schedule["interval"]; ok { + interval := v.(int) + if interval > 0 { + alertSchedule = fmt.Sprintf("%s MINUTE", strconv.Itoa(interval)) + } } + return alertSchedule +} +// UpdateAlert implements schema.UpdateFunc. +func UpdateAlert(d *schema.ResourceData, meta interface{}) error { db := meta.(*sql.DB) - database := alertID.DatabaseName - schemaName := alertID.SchemaName - name := alertID.AlertName - builder := snowflake.NewAlertBuilder(name, database, schemaName) + client := sdk.NewClientFromDB(db) + objectIdentifier := helpers.DecodeSnowflakeID(d.Id()).(sdk.SchemaObjectIdentifier) + ctx := context.Background() enabled := d.Get("enabled").(bool) if d.HasChanges("enabled", "warehouse", "alert_schedule", "condition", "action", "comment") { - if err := snowflake.WaitSuspendAlert(db, name, database, schemaName); err != nil { - log.Printf("[WARN] failed to suspend alert %s", name) + if err := snowflake.WaitSuspendAlert(ctx, client, objectIdentifier); err != nil { + log.Printf("[WARN] failed to suspend alert %s", objectIdentifier.Name()) } } - if d.HasChange("warehouse") { - var q string - newWarehouse := d.Get("warehouse") - q = builder.ChangeWarehouse(newWarehouse.(string)) + opts := &sdk.AlterAlertOptions{ + Set: &sdk.AlertSet{}, + Unset: &sdk.AlertUnset{}, + } + runSetStatement := false - if err := snowflake.Exec(db, q); err != nil { - return fmt.Errorf("error updating warehouse on alert %v", d.Id()) - } + if d.HasChange("warehouse") { + runSetStatement = true + _, v := d.GetChange("warehouse") + warehouseName := v.(string) + warehouse := sdk.NewAccountObjectIdentifier(warehouseName) + opts.Set.Warehouse = &warehouse } if d.HasChange("alert_schedule") { - _, n := d.GetChange("alert_schedule") - alertSchedule := n.([]interface{})[0].(map[string]interface{}) - log.Printf("[DEBUG] alertSchedule: %v", alertSchedule) - log.Printf("[DEBUG] alertSchedule[cron]: %v", alertSchedule["cron"]) - c := alertSchedule["cron"].([]interface{}) - if len(c) > 0 { - if len(c) > 0 { - cron := c[0].(map[string]interface{}) - cronExpression := cron["expression"].(string) - - timeZone := "" - if v, ok := cron["time_zone"]; ok { - timeZone = v.(string) - } - stmt := builder.ChangeAlertCronSchedule(cronExpression, timeZone) - if err := snowflake.Exec(db, stmt); err != nil { - return fmt.Errorf("error updating alert cron schedule %v err = %w", name, err) - } - } - } else { - log.Printf("[DEBUG] alertSchedule[interval]: %v", alertSchedule["interval"]) - interval := alertSchedule["interval"].(int) - stmt := builder.ChangeAlertIntervalSchedule(interval) - if err := snowflake.Exec(db, stmt); err != nil { - return fmt.Errorf("error updating alert interval schedule %v err = %w", name, err) - } - } + runSetStatement = true + _, v := d.GetChange("alert_schedule") + alertSchedule := getAlertSchedule(v) + opts.Set.Schedule = &alertSchedule } if d.HasChange("comment") { - var q string - _, newVal := d.GetChange("comment") - q = builder.ChangeComment(newVal.(string)) - if err := snowflake.Exec(db, q); err != nil { - return fmt.Errorf("error updating comment on alert %v", d.Id()) + _, v := d.GetChange("comment") + runSetStatement = true + newComment := v.(string) + opts.Set.Comment = &newComment + } + + if runSetStatement { + setOptions := &sdk.AlterAlertOptions{Set: opts.Set} + err := client.Alerts.Alter(ctx, objectIdentifier, setOptions) + if err != nil { + return fmt.Errorf("error updating alert %v: %w", objectIdentifier.Name(), err) } } if d.HasChange("condition") { - newVal := d.Get("condition") - q := builder.ChangeCondition(newVal.(string)) - if err := snowflake.Exec(db, q); err != nil { - return fmt.Errorf("error updating condition on alert %v", d.Id()) + condition := d.Get("condition").(string) + alterOptions := &sdk.AlterAlertOptions{} + alterOptions.ModifyCondition = &[]string{condition} + err := client.Alerts.Alter(ctx, objectIdentifier, alterOptions) + if err != nil { + return fmt.Errorf("error updating schedule on condition %v: %w", objectIdentifier.Name(), err) } } if d.HasChange("action") { - newVal := d.Get("action") - q := builder.ChangeAction(newVal.(string)) - if err := snowflake.Exec(db, q); err != nil { - return fmt.Errorf("error updating action on alert %v", d.Id()) + action := d.Get("action").(string) + alterOptions := &sdk.AlterAlertOptions{} + alterOptions.ModifyAction = &action + err := client.Alerts.Alter(ctx, objectIdentifier, alterOptions) + if err != nil { + return fmt.Errorf("error updating schedule on action %v: %w", objectIdentifier.Name(), err) } } if enabled { - if err := snowflake.WaitResumeAlert(db, name, database, schemaName); err != nil { - log.Printf("[WARN] failed to resume alert %s", name) + if err := snowflake.WaitResumeAlert(ctx, client, objectIdentifier); err != nil { + log.Printf("[WARN] failed to resume alert %s", objectIdentifier.Name()) } } else { - if err := snowflake.WaitSuspendAlert(db, name, database, schemaName); err != nil { - log.Printf("[WARN] failed to suspend alert %s", name) + if err := snowflake.WaitSuspendAlert(ctx, client, objectIdentifier); err != nil { + log.Printf("[WARN] failed to suspend alert %s", objectIdentifier.Name()) } } return ReadAlert(d, meta) @@ -435,20 +358,15 @@ func UpdateAlert(d *schema.ResourceData, meta interface{}) error { // DeleteAlert implements schema.DeleteFunc. func DeleteAlert(d *schema.ResourceData, meta interface{}) error { db := meta.(*sql.DB) - alterID, err := alertIDFromString(d.Id()) + client := sdk.NewClientFromDB(db) + ctx := context.Background() + objectIdentifier := helpers.DecodeSnowflakeID(d.Id()).(sdk.SchemaObjectIdentifier) + + err := client.Alerts.Drop(ctx, objectIdentifier) if err != nil { return err } - database := alterID.DatabaseName - schemaName := alterID.SchemaName - name := alterID.AlertName - - q := snowflake.NewAlertBuilder(name, database, schemaName).Drop() - if err := snowflake.Exec(db, q); err != nil { - return fmt.Errorf("error deleting alert %v err = %w", d.Id(), err) - } - d.SetId("") return nil } diff --git a/pkg/sdk/alerts.go b/pkg/sdk/alerts.go new file mode 100644 index 0000000000..3c42ac9bfd --- /dev/null +++ b/pkg/sdk/alerts.go @@ -0,0 +1,382 @@ +package sdk + +import ( + "context" + "errors" + "fmt" + "time" +) + +// Compile-time proof of interface implementation. +var _ Alerts = (*alerts)(nil) + +type Alerts interface { + // Create creates a new alert. + Create(ctx context.Context, id SchemaObjectIdentifier, warehouse AccountObjectIdentifier, schedule string, condition string, action string, opts *CreateAlertOptions) error + // Alter modifies an existing alert. + Alter(ctx context.Context, id SchemaObjectIdentifier, opts *AlterAlertOptions) error + // Drop removes an alert. + Drop(ctx context.Context, id SchemaObjectIdentifier) error + // Show returns a list of alerts + Show(ctx context.Context, opts *ShowAlertOptions) ([]*Alert, error) + // ShowByID returns an alert by ID + ShowByID(ctx context.Context, id SchemaObjectIdentifier) (*Alert, error) + // Describe returns the details of an alert. + Describe(ctx context.Context, id SchemaObjectIdentifier) (*AlertDetails, error) +} + +// alerts implements Alerts +type alerts struct { + client *Client +} + +type CreateAlertOptions struct { + create bool `ddl:"static" sql:"CREATE"` //lint:ignore U1000 This is used in the ddl tag + OrReplace *bool `ddl:"keyword" sql:"OR REPLACE"` + alert bool `ddl:"static" sql:"ALERT"` //lint:ignore U1000 This is used in the ddl tag + IfNotExists *bool `ddl:"keyword" sql:"IF NOT EXISTS"` + name SchemaObjectIdentifier `ddl:"identifier"` + + // required + warehouse AccountObjectIdentifier `ddl:"identifier,equals" sql:"WAREHOUSE"` + schedule string `ddl:"parameter,single_quotes" sql:"SCHEDULE"` + + // optional + Comment *string `ddl:"parameter,single_quotes" sql:"COMMENT"` + + // required + condition []AlertCondition `ddl:"keyword,parentheses,no_comma" sql:"IF"` + action string `ddl:"parameter,no_equals" sql:"THEN"` +} + +type AlertCondition struct { + Condition []string `ddl:"keyword,parentheses,no_comma" sql:"EXISTS"` +} + +func (opts *CreateAlertOptions) validate() error { + if !validObjectidentifier(opts.name) { + return errors.New("invalid object identifier") + } + + return nil +} + +func (v *alerts) Create(ctx context.Context, id SchemaObjectIdentifier, warehouse AccountObjectIdentifier, schedule string, condition string, action string, opts *CreateAlertOptions) error { + if opts == nil { + opts = &CreateAlertOptions{} + } + opts.name = id + opts.warehouse = warehouse + opts.schedule = schedule + opts.name = id + opts.condition = []AlertCondition{{Condition: []string{condition}}} + opts.action = action + if err := opts.validate(); err != nil { + return err + } + sql, err := structToSQL(opts) + if err != nil { + return err + } + _, err = v.client.exec(ctx, sql) + return err +} + +type AlertAction string + +var ( + // AlertActionResume makes a suspended alert active. + AlertActionResume AlertAction = "RESUME" + // AlertActionSuspend puts the alert into a “Suspended” state. + AlertActionSuspend AlertAction = "SUSPEND" +) + +type AlertState string + +var ( + AlertStateStarted AlertState = "started" + AlertStateSuspended AlertState = "suspended" +) + +type AlterAlertOptions struct { + alter bool `ddl:"static" sql:"ALTER"` //lint:ignore U1000 This is used in the ddl tag + alert bool `ddl:"static" sql:"ALERT"` //lint:ignore U1000 This is used in the ddl tag + IfExists *bool `ddl:"keyword" sql:"IF EXISTS"` + name SchemaObjectIdentifier `ddl:"identifier"` + + // One of + Action *AlertAction `ddl:"keyword"` + Set *AlertSet `ddl:"keyword" sql:"SET"` + Unset *AlertUnset `ddl:"keyword" sql:"UNSET"` + ModifyCondition *[]string `ddl:"keyword,parentheses,no_comma" sql:"MODIFY CONDITION EXISTS"` + ModifyAction *string `ddl:"parameter,no_equals" sql:"MODIFY ACTION"` +} + +func (opts *AlterAlertOptions) validate() error { + if !validObjectidentifier(opts.name) { + return errors.New("invalid object identifier") + } + + if everyValueNil(opts.Action, opts.Set, opts.Unset, opts.ModifyCondition, opts.ModifyAction) { + return errors.New("No alter action specified") + } + if !exactlyOneValueSet(opts.Action, opts.Set, opts.Unset, opts.ModifyCondition, opts.ModifyAction) { + return errors.New(` + Only one of the following actions can be performed at a time: + { + RESUME | SUSPEND, + SET, + UNSET, + MODIFY CONDITION EXISTS, + MODIFY ACTION + } + `) + } + + return nil +} + +type AlertSet struct { + Warehouse *AccountObjectIdentifier `ddl:"identifier,equals" sql:"WAREHOUSE"` + Schedule *string `ddl:"parameter,single_quotes" sql:"SCHEDULE"` + Comment *string `ddl:"parameter,single_quotes" sql:"COMMENT"` +} + +type AlertUnset struct { + Warehouse *bool `ddl:"keyword" sql:"WAREHOUSE"` + Schedule *bool `ddl:"keyword" sql:"SCHEDULE"` + Comment *bool `ddl:"keyword" sql:"COMMENT"` +} + +func (v *alerts) Alter(ctx context.Context, id SchemaObjectIdentifier, opts *AlterAlertOptions) error { + if opts == nil { + return errors.New("alter alert options cannot be empty") + } + + opts.name = id + if err := opts.validate(); err != nil { + return err + } + sql, err := structToSQL(opts) + if err != nil { + return err + } + _, err = v.client.exec(ctx, sql) + return err +} + +type dropAlertOptions struct { + drop bool `ddl:"static" sql:"DROP"` //lint:ignore U1000 This is used in the ddl tag + alert bool `ddl:"static" sql:"ALERT"` //lint:ignore U1000 This is used in the ddl tag + name SchemaObjectIdentifier `ddl:"identifier"` +} + +func (opts *dropAlertOptions) validate() error { + if !validObjectidentifier(opts.name) { + return ErrInvalidObjectIdentifier + } + return nil +} + +func (v *alerts) Drop(ctx context.Context, id SchemaObjectIdentifier) error { + // alert drop does not support [IF EXISTS] so there are no drop options. + opts := &dropAlertOptions{ + name: id, + } + if err := opts.validate(); err != nil { + return fmt.Errorf("validate alert options: %w", err) + } + sql, err := structToSQL(opts) + if err != nil { + return err + } + _, err = v.client.exec(ctx, sql) + if err != nil { + return err + } + return err +} + +type ShowAlertOptions struct { + show bool `ddl:"static" sql:"SHOW"` //lint:ignore U1000 This is used in the ddl tag + Terse *bool `ddl:"keyword" sql:"TERSE"` + alerts bool `ddl:"static" sql:"ALERTS"` //lint:ignore U1000 This is used in the ddl tag + + // optional + Like *Like `ddl:"keyword" sql:"LIKE"` + In *In `ddl:"keyword" sql:"IN"` + StartsWith *string `ddl:"parameter,no_equals,single_quotes" sql:"STARTS WITH"` + Limit *int `ddl:"parameter,no_equals" sql:"LIMIT"` +} + +func (v *Alert) ID() SchemaObjectIdentifier { + return NewSchemaObjectIdentifier(v.DatabaseName, v.SchemaName, v.Name) +} + +func (v *Alert) ObjectType() ObjectType { + return ObjectTypeAlert +} + +type Alert struct { + CreatedOn time.Time + Name string + DatabaseName string + SchemaName string + Owner string + Comment *string + Warehouse string + Schedule string + State AlertState + Condition string + Action string +} + +type alertDBRow struct { + CreatedOn time.Time `db:"created_on"` + Name string `db:"name"` + DatabaseName string `db:"database_name"` + SchemaName string `db:"schema_name"` + Owner string `db:"owner"` + Comment *string `db:"comment"` + Warehouse string `db:"warehouse"` + Schedule string `db:"schedule"` + State string `db:"state"` // suspended, started + Condition string `db:"condition"` + Action string `db:"action"` +} + +func (row alertDBRow) toAlert() (*Alert, error) { + return &Alert{ + CreatedOn: row.CreatedOn, + Name: row.Name, + DatabaseName: row.DatabaseName, + SchemaName: row.SchemaName, + Owner: row.Owner, + Comment: row.Comment, + Warehouse: row.Warehouse, + Schedule: row.Schedule, + State: AlertState(row.State), + Condition: row.Condition, + Action: row.Action, + }, nil +} + +func (opts *ShowAlertOptions) validate() error { + return nil +} + +func (v *alerts) Show(ctx context.Context, opts *ShowAlertOptions) ([]*Alert, error) { + if opts == nil { + opts = &ShowAlertOptions{} + } + if err := opts.validate(); err != nil { + return nil, err + } + sql, err := structToSQL(opts) + if err != nil { + return nil, err + } + dest := []alertDBRow{} + + err = v.client.query(ctx, &dest, sql) + if err != nil { + return nil, err + } + resultList := make([]*Alert, len(dest)) + for i, row := range dest { + alert, err := row.toAlert() + if err != nil { + return nil, err + } + resultList[i] = alert + } + + return resultList, nil +} + +func (v *alerts) ShowByID(ctx context.Context, id SchemaObjectIdentifier) (*Alert, error) { + alerts, err := v.Show(ctx, &ShowAlertOptions{ + Like: &Like{ + Pattern: String(id.Name()), + }, + In: &In{ + Schema: NewSchemaIdentifier(id.DatabaseName(), id.SchemaName()), + }, + }) + if err != nil { + return nil, err + } + + for _, alert := range alerts { + if alert.ID().name == id.Name() { + return alert, nil + } + } + return nil, ErrObjectNotExistOrAuthorized +} + +type describeAlertOptions struct { + describe bool `ddl:"static" sql:"DESCRIBE"` //lint:ignore U1000 This is used in the ddl tag + alert bool `ddl:"static" sql:"ALERT"` //lint:ignore U1000 This is used in the ddl tag + name SchemaObjectIdentifier `ddl:"identifier"` +} + +func (v *describeAlertOptions) validate() error { + if !validObjectidentifier(v.name) { + return ErrInvalidObjectIdentifier + } + return nil +} + +type AlertDetails struct { + CreatedOn time.Time + Name string + DatabaseName string + SchemaName string + Owner string + Comment *string + Warehouse string + Schedule string + State string + Condition string + Action string +} + +func (row alertDBRow) toAlertDetails() (*AlertDetails, error) { + return &AlertDetails{ + CreatedOn: row.CreatedOn, + Name: row.Name, + DatabaseName: row.DatabaseName, + SchemaName: row.SchemaName, + Owner: row.Owner, + Comment: row.Comment, + Warehouse: row.Warehouse, + Schedule: row.Schedule, + State: row.State, + Condition: row.Condition, + Action: row.Action, + }, nil +} + +func (v *alerts) Describe(ctx context.Context, id SchemaObjectIdentifier) (*AlertDetails, error) { + opts := &describeAlertOptions{ + name: id, + } + if err := opts.validate(); err != nil { + return nil, err + } + + sql, err := structToSQL(opts) + if err != nil { + return nil, err + } + + // SHOW ALERTS and DESCRIBE ALERT SQL statements return the same output + dest := alertDBRow{} + err = v.client.queryOne(ctx, &dest, sql) + if err != nil { + return nil, err + } + + return dest.toAlertDetails() +} diff --git a/pkg/sdk/alerts_integration_test.go b/pkg/sdk/alerts_integration_test.go new file mode 100644 index 0000000000..6f05c09a4a --- /dev/null +++ b/pkg/sdk/alerts_integration_test.go @@ -0,0 +1,423 @@ +package sdk + +import ( + "context" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestInt_AlertsShow(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + databaseTest, databaseCleanup := createDatabase(t, client) + t.Cleanup(databaseCleanup) + + schemaTest, schemaCleanup := createSchema(t, client, databaseTest) + t.Cleanup(schemaCleanup) + + testWarehouse, warehouseCleanup := createWarehouse(t, client) + t.Cleanup(warehouseCleanup) + + alertTest, alertCleanup := createAlert(t, client, databaseTest, schemaTest, testWarehouse) + t.Cleanup(alertCleanup) + + alert2Test, alert2Cleanup := createAlert(t, client, databaseTest, schemaTest, testWarehouse) + t.Cleanup(alert2Cleanup) + + t.Run("without show options", func(t *testing.T) { + alerts, err := client.Alerts.Show(ctx, nil) + require.NoError(t, err) + assert.Equal(t, 2, len(alerts)) + }) + + t.Run("with show options", func(t *testing.T) { + showOptions := &ShowAlertOptions{ + In: &In{ + Schema: schemaTest.ID(), + }, + } + alerts, err := client.Alerts.Show(ctx, showOptions) + require.NoError(t, err) + assert.Contains(t, alerts, alertTest) + assert.Contains(t, alerts, alert2Test) + assert.Equal(t, 2, len(alerts)) + }) + + t.Run("with show options and like", func(t *testing.T) { + showOptions := &ShowAlertOptions{ + Like: &Like{ + Pattern: String(alertTest.Name), + }, + In: &In{ + Database: databaseTest.ID(), + }, + } + alerts, err := client.Alerts.Show(ctx, showOptions) + require.NoError(t, err) + assert.Contains(t, alerts, alertTest) + assert.Equal(t, 1, len(alerts)) + }) + + t.Run("when searching a non-existent alert", func(t *testing.T) { + showOptions := &ShowAlertOptions{ + Like: &Like{ + Pattern: String("non-existent"), + }, + } + alerts, err := client.Alerts.Show(ctx, showOptions) + require.NoError(t, err) + assert.Equal(t, 0, len(alerts)) + }) + + t.Run("when limiting the number of results", func(t *testing.T) { + showOptions := &ShowAlertOptions{ + In: &In{ + Schema: schemaTest.ID(), + }, + Limit: Int(1), + } + alerts, err := client.Alerts.Show(ctx, showOptions) + require.NoError(t, err) + assert.Equal(t, 1, len(alerts)) + }) +} + +func TestInt_AlertCreate(t *testing.T) { + client := testClient(t) + ctx := context.Background() + databaseTest, databaseCleanup := createDatabase(t, client) + t.Cleanup(databaseCleanup) + + schemaTest, schemaCleanup := createSchema(t, client, databaseTest) + t.Cleanup(schemaCleanup) + + testWarehouse, warehouseCleanup := createWarehouse(t, client) + t.Cleanup(warehouseCleanup) + + t.Run("test complete case", func(t *testing.T) { + name := randomString(t) + schedule := "USING CRON * * * * TUE,THU UTC" + condition := "SELECT 1" + action := "SELECT 1" + comment := randomComment(t) + id := NewSchemaObjectIdentifier(databaseTest.Name, schemaTest.Name, name) + err := client.Alerts.Create(ctx, id, testWarehouse.ID(), schedule, condition, action, &CreateAlertOptions{ + OrReplace: Bool(true), + IfNotExists: Bool(false), + Comment: String(comment), + }) + require.NoError(t, err) + alertDetails, err := client.Alerts.Describe(ctx, id) + require.NoError(t, err) + assert.Equal(t, name, alertDetails.Name) + assert.Equal(t, testWarehouse.Name, alertDetails.Warehouse) + assert.Equal(t, schedule, alertDetails.Schedule) + assert.Equal(t, comment, *alertDetails.Comment) + assert.Equal(t, condition, alertDetails.Condition) + assert.Equal(t, action, alertDetails.Action) + + alert, err := client.Alerts.Show(ctx, &ShowAlertOptions{ + Like: &Like{ + Pattern: String(name), + }, + In: &In{ + Schema: schemaTest.ID(), + }, + }) + require.NoError(t, err) + assert.Equal(t, 1, len(alert)) + assert.Equal(t, name, alert[0].Name) + assert.Equal(t, comment, *alert[0].Comment) + }) + + t.Run("test if_not_exists", func(t *testing.T) { + name := randomString(t) + schedule := "USING CRON * * * * TUE,THU UTC" + condition := "SELECT 1" + action := "SELECT 1" + comment := randomComment(t) + id := NewSchemaObjectIdentifier(databaseTest.Name, schemaTest.Name, name) + err := client.Alerts.Create(ctx, id, testWarehouse.ID(), schedule, condition, action, &CreateAlertOptions{ + OrReplace: Bool(false), + IfNotExists: Bool(true), + Comment: String(comment), + }) + require.NoError(t, err) + alertDetails, err := client.Alerts.Describe(ctx, id) + require.NoError(t, err) + assert.Equal(t, name, alertDetails.Name) + assert.Equal(t, testWarehouse.Name, alertDetails.Warehouse) + assert.Equal(t, schedule, alertDetails.Schedule) + assert.Equal(t, comment, *alertDetails.Comment) + assert.Equal(t, condition, alertDetails.Condition) + assert.Equal(t, action, alertDetails.Action) + + alert, err := client.Alerts.Show(ctx, &ShowAlertOptions{ + Like: &Like{ + Pattern: String(name), + }, + In: &In{ + Schema: schemaTest.ID(), + }, + }) + require.NoError(t, err) + assert.Equal(t, 1, len(alert)) + assert.Equal(t, name, alert[0].Name) + assert.Equal(t, comment, *alert[0].Comment) + }) + + t.Run("test no options", func(t *testing.T) { + name := randomString(t) + schedule := "USING CRON * * * * TUE,THU UTC" + condition := "SELECT 1" + action := "SELECT 1" + id := NewSchemaObjectIdentifier(databaseTest.Name, schemaTest.Name, name) + err := client.Alerts.Create(ctx, id, testWarehouse.ID(), schedule, condition, action, nil) + require.NoError(t, err) + alertDetails, err := client.Alerts.Describe(ctx, id) + require.NoError(t, err) + assert.Equal(t, name, alertDetails.Name) + assert.Equal(t, testWarehouse.Name, alertDetails.Warehouse) + assert.Equal(t, schedule, alertDetails.Schedule) + assert.Equal(t, condition, alertDetails.Condition) + assert.Equal(t, action, alertDetails.Action) + + alert, err := client.Alerts.Show(ctx, &ShowAlertOptions{ + Like: &Like{ + Pattern: String(name), + }, + In: &In{ + Schema: schemaTest.ID(), + }, + }) + require.NoError(t, err) + assert.Equal(t, 1, len(alert)) + assert.Equal(t, name, alert[0].Name) + assert.Equal(t, "", *alert[0].Comment) + }) + + t.Run("test multiline action", func(t *testing.T) { + name := randomString(t) + schedule := "USING CRON * * * * TUE,THU UTC" + condition := "SELECT 1" + action := ` + select + case + when true then + 1 + else + 2 + end + ` + id := NewSchemaObjectIdentifier(databaseTest.Name, schemaTest.Name, name) + err := client.Alerts.Create(ctx, id, testWarehouse.ID(), schedule, condition, action, nil) + require.NoError(t, err) + alertDetails, err := client.Alerts.Describe(ctx, id) + require.NoError(t, err) + assert.Equal(t, name, alertDetails.Name) + assert.Equal(t, testWarehouse.Name, alertDetails.Warehouse) + assert.Equal(t, schedule, alertDetails.Schedule) + assert.Equal(t, condition, alertDetails.Condition) + assert.Equal(t, strings.TrimSpace(action), alertDetails.Action) + + alert, err := client.Alerts.Show(ctx, &ShowAlertOptions{ + Like: &Like{ + Pattern: String(name), + }, + In: &In{ + Schema: schemaTest.ID(), + }, + }) + require.NoError(t, err) + assert.Equal(t, 1, len(alert)) + assert.Equal(t, name, alert[0].Name) + assert.Equal(t, "", *alert[0].Comment) + }) +} + +func TestInt_AlertDescribe(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + databaseTest, databaseCleanup := createDatabase(t, client) + t.Cleanup(databaseCleanup) + + schemaTest, schemaCleanup := createSchema(t, client, databaseTest) + t.Cleanup(schemaCleanup) + + warehouseTest, warehouseCleanup := createWarehouse(t, client) + t.Cleanup(warehouseCleanup) + + alert, alertCleanup := createAlert(t, client, databaseTest, schemaTest, warehouseTest) + t.Cleanup(alertCleanup) + + t.Run("when alert exists", func(t *testing.T) { + alertDetails, err := client.Alerts.Describe(ctx, alert.ID()) + require.NoError(t, err) + assert.Equal(t, alert.Name, alertDetails.Name) + }) + + t.Run("when alert does not exist", func(t *testing.T) { + id := NewSchemaObjectIdentifier(databaseTest.Name, schemaTest.Name, "does_not_exist") + _, err := client.Alerts.Describe(ctx, id) + assert.ErrorIs(t, err, ErrObjectNotExistOrAuthorized) + }) +} + +func TestInt_AlertAlter(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + databaseTest, databaseCleanup := createDatabase(t, client) + t.Cleanup(databaseCleanup) + + schemaTest, schemaCleanup := createSchema(t, client, databaseTest) + t.Cleanup(schemaCleanup) + + warehouseTest, warehouseCleanup := createWarehouse(t, client) + t.Cleanup(warehouseCleanup) + + t.Run("when setting and unsetting a value", func(t *testing.T) { + alert, alertCleanup := createAlert(t, client, databaseTest, schemaTest, warehouseTest) + t.Cleanup(alertCleanup) + newSchedule := "USING CRON * * * * TUE,FRI GMT" + + alterOptions := &AlterAlertOptions{ + Set: &AlertSet{ + Schedule: &newSchedule, + }, + } + + err := client.Alerts.Alter(ctx, alert.ID(), alterOptions) + require.NoError(t, err) + alerts, err := client.Alerts.Show(ctx, &ShowAlertOptions{ + Like: &Like{ + Pattern: String(alert.Name), + }, + In: &In{ + Schema: schemaTest.ID(), + }, + }) + require.NoError(t, err) + assert.Equal(t, 1, len(alerts)) + assert.Equal(t, newSchedule, alerts[0].Schedule) + }) + + t.Run("when modifying condition and action", func(t *testing.T) { + alert, alertCleanup := createAlert(t, client, databaseTest, schemaTest, warehouseTest) + t.Cleanup(alertCleanup) + newCondition := "select * from DUAL where false" + + alterOptions := &AlterAlertOptions{ + ModifyCondition: &[]string{newCondition}, + } + + err := client.Alerts.Alter(ctx, alert.ID(), alterOptions) + require.NoError(t, err) + alerts, err := client.Alerts.Show(ctx, &ShowAlertOptions{ + Like: &Like{ + Pattern: String(alert.Name), + }, + In: &In{ + Schema: schemaTest.ID(), + }, + }) + require.NoError(t, err) + assert.Equal(t, 1, len(alerts)) + assert.Equal(t, newCondition, alerts[0].Condition) + + newAction := "create table FOO(ID INT)" + + alterOptions = &AlterAlertOptions{ + ModifyAction: &newAction, + } + + err = client.Alerts.Alter(ctx, alert.ID(), alterOptions) + require.NoError(t, err) + alerts, err = client.Alerts.Show(ctx, &ShowAlertOptions{ + Like: &Like{ + Pattern: String(alert.Name), + }, + In: &In{ + Schema: schemaTest.ID(), + }, + }) + require.NoError(t, err) + assert.Equal(t, 1, len(alerts)) + assert.Equal(t, newAction, alerts[0].Action) + }) + + t.Run("resume and then suspend", func(t *testing.T) { + alert, alertCleanup := createAlert(t, client, databaseTest, schemaTest, warehouseTest) + t.Cleanup(alertCleanup) + + alterOptions := &AlterAlertOptions{ + Action: &AlertActionResume, + } + + err := client.Alerts.Alter(ctx, alert.ID(), alterOptions) + require.NoError(t, err) + alerts, err := client.Alerts.Show(ctx, &ShowAlertOptions{ + Like: &Like{ + Pattern: String(alert.Name), + }, + In: &In{ + Schema: schemaTest.ID(), + }, + }) + require.NoError(t, err) + assert.Equal(t, 1, len(alerts)) + assert.True(t, alerts[0].State == AlertStateStarted) + + alterOptions = &AlterAlertOptions{ + Action: &AlertActionSuspend, + } + + err = client.Alerts.Alter(ctx, alert.ID(), alterOptions) + require.NoError(t, err) + alerts, err = client.Alerts.Show(ctx, &ShowAlertOptions{ + Like: &Like{ + Pattern: String(alert.Name), + }, + In: &In{ + Schema: schemaTest.ID(), + }, + }) + require.NoError(t, err) + assert.Equal(t, 1, len(alerts)) + assert.True(t, alerts[0].State == AlertStateSuspended) + }) +} + +func TestInt_AlertDrop(t *testing.T) { + client := testClient(t) + ctx := context.Background() + + databaseTest, databaseCleanup := createDatabase(t, client) + t.Cleanup(databaseCleanup) + + schemaTest, schemaCleanup := createSchema(t, client, databaseTest) + t.Cleanup(schemaCleanup) + + warehouseTest, warehouseCleanup := createWarehouse(t, client) + t.Cleanup(warehouseCleanup) + + t.Run("when alert exists", func(t *testing.T) { + alert, _ := createAlert(t, client, databaseTest, schemaTest, warehouseTest) + id := alert.ID() + err := client.Alerts.Drop(ctx, id) + require.NoError(t, err) + _, err = client.PasswordPolicies.Describe(ctx, id) + assert.ErrorIs(t, err, ErrObjectNotExistOrAuthorized) + }) + + t.Run("when alert does not exist", func(t *testing.T) { + id := NewSchemaObjectIdentifier(databaseTest.Name, schemaTest.Name, "does_not_exist") + err := client.Alerts.Drop(ctx, id) + assert.ErrorIs(t, err, ErrObjectNotExistOrAuthorized) + }) +} diff --git a/pkg/sdk/alerts_test.go b/pkg/sdk/alerts_test.go new file mode 100644 index 0000000000..31b37b8af7 --- /dev/null +++ b/pkg/sdk/alerts_test.go @@ -0,0 +1,293 @@ +package sdk + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/stretchr/testify/require" +) + +func TestAlertCreate(t *testing.T) { + id := randomSchemaObjectIdentifier(t) + + t.Run("with complete options", func(t *testing.T) { + newComment := randomString(t) + warehouse := AccountObjectIdentifier{"warehouse"} + existsCondition := "SELECT 1" + condition := AlertCondition{[]string{existsCondition}} + schedule := "1 minute" + action := "INSERT INTO FOO VALUES (1)" + + opts := &CreateAlertOptions{ + name: id, + warehouse: warehouse, + schedule: schedule, + condition: []AlertCondition{condition}, + action: action, + Comment: String(newComment), + } + + err := opts.validate() + assert.NoError(t, err) + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := fmt.Sprintf(`CREATE ALERT %s WAREHOUSE = "%s" SCHEDULE = '%s' COMMENT = '%s' IF (EXISTS (%s)) THEN %s`, id.FullyQualifiedName(), warehouse.name, schedule, newComment, existsCondition, action) + assert.Equal(t, expected, actual) + }) +} + +func TestAlertAlter(t *testing.T) { + id := randomSchemaObjectIdentifier(t) + + t.Run("fail without alter action specified", func(t *testing.T) { + opts := &AlterAlertOptions{ + name: id, + } + err := opts.validate() + assert.Error(t, err) + }) + + t.Run("fail when 2 alter actions specified", func(t *testing.T) { + newComment := randomString(t) + opts := &AlterAlertOptions{ + name: id, + Action: &AlertActionResume, + Set: &AlertSet{ + Comment: String(newComment), + }, + } + err := opts.validate() + assert.Error(t, err) + }) + + t.Run("with resume", func(t *testing.T) { + opts := &AlterAlertOptions{ + name: id, + Action: &AlertActionResume, + } + + err := opts.validate() + assert.NoError(t, err) + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := fmt.Sprintf("ALTER ALERT %s RESUME", id.FullyQualifiedName()) + assert.Equal(t, expected, actual) + }) + + t.Run("with suspend", func(t *testing.T) { + opts := &AlterAlertOptions{ + name: id, + Action: &AlertActionSuspend, + } + + err := opts.validate() + assert.NoError(t, err) + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := fmt.Sprintf("ALTER ALERT %s SUSPEND", id.FullyQualifiedName()) + assert.Equal(t, expected, actual) + }) + + t.Run("with set", func(t *testing.T) { + newComment := randomString(t) + opts := &AlterAlertOptions{ + name: id, + Set: &AlertSet{ + Comment: String(newComment), + }, + } + err := opts.validate() + assert.NoError(t, err) + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := fmt.Sprintf("ALTER ALERT %s SET COMMENT = '%s'", id.FullyQualifiedName(), newComment) + assert.Equal(t, expected, actual) + }) + + t.Run("with unset", func(t *testing.T) { + opts := &AlterAlertOptions{ + name: id, + Unset: &AlertUnset{ + Comment: Bool(true), + }, + } + err := opts.validate() + assert.NoError(t, err) + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := fmt.Sprintf("ALTER ALERT %s UNSET COMMENT", id.FullyQualifiedName()) + assert.Equal(t, expected, actual) + }) + + t.Run("with modify condition", func(t *testing.T) { + modifyCondition := "SELECT * FROM FOO" + opts := &AlterAlertOptions{ + name: id, + ModifyCondition: &[]string{modifyCondition}, + } + err := opts.validate() + assert.NoError(t, err) + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := fmt.Sprintf("ALTER ALERT %s MODIFY CONDITION EXISTS (%s)", id.FullyQualifiedName(), modifyCondition) + assert.Equal(t, expected, actual) + }) + t.Run("with modify action", func(t *testing.T) { + modifyAction := String("INSERT INTO FOO VALUES (1)") + opts := &AlterAlertOptions{ + name: id, + ModifyAction: modifyAction, + } + err := opts.validate() + assert.NoError(t, err) + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := fmt.Sprintf("ALTER ALERT %s MODIFY ACTION %s", id.FullyQualifiedName(), *modifyAction) + assert.Equal(t, expected, actual) + }) +} + +func TestAlertDrop(t *testing.T) { + id := randomSchemaObjectIdentifier(t) + + t.Run("empty options", func(t *testing.T) { + opts := &dropAlertOptions{} + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := "DROP ALERT" + assert.Equal(t, expected, actual) + }) + + t.Run("only name", func(t *testing.T) { + opts := &dropAlertOptions{ + name: id, + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := fmt.Sprintf("DROP ALERT %s", id.FullyQualifiedName()) + assert.Equal(t, expected, actual) + }) +} + +func TestAlertShow(t *testing.T) { + id := randomSchemaObjectIdentifier(t) + + t.Run("empty options", func(t *testing.T) { + opts := &ShowAlertOptions{} + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := "SHOW ALERTS" + assert.Equal(t, expected, actual) + }) + + t.Run("terse", func(t *testing.T) { + opts := &ShowAlertOptions{Terse: Bool(true)} + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := "SHOW TERSE ALERTS" + assert.Equal(t, expected, actual) + }) + + t.Run("with like", func(t *testing.T) { + opts := &ShowAlertOptions{ + Like: &Like{ + Pattern: String(id.Name()), + }, + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := fmt.Sprintf("SHOW ALERTS LIKE '%s'", id.Name()) + assert.Equal(t, expected, actual) + }) + + t.Run("with like and in account", func(t *testing.T) { + opts := &ShowAlertOptions{ + Like: &Like{ + Pattern: String(id.Name()), + }, + In: &In{ + Account: Bool(true), + }, + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := fmt.Sprintf("SHOW ALERTS LIKE '%s' IN ACCOUNT", id.Name()) + assert.Equal(t, expected, actual) + }) + + t.Run("with like and in database", func(t *testing.T) { + databaseIdentifier := NewAccountObjectIdentifier(id.DatabaseName()) + opts := &ShowAlertOptions{ + Like: &Like{ + Pattern: String(id.Name()), + }, + In: &In{ + Database: databaseIdentifier, + }, + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := fmt.Sprintf("SHOW ALERTS LIKE '%s' IN DATABASE %s", id.Name(), databaseIdentifier.FullyQualifiedName()) + assert.Equal(t, expected, actual) + }) + + t.Run("with like and in schema", func(t *testing.T) { + schemaIdentifier := NewSchemaIdentifier(id.DatabaseName(), id.SchemaName()) + opts := &ShowAlertOptions{ + Like: &Like{ + Pattern: String(id.Name()), + }, + In: &In{ + Schema: schemaIdentifier, + }, + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := fmt.Sprintf("SHOW ALERTS LIKE '%s' IN SCHEMA %s", id.Name(), schemaIdentifier.FullyQualifiedName()) + assert.Equal(t, expected, actual) + }) + + t.Run("with 'starts with'", func(t *testing.T) { + opts := &ShowAlertOptions{ + StartsWith: String("FOO"), + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := "SHOW ALERTS STARTS WITH 'FOO'" + assert.Equal(t, expected, actual) + }) + + t.Run("with limit", func(t *testing.T) { + opts := &ShowAlertOptions{ + Limit: Int(10), + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := "SHOW ALERTS LIMIT 10" + assert.Equal(t, expected, actual) + }) +} + +func TestAlertDescribe(t *testing.T) { + id := randomSchemaObjectIdentifier(t) + + t.Run("empty options", func(t *testing.T) { + opts := &describeAlertOptions{} + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := "DESCRIBE ALERT" + assert.Equal(t, expected, actual) + }) + + t.Run("only name", func(t *testing.T) { + opts := &describeAlertOptions{ + name: id, + } + actual, err := structToSQL(opts) + require.NoError(t, err) + expected := fmt.Sprintf("DESCRIBE ALERT %s", id.FullyQualifiedName()) + assert.Equal(t, expected, actual) + }) +} diff --git a/pkg/sdk/client.go b/pkg/sdk/client.go index a15cd7f568..a3a9ef12bb 100644 --- a/pkg/sdk/client.go +++ b/pkg/sdk/client.go @@ -27,6 +27,7 @@ type Client struct { // DDL Commands Accounts Accounts + Alerts Alerts Comments Comments Databases Databases FailoverGroups FailoverGroups @@ -115,6 +116,7 @@ func NewClientFromDB(db *sql.DB) *Client { func (c *Client) initialize() { c.Accounts = &accounts{client: c} + c.Alerts = &alerts{client: c} c.Comments = &comments{client: c} c.ContextFunctions = &contextFunctions{client: c} c.ConversionFunctions = &conversionFunctions{client: c} diff --git a/pkg/sdk/helper_test.go b/pkg/sdk/helper_test.go index d2b3e106a9..cc4fc1652b 100644 --- a/pkg/sdk/helper_test.go +++ b/pkg/sdk/helper_test.go @@ -434,3 +434,58 @@ func createMaskingPolicy(t *testing.T, client *Client, database *Database, schem expression := "REPLACE('X', 1, 2)" return createMaskingPolicyWithOptions(t, client, database, schema, signature, DataTypeVARCHAR, expression, &CreateMaskingPolicyOptions{}) } + +func createAlertWithOptions(t *testing.T, client *Client, database *Database, schema *Schema, warehouse *Warehouse, schedule string, condition string, action string, opts *CreateAlertOptions) (*Alert, func()) { + t.Helper() + var databaseCleanup func() + if database == nil { + database, databaseCleanup = createDatabase(t, client) + } + var schemaCleanup func() + if schema == nil { + schema, schemaCleanup = createSchema(t, client, database) + } + var warehouseCleanup func() + if warehouse == nil { + warehouse, warehouseCleanup = createWarehouse(t, client) + } + + name := randomString(t) + id := NewSchemaObjectIdentifier(schema.DatabaseName, schema.Name, name) + ctx := context.Background() + err := client.Alerts.Create(ctx, id, warehouse.ID(), schedule, condition, action, opts) + require.NoError(t, err) + + showOptions := &ShowAlertOptions{ + Like: &Like{ + Pattern: String(name), + }, + In: &In{ + Schema: schema.ID(), + }, + } + alertList, err := client.Alerts.Show(ctx, showOptions) + require.NoError(t, err) + require.Equal(t, 1, len(alertList)) + return alertList[0], func() { + err := client.Alerts.Drop(ctx, id) + require.NoError(t, err) + if schemaCleanup != nil { + schemaCleanup() + } + if databaseCleanup != nil { + databaseCleanup() + } + if warehouseCleanup != nil { + warehouseCleanup() + } + } +} + +func createAlert(t *testing.T, client *Client, database *Database, schema *Schema, warehouse *Warehouse) (*Alert, func()) { + t.Helper() + schedule := "USING CRON * * * * * UTC" + condition := "SELECT 1" + action := "SELECT 1" + return createAlertWithOptions(t, client, database, schema, warehouse, schedule, condition, action, &CreateAlertOptions{}) +} diff --git a/pkg/sdk/object_types.go b/pkg/sdk/object_types.go index 729abe7110..b938bb3db7 100644 --- a/pkg/sdk/object_types.go +++ b/pkg/sdk/object_types.go @@ -17,6 +17,7 @@ type ObjectType string const ( ObjectTypeAccount ObjectType = "ACCOUNT" + ObjectTypeAlert ObjectType = "ALERT" ObjectTypeAccountParameter ObjectType = "ACCOUNT PARAMETER" ObjectTypeDatabase ObjectType = "DATABASE" ObjectTypeFailoverGroup ObjectType = "FAILOVER GROUP" diff --git a/pkg/snowflake/alert.go b/pkg/snowflake/alert.go index 28ae72fc93..f29062d94b 100644 --- a/pkg/snowflake/alert.go +++ b/pkg/snowflake/alert.go @@ -1,6 +1,7 @@ package snowflake import ( + "context" "database/sql" "errors" "fmt" @@ -8,6 +9,7 @@ import ( "strings" "time" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/sdk" "github.com/jmoiron/sqlx" ) @@ -295,52 +297,47 @@ func ListAlerts(databaseName, schemaName, pattern string, db *sql.DB) ([]Alert, return dbs, nil } -func WaitResumeAlert(db *sql.DB, name string, database string, schema string) error { - builder := NewAlertBuilder(name, database, schema) - +func WaitResumeAlert(ctx context.Context, client *sdk.Client, id sdk.SchemaObjectIdentifier) error { + opts := sdk.AlterAlertOptions{Action: &sdk.AlertActionResume} // try to resume the alert, and verify that it was resumed. // if it's not resumed then try again up until a maximum of 5 times for i := 0; i < 5; i++ { - q := builder.Resume() - if err := Exec(db, q); err != nil { - return fmt.Errorf("error resuming alert %v err = %w", name, err) + err := client.Alerts.Alter(ctx, id, &opts) + if err != nil { + return fmt.Errorf("error resuming alert %v err = %w", id.Name(), err) } - q = builder.Show() - row := QueryRow(db, q) - t, err := ScanAlert(row) + alert, err := client.Alerts.ShowByID(ctx, id) if err != nil { return err } - if t.IsEnabled() { + if alert.State == sdk.AlertStateStarted { return nil } time.Sleep(10 * time.Second) } - return fmt.Errorf("unable to resume alert %v after 5 attempts", name) + return fmt.Errorf("unable to resume alert %v after 5 attempts", id.Name()) } -func WaitSuspendAlert(db *sql.DB, name string, database string, schema string) error { - builder := NewAlertBuilder(name, database, schema) +func WaitSuspendAlert(ctx context.Context, client *sdk.Client, id sdk.SchemaObjectIdentifier) error { + opts := sdk.AlterAlertOptions{Action: &sdk.AlertActionSuspend} // try to suspend the alert, and verify that it was suspended. // if it's not suspended then try again up until a maximum of 5 times for i := 0; i < 5; i++ { - q := builder.Suspend() - if err := Exec(db, q); err != nil { - return fmt.Errorf("error suspending alert %v err = %w", name, err) + err := client.Alerts.Alter(ctx, id, &opts) + if err != nil { + return fmt.Errorf("error suspending alert %v err = %w", id.Name(), err) } - q = builder.Show() - row := QueryRow(db, q) - alert, err := ScanAlert(row) + alert, err := client.Alerts.ShowByID(ctx, id) if err != nil { return err } - if alert.IsSuspended() { + if alert.State == sdk.AlertStateSuspended { return nil } time.Sleep(10 * time.Second) } - return fmt.Errorf("unable to suspend alert %v after 5 attempts", name) + return fmt.Errorf("unable to suspend alert %v after 5 attempts", id.Name()) }