From e167e735124ae570dc5bc451328a2fbf18a34370 Mon Sep 17 00:00:00 2001 From: obs-gh-owengoebel <159819293+obs-gh-owengoebel@users.noreply.github.com> Date: Mon, 5 Aug 2024 16:31:05 -0700 Subject: [PATCH] feat: add ability to link monitorv2 to shared actions (#134) --- client/api.go | 8 + .../internal/meta/operation/monitorv2.graphql | 23 +- .../meta/schema/monitorv2_extend.graphql | 4 +- client/meta/genqlient.generated.go | 291 ++++++++++++++++++ client/meta/monitorv2.go | 5 + client/oid/oid.go | 4 + docs/data-sources/monitor_v2.md | 10 + docs/resources/monitor_v2.md | 13 + observe/data_source_monitor_v2.go | 22 ++ observe/descriptions/monitorv2.yaml | 9 +- observe/resource_monitor_v2.go | 109 ++++++- observe/resource_monitor_v2_action_test.go | 140 +++++++++ 12 files changed, 630 insertions(+), 8 deletions(-) diff --git a/client/api.go b/client/api.go index ae61c133..e4c51852 100644 --- a/client/api.go +++ b/client/api.go @@ -521,6 +521,14 @@ func (c *Client) DeleteMonitorV2(ctx context.Context, id string) error { return c.Meta.DeleteMonitorV2(ctx, id) } +func (c *Client) SaveMonitorV2Relations(ctx context.Context, monitorId string, actionRelations []meta.ActionRelationInput) (*meta.MonitorV2, error) { + if !c.Flags[flagObs2110] { + c.obs2110.Lock() + defer c.obs2110.Unlock() + } + return c.Meta.SaveMonitorV2Relations(ctx, monitorId, actionRelations) +} + func (c *Client) GetMonitorV2(ctx context.Context, id string) (*meta.MonitorV2, error) { return c.Meta.GetMonitorV2(ctx, id) } diff --git a/client/internal/meta/operation/monitorv2.graphql b/client/internal/meta/operation/monitorv2.graphql index ef92f657..c6500c78 100644 --- a/client/internal/meta/operation/monitorv2.graphql +++ b/client/internal/meta/operation/monitorv2.graphql @@ -150,6 +150,11 @@ fragment MonitorV2SearchResult on MonitorV2SearchResult { } } +fragment MonitorV2ActionRule on MonitorV2ActionRule { + actionID + levels +} + # @genqlient(for: "MonitorV2Input.comment", omitempty: true) # @genqlient(for: "MonitorV2Input.iconUrl", omitempty: true) # @genqlient(for: "MonitorV2Input.description", omitempty: true) @@ -191,6 +196,10 @@ fragment MonitorV2 on MonitorV2 { definition { ...MonitorV2Definition } + # @genqlient(flatten: true) + actionRules { + ...MonitorV2ActionRule + } } # definitions of monitorv2 CRUD ops @@ -282,4 +291,16 @@ query lookupMonitorV2($workspaceId: ObjectId, $folderId: ObjectId, $nameExact: S monitorV2s: searchMonitorV2(workspaceId: $workspaceId, folderId: $folderId, nameExact: $nameExact, nameSubstring: $nameSubstring) { ...MonitorV2SearchResult } -} \ No newline at end of file +} + +# @genqlient(for: "ActionDestinationLinkInput.sendEndNotifications", omitempty: true) +# @genqlient(for: "ActionDestinationLinkInput.sendRemindersInterval", omitempty: true) +mutation saveMonitorV2Relations( + $monitorId: ObjectId!, + $actionRelations: [ActionRelationInput!] +) { + # @genqlient(flatten: true) + monitorV2: saveMonitorV2Relations(monitorId: $monitorId, actionRelations: $actionRelations) { + ...MonitorV2 + } +} diff --git a/client/internal/meta/schema/monitorv2_extend.graphql b/client/internal/meta/schema/monitorv2_extend.graphql index 683d77f3..845cc601 100644 --- a/client/internal/meta/schema/monitorv2_extend.graphql +++ b/client/internal/meta/schema/monitorv2_extend.graphql @@ -38,7 +38,7 @@ extend type Mutation { relationships with the destinations are mutateable.Hence, this API will error out if you provide destinationLinks where the action is shared. """ - saveMonitorV2Relations(monitorId: ObjectId!, actionRelations: [ActionRelationInput!]!): MonitorV2! + saveMonitorV2Relations(monitorId: ObjectId!, actionRelations: [ActionRelationInput!]): MonitorV2! """ saveActionsWithDestinations replaces all action's links to the destinations (MonitorV2) for the provided @@ -47,7 +47,7 @@ extend type Mutation { The purpose of the API is such that the users can create a shared action and make links to the destinations from the shared actions page or when an action has been shared from within the monitor editing page. """ - saveActionWithDestinationLinks(actionId: ObjectId!, destinationLinks: [ActionDestinationLinkInput!]!): MonitorV2Action! + saveActionWithDestinationLinks(actionId: ObjectId!, destinationLinks: [ActionDestinationLinkInput!]): MonitorV2Action! """ terminateMonitorV2Alarm allows an explicit termination of an active alarm. The purpose is to diff --git a/client/meta/genqlient.generated.go b/client/meta/genqlient.generated.go index 2a168211..edb89db6 100644 --- a/client/meta/genqlient.generated.go +++ b/client/meta/genqlient.generated.go @@ -91,6 +91,18 @@ func (v *ActionInput) GetEmail() *EmailActionInput { return v.Email } // GetWebhook returns ActionInput.Webhook, and is useful for accessing the field via an interface. func (v *ActionInput) GetWebhook() *WebhookActionInput { return v.Webhook } +// ActionRelationInput maps the action's relationship to the destinations the user desires to link with. +type ActionRelationInput struct { + ActionRule MonitorV2ActionRuleInput `json:"actionRule"` + DestLinks []ActionDestinationLinkInput `json:"destLinks"` +} + +// GetActionRule returns ActionRelationInput.ActionRule, and is useful for accessing the field via an interface. +func (v *ActionRelationInput) GetActionRule() MonitorV2ActionRuleInput { return v.ActionRule } + +// GetDestLinks returns ActionRelationInput.DestLinks, and is useful for accessing the field via an interface. +func (v *ActionRelationInput) GetDestLinks() []ActionDestinationLinkInput { return v.DestLinks } + type AggregateFunction string const ( @@ -4402,6 +4414,10 @@ type MonitorV2 struct { // Describes the type of each of the rules in the definition (they must all be the same type). RuleKind MonitorV2RuleKind `json:"ruleKind"` Definition MonitorV2Definition `json:"definition"` + // List of actions and conditions for dispatching. Each entry will + // contain the action definition regardless of whether the definition is + // shared or provided inline. + ActionRules []MonitorV2ActionRule `json:"actionRules"` } // GetId returns MonitorV2.Id, and is useful for accessing the field via an interface. @@ -4443,6 +4459,9 @@ func (v *MonitorV2) GetRuleKind() MonitorV2RuleKind { return v.RuleKind } // GetDefinition returns MonitorV2.Definition, and is useful for accessing the field via an interface. func (v *MonitorV2) GetDefinition() MonitorV2Definition { return v.Definition } +// GetActionRules returns MonitorV2.ActionRules, and is useful for accessing the field via an interface. +func (v *MonitorV2) GetActionRules() []MonitorV2ActionRule { return v.ActionRules } + // MonitorV2Action includes the GraphQL fields of MonitorV2Action requested by the fragment MonitorV2Action. type MonitorV2Action struct { // The inline field determines whether the object is inlined within another object or not. If not inlined, it can be shared with other objects. @@ -4538,6 +4557,31 @@ func (v *MonitorV2ActionInput) GetManagedById() *string { return v.ManagedById } // GetFolderId returns MonitorV2ActionInput.FolderId, and is useful for accessing the field via an interface. func (v *MonitorV2ActionInput) GetFolderId() *string { return v.FolderId } +// MonitorV2ActionRule includes the GraphQL fields of MonitorV2ActionRule requested by the fragment MonitorV2ActionRule. +type MonitorV2ActionRule struct { + // Takes in a private or public action id created from an earlier createAction API call. + ActionID string `json:"actionID"` + // Dispatch this action when the alarm matches any of the provided levels. + Levels []MonitorV2AlarmLevel `json:"levels"` +} + +// GetActionID returns MonitorV2ActionRule.ActionID, and is useful for accessing the field via an interface. +func (v *MonitorV2ActionRule) GetActionID() string { return v.ActionID } + +// GetLevels returns MonitorV2ActionRule.Levels, and is useful for accessing the field via an interface. +func (v *MonitorV2ActionRule) GetLevels() []MonitorV2AlarmLevel { return v.Levels } + +type MonitorV2ActionRuleInput struct { + ActionID string `json:"actionID"` + Levels []MonitorV2AlarmLevel `json:"levels"` +} + +// GetActionID returns MonitorV2ActionRuleInput.ActionID, and is useful for accessing the field via an interface. +func (v *MonitorV2ActionRuleInput) GetActionID() string { return v.ActionID } + +// GetLevels returns MonitorV2ActionRuleInput.Levels, and is useful for accessing the field via an interface. +func (v *MonitorV2ActionRuleInput) GetLevels() []MonitorV2AlarmLevel { return v.Levels } + // MonitorV2ActionType defines the type of monitor returned when querying all // actions for a monitor. type MonitorV2ActionType string @@ -9384,6 +9428,20 @@ func (v *__saveDatasetInput) GetQuery() MultiStageQueryInput { return v.Query } // GetDep returns __saveDatasetInput.Dep, and is useful for accessing the field via an interface. func (v *__saveDatasetInput) GetDep() *DependencyHandlingInput { return v.Dep } +// __saveMonitorV2RelationsInput is used internally by genqlient +type __saveMonitorV2RelationsInput struct { + MonitorId string `json:"monitorId"` + ActionRelations []ActionRelationInput `json:"actionRelations"` +} + +// GetMonitorId returns __saveMonitorV2RelationsInput.MonitorId, and is useful for accessing the field via an interface. +func (v *__saveMonitorV2RelationsInput) GetMonitorId() string { return v.MonitorId } + +// GetActionRelations returns __saveMonitorV2RelationsInput.ActionRelations, and is useful for accessing the field via an interface. +func (v *__saveMonitorV2RelationsInput) GetActionRelations() []ActionRelationInput { + return v.ActionRelations +} + // __saveSourceDatasetInput is used internally by genqlient type __saveSourceDatasetInput struct { WorkspaceId string `json:"workspaceId"` @@ -11228,6 +11286,21 @@ type saveDatasetResponse struct { // GetDataset returns saveDatasetResponse.Dataset, and is useful for accessing the field via an interface. func (v *saveDatasetResponse) GetDataset() *saveDatasetDatasetDatasetSaveResult { return v.Dataset } +// saveMonitorV2RelationsResponse is returned by saveMonitorV2Relations on success. +type saveMonitorV2RelationsResponse struct { + // saveMonitorV2Relations replaces all monitor relations (MonitorV2ActionRule, ActionDestinationLink) + // for the provided monitor with the provided list of actionRules and destinationLinks. + // Shared Actions can't be mutated through this call other than attaching it to the monitor, so you will need to used + // saveActionWithDestinationLinks to mutate sharedAction's links to the destinations. + // It does not allow you to mutate any shared actions' relationships with the destinations. Only the inlined actions' + // relationships with the destinations are mutateable.Hence, this API will error out if you provide destinationLinks + // where the action is shared. + MonitorV2 MonitorV2 `json:"monitorV2"` +} + +// GetMonitorV2 returns saveMonitorV2RelationsResponse.MonitorV2, and is useful for accessing the field via an interface. +func (v *saveMonitorV2RelationsResponse) GetMonitorV2() MonitorV2 { return v.MonitorV2 } + // saveSourceDatasetDatasetDatasetSaveResult includes the requested fields of the GraphQL type DatasetSaveResult. type saveSourceDatasetDatasetDatasetSaveResult struct { // this is what you got out when saving @@ -12789,6 +12862,9 @@ fragment MonitorV2 on MonitorV2 { definition { ... MonitorV2Definition } + actionRules { + ... MonitorV2ActionRule + } } fragment MonitorV2Definition on MonitorV2Definition { inputQuery { @@ -12809,6 +12885,10 @@ fragment MonitorV2Definition on MonitorV2Definition { ... MonitorV2Scheduling } } +fragment MonitorV2ActionRule on MonitorV2ActionRule { + actionID + levels +} fragment StageQuery on StageQuery { id pipeline @@ -16332,6 +16412,9 @@ fragment MonitorV2 on MonitorV2 { definition { ... MonitorV2Definition } + actionRules { + ... MonitorV2ActionRule + } } fragment MonitorV2Definition on MonitorV2Definition { inputQuery { @@ -16352,6 +16435,10 @@ fragment MonitorV2Definition on MonitorV2Definition { ... MonitorV2Scheduling } } +fragment MonitorV2ActionRule on MonitorV2ActionRule { + actionID + levels +} fragment StageQuery on StageQuery { id pipeline @@ -17880,6 +17967,9 @@ fragment MonitorV2 on MonitorV2 { definition { ... MonitorV2Definition } + actionRules { + ... MonitorV2ActionRule + } } fragment MonitorV2Definition on MonitorV2Definition { inputQuery { @@ -17900,6 +17990,10 @@ fragment MonitorV2Definition on MonitorV2Definition { ... MonitorV2Scheduling } } +fragment MonitorV2ActionRule on MonitorV2ActionRule { + actionID + levels +} fragment StageQuery on StageQuery { id pipeline @@ -18508,6 +18602,196 @@ func saveDataset( return &data, err } +// The query or mutation executed by saveMonitorV2Relations. +const saveMonitorV2Relations_Operation = ` +mutation saveMonitorV2Relations ($monitorId: ObjectId!, $actionRelations: [ActionRelationInput!]) { + monitorV2: saveMonitorV2Relations(monitorId: $monitorId, actionRelations: $actionRelations) { + ... MonitorV2 + } +} +fragment MonitorV2 on MonitorV2 { + id + workspaceId + createdBy + createdDate + name + iconUrl + description + managedById + folderId + comment + rollupStatus + ruleKind + definition { + ... MonitorV2Definition + } + actionRules { + ... MonitorV2ActionRule + } +} +fragment MonitorV2Definition on MonitorV2Definition { + inputQuery { + outputStage + stages { + ... StageQuery + } + } + rules { + ... MonitorV2Rule + } + lookbackTime + dataStabilizationDelay + groupings { + ... MonitorV2Column + } + scheduling { + ... MonitorV2Scheduling + } +} +fragment MonitorV2ActionRule on MonitorV2ActionRule { + actionID + levels +} +fragment StageQuery on StageQuery { + id + pipeline + params + layout + input { + inputName + inputRole + datasetId + datasetPath + stageId + } +} +fragment MonitorV2Rule on MonitorV2Rule { + level + count { + ... MonitorV2CountRule + } + threshold { + ... MonitorV2ThresholdRule + } + promote { + ... MonitorV2PromoteRule + } +} +fragment MonitorV2Column on MonitorV2Column { + linkColumn { + ... MonitorV2LinkColumn + } + columnPath { + ... MonitorV2ColumnPath + } +} +fragment MonitorV2Scheduling on MonitorV2Scheduling { + interval { + ... MonitorV2IntervalSchedule + } + transform { + ... MonitorV2TransformSchedule + } +} +fragment MonitorV2CountRule on MonitorV2CountRule { + compareValues { + ... MonitorV2Comparison + } + compareGroups { + ... MonitorV2ColumnComparison + } +} +fragment MonitorV2ThresholdRule on MonitorV2ThresholdRule { + compareValues { + ... MonitorV2Comparison + } + valueColumnName + aggregation + compareGroups { + ... MonitorV2ColumnComparison + } +} +fragment MonitorV2PromoteRule on MonitorV2PromoteRule { + compareColumns { + ... MonitorV2ColumnComparison + } +} +fragment MonitorV2LinkColumn on MonitorV2LinkColumn { + name + meta { + ... MonitorV2LinkColumnMeta + } +} +fragment MonitorV2ColumnPath on MonitorV2ColumnPath { + name + path +} +fragment MonitorV2IntervalSchedule on MonitorV2IntervalSchedule { + interval + randomize +} +fragment MonitorV2TransformSchedule on MonitorV2TransformSchedule { + freshnessGoal +} +fragment MonitorV2Comparison on MonitorV2Comparison { + compareFn + compareValue { + ... PrimitiveValue + } +} +fragment MonitorV2ColumnComparison on MonitorV2ColumnComparison { + column { + ... MonitorV2Column + } + compareValues { + ... MonitorV2Comparison + } +} +fragment MonitorV2LinkColumnMeta on MonitorV2LinkColumnMeta { + srcFields { + ... MonitorV2ColumnPath + } + dstFields + targetDataset +} +fragment PrimitiveValue on PrimitiveValue { + bool + float64 + int64 + string + timestamp + duration +} +` + +func saveMonitorV2Relations( + ctx context.Context, + client graphql.Client, + monitorId string, + actionRelations []ActionRelationInput, +) (*saveMonitorV2RelationsResponse, error) { + req := &graphql.Request{ + OpName: "saveMonitorV2Relations", + Query: saveMonitorV2Relations_Operation, + Variables: &__saveMonitorV2RelationsInput{ + MonitorId: monitorId, + ActionRelations: actionRelations, + }, + } + var err error + + var data saveMonitorV2RelationsResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} + // The query or mutation executed by saveSourceDataset. const saveSourceDataset_Operation = ` mutation saveSourceDataset ($workspaceId: ObjectId!, $datasetDefinition: DatasetDefinitionInput!, $sourceTable: SourceTableDefinitionInput!, $dep: DependencyHandlingInput) { @@ -19903,6 +20187,9 @@ fragment MonitorV2 on MonitorV2 { definition { ... MonitorV2Definition } + actionRules { + ... MonitorV2ActionRule + } } fragment MonitorV2Definition on MonitorV2Definition { inputQuery { @@ -19923,6 +20210,10 @@ fragment MonitorV2Definition on MonitorV2Definition { ... MonitorV2Scheduling } } +fragment MonitorV2ActionRule on MonitorV2ActionRule { + actionID + levels +} fragment StageQuery on StageQuery { id pipeline diff --git a/client/meta/monitorv2.go b/client/meta/monitorv2.go index fdd00ac8..cf62a7e6 100644 --- a/client/meta/monitorv2.go +++ b/client/meta/monitorv2.go @@ -38,6 +38,11 @@ func (client *Client) DeleteMonitorV2(ctx context.Context, id string) error { return resultStatusError(resp, err) } +func (client *Client) SaveMonitorV2Relations(ctx context.Context, monitorId string, actionRelations []ActionRelationInput) (*MonitorV2, error) { + resp, err := saveMonitorV2Relations(ctx, client.Gql, monitorId, actionRelations) + return monitorV2OrError(resp, err) +} + func (client *Client) LookupMonitorV2(ctx context.Context, workspaceId *string, nameExact *string) (*MonitorV2, error) { resp, err := lookupMonitorV2(ctx, client.Gql, workspaceId, nil, nameExact, nil) if err != nil || resp == nil || len(resp.MonitorV2s.Results) != 1 { diff --git a/client/oid/oid.go b/client/oid/oid.go index 3602c856..2bc7e010 100644 --- a/client/oid/oid.go +++ b/client/oid/oid.go @@ -234,6 +234,10 @@ func MonitorV2Oid(id string) OID { return OID{Id: id, Type: TypeMonitorV2} } +func MonitorV2ActionOid(id string) OID { + return OID{Id: id, Type: TypeMonitorV2Action} +} + func PollerOid(id string) OID { return OID{Id: id, Type: TypePoller} } diff --git a/docs/data-sources/monitor_v2.md b/docs/data-sources/monitor_v2.md index 94199da4..d3335cbe 100644 --- a/docs/data-sources/monitor_v2.md +++ b/docs/data-sources/monitor_v2.md @@ -32,6 +32,7 @@ template and destinations to configure the receiver. ### Read-Only +- `actions` (Block List) The list of shared actions to which this monitor is connected. (see [below for nested schema](#nestedblock--actions)) - `comment` (String) A longer description of the monitor. This can include details like how to resolve the issue, links to runbooks, etc. - `data_stabilization_delay` (String) expresses the minimum time that should elapse before data is considered "good enough" to evaluate. Choosing a delay really depends on the expectations of latency of data and whether data is expected to arrive later than other data and thus would change previously evaluated results. - `description` (String) A brief description of the monitor. @@ -48,6 +49,15 @@ stage pipelines. input is provided, a stage will implicitly follow on from the result of its predecessor. (see [below for nested schema](#nestedblock--stage)) + +### Nested Schema for `actions` + +Read-Only: + +- `levels` (List of String) The alarm level(s) at which this monitor should trigger this shared action. +- `oid` (String) The OID of this shared action. + + ### Nested Schema for `groupings` diff --git a/docs/resources/monitor_v2.md b/docs/resources/monitor_v2.md index 33289bd0..8923bcee 100644 --- a/docs/resources/monitor_v2.md +++ b/docs/resources/monitor_v2.md @@ -36,6 +36,7 @@ its predecessor. (see [below for nested schema](#nestedblock--stage)) ### Optional +- `actions` (Block List) The list of shared actions to which this monitor is connected. (see [below for nested schema](#nestedblock--actions)) - `comment` (String) A longer description of the monitor. This can include details like how to resolve the issue, links to runbooks, etc. - `data_stabilization_delay` (String) expresses the minimum time that should elapse before data is considered "good enough" to evaluate. Choosing a delay really depends on the expectations of latency of data and whether data is expected to arrive later than other data and thus would change previously evaluated results. - `description` (String) A brief description of the monitor. @@ -416,6 +417,18 @@ a stage preceding the last stage. The last stage is an output stage by default. - `pipeline` (String) An OPAL snippet defining a transformation on the selected input. + +### Nested Schema for `actions` + +Required: + +- `oid` (String) The OID of this shared action. + +Optional: + +- `levels` (List of String) The alarm level(s) at which this monitor should trigger this shared action. + + ### Nested Schema for `groupings` diff --git a/observe/data_source_monitor_v2.go b/observe/data_source_monitor_v2.go index a4cb8064..37aaf442 100644 --- a/observe/data_source_monitor_v2.go +++ b/observe/data_source_monitor_v2.go @@ -250,6 +250,28 @@ func dataSourceMonitorV2() *schema.Resource { Type: schema.TypeString, Computed: true, }, + // the following field describes how monitorv2 is connected to shared actions. + "actions": { // [MonitorV2ActionRuleInput] + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "oid": { + Type: schema.TypeString, + Computed: true, + Description: descriptions.Get("monitorv2", "schema", "actions", "oid"), + }, + "levels": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Description: descriptions.Get("monitorv2", "schema", "actions", "levels"), + }, + }, + }, + Description: descriptions.Get("monitorv2", "schema", "actions", "description"), + }, }, } } diff --git a/observe/descriptions/monitorv2.yaml b/observe/descriptions/monitorv2.yaml index b6753d56..928d1cb2 100644 --- a/observe/descriptions/monitorv2.yaml +++ b/observe/descriptions/monitorv2.yaml @@ -102,4 +102,11 @@ schema: Represents two possible column types (link column, columnPath) of an observe dataset. column_comparison: description: | - Specifies the one or multiple values you'd like to compare against the column. \ No newline at end of file + Specifies the one or multiple values you'd like to compare against the column. + actions: + description: | + The list of shared actions to which this monitor is connected. + oid: | + The OID of this shared action. + levels: | + The alarm level(s) at which this monitor should trigger this shared action. \ No newline at end of file diff --git a/observe/resource_monitor_v2.go b/observe/resource_monitor_v2.go index 8165957f..f2187e1b 100644 --- a/observe/resource_monitor_v2.go +++ b/observe/resource_monitor_v2.go @@ -16,9 +16,6 @@ import ( "github.com/observeinc/terraform-provider-observe/observe/descriptions" ) -// TODO: make the schema keys constants? -// annoying to change varnames in 3 non-obvious places - func resourceMonitorV2() *schema.Resource { return &schema.Resource{ Description: descriptions.Get("monitorv2", "description"), @@ -268,6 +265,31 @@ func resourceMonitorV2() *schema.Resource { }, }, // end of fields of MonitorV2DefinitionInput + // the following field describes how monitorv2 is connected to shared actions. + "actions": { // [MonitorV2ActionRuleInput] + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "oid": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validateOID(oid.TypeMonitorV2Action), + Description: descriptions.Get("monitorv2", "schema", "actions", "oid"), + }, + "levels": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validateEnums(gql.AllMonitorV2AlarmLevels), + }, + Description: descriptions.Get("monitorv2", "schema", "actions", "levels"), + }, + }, + }, + Description: descriptions.Get("monitorv2", "schema", "actions", "description"), + }, // the following fields are those that aren't given as input to CU ops, but can be read by R ops. "oid": { // ObjectId! Type: schema.TypeString, @@ -456,6 +478,11 @@ func resourceMonitorV2Create(ctx context.Context, data *schema.ResourceData, met return diag.Errorf("failed to create monitor: %s", err.Error()) } + result, err = relateMonitorV2ToActions(ctx, result.Id, data, client) + if err != nil { + return diags + } + data.SetId(result.Id) return append(diags, resourceMonitorV2Read(ctx, data, meta)...) } @@ -477,7 +504,12 @@ func resourceMonitorV2Update(ctx context.Context, data *schema.ResourceData, met } return nil } - return diag.Errorf("failed to create monitor: %s", err.Error()) + return diag.Errorf("failed to update monitor: %s", err.Error()) + } + + _, err = relateMonitorV2ToActions(ctx, data.Id(), data, client) + if err != nil { + return diag.Errorf("failed to update monitor: %s", err.Error()) } return append(diags, resourceMonitorV2Read(ctx, data, meta)...) @@ -557,6 +589,12 @@ func resourceMonitorV2Read(ctx context.Context, data *schema.ResourceData, meta } } + if len(monitor.ActionRules) > 0 { + if err := data.Set("actions", monitorV2FlattenActionRules(monitor.ActionRules)); err != nil { + diags = append(diags, diag.FromErr(err)...) + } + } + return diags } @@ -592,6 +630,28 @@ func monitorV2FlattenRule(gqlRule gql.MonitorV2Rule) interface{} { return rule } +func monitorV2FlattenActionRules(gqlActionRules []gql.MonitorV2ActionRule) []interface{} { + var actionRules []interface{} + for _, gqlActionRule := range gqlActionRules { + actionRules = append(actionRules, monitorV2FlattenActionRule(gqlActionRule)) + } + return actionRules +} + +func monitorV2FlattenActionRule(gqlActionRule gql.MonitorV2ActionRule) interface{} { + rules := map[string]interface{}{ + "oid": oid.MonitorV2ActionOid(gqlActionRule.ActionID).String(), + } + if len(gqlActionRule.Levels) > 0 { + levels := make([]interface{}, 0) + for _, level := range gqlActionRule.Levels { + levels = append(levels, toSnake(string(level))) + } + rules["levels"] = levels + } + return rules +} + func monitorV2FlattenCountRule(gqlCount gql.MonitorV2CountRule) []interface{} { countRule := map[string]interface{}{} if gqlCount.CompareValues != nil { @@ -1016,6 +1076,7 @@ func newMonitorV2ThresholdRuleInput(path string, data *schema.ResourceData) (thr } compareValues = append(compareValues, *comparisonInput) } + valueColumnName := data.Get(fmt.Sprintf("%svalue_column_name", path)).(string) aggregation := gql.MonitorV2ValueAggregation(toCamel(data.Get(fmt.Sprintf("%saggregation", path)).(string))) @@ -1220,3 +1281,43 @@ func newMonitorV2PrimitiveValue(path string, data *schema.ResourceData, ret *gql } return nil } + +func relateMonitorV2ToActions(ctx context.Context, monitorId string, data *schema.ResourceData, client *observe.Client) (*gql.MonitorV2, error) { + var actionRelations []gql.ActionRelationInput + if _, ok := data.GetOk("actions"); ok { + actionRelations = make([]gql.ActionRelationInput, 0) + for i := range data.Get("actions").([]interface{}) { + actionRule, err := newMonitorV2ActionRuleInput(fmt.Sprintf("actions.%d.", i), data) + if err != nil { + return nil, err + } + actionRelations = append(actionRelations, gql.ActionRelationInput{ + ActionRule: *actionRule, + }) + } + } + return client.SaveMonitorV2Relations(ctx, monitorId, actionRelations) +} + +func newMonitorV2ActionRuleInput(path string, data *schema.ResourceData) (*gql.MonitorV2ActionRuleInput, error) { + // required + actOID, err := oid.NewOID(data.Get(fmt.Sprintf("%soid", path)).(string)) + if err != nil { + return nil, err + } + + // instantiation + act := &gql.MonitorV2ActionRuleInput{ + ActionID: actOID.Id, + } + + // optional + if _, ok := data.GetOk(fmt.Sprintf("%slevels", path)); ok { + act.Levels = make([]gql.MonitorV2AlarmLevel, 0) + for i := range data.Get(fmt.Sprintf("%slevels", path)).([]interface{}) { + act.Levels = append(act.Levels, gql.MonitorV2AlarmLevel(toCamel(data.Get(fmt.Sprintf("%slevels.%d", path, i)).(string)))) + } + } + + return act, nil +} diff --git a/observe/resource_monitor_v2_action_test.go b/observe/resource_monitor_v2_action_test.go index 9158f32e..b1ac2a8c 100644 --- a/observe/resource_monitor_v2_action_test.go +++ b/observe/resource_monitor_v2_action_test.go @@ -52,6 +52,9 @@ func TestAccObserveMonitorV2ActionEmail(t *testing.T) { randomize = "0" } } + actions { + oid = observe_monitor_v2_action.act.oid + } } data "observe_user" "system" { @@ -80,6 +83,7 @@ func TestAccObserveMonitorV2ActionEmail(t *testing.T) { } `, randomPrefix, systemUser()), Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("observe_monitor_v2.first", "actions.#", "1"), resource.TestCheckResourceAttrSet("observe_monitor_v2_action.act", "workspace"), resource.TestCheckResourceAttr("observe_monitor_v2_action.act", "name", randomPrefix), resource.TestCheckResourceAttr("observe_monitor_v2_action.act", "type", "email"), @@ -140,6 +144,9 @@ func TestAccObserveMonitorV2ActionWebhook(t *testing.T) { randomize = "0" } } + actions { + oid = observe_monitor_v2_action.act.oid + } } resource "observe_monitor_v2_action" "act" { @@ -167,6 +174,7 @@ func TestAccObserveMonitorV2ActionWebhook(t *testing.T) { } `, randomPrefix), Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("observe_monitor_v2.first", "actions.#", "1"), resource.TestCheckResourceAttrSet("observe_monitor_v2_action.act", "workspace"), resource.TestCheckResourceAttr("observe_monitor_v2_action.act", "name", randomPrefix), resource.TestCheckResourceAttr("observe_monitor_v2_action.act", "type", "webhook"), @@ -183,3 +191,135 @@ func TestAccObserveMonitorV2ActionWebhook(t *testing.T) { }, }) } + +func TestAccObserveMonitorV2MultipleActionsEmail(t *testing.T) { + randomPrefix := acctest.RandomWithPrefix("tf") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(monitorV2ConfigPreamble+` + resource "observe_monitor_v2" "first" { + workspace = data.observe_workspace.default.oid + rule_kind = "count" + name = "%[1]s" + lookback_time = "30m" + comment = "a descriptive comment" + inputs = { + "test" = observe_datastream.test.dataset + } + stage { + pipeline = <<-EOF + colmake kind:"test", description:"test" + EOF + output_stage = true + } + stage { + pipeline = <<-EOF + filter kind ~ "test" + EOF + } + rules { + level = "informational" + count { + compare_values { + compare_fn = "greater" + value_int64 = [0] + } + } + } + scheduling { + interval { + interval = "15m" + randomize = "0" + } + } + actions { + oid = observe_monitor_v2_action.act1.oid + levels = ["informational"] + } + actions { + oid = observe_monitor_v2_action.act2.oid + levels = ["informational"] + } + } + + data "observe_user" "system" { + email = "%[2]s" + } + + resource "observe_monitor_v2_action" "act1" { + workspace = data.observe_workspace.default.oid + type = "email" + email { + subject = "somebody once told me" + body = "the world is gonna roll me" + fragments = jsonencode({ + foo = "bar" + }) + } + destination { + email { + addresses = ["test@observeinc.com"] + users = [data.observe_user.system.oid] + } + description = "an interesting dest description 1" + } + name = "%[1]s-1" + description = "an interesting description 1" + } + + resource "observe_monitor_v2_action" "act2" { + workspace = data.observe_workspace.default.oid + type = "email" + email { + subject = "never gonna give you up" + body = "never gonna let you down" + fragments = jsonencode({ + fizz = "buzz" + }) + } + destination { + email { + addresses = ["test@observeinc.com"] + users = [data.observe_user.system.oid] + } + description = "an interesting dest description 2" + } + name = "%[1]s-2" + description = "an interesting description 2" + } + `, randomPrefix, systemUser()), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("observe_monitor_v2.first", "actions.#", "2"), + resource.TestCheckResourceAttr("observe_monitor_v2.first", "actions.0.levels.0", "informational"), + resource.TestCheckResourceAttr("observe_monitor_v2.first", "actions.1.levels.0", "informational"), + + resource.TestCheckResourceAttrSet("observe_monitor_v2_action.act1", "workspace"), + resource.TestCheckResourceAttr("observe_monitor_v2_action.act1", "name", randomPrefix+"-1"), + resource.TestCheckResourceAttr("observe_monitor_v2_action.act1", "type", "email"), + resource.TestCheckResourceAttr("observe_monitor_v2_action.act1", "description", "an interesting description 1"), + resource.TestCheckResourceAttr("observe_monitor_v2_action.act1", "email.0.fragments", "{\"foo\":\"bar\"}"), + resource.TestCheckResourceAttr("observe_monitor_v2_action.act1", "email.0.subject", "somebody once told me"), + resource.TestCheckResourceAttr("observe_monitor_v2_action.act1", "email.0.body", "the world is gonna roll me"), + resource.TestCheckResourceAttr("observe_monitor_v2_action.act1", "destination.0.email.0.addresses.0", "test@observeinc.com"), + resource.TestCheckResourceAttr("observe_monitor_v2_action.act1", "destination.0.description", "an interesting dest description 1"), + resource.TestCheckResourceAttr("observe_monitor_v2_action.act1", "destination.0.email.0.users.#", "1"), + + resource.TestCheckResourceAttrSet("observe_monitor_v2_action.act2", "workspace"), + resource.TestCheckResourceAttr("observe_monitor_v2_action.act2", "name", randomPrefix+"-2"), + resource.TestCheckResourceAttr("observe_monitor_v2_action.act2", "type", "email"), + resource.TestCheckResourceAttr("observe_monitor_v2_action.act2", "description", "an interesting description 2"), + resource.TestCheckResourceAttr("observe_monitor_v2_action.act2", "email.0.fragments", "{\"fizz\":\"buzz\"}"), + resource.TestCheckResourceAttr("observe_monitor_v2_action.act2", "email.0.subject", "never gonna give you up"), + resource.TestCheckResourceAttr("observe_monitor_v2_action.act2", "email.0.body", "never gonna let you down"), + resource.TestCheckResourceAttr("observe_monitor_v2_action.act2", "destination.0.email.0.addresses.0", "test@observeinc.com"), + resource.TestCheckResourceAttr("observe_monitor_v2_action.act2", "destination.0.description", "an interesting dest description 2"), + resource.TestCheckResourceAttr("observe_monitor_v2_action.act2", "destination.0.email.0.users.#", "1"), + ), + }, + }, + }) +}