Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add in more functionality for UpdateResourceMonitor #1456

Merged
merged 4 commits into from
Jan 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions pkg/resources/helper_expansion.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package resources

import "golang.org/x/exp/slices"

// borrowed from https://github.com/terraform-providers/terraform-provider-aws/blob/master/aws/structure.go#L924:6

func expandIntList(configured []interface{}) []int {
Expand Down Expand Up @@ -36,3 +38,16 @@ func expandObjectIdentifier(objectIdentifier interface{}) (string, string, strin
}
return objectDatabase, objectSchema, objectName
}

// intersectionAAndNotB takes the intersection of set A and the intersection of not set B. A∩B′ in set notation.
func intersectionAAndNotB(setA []interface{}, setB []interface{}) []string {
res := make([]string, 0)
sliceA := expandStringList(setA)
sliceB := expandStringList(setB)
for _, s := range sliceA {
if !slices.Contains(sliceB, s) {
res = append(res, s)
}
}
return res
}
97 changes: 76 additions & 21 deletions pkg/resources/resource_monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ var resourceMonitorSchema = map[string]*schema.Schema{
"notify_users": {
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Description: "Specifies the list of users to receive email notifications on resource monitors.",
Elem: &schema.Schema{
Type: schema.TypeString,
Expand Down Expand Up @@ -60,35 +59,32 @@ var resourceMonitorSchema = map[string]*schema.Schema{
Elem: &schema.Schema{Type: schema.TypeInt},
Optional: true,
Description: "A list of percentage thresholds at which to suspend all warehouses.",
ForceNew: true,
},
"suspend_immediate_triggers": {
Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeInt},
Optional: true,
Description: "A list of percentage thresholds at which to immediately suspend all warehouses.",
ForceNew: true,
},
"notify_triggers": {
Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeInt},
Optional: true,
Description: "A list of percentage thresholds at which to send an alert to subscribed users.",
ForceNew: true,
},
"set_for_account": {
Type: schema.TypeBool,
Optional: true,
Description: "Specifies whether the resource monitor should be applied globally to your Snowflake account.",
Default: false,
ForceNew: true,
Type: schema.TypeBool,
Optional: true,
ConflictsWith: []string{"warehouses"},
Description: "Specifies whether the resource monitor should be applied globally to your Snowflake account.",
Default: false,
},
"warehouses": {
Type: schema.TypeSet,
Optional: true,
Description: "A list of warehouses to apply the resource monitor to.",
Elem: &schema.Schema{Type: schema.TypeString},
ForceNew: true,
Type: schema.TypeSet,
Optional: true,
ConflictsWith: []string{"set_for_account"},
Description: "A list of warehouses to apply the resource monitor to.",
Elem: &schema.Schema{Type: schema.TypeString},
},
}

Expand All @@ -107,7 +103,7 @@ func ResourceMonitor() *schema.Resource {
}
}

