diff --git a/http/swagger.yml b/http/swagger.yml index c6080b3142b..7dae54a4db6 100644 --- a/http/swagger.yml +++ b/http/swagger.yml @@ -11259,6 +11259,7 @@ components: - $ref: "#/components/schemas/PagerDutyNotificationRule" - $ref: "#/components/schemas/HTTPNotificationRule" - $ref: "#/components/schemas/TelegramNotificationRule" + - $ref: "#/components/schemas/TeamsNotificationRule" discriminator: propertyName: type mapping: @@ -11267,6 +11268,7 @@ components: pagerduty: "#/components/schemas/PagerDutyNotificationRule" http: "#/components/schemas/HTTPNotificationRule" telegram: "#/components/schemas/TelegramNotificationRule" + teams: "#/components/schemas/TeamsNotificationRule" NotificationRule: allOf: - $ref: "#/components/schemas/NotificationRuleDiscriminator" @@ -11438,6 +11440,27 @@ components: allOf: - $ref: "#/components/schemas/NotificationRuleBase" - $ref: "#/components/schemas/SlackNotificationRuleBase" + TeamsNotificationRule: + allOf: + - $ref: "#/components/schemas/NotificationRuleBase" + - $ref: "#/components/schemas/TeamsNotificationRuleBase" + TeamsNotificationRuleBase: + type: object + required: [type, title, messageTemplate] + properties: + type: + description: The discriminator between other types of notification rules is "teams". + type: string + enum: [teams] + title: + description: The message title as a flux interpolated string. + type: string + messageTemplate: + description: The message template as a flux interpolated string. + type: string + summary: + description: The message summary as a flux interpolated string. + type: string SMTPNotificationRule: allOf: - $ref: "#/components/schemas/NotificationRuleBase" @@ -11512,6 +11535,7 @@ components: - $ref: "#/components/schemas/PagerDutyNotificationEndpoint" - $ref: "#/components/schemas/HTTPNotificationEndpoint" - $ref: "#/components/schemas/TelegramNotificationEndpoint" + - $ref: "#/components/schemas/TeamsNotificationEndpoint" discriminator: propertyName: type mapping: @@ -11519,6 +11543,7 @@ components: pagerduty: "#/components/schemas/PagerDutyNotificationEndpoint" http: "#/components/schemas/HTTPNotificationEndpoint" telegram: "#/components/schemas/TelegramNotificationEndpoint" + teams: "#/components/schemas/TeamsNotificationEndpoint" NotificationEndpoint: allOf: - $ref: "#/components/schemas/NotificationEndpointDiscrimator" @@ -11650,9 +11675,22 @@ components: channel: description: ID of the telegram channel, a chat_id in https://core.telegram.org/bots/api#sendmessage . type: string + TeamsNotificationEndpoint: + type: object + allOf: + - $ref: "#/components/schemas/NotificationEndpointBase" + - type: object + required: [url] + properties: + url: + description: Teams incoming webhook URL, see https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using#setting-up-a-custom-incoming-webhook . + type: string + secretURLSuffix: + description: A secret suffix that is appended to teams incoming webhook URL. + type: string NotificationEndpointType: type: string - enum: ["slack", "pagerduty", "http", "telegram"] + enum: ["slack", "pagerduty", "http", "telegram", "teams"] DBRP: required: - orgID diff --git a/notification/endpoint/endpoint.go b/notification/endpoint/endpoint.go index 05956ba247e..6656cef1e11 100644 --- a/notification/endpoint/endpoint.go +++ b/notification/endpoint/endpoint.go @@ -13,6 +13,7 @@ const ( PagerDutyType = "pagerduty" HTTPType = "http" TelegramType = "telegram" + TeamsType = "teams" ) var typeToEndpoint = map[string]func() influxdb.NotificationEndpoint{ @@ -20,6 +21,7 @@ var typeToEndpoint = map[string]func() influxdb.NotificationEndpoint{ PagerDutyType: func() influxdb.NotificationEndpoint { return &PagerDuty{} }, HTTPType: func() influxdb.NotificationEndpoint { return &HTTP{} }, TelegramType: func() influxdb.NotificationEndpoint { return &Telegram{} }, + TeamsType: func() influxdb.NotificationEndpoint { return &Teams{} }, } // UnmarshalJSON will convert the bytes to notification endpoint. diff --git a/notification/endpoint/endpoint_test.go b/notification/endpoint/endpoint_test.go index 278d398676f..ce08c681122 100644 --- a/notification/endpoint/endpoint_test.go +++ b/notification/endpoint/endpoint_test.go @@ -182,6 +182,24 @@ func TestValidEndpoint(t *testing.T) { }, err: nil, }, + { + name: "empty teams url", + src: &endpoint.Teams{ + Base: goodBase, + }, + err: &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "teams: empty URL", + }, + }, + { + name: "empty teams SecretURLSuffix", + src: &endpoint.Teams{ + Base: goodBase, + URL: "http://localhost", + }, + err: nil, + }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { @@ -288,6 +306,39 @@ func TestJSON(t *testing.T) { Token: influxdb.SecretField{Key: "token-key-1"}, }, }, + { + name: "teams with secretURLSuffix", + src: &endpoint.Teams{ + Base: endpoint.Base{ + ID: influxTesting.MustIDBase16Ptr(id1), + Name: "name1", + OrgID: influxTesting.MustIDBase16Ptr(id3), + Status: influxdb.Active, + CRUDLog: influxdb.CRUDLog{ + CreatedAt: timeGen1.Now(), + UpdatedAt: timeGen2.Now(), + }, + }, + URL: "https://outlook.office.com/webhook/", + SecretURLSuffix: influxdb.SecretField{Key: "token-key-1"}, + }, + }, + { + name: "teams without secretURLSuffix", + src: &endpoint.Teams{ + Base: endpoint.Base{ + ID: influxTesting.MustIDBase16Ptr(id1), + Name: "name1", + OrgID: influxTesting.MustIDBase16Ptr(id3), + Status: influxdb.Active, + CRUDLog: influxdb.CRUDLog{ + CreatedAt: timeGen1.Now(), + UpdatedAt: timeGen2.Now(), + }, + }, + URL: "https://outlook.office.com/webhook/0acbc9c2-c262-11ea-b3de-0242ac130004", + }, + }, } for _, c := range cases { b, err := json.Marshal(c.src) @@ -461,6 +512,42 @@ func TestBackFill(t *testing.T) { }, }, }, + { + name: "simple Teams", + src: &endpoint.Teams{ + Base: endpoint.Base{ + ID: influxTesting.MustIDBase16Ptr(id1), + Name: "name1", + OrgID: influxTesting.MustIDBase16Ptr(id3), + Status: influxdb.Active, + CRUDLog: influxdb.CRUDLog{ + CreatedAt: timeGen1.Now(), + UpdatedAt: timeGen2.Now(), + }, + }, + URL: "https://outlook.office.com/webhook/", + SecretURLSuffix: influxdb.SecretField{ + Value: strPtr("token-value"), + }, + }, + target: &endpoint.Teams{ + Base: endpoint.Base{ + ID: influxTesting.MustIDBase16Ptr(id1), + Name: "name1", + OrgID: influxTesting.MustIDBase16Ptr(id3), + Status: influxdb.Active, + CRUDLog: influxdb.CRUDLog{ + CreatedAt: timeGen1.Now(), + UpdatedAt: timeGen2.Now(), + }, + }, + URL: "https://outlook.office.com/webhook/", + SecretURLSuffix: influxdb.SecretField{ + Key: id1 + "-token", + Value: strPtr("token-value"), + }, + }, + }, } for _, c := range cases { c.src.BackfillSecretKeys() @@ -588,6 +675,32 @@ func TestSecretFields(t *testing.T) { }, }, }, + { + name: "simple Teams", + src: &endpoint.Teams{ + Base: endpoint.Base{ + ID: influxTesting.MustIDBase16Ptr(id1), + Name: "name1", + OrgID: influxTesting.MustIDBase16Ptr(id3), + Status: influxdb.Active, + CRUDLog: influxdb.CRUDLog{ + CreatedAt: timeGen1.Now(), + UpdatedAt: timeGen2.Now(), + }, + }, + URL: "https://outlook.office.com/webhook/", + SecretURLSuffix: influxdb.SecretField{ + Key: id1 + "-token", + Value: strPtr("token-value"), + }, + }, + secrets: []influxdb.SecretField{ + { + Key: id1 + "-token", + Value: strPtr("token-value"), + }, + }, + }, } for _, c := range cases { secretFields := c.src.SecretFields() diff --git a/notification/endpoint/teams.go b/notification/endpoint/teams.go new file mode 100644 index 00000000000..8e428d613be --- /dev/null +++ b/notification/endpoint/teams.go @@ -0,0 +1,72 @@ +package endpoint + +import ( + "encoding/json" + + "github.com/influxdata/influxdb/v2" +) + +var _ influxdb.NotificationEndpoint = &Teams{} + +const teamsSecretSuffix = "-token" + +// Teams is the notification endpoint config of Microdoft teams. +type Teams struct { + Base + // URL is the teams incoming webhook URL, see https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using#setting-up-a-custom-incoming-webhook , + // for example: https://outlook.office.com/webhook/0acbc9c2-c262-11ea-b3de-0242ac130004 + URL string `json:"url"` + // SecretURLSuffix is an optional secret suffix that is added to URL , + // for example: 0acbc9c2-c262-11ea-b3de-0242ac130004 is the secret part that is added to https://outlook.office.com/webhook/ + SecretURLSuffix influxdb.SecretField `json:"secretURLSuffix"` +} + +// BackfillSecretKeys fill back the secret field key during the unmarshalling +// if value of that secret field is not nil. +func (s *Teams) BackfillSecretKeys() { + if s.SecretURLSuffix.Key == "" && s.SecretURLSuffix.Value != nil { + s.SecretURLSuffix.Key = s.idStr() + teamsSecretSuffix + } +} + +// SecretFields return available secret fields. +func (s Teams) SecretFields() []influxdb.SecretField { + arr := []influxdb.SecretField{} + if s.SecretURLSuffix.Key != "" { + arr = append(arr, s.SecretURLSuffix) + } + return arr +} + +// Valid returns error if some configuration is invalid +func (s Teams) Valid() error { + if err := s.Base.valid(); err != nil { + return err + } + if s.URL == "" { + return &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "teams: empty URL", + } + } + return nil +} + +type teamsAlias Teams + +// MarshalJSON implement json.Marshaler interface. +func (s Teams) MarshalJSON() ([]byte, error) { + return json.Marshal( + struct { + teamsAlias + Type string `json:"type"` + }{ + teamsAlias: teamsAlias(s), + Type: s.Type(), + }) +} + +// Type returns the type. +func (s Teams) Type() string { + return TeamsType +} diff --git a/notification/rule/rule.go b/notification/rule/rule.go index ec875b48360..7b01b7c99a7 100644 --- a/notification/rule/rule.go +++ b/notification/rule/rule.go @@ -17,6 +17,7 @@ var typeToRule = map[string](func() influxdb.NotificationRule){ "pagerduty": func() influxdb.NotificationRule { return &PagerDuty{} }, "http": func() influxdb.NotificationRule { return &HTTP{} }, "telegram": func() influxdb.NotificationRule { return &Telegram{} }, + "teams": func() influxdb.NotificationRule { return &Teams{} }, } // UnmarshalJSON will convert diff --git a/notification/rule/rule_test.go b/notification/rule/rule_test.go index 91475cd950b..e0fc8c90d96 100644 --- a/notification/rule/rule_test.go +++ b/notification/rule/rule_test.go @@ -356,6 +356,42 @@ func TestJSON(t *testing.T) { MessageTemplate: "blah", }, }, + { + name: "simple teams", + src: &rule.Teams{ + Base: rule.Base{ + ID: influxTesting.MustIDBase16(id1), + OwnerID: influxTesting.MustIDBase16(id2), + Name: "name1", + OrgID: influxTesting.MustIDBase16(id3), + RunbookLink: "runbooklink1", + SleepUntil: &time3, + Every: mustDuration("1h"), + TagRules: []notification.TagRule{ + { + Tag: influxdb.Tag{ + Key: "k1", + Value: "v1", + }, + Operator: influxdb.NotEqual, + }, + { + Tag: influxdb.Tag{ + Key: "k2", + Value: "v2", + }, + Operator: influxdb.RegexEqual, + }, + }, + CRUDLog: influxdb.CRUDLog{ + CreatedAt: timeGen1.Now(), + UpdatedAt: timeGen2.Now(), + }, + }, + Title: "my title", + MessageTemplate: "msg1", + }, + }, } for _, c := range cases { b, err := json.Marshal(c.src) diff --git a/notification/rule/teams.go b/notification/rule/teams.go new file mode 100644 index 00000000000..71be3d8b19c --- /dev/null +++ b/notification/rule/teams.go @@ -0,0 +1,127 @@ +package rule + +import ( + "encoding/json" + "fmt" + + "github.com/influxdata/flux/ast" + "github.com/influxdata/influxdb/v2" + "github.com/influxdata/influxdb/v2/notification/endpoint" + "github.com/influxdata/influxdb/v2/notification/flux" +) + +// Teams is the notification rule config of Microsoft Teams. +type Teams struct { + Base + Title string `json:"title"` + MessageTemplate string `json:"messageTemplate"` + Summary string `json:"summary"` +} + +// GenerateFlux generates a flux script for the teams notification rule. +func (s *Teams) GenerateFlux(e influxdb.NotificationEndpoint) (string, error) { + teamsEndpoint, ok := e.(*endpoint.Teams) + if !ok { + return "", fmt.Errorf("endpoint provided is a %s, not a Teams endpoint", e.Type()) + } + p, err := s.GenerateFluxAST(teamsEndpoint) + if err != nil { + return "", err + } + return ast.Format(p), nil +} + +// GenerateFluxAST generates a flux AST for the teams notification rule. +func (s *Teams) GenerateFluxAST(e *endpoint.Teams) (*ast.Package, error) { + f := flux.File( + s.Name, + flux.Imports("influxdata/influxdb/monitor", "contrib/sranka/teams", "influxdata/influxdb/secrets", "experimental"), + s.generateFluxASTBody(e), + ) + return &ast.Package{Package: "main", Files: []*ast.File{f}}, nil +} + +func (s *Teams) generateFluxASTBody(e *endpoint.Teams) []ast.Statement { + var statements []ast.Statement + statements = append(statements, s.generateTaskOption()) + statements = append(statements, s.generateFluxASTSecrets(e)) + statements = append(statements, s.generateFluxASTEndpoint(e)) + statements = append(statements, s.generateFluxASTNotificationDefinition(e)) + statements = append(statements, s.generateFluxASTStatuses()) + statements = append(statements, s.generateLevelChecks()...) + statements = append(statements, s.generateFluxASTNotifyPipe(e)) + + return statements +} + +func (s *Teams) generateFluxASTSecrets(e *endpoint.Teams) ast.Statement { + if e.SecretURLSuffix.Key != "" { + call := flux.Call(flux.Member("secrets", "get"), flux.Object(flux.Property("key", flux.String(e.SecretURLSuffix.Key)))) + return flux.DefineVariable("teams_url_suffix", call) + } + return flux.DefineVariable("teams_url_suffix", flux.String("")) +} + +func (s *Teams) generateFluxASTEndpoint(e *endpoint.Teams) ast.Statement { + props := []*ast.Property{} + props = append(props, flux.Property("url", flux.String(e.URL+"${teams_url_suffix}"))) + call := flux.Call(flux.Member("teams", "endpoint"), flux.Object(props...)) + + return flux.DefineVariable("teams_endpoint", call) +} + +func (s *Teams) generateFluxASTNotifyPipe(e *endpoint.Teams) ast.Statement { + endpointProps := []*ast.Property{} + endpointProps = append(endpointProps, flux.Property("title", flux.String(s.Title))) + endpointProps = append(endpointProps, flux.Property("text", flux.String(s.MessageTemplate))) + endpointProps = append(endpointProps, flux.Property("summary", flux.String(s.Summary))) + endpointFn := flux.Function(flux.FunctionParams("r"), flux.Object(endpointProps...)) + + props := []*ast.Property{} + props = append(props, flux.Property("data", flux.Identifier("notification"))) + props = append(props, flux.Property("endpoint", + flux.Call(flux.Identifier("teams_endpoint"), flux.Object(flux.Property("mapFn", endpointFn))))) + + call := flux.Call(flux.Member("monitor", "notify"), flux.Object(props...)) + + return flux.ExpressionStatement(flux.Pipe(flux.Identifier("all_statuses"), call)) +} + +type teamsAlias Teams + +// MarshalJSON implement json.Marshaler interface. +func (s Teams) MarshalJSON() ([]byte, error) { + return json.Marshal( + struct { + teamsAlias + Type string `json:"type"` + }{ + teamsAlias: teamsAlias(s), + Type: s.Type(), + }) +} + +// Valid returns where the config is valid. +func (s Teams) Valid() error { + if err := s.Base.valid(); err != nil { + return err + } + if s.Title == "" { + return &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "teams: empty title", + } + } + if s.MessageTemplate == "" { + return &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "teams: empty messageTemplate", + } + } + return nil +} + +// Type returns the type of the rule config. +func (s Teams) Type() string { + return endpoint.TeamsType +} diff --git a/notification/rule/teams_test.go b/notification/rule/teams_test.go new file mode 100644 index 00000000000..6d6390b4104 --- /dev/null +++ b/notification/rule/teams_test.go @@ -0,0 +1,335 @@ +package rule_test + +import ( + "testing" + + "github.com/andreyvit/diff" + "github.com/influxdata/influxdb/v2" + "github.com/influxdata/influxdb/v2/notification" + "github.com/influxdata/influxdb/v2/notification/endpoint" + "github.com/influxdata/influxdb/v2/notification/rule" + influxTesting "github.com/influxdata/influxdb/v2/testing" +) + +var _ influxdb.NotificationRule = &rule.Teams{} + +func TestTeams_GenerateFlux(t *testing.T) { + tests := []struct { + name string + rule *rule.Teams + endpoint influxdb.NotificationEndpoint + script string + }{ + { + name: "incompatible with endpoint", + endpoint: &endpoint.Slack{ + Base: endpoint.Base{ + ID: idPtr(3), + Name: "foo", + }, + URL: "http://whatever", + }, + rule: &rule.Teams{ + Title: "blah", + MessageTemplate: "blah", + Base: rule.Base{ + ID: 1, + EndpointID: 3, + Name: "foo", + Every: mustDuration("1h"), + StatusRules: []notification.StatusRule{ + { + CurrentLevel: notification.Critical, + }, + }, + TagRules: []notification.TagRule{ + { + Tag: influxdb.Tag{ + Key: "foo", + Value: "bar", + }, + Operator: influxdb.Equal, + }, + { + Tag: influxdb.Tag{ + Key: "baz", + Value: "bang", + }, + Operator: influxdb.Equal, + }, + }, + }, + }, + script: "", //no script generated because of incompatible endpoint + }, + { + name: "notify on crit", + endpoint: &endpoint.Teams{ + Base: endpoint.Base{ + ID: idPtr(3), + Name: "foo", + }, + URL: "http://whatever", + }, + rule: &rule.Teams{ + Title: "bleh", + MessageTemplate: "blah", + Base: rule.Base{ + ID: 1, + EndpointID: 3, + Name: "foo", + Every: mustDuration("1h"), + StatusRules: []notification.StatusRule{ + { + CurrentLevel: notification.Critical, + }, + }, + TagRules: []notification.TagRule{ + { + Tag: influxdb.Tag{ + Key: "foo", + Value: "bar", + }, + Operator: influxdb.Equal, + }, + { + Tag: influxdb.Tag{ + Key: "baz", + Value: "bang", + }, + Operator: influxdb.Equal, + }, + }, + }, + }, + script: `package main +// foo +import "influxdata/influxdb/monitor" +import "contrib/sranka/teams" +import "influxdata/influxdb/secrets" +import "experimental" + +option task = {name: "foo", every: 1h} + +teams_url_suffix = "" +teams_endpoint = teams["endpoint"](url: "http://whatever${teams_url_suffix}") +notification = { + _notification_rule_id: "0000000000000001", + _notification_rule_name: "foo", + _notification_endpoint_id: "0000000000000003", + _notification_endpoint_name: "foo", +} +statuses = monitor["from"](start: -2h, fn: (r) => + (r["foo"] == "bar" and r["baz"] == "bang")) +crit = statuses + |> filter(fn: (r) => + (r["_level"] == "crit")) +all_statuses = crit + |> filter(fn: (r) => + (r["_time"] > experimental["subDuration"](from: now(), d: 1h))) + +all_statuses + |> monitor["notify"](data: notification, endpoint: teams_endpoint(mapFn: (r) => + ({title: "bleh", text: "blah", summary: ""})))`, + }, + { + name: "with SecretUrlSuffix", + endpoint: &endpoint.Teams{ + Base: endpoint.Base{ + ID: idPtr(3), + Name: "foo", + }, + URL: "http://whatever", + SecretURLSuffix: influxdb.SecretField{Key: "3-token"}, + }, + rule: &rule.Teams{ + Title: "bleh", + MessageTemplate: "blah", + Base: rule.Base{ + ID: 1, + EndpointID: 3, + Name: "foo", + Every: mustDuration("1h"), + StatusRules: []notification.StatusRule{ + { + CurrentLevel: notification.Any, + }, + }, + TagRules: []notification.TagRule{ + { + Tag: influxdb.Tag{ + Key: "foo", + Value: "bar", + }, + Operator: influxdb.Equal, + }, + { + Tag: influxdb.Tag{ + Key: "baz", + Value: "bang", + }, + Operator: influxdb.Equal, + }, + }, + }, + }, + script: `package main +// foo +import "influxdata/influxdb/monitor" +import "contrib/sranka/teams" +import "influxdata/influxdb/secrets" +import "experimental" + +option task = {name: "foo", every: 1h} + +teams_url_suffix = secrets["get"](key: "3-token") +teams_endpoint = teams["endpoint"](url: "http://whatever${teams_url_suffix}") +notification = { + _notification_rule_id: "0000000000000001", + _notification_rule_name: "foo", + _notification_endpoint_id: "0000000000000003", + _notification_endpoint_name: "foo", +} +statuses = monitor["from"](start: -2h, fn: (r) => + (r["foo"] == "bar" and r["baz"] == "bang")) +any = statuses + |> filter(fn: (r) => + (true)) +all_statuses = any + |> filter(fn: (r) => + (r["_time"] > experimental["subDuration"](from: now(), d: 1h))) + +all_statuses + |> monitor["notify"](data: notification, endpoint: teams_endpoint(mapFn: (r) => + ({title: "bleh", text: "blah", summary: ""})))`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + script, err := tt.rule.GenerateFlux(tt.endpoint) + if err != nil { + if script != "" { + t.Errorf("Failed to generate flux: %v", err) + } + return + } + + if got, want := script, tt.script; got != want { + t.Errorf("\n\nStrings do not match:\n\n%s", diff.LineDiff(got, want)) + } + }) + } +} + +func TestTeams_Valid(t *testing.T) { + cases := []struct { + name string + rule *rule.Teams + err error + }{ + { + name: "valid template", + rule: &rule.Teams{ + Title: "abc", + MessageTemplate: "blah", + Base: rule.Base{ + ID: 1, + EndpointID: 3, + OwnerID: 4, + OrgID: 5, + Name: "foo", + Every: mustDuration("1h"), + StatusRules: []notification.StatusRule{ + { + CurrentLevel: notification.Critical, + }, + }, + TagRules: []notification.TagRule{}, + }, + }, + err: nil, + }, + { + name: "missing MessageTemplate", + rule: &rule.Teams{ + Title: "abc", + MessageTemplate: "", + Base: rule.Base{ + ID: 1, + EndpointID: 3, + OwnerID: 4, + OrgID: 5, + Name: "foo", + Every: mustDuration("1h"), + StatusRules: []notification.StatusRule{ + { + CurrentLevel: notification.Critical, + }, + }, + TagRules: []notification.TagRule{}, + }, + }, + err: &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "teams: empty messageTemplate", + }, + }, + { + name: "missing Title", + rule: &rule.Teams{ + Title: "", + MessageTemplate: "abc", + Base: rule.Base{ + ID: 1, + EndpointID: 3, + OwnerID: 4, + OrgID: 5, + Name: "foo", + Every: mustDuration("1h"), + StatusRules: []notification.StatusRule{ + { + CurrentLevel: notification.Critical, + }, + }, + TagRules: []notification.TagRule{}, + }, + }, + err: &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "teams: empty title", + }, + }, + { + name: "missing EndpointID", + rule: &rule.Teams{ + MessageTemplate: "", + Base: rule.Base{ + ID: 1, + // EndpointID: 3, + OwnerID: 4, + OrgID: 5, + Name: "foo", + Every: mustDuration("1h"), + StatusRules: []notification.StatusRule{ + { + CurrentLevel: notification.Critical, + }, + }, + TagRules: []notification.TagRule{}, + }, + }, + err: &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "Notification Rule EndpointID is invalid", + }, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got := c.rule.Valid() + influxTesting.ErrorsEqual(t, got, c.err) + }) + } + +} diff --git a/ui/src/alerting/constants/index.ts b/ui/src/alerting/constants/index.ts index 1caeb14d081..12bdce526d6 100644 --- a/ui/src/alerting/constants/index.ts +++ b/ui/src/alerting/constants/index.ts @@ -74,6 +74,7 @@ export const DEFAULT_ENDPOINT_URLS = { slack: 'https://hooks.slack.com/services/X/X/X', pagerduty: 'https://events.pagerduty.com/v2/enqueue', http: 'https://www.example.com/endpoint', + teams: 'https://office.outlook.com/hook/XXXX', } export const NEW_ENDPOINT_DRAFT: NotificationEndpoint = { diff --git a/ui/src/notifications/endpoints/components/EndpointCards.tsx b/ui/src/notifications/endpoints/components/EndpointCards.tsx index e88c8097452..82d1d89746b 100644 --- a/ui/src/notifications/endpoints/components/EndpointCards.tsx +++ b/ui/src/notifications/endpoints/components/EndpointCards.tsx @@ -56,6 +56,9 @@ const EmptyEndpointList: FC<{searchTerm: string}> = ({searchTerm}) => { if (isFlagEnabled('notification-endpoint-telegram')) { conditionalEndpoints.push('Telegram') } + if (isFlagEnabled('notification-endpoint-teams')) { + conditionalEndpoints.push('Teams') + } return ( diff --git a/ui/src/notifications/endpoints/components/EndpointOptions.tsx b/ui/src/notifications/endpoints/components/EndpointOptions.tsx index 542a906e77a..8a75266e338 100644 --- a/ui/src/notifications/endpoints/components/EndpointOptions.tsx +++ b/ui/src/notifications/endpoints/components/EndpointOptions.tsx @@ -6,6 +6,7 @@ import EndpointOptionsSlack from './EndpointOptionsSlack' import EndpointOptionsPagerDuty from './EndpointOptionsPagerDuty' import EndpointOptionsHTTP from './EndpointOptionsHTTP' import EndpointOptionsTelegram from './EndpointOptionsTelegram' +import EndpointOptionsTeams from './EndpointOptionsTeams' // Types import { @@ -14,6 +15,7 @@ import { PagerDutyNotificationEndpoint, HTTPNotificationEndpoint, TelegramNotificationEndpoint, + TeamsNotificationEndpoint, } from 'src/types' interface Props { @@ -77,6 +79,17 @@ const EndpointOptions: FC = ({ ) } + case 'teams': { + const {url, secretURLSuffix} = endpoint as TeamsNotificationEndpoint + return ( + + ) + } + default: throw new Error( `Unknown endpoint type for endpoint: ${JSON.stringify( diff --git a/ui/src/notifications/endpoints/components/EndpointOptionsTeams.tsx b/ui/src/notifications/endpoints/components/EndpointOptionsTeams.tsx new file mode 100644 index 00000000000..4de6bbf11e5 --- /dev/null +++ b/ui/src/notifications/endpoints/components/EndpointOptionsTeams.tsx @@ -0,0 +1,57 @@ +// Libraries +import React, {FC, ChangeEvent} from 'react' + +// Components +import { + Input, + FormElement, + Panel, + Grid, + Columns, + InputType, +} from '@influxdata/clockface' + +interface Props { + url: string + secretURLSuffix: string + onChange: (e: ChangeEvent) => void +} + +const EndpointOptionsTeams: FC = ({url, secretURLSuffix, onChange}) => { + return ( + + +