// CreateResourceMonitor implents schema.CreateFunc.
// CreateResourceMonitor implements schema.CreateFunc.
func CreateResourceMonitor(d *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
name := d.Get("name").(string)
Expand Down Expand Up @@ -283,39 +279,98 @@ func extractTriggerInts(s sql.NullString) ([]int, error) {
return out, nil
}

// UpdateResourceMonitor implements schema.UpdateFunc.
func UpdateResourceMonitor(d *schema.ResourceData, meta interface{}) error {
db := meta.(*sql.DB)
id := d.Id()

stmt := snowflake.NewResourceMonitorBuilder(id).Alter()
ub := snowflake.NewResourceMonitorBuilder(id).Alter()
var runSetStatement bool

if d.HasChange("notify_users") {
runSetStatement = true
ub.SetStringList(`NOTIFY_USERS`, expandStringList(d.Get("notify_users").(*schema.Set).List()))
}

if d.HasChange("credit_quota") {
runSetStatement = true
stmt.SetInt(`CREDIT_QUOTA`, d.Get("credit_quota").(int))
ub.SetInt(`CREDIT_QUOTA`, d.Get("credit_quota").(int))
}

if d.HasChange("frequency") {
runSetStatement = true
stmt.SetString(`FREQUENCY`, d.Get("frequency").(string))
ub.SetString(`FREQUENCY`, d.Get("frequency").(string))
}

if d.HasChange("start_timestamp") {
runSetStatement = true
stmt.SetString(`START_TIMESTAMP`, d.Get("start_timestamp").(string))
ub.SetString(`START_TIMESTAMP`, d.Get("start_timestamp").(string))
}

if d.HasChange("end_timestamp") {
runSetStatement = true
stmt.SetString(`END_TIMESTAMP`, d.Get("end_timestamp").(string))
ub.SetString(`END_TIMESTAMP`, d.Get("end_timestamp").(string))
}

// Set triggers
sTrigs := expandIntList(d.Get("suspend_triggers").(*schema.Set).List())
for _, t := range sTrigs {
runSetStatement = true
ub.SuspendAt(t)
}
siTrigs := expandIntList(d.Get("suspend_immediate_triggers").(*schema.Set).List())
for _, t := range siTrigs {
runSetStatement = true
ub.SuspendImmediatelyAt(t)
}
nTrigs := expandIntList(d.Get("notify_triggers").(*schema.Set).List())
for _, t := range nTrigs {
runSetStatement = true
ub.NotifyAt(t)
}

if runSetStatement {
if err := snowflake.Exec(db, stmt.Statement()); err != nil {
if err := snowflake.Exec(db, ub.Statement()); err != nil {
return fmt.Errorf("error updating resource monitor %v\n%w", id, err)
}
}

// Remove from account
if d.HasChange("set_for_account") && !d.Get("set_for_account").(bool) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be combined with the add to account. something like

if d.HasChange("set_for_account")
  old, new := d.GetChange("set_for_account")
 if new == nil {
  // remove from account
} else {
 // set on new account
}
f d.Get("set_

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was intentional the issue is you can't have it set for both an account and a warehouse. So if you are going from setting a resource monitor from the account level to the warehouse level or vice versa you have to make sure it is unset first.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in that case you should also use"ConflictsWith", it works like setting ForceNew on an attirbute. Will prevent people from setting both at the same time.

// ConflictsWith is a set of schema keys that conflict with this schema.
// This will only check that they're set in the _config_. This will not
// raise an error for a malfunctioning resource that sets a conflicting
// key.
ConflictsWith []string

if err := snowflake.Exec(db, ub.UnsetOnAccount()); err != nil {
return fmt.Errorf("error unsetting resource monitor %v on account err = %w", id, err)
}
}

// Remove from all old warehouses
if d.HasChange("warehouses") {
oldV, v := d.GetChange("warehouses")
res := intersectionAAndNotB(oldV.(*schema.Set).List(), v.(*schema.Set).List())
for _, w := range res {
if err := snowflake.Exec(db, ub.UnsetOnWarehouse(w)); err != nil {
return fmt.Errorf("error setting resource monitor %v on warehouse %v err = %w", id, w, err)
}
}
}

// Add to account
if d.HasChange("set_for_account") && d.Get("set_for_account").(bool) {
if err := snowflake.Exec(db, ub.SetOnAccount()); err != nil {
return fmt.Errorf("error setting resource monitor %v on account err = %w", id, err)
}
}

// Add to all new warehouses
if d.HasChange("warehouses") {
oldV, v := d.GetChange("warehouses")
res := intersectionAAndNotB(v.(*schema.Set).List(), oldV.(*schema.Set).List())
for _, w := range res {
if err := snowflake.Exec(db, ub.SetOnWarehouse(w)); err != nil {
return fmt.Errorf("error setting resource monitor %v on warehouse %v err = %w", id, w, err)
}
}
}

return ReadResourceMonitor(d, meta)
}

Expand Down
8 changes: 6 additions & 2 deletions pkg/resources/resource_monitor_acceptance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func TestAcc_ResourceMonitor(t *testing.T) {
resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "name", name),
resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "credit_quota", "100"),
resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "set_for_account", "false"),
resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "notify_triggers.0", "40"),
),
},
// CHANGE PROPERTIES
Expand All @@ -33,7 +34,8 @@ func TestAcc_ResourceMonitor(t *testing.T) {
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "name", name),
resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "credit_quota", "150"),
resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "set_for_account", "false"),
resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "set_for_account", "true"),
resource.TestCheckResourceAttr("snowflake_resource_monitor.test", "notify_triggers.0", "50"),
),
},
// IMPORT
Expand All @@ -52,6 +54,7 @@ resource "snowflake_resource_monitor" "test" {
name = "%v"
credit_quota = 100
set_for_account = false
notify_triggers = [40]
}
`, accName)
}
Expand All @@ -61,7 +64,8 @@ func resourceMonitorConfig2(accName string) string {
resource "snowflake_resource_monitor" "test" {
name = "%v"
credit_quota = 150
set_for_account = false
set_for_account = true
notify_triggers = [50]
}
`, accName)
}
Expand Down
103 changes: 99 additions & 4 deletions pkg/snowflake/resource_monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,28 @@ type ResourceMonitorCreateBuilder struct {
triggers []trigger
}

type ResourceMonitorAlterBuilder struct {
AlterPropertiesBuilder

// triggers consist of the type (DO SUSPEND | SUSPEND_IMMEDIATE | NOTIFY) and
// the threshold (a percentage value)
triggers []trigger
}

type action string

type trigger struct {
action string
action action
threshold int
}