Teams Options

+
+ + + + + + + + + + + + + + + + +
+ ) +} + +export default EndpointOptionsTeams diff --git a/ui/src/notifications/endpoints/components/EndpointOverlay.reducer.ts b/ui/src/notifications/endpoints/components/EndpointOverlay.reducer.ts index c8532a5cae2..6690b4af6d6 100644 --- a/ui/src/notifications/endpoints/components/EndpointOverlay.reducer.ts +++ b/ui/src/notifications/endpoints/components/EndpointOverlay.reducer.ts @@ -34,6 +34,8 @@ export const reducer = ( 'headers', 'clientURL', 'routingKey', + 'channel', + 'secretURLSuffix', ]) switch (endpoint.type) { @@ -66,6 +68,13 @@ export const reducer = ( token: '', channel: '', } + case 'teams': + return { + ...baseProps, + type: 'teams', + url: DEFAULT_ENDPOINT_URLS.teams, + token: '', + } } } return state diff --git a/ui/src/notifications/endpoints/components/EndpointTypeDropdown.tsx b/ui/src/notifications/endpoints/components/EndpointTypeDropdown.tsx index 6e795b69e82..98b83a2c95e 100644 --- a/ui/src/notifications/endpoints/components/EndpointTypeDropdown.tsx +++ b/ui/src/notifications/endpoints/components/EndpointTypeDropdown.tsx @@ -22,6 +22,9 @@ function isFlaggedOn(type: string) { if (type === 'telegram') { return isFlagEnabled('notification-endpoint-telegram') } + if (type === 'teams') { + return isFlagEnabled('notification-endpoint-teams') + } return true } @@ -41,6 +44,7 @@ const types: EndpointType[] = [ {name: 'Slack', type: 'slack', id: 'slack'}, {name: 'Pagerduty', type: 'pagerduty', id: 'pagerduty'}, {name: 'Telegram', type: 'telegram', id: 'telegram'}, + {name: 'Teams', type: 'teams', id: 'teams'}, ] const EndpointTypeDropdown: FC = ({ diff --git a/ui/src/notifications/endpoints/components/EndpointsColumn.tsx b/ui/src/notifications/endpoints/components/EndpointsColumn.tsx index 6ed38cb3cb8..f31a0ba900f 100644 --- a/ui/src/notifications/endpoints/components/EndpointsColumn.tsx +++ b/ui/src/notifications/endpoints/components/EndpointsColumn.tsx @@ -32,6 +32,9 @@ const EndpointsColumn: FC = ({history, match, endpoints, tabIndex}) => { if (isFlagEnabled('notification-endpoint-telegram')) { conditionalEndpoints.push('Telegram') } + if (isFlagEnabled('notification-endpoint-teams')) { + conditionalEndpoints.push('Teams') + } const tooltipContents = ( <> diff --git a/ui/src/notifications/rules/components/RuleMessageContents.tsx b/ui/src/notifications/rules/components/RuleMessageContents.tsx index 1106263cc43..adbe22af5b9 100644 --- a/ui/src/notifications/rules/components/RuleMessageContents.tsx +++ b/ui/src/notifications/rules/components/RuleMessageContents.tsx @@ -6,6 +6,7 @@ import SlackMessage from './SlackMessage' import SMTPMessage from './SMTPMessage' import PagerDutyMessage from './PagerDutyMessage' import TelegramMessage from './TelegramMessage' +import TeamsMessage from './TeamsMessage' // Utils import {useRuleDispatch} from './RuleOverlayProvider' @@ -71,6 +72,18 @@ const RuleMessageContents: FC = ({rule}) => { /> ) } + + case 'teams': { + const {title, messageTemplate} = rule + return ( + + ) + } + case 'http': { return <> } diff --git a/ui/src/notifications/rules/components/TeamsMessage.tsx b/ui/src/notifications/rules/components/TeamsMessage.tsx new file mode 100644 index 00000000000..9b837652852 --- /dev/null +++ b/ui/src/notifications/rules/components/TeamsMessage.tsx @@ -0,0 +1,32 @@ +// Libraries +import React, {FC, ChangeEvent} from 'react' + +// Components +import {Form, Input, TextArea} from '@influxdata/clockface' + +interface Props { + title: string + messageTemplate: string + onChange: (e: ChangeEvent) => void +} + +const TeamsMessage: FC = ({title, messageTemplate, onChange}) => { + return ( + <> + + + + +