const (
// SuspendTrigger suspends all assigned warehouses while allowing currently running queries to complete.
SuspendTrigger = "SUSPEND"
SuspendTrigger action = "SUSPEND"
// SuspendImmediatelyTrigger suspends all assigned warehouses immediately and cancel any currently running queries or statements using the warehouses.
SuspendImmediatelyTrigger = "SUSPEND_IMMEDIATE"
SuspendImmediatelyTrigger action = "SUSPEND_IMMEDIATE"
// NotifyTrigger sends an alert (to all users who have enabled notifications for themselves), but do not take any other action.
NotifyTrigger = "NOTIFY"
NotifyTrigger action = "NOTIFY"
)

// Create returns a pointer to a ResourceMonitorCreateBuilder.
Expand Down Expand Up @@ -134,6 +144,91 @@ func (rcb *ResourceMonitorCreateBuilder) SetOnWarehouse(warehouse string) string
return fmt.Sprintf(`ALTER WAREHOUSE "%v" SET RESOURCE_MONITOR = "%v"`, warehouse, rcb.name)
}

// Alter returns a pointer to a ResourceMonitorAlterBuilder.
func (rb *ResourceMonitorBuilder) Alter() *ResourceMonitorAlterBuilder {
return &ResourceMonitorAlterBuilder{
AlterPropertiesBuilder{
name: rb.name,
entityType: rb.entityType,
stringProperties: make(map[string]string),
boolProperties: make(map[string]bool),
intProperties: make(map[string]int),
floatProperties: make(map[string]float64),
stringListProperties: make(map[string][]string),
},
make([]trigger, 0),
}
}

// NotifyAt adds a notify trigger at the specified percentage threshold.
func (rcb *ResourceMonitorAlterBuilder) NotifyAt(pct int) *ResourceMonitorAlterBuilder {
rcb.triggers = append(rcb.triggers, trigger{NotifyTrigger, pct})
return rcb
}

// SuspendAt adds a suspend trigger at the specified percentage threshold.
func (rcb *ResourceMonitorAlterBuilder) SuspendAt(pct int) *ResourceMonitorAlterBuilder {
rcb.triggers = append(rcb.triggers, trigger{SuspendTrigger, pct})
return rcb
}

// SuspendImmediatelyAt adds a suspend immediately trigger at the specified percentage threshold.
func (rcb *ResourceMonitorAlterBuilder) SuspendImmediatelyAt(pct int) *ResourceMonitorAlterBuilder {
rcb.triggers = append(rcb.triggers, trigger{SuspendImmediatelyTrigger, pct})
return rcb
}

// Statement returns the SQL statement needed to actually alter the resource.
func (rcb *ResourceMonitorAlterBuilder) Statement() string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf(`ALTER %v "%v" SET`, rcb.entityType, rcb.name))

for k, v := range rcb.stringProperties {
sb.WriteString(fmt.Sprintf(` %v='%v'`, strings.ToUpper(k), EscapeString(v)))
}

for k, v := range rcb.intProperties {
sb.WriteString(fmt.Sprintf(` %v=%d`, strings.ToUpper(k), v))
}

for k, v := range rcb.floatProperties {
sb.WriteString(fmt.Sprintf(` %v=%.2f`, strings.ToUpper(k), v))
}

for k, v := range rcb.stringListProperties {
sb.WriteString(fmt.Sprintf(" %s=%s", strings.ToUpper(k), formatStringList(v)))
}

if len(rcb.triggers) > 0 {
sb.WriteString(" TRIGGERS")
}

for _, trig := range rcb.triggers {
sb.WriteString(fmt.Sprintf(` ON %d PERCENT DO %v`, trig.threshold, trig.action))
}

return sb.String()
}

// SetOnAccount returns the SQL query that will set the resource monitor globally on your Snowflake account.
func (rcb *ResourceMonitorAlterBuilder) SetOnAccount() string {
return fmt.Sprintf(`ALTER ACCOUNT SET RESOURCE_MONITOR = "%v"`, rcb.name)
}

func (rcb *ResourceMonitorAlterBuilder) UnsetOnAccount() string {
return `ALTER ACCOUNT SET RESOURCE_MONITOR = NULL`
}

// SetOnWarehouse returns the SQL query that will set the resource monitor on the specified warehouse.
func (rcb *ResourceMonitorAlterBuilder) SetOnWarehouse(warehouse string) string {
return fmt.Sprintf(`ALTER WAREHOUSE "%v" SET RESOURCE_MONITOR = "%v"`, warehouse, rcb.name)
}

// UnsetOnWarehouse returns the SQL query that will unset the resource monitor on the specified warehouse.
func (rcb *ResourceMonitorAlterBuilder) UnsetOnWarehouse(warehouse string) string {
return fmt.Sprintf(`ALTER WAREHOUSE "%v" SET RESOURCE_MONITOR = NULL`, warehouse)
}

type ResourceMonitor struct {
Name sql.NullString `db:"name"`
CreditQuota sql.NullString `db:"credit_quota"`
Expand Down