diff --git a/check.go b/check.go index 16bfdd1b27..503f92441b 100644 --- a/check.go +++ b/check.go @@ -21,7 +21,7 @@ type Check interface { GenerateFlux() (string, error) GetAuthID() ID json.Marshaler - Updator + Updater Getter } diff --git a/notification.go b/notification.go index 3bb34e95c8..8921e90168 100644 --- a/notification.go +++ b/notification.go @@ -5,9 +5,9 @@ import ( "encoding/json" ) -// Updator is general interface to embed +// Updater is general interface to embed // with any domain level interface to do crud related ops. -type Updator interface { +type Updater interface { CRUDLogSetter SetID(id ID) SetOrgID(id ID) @@ -34,7 +34,7 @@ type NotificationRule interface { Valid() error Type() string json.Marshaler - Updator + Updater Getter SetOwnerID(id ID) GetOwnerID() ID @@ -71,12 +71,14 @@ func (f NotificationRuleFilter) QueryParams() map[string][]string { return qp } +// NotificationRuleUpdate is the set of upgrade fields for patch request. type NotificationRuleUpdate struct { Name *string `json:"name,omitempty"` Description *string `json:"description,omitempty"` Status *Status `json:"status,omitempty"` } +// Valid will verify if the NotificationRuleUpdate is valid. func (n *NotificationRuleUpdate) Valid() error { if n.Name != nil && *n.Name == "" { return &Error{ diff --git a/notification/endpoint/endpoint.go b/notification/endpoint/endpoint.go new file mode 100644 index 0000000000..003bc1d69f --- /dev/null +++ b/notification/endpoint/endpoint.go @@ -0,0 +1,131 @@ +package endpoint + +import ( + "encoding/json" + "fmt" + + "github.com/influxdata/influxdb" +) + +// types of endpoints. +const ( + SlackType = "slack" + PagerDutyType = "pagerduty" + WebhookType = "webhook" +) + +var typeToEndpoint = map[string](func() influxdb.NotificationEndpoint){ + SlackType: func() influxdb.NotificationEndpoint { return &Slack{} }, + PagerDutyType: func() influxdb.NotificationEndpoint { return &PagerDuty{} }, + WebhookType: func() influxdb.NotificationEndpoint { return &WebHook{} }, +} + +type rawJSON struct { + Type string `json:"type"` +} + +// UnmarshalJSON will convert the bytes to notification endpoint. +func UnmarshalJSON(b []byte) (influxdb.NotificationEndpoint, error) { + var raw rawJSON + if err := json.Unmarshal(b, &raw); err != nil { + return nil, &influxdb.Error{ + Msg: "unable to detect the notification endpoint type from json", + } + } + convertedFunc, ok := typeToEndpoint[raw.Type] + if !ok { + return nil, &influxdb.Error{ + Msg: fmt.Sprintf("invalid notification endpoint type %s", raw.Type), + } + } + converted := convertedFunc() + err := json.Unmarshal(b, converted) + return converted, err +} + +// Base is the embed struct of every notification endpoint. +type Base struct { + ID influxdb.ID `json:"id,omitempty"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + OrgID influxdb.ID `json:"orgID,omitempty"` + Status influxdb.Status `json:"status"` + influxdb.CRUDLog +} + +func (b Base) valid() error { + if !b.ID.Valid() { + return &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "Notification Endpoint ID is invalid", + } + } + if b.Name == "" { + return &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "Notification Endpoint Name can't be empty", + } + } + if b.Status != influxdb.Active && b.Status != influxdb.Inactive { + return &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "invalid status", + } + } + return nil +} + +// GetID implements influxdb.Getter interface. +func (b Base) GetID() influxdb.ID { + return b.ID +} + +// GetName implements influxdb.Getter interface. +func (b *Base) GetName() string { + return b.Name +} + +// GetOrgID implements influxdb.Getter interface. +func (b Base) GetOrgID() influxdb.ID { + return b.OrgID +} + +// GetCRUDLog implements influxdb.Getter interface. +func (b Base) GetCRUDLog() influxdb.CRUDLog { + return b.CRUDLog +} + +// GetDescription implements influxdb.Getter interface. +func (b *Base) GetDescription() string { + return b.Description +} + +// GetStatus implements influxdb.Getter interface. +func (b *Base) GetStatus() influxdb.Status { + return b.Status +} + +// SetID will set the primary key. +func (b *Base) SetID(id influxdb.ID) { + b.ID = id +} + +// SetOrgID will set the org key. +func (b *Base) SetOrgID(id influxdb.ID) { + b.OrgID = id +} + +// SetName implements influxdb.Updator interface. +func (b *Base) SetName(name string) { + b.Name = name +} + +// SetDescription implements influxdb.Updator interface. +func (b *Base) SetDescription(description string) { + b.Description = description +} + +// SetStatus implements influxdb.Updator interface. +func (b *Base) SetStatus(status influxdb.Status) { + b.Status = status +} diff --git a/notification/endpoint/endpoint_test.go b/notification/endpoint/endpoint_test.go new file mode 100644 index 0000000000..a7286ab84c --- /dev/null +++ b/notification/endpoint/endpoint_test.go @@ -0,0 +1,391 @@ +package endpoint_test + +import ( + "encoding/json" + "net/http" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/influxdata/influxdb" + "github.com/influxdata/influxdb/mock" + "github.com/influxdata/influxdb/notification/endpoint" + influxTesting "github.com/influxdata/influxdb/testing" +) + +const ( + id1 = "020f755c3c082000" + id3 = "020f755c3c082002" +) + +var goodBase = endpoint.Base{ + ID: influxTesting.MustIDBase16(id1), + Name: "name1", + OrgID: influxTesting.MustIDBase16(id3), + Status: influxdb.Active, + Description: "desc1", +} + +func TestValidEndpoint(t *testing.T) { + cases := []struct { + name string + src influxdb.NotificationEndpoint + err error + }{ + { + name: "invalid endpoint id", + src: &endpoint.Slack{}, + err: &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "Notification Endpoint ID is invalid", + }, + }, + { + name: "invalid status", + src: &endpoint.PagerDuty{ + Base: endpoint.Base{ + ID: influxTesting.MustIDBase16(id1), + Name: "name1", + OrgID: influxTesting.MustIDBase16(id3), + }, + }, + err: &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "invalid status", + }, + }, + { + name: "empty slack url", + src: &endpoint.Slack{ + Base: goodBase, + }, + err: &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "slack endpoint URL is empty", + }, + }, + { + name: "invalid slack url", + src: &endpoint.Slack{ + Base: goodBase, + URL: "posts://er:{DEf1=ghi@:5432/db?ssl", + }, + err: &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "slack endpoint URL is invalid: parse posts://er:{DEf1=ghi@:5432/db?ssl: net/url: invalid userinfo", + }, + }, + { + name: "empty slack token", + src: &endpoint.Slack{ + Base: goodBase, + URL: "localhost", + }, + err: &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "slack endpoint token is invalid", + }, + }, + { + name: "invalid slack token", + src: &endpoint.Slack{ + Base: goodBase, + URL: "localhost", + Token: influxdb.SecretField{Key: "bad-key"}, + }, + err: &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "slack endpoint token is invalid", + }, + }, + { + name: "empty pagerduty url", + src: &endpoint.PagerDuty{ + Base: goodBase, + }, + err: &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "pagerduty endpoint URL is empty", + }, + }, + { + name: "invalid pagerduty url", + src: &endpoint.PagerDuty{ + Base: goodBase, + URL: "posts://er:{DEf1=ghi@:5432/db?ssl", + }, + err: &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "pagerduty endpoint URL is invalid: parse posts://er:{DEf1=ghi@:5432/db?ssl: net/url: invalid userinfo", + }, + }, + { + name: "invalid routine key", + src: &endpoint.PagerDuty{ + Base: goodBase, + URL: "localhost", + RoutingKey: influxdb.SecretField{Key: "bad-key"}, + }, + err: &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "pagerduty routing key is invalid", + }, + }, + { + name: "empty webhook http method", + src: &endpoint.WebHook{ + Base: goodBase, + URL: "localhost", + }, + err: &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "invalid webhook http method", + }, + }, + { + name: "empty webhook token", + src: &endpoint.WebHook{ + Base: goodBase, + URL: "localhost", + Method: "GET", + AuthMethod: "bearer", + }, + err: &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "invalid webhook token for bearer auth", + }, + }, + { + name: "empty webhook username", + src: &endpoint.WebHook{ + Base: goodBase, + URL: "localhost", + Method: http.MethodGet, + AuthMethod: "basic", + }, + err: &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "invalid webhook username/password for basic auth", + }, + }, + } + for _, c := range cases { + got := c.src.Valid() + influxTesting.ErrorsEqual(t, got, c.err) + } +} + +var timeGen1 = mock.TimeGenerator{FakeValue: time.Date(2006, time.July, 13, 4, 19, 10, 0, time.UTC)} +var timeGen2 = mock.TimeGenerator{FakeValue: time.Date(2006, time.July, 14, 5, 23, 53, 10, time.UTC)} + +func TestJSON(t *testing.T) { + cases := []struct { + name string + src influxdb.NotificationEndpoint + }{ + { + name: "simple Slack", + src: &endpoint.Slack{ + Base: endpoint.Base{ + ID: influxTesting.MustIDBase16(id1), + Name: "name1", + OrgID: influxTesting.MustIDBase16(id3), + Status: influxdb.Active, + CRUDLog: influxdb.CRUDLog{ + CreatedAt: timeGen1.Now(), + UpdatedAt: timeGen2.Now(), + }, + }, + URL: "https://slack.com/api/chat.postMessage", + Token: influxdb.SecretField{Key: "token-key-1"}, + }, + }, + { + name: "simple pagerduty", + src: &endpoint.PagerDuty{ + Base: endpoint.Base{ + ID: influxTesting.MustIDBase16(id1), + Name: "name1", + OrgID: influxTesting.MustIDBase16(id3), + Status: influxdb.Active, + CRUDLog: influxdb.CRUDLog{ + CreatedAt: timeGen1.Now(), + UpdatedAt: timeGen2.Now(), + }, + }, + URL: "https://events.pagerduty.com/v2/enqueue", + RoutingKey: influxdb.SecretField{Key: "pagerduty-routing-key"}, + }, + }, + { + name: "simple webhook", + src: &endpoint.WebHook{ + Base: endpoint.Base{ + ID: influxTesting.MustIDBase16(id1), + Name: "name1", + OrgID: influxTesting.MustIDBase16(id3), + Status: influxdb.Active, + CRUDLog: influxdb.CRUDLog{ + CreatedAt: timeGen1.Now(), + UpdatedAt: timeGen2.Now(), + }, + }, + AuthMethod: "basic", + URL: "http://example.com", + Username: influxdb.SecretField{Key: "username-key"}, + Password: influxdb.SecretField{Key: "password-key"}, + }, + }, + } + for _, c := range cases { + b, err := json.Marshal(c.src) + if err != nil { + t.Fatalf("%s marshal failed, err: %s", c.name, err.Error()) + } + got, err := endpoint.UnmarshalJSON(b) + if err != nil { + t.Fatalf("%s unmarshal failed, err: %s", c.name, err.Error()) + } + if diff := cmp.Diff(got, c.src); diff != "" { + t.Errorf("failed %s, NotificationEndpoint are different -got/+want\ndiff %s", c.name, diff) + } + } +} + +func TestBackFill(t *testing.T) { + cases := []struct { + name string + src influxdb.NotificationEndpoint + target influxdb.NotificationEndpoint + }{ + { + name: "simple Slack", + src: &endpoint.Slack{ + Base: endpoint.Base{ + ID: influxTesting.MustIDBase16(id1), + Name: "name1", + OrgID: influxTesting.MustIDBase16(id3), + Status: influxdb.Active, + CRUDLog: influxdb.CRUDLog{ + CreatedAt: timeGen1.Now(), + UpdatedAt: timeGen2.Now(), + }, + }, + URL: "https://slack.com/api/chat.postMessage", + Token: influxdb.SecretField{ + Value: strPtr("token-value"), + }, + }, + target: &endpoint.Slack{ + Base: endpoint.Base{ + ID: influxTesting.MustIDBase16(id1), + Name: "name1", + OrgID: influxTesting.MustIDBase16(id3), + Status: influxdb.Active, + CRUDLog: influxdb.CRUDLog{ + CreatedAt: timeGen1.Now(), + UpdatedAt: timeGen2.Now(), + }, + }, + URL: "https://slack.com/api/chat.postMessage", + Token: influxdb.SecretField{ + Key: id1 + "-token", + Value: strPtr("token-value"), + }, + }, + }, + { + name: "simple pagerduty", + src: &endpoint.PagerDuty{ + Base: endpoint.Base{ + ID: influxTesting.MustIDBase16(id1), + Name: "name1", + OrgID: influxTesting.MustIDBase16(id3), + Status: influxdb.Active, + CRUDLog: influxdb.CRUDLog{ + CreatedAt: timeGen1.Now(), + UpdatedAt: timeGen2.Now(), + }, + }, + URL: "https://events.pagerduty.com/v2/enqueue", + RoutingKey: influxdb.SecretField{ + Value: strPtr("routing-key-value"), + }, + }, + target: &endpoint.PagerDuty{ + Base: endpoint.Base{ + ID: influxTesting.MustIDBase16(id1), + Name: "name1", + OrgID: influxTesting.MustIDBase16(id3), + Status: influxdb.Active, + CRUDLog: influxdb.CRUDLog{ + CreatedAt: timeGen1.Now(), + UpdatedAt: timeGen2.Now(), + }, + }, + URL: "https://events.pagerduty.com/v2/enqueue", + RoutingKey: influxdb.SecretField{ + Key: id1 + "-routing-key", + Value: strPtr("routing-key-value"), + }, + }, + }, + { + name: "webhook with token", + src: &endpoint.WebHook{ + Base: endpoint.Base{ + ID: influxTesting.MustIDBase16(id1), + Name: "name1", + OrgID: influxTesting.MustIDBase16(id3), + Status: influxdb.Active, + CRUDLog: influxdb.CRUDLog{ + CreatedAt: timeGen1.Now(), + UpdatedAt: timeGen2.Now(), + }, + }, + AuthMethod: "basic", + URL: "http://example.com", + Username: influxdb.SecretField{ + Value: strPtr("username1"), + }, + Password: influxdb.SecretField{ + Value: strPtr("password1"), + }, + }, + target: &endpoint.WebHook{ + Base: endpoint.Base{ + ID: influxTesting.MustIDBase16(id1), + Name: "name1", + OrgID: influxTesting.MustIDBase16(id3), + Status: influxdb.Active, + CRUDLog: influxdb.CRUDLog{ + CreatedAt: timeGen1.Now(), + UpdatedAt: timeGen2.Now(), + }, + }, + AuthMethod: "basic", + URL: "http://example.com", + Username: influxdb.SecretField{ + Key: id1 + "-username", + Value: strPtr("username1"), + }, + Password: influxdb.SecretField{ + Key: id1 + "-password", + Value: strPtr("password1"), + }, + }, + }, + } + for _, c := range cases { + c.src.BackfillSecretKeys() + if diff := cmp.Diff(c.target, c.src); diff != "" { + t.Errorf("failed %s, NotificationEndpoint are different -got/+want\ndiff %s", c.name, diff) + } + } +} + +func strPtr(s string) *string { + ss := new(string) + *ss = s + return ss +} diff --git a/notification/endpoint/pagerduty.go b/notification/endpoint/pagerduty.go new file mode 100644 index 0000000000..b54e795283 --- /dev/null +++ b/notification/endpoint/pagerduty.go @@ -0,0 +1,83 @@ +package endpoint + +import ( + "encoding/json" + "fmt" + "net/url" + + "github.com/influxdata/influxdb" +) + +var _ influxdb.NotificationEndpoint = &PagerDuty{} + +const routingKeySuffix = "-routing-key" + +// PagerDuty is the notification endpoint config of pagerduty. +type PagerDuty struct { + Base + // Path is the PagerDuty API URL, should not need to be changed. + URL string `json:"url"` + // RoutingKey is a version 4 UUID expressed as a 32-digit hexadecimal number. + // This is the Integration Key for an integration on any given service. + RoutingKey influxdb.SecretField `json:"routing-key"` +} + +// BackfillSecretKeys fill back fill the secret field key during the unmarshalling +// if value of that secret field is not nil. +func (s *PagerDuty) BackfillSecretKeys() { + if s.RoutingKey.Key == "" && s.RoutingKey.Value != nil { + s.RoutingKey.Key = s.ID.String() + routingKeySuffix + } +} + +// SecretFields return available secret fields. +func (s PagerDuty) SecretFields() []influxdb.SecretField { + return []influxdb.SecretField{ + s.RoutingKey, + } +} + +// Valid returns error if some configuration is invalid +func (s PagerDuty) Valid() error { + if err := s.Base.valid(); err != nil { + return err + } + if s.URL == "" { + return &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "pagerduty endpoint URL is empty", + } + } + if _, err := url.Parse(s.URL); err != nil { + return &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: fmt.Sprintf("pagerduty endpoint URL is invalid: %s", err.Error()), + } + } + if s.RoutingKey.Key != s.ID.String()+routingKeySuffix { + return &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "pagerduty routing key is invalid", + } + } + return nil +} + +type pagerdutyAlias PagerDuty + +// MarshalJSON implement json.Marshaler interface. +func (s PagerDuty) MarshalJSON() ([]byte, error) { + return json.Marshal( + struct { + pagerdutyAlias + Type string `json:"type"` + }{ + pagerdutyAlias: pagerdutyAlias(s), + Type: s.Type(), + }) +} + +// Type returns the type. +func (s PagerDuty) Type() string { + return PagerDutyType +} diff --git a/notification/endpoint/slack.go b/notification/endpoint/slack.go new file mode 100644 index 0000000000..83af0652e9 --- /dev/null +++ b/notification/endpoint/slack.go @@ -0,0 +1,84 @@ +package endpoint + +import ( + "encoding/json" + "fmt" + "net/url" + + "github.com/influxdata/influxdb" +) + +var _ influxdb.NotificationEndpoint = &Slack{} + +const slackTokenSuffix = "-token" + +// Slack is the notification endpoint config of slack. +type Slack struct { + Base + // URL is a valid slack webhook URL + // TODO(jm): validate this in unmarshaler + // example: https://slack.com/api/chat.postMessage + URL string `json:"url"` + // Token is the bearer token for authorization + Token influxdb.SecretField `json:"token"` +} + +// BackfillSecretKeys fill back fill the secret field key during the unmarshalling +// if value of that secret field is not nil. +func (s *Slack) BackfillSecretKeys() { + if s.Token.Key == "" && s.Token.Value != nil { + s.Token.Key = s.ID.String() + slackTokenSuffix + } +} + +// SecretFields return available secret fields. +func (s Slack) SecretFields() []influxdb.SecretField { + return []influxdb.SecretField{ + s.Token, + } +} + +// Valid returns error if some configuration is invalid +func (s Slack) Valid() error { + if err := s.Base.valid(); err != nil { + return err + } + if s.URL == "" { + return &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "slack endpoint URL is empty", + } + } + if _, err := url.Parse(s.URL); err != nil { + return &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: fmt.Sprintf("slack endpoint URL is invalid: %s", err.Error()), + } + } + if s.Token.Key != s.ID.String()+slackTokenSuffix { + return &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "slack endpoint token is invalid", + } + } + return nil +} + +type slackAlias Slack + +// MarshalJSON implement json.Marshaler interface. +func (s Slack) MarshalJSON() ([]byte, error) { + return json.Marshal( + struct { + slackAlias + Type string `json:"type"` + }{ + slackAlias: slackAlias(s), + Type: s.Type(), + }) +} + +// Type returns the type. +func (s Slack) Type() string { + return SlackType +} diff --git a/notification/endpoint/webhook.go b/notification/endpoint/webhook.go new file mode 100644 index 0000000000..abaf782d6f --- /dev/null +++ b/notification/endpoint/webhook.go @@ -0,0 +1,154 @@ +package endpoint + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + + "github.com/influxdata/influxdb" +) + +var _ influxdb.NotificationEndpoint = &WebHook{} + +const ( + webhookTokenSuffix = "-token" + webhookUsernameSuffix = "-username" + webhookPasswordSuffix = "-password" +) + +// WebHook is the notification endpoint config of webhook. +type WebHook struct { + Base + // Path is the API path of WebHook + URL string `json:"url"` + // Token is the bearer token for authorization + Token influxdb.SecretField `json:"token,omitempty"` + Username influxdb.SecretField `json:"username,omitempty"` + Password influxdb.SecretField `json:"password,omitempty"` + AuthMethod string `json:"authmethod"` + Method string `json:"method"` + ContentTemplate string `json:"contentTemplate"` +} + +// BackfillSecretKeys fill back fill the secret field key during the unmarshalling +// if value of that secret field is not nil. +func (s *WebHook) BackfillSecretKeys() { + if s.Token.Key == "" && s.Token.Value != nil { + s.Token.Key = s.ID.String() + webhookTokenSuffix + } + if s.Username.Key == "" && s.Username.Value != nil { + s.Username.Key = s.ID.String() + webhookUsernameSuffix + } + if s.Password.Key == "" && s.Password.Value != nil { + s.Password.Key = s.ID.String() + webhookPasswordSuffix + } +} + +// SecretFields return available secret fields. +func (s WebHook) SecretFields() []influxdb.SecretField { + arr := make([]influxdb.SecretField, 0) + if s.Token.Key != "" { + arr = append(arr, s.Token) + } + if s.Username.Key != "" { + arr = append(arr, s.Username) + } + if s.Password.Key != "" { + arr = append(arr, s.Password) + } + return arr +} + +var goodWebHookAuthMethod = map[string]bool{ + "none": true, + "basic": true, + "bearer": true, +} + +var goodHTTPMethod = map[string]bool{ + http.MethodGet: true, + http.MethodPost: true, + http.MethodPut: true, +} + +// Valid returns error if some configuration is invalid +func (s WebHook) Valid() error { + if err := s.Base.valid(); err != nil { + return err + } + if s.URL == "" { + return &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "webhook endpoint URL is empty", + } + } + if _, err := url.Parse(s.URL); err != nil { + return &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: fmt.Sprintf("webhook endpoint URL is invalid: %s", err.Error()), + } + } + if !goodHTTPMethod[s.Method] { + return &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "invalid webhook http method", + } + } + if !goodWebHookAuthMethod[s.AuthMethod] { + return &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "invalid webhook auth method", + } + } + if s.AuthMethod == "basic" && + (s.Username.Key != s.ID.String()+webhookUsernameSuffix || + s.Password.Key != s.ID.String()+webhookPasswordSuffix) { + return &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "invalid webhook username/password for basic auth", + } + } + if s.AuthMethod == "bearer" && s.Token.Key != webhookTokenSuffix { + return &influxdb.Error{ + Code: influxdb.EInvalid, + Msg: "invalid webhook token for bearer auth", + } + } + + return nil +} + +type webhookAlias WebHook + +// MarshalJSON implement json.Marshaler interface. +func (s WebHook) MarshalJSON() ([]byte, error) { + return json.Marshal( + struct { + webhookAlias + Type string `json:"type"` + }{ + webhookAlias: webhookAlias(s), + Type: s.Type(), + }) +} + +// Type returns the type. +func (s WebHook) Type() string { + return WebhookType +} + +// ParseResponse will parse the http response from webhook. +func (s WebHook) ParseResponse(resp *http.Response) error { + if resp.StatusCode != http.StatusOK { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + return &influxdb.Error{ + Msg: string(body), + } + } + return nil +} diff --git a/notification_endpoint.go b/notification_endpoint.go new file mode 100644 index 0000000000..7272afd0bc --- /dev/null +++ b/notification_endpoint.go @@ -0,0 +1,123 @@ +package influxdb + +import ( + "context" + "encoding/json" + "errors" +) + +var ( + // ErrInvalidNotificationEndpointType denotes that the provided NotificationEndpoint is not a valid type + ErrInvalidNotificationEndpointType = errors.New("unknown notification endpoint type") +) + +// NotificationEndpoint is the configuration describing +// how to call a 3rd party service. E.g. Slack, Pagerduty +type NotificationEndpoint interface { + Valid() error + Type() string + json.Marshaler + Updater + Getter + // SecretFields return available secret fields. + SecretFields() []SecretField + // BackfillSecretKeys fill back fill the secret field key during the unmarshalling + // if value of that secret field is not nil. + BackfillSecretKeys() +} + +// ops for checks error +var ( + OpFindNotificationEndpointByID = "FindNotificationEndpointByID" + OpFindNotificationEndpoint = "FindNotificationEndpoint" + OpFindNotificationEndpoints = "FindNotificationEndpoints" + OpCreateNotificationEndpoint = "CreateNotificationEndpoint" + OpUpdateNotificationEndpoint = "UpdateNotificationEndpoint" + OpDeleteNotificationEndpoint = "DeleteNotificationEndpoint" +) + +// NotificationEndpointFilter represents a set of filter that restrict the returned notification endpoints. +type NotificationEndpointFilter struct { + ID *ID + OrgID *ID + Org *string +} + +// QueryParams Converts NotificationEndpointFilter fields to url query params. +func (f NotificationEndpointFilter) QueryParams() map[string][]string { + qp := map[string][]string{} + + if f.OrgID != nil { + qp["orgID"] = []string{f.OrgID.String()} + } + + if f.Org != nil { + qp["org"] = []string{*f.Org} + } + + return qp +} + +// NotificationEndpointUpdate is the set of upgrade fields for patch request. +type NotificationEndpointUpdate struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Status *Status `json:"status,omitempty"` +} + +// Valid will verify if the NotificationEndpointUpdate is valid. +func (n *NotificationEndpointUpdate) Valid() error { + if n.Name != nil && *n.Name == "" { + return &Error{ + Code: EInvalid, + Msg: "Notification Endpoint Name can't be empty", + } + } + + if n.Description != nil && *n.Description == "" { + return &Error{ + Code: EInvalid, + Msg: "Notification Endpoint Description can't be empty", + } + } + + if n.Status != nil { + if err := n.Status.Valid(); err != nil { + return err + } + } + + return nil +} + +// NotificationEndpointService represents a service for managing notification endpoints. +type NotificationEndpointService interface { + // UserResourceMappingService must be part of all NotificationEndpointStore service, + // for create, delete. + UserResourceMappingService + // OrganizationService is needed for search filter + OrganizationService + // SecretService is needed to check if the secret key exists. + SecretService + + // FindNotificationEndpointByID returns a single notification endpoint by ID. + FindNotificationEndpointByID(ctx context.Context, id ID) (NotificationEndpoint, error) + + // FindNotificationEndpoints returns a list of notification endpoints that match filter and the total count of matching notification endpoints. + // Additional options provide pagination & sorting. + FindNotificationEndpoints(ctx context.Context, filter NotificationEndpointFilter, opt ...FindOptions) ([]NotificationEndpoint, int, error) + + // CreateNotificationEndpoint creates a new notification endpoint and sets b.ID with the new identifier. + CreateNotificationEndpoint(ctx context.Context, ne NotificationEndpoint, userID ID) error + + // UpdateNotificationEndpointUpdateNotificationEndpoint updates a single notification endpoint. + // Returns the new notification endpoint after update. + UpdateNotificationEndpoint(ctx context.Context, id ID, nr NotificationEndpoint, userID ID) (NotificationEndpoint, error) + + // PatchNotificationEndpoint updates a single notification endpoint with changeset. + // Returns the new notification endpoint state after update. + PatchNotificationEndpoint(ctx context.Context, id ID, upd NotificationEndpointUpdate) (NotificationEndpoint, error) + + // DeleteNotificationEndpoint removes a notification endpoint by ID. + DeleteNotificationEndpoint(ctx context.Context, id ID) error +} diff --git a/secret.go b/secret.go index 257826eadf..5cc9e08a1b 100644 --- a/secret.go +++ b/secret.go @@ -1,6 +1,10 @@ package influxdb -import "context" +import ( + "context" + "encoding/json" + "strings" +) // ErrSecretNotFound is the error msg for a missing secret. const ErrSecretNotFound = "secret not found" @@ -25,3 +29,46 @@ type SecretService interface { // DeleteSecret removes a single secret from the secret store. DeleteSecret(ctx context.Context, orgID ID, ks ...string) error } + +// SecretField contains a key string, and value pointer. +type SecretField struct { + Key string `json:"key"` + Value *string `json:"value,omitempty"` +} + +// String returns the key of the secret. +func (s SecretField) String() string { + if s.Key == "" { + return "" + } + return "secret: " + string(s.Key) +} + +// MarshalJSON implement the json marshaler interface. +func (s SecretField) MarshalJSON() ([]byte, error) { + return json.Marshal(s.String()) +} + +// UnmarshalJSON implement the json unmarshaler interface. +func (s *SecretField) UnmarshalJSON(b []byte) error { + var ss string + if err := json.Unmarshal(b, &ss); err != nil { + return err + } + if ss == "" { + s.Key = "" + return nil + } + if strings.HasPrefix(ss, "secret: ") { + s.Key = ss[len("secret: "):] + } else { + s.Value = strPtr(ss) + } + return nil +} + +func strPtr(s string) *string { + ss := new(string) + *ss = s + return ss +} diff --git a/secret_test.go b/secret_test.go new file mode 100644 index 0000000000..ad9bd6cdf6 --- /dev/null +++ b/secret_test.go @@ -0,0 +1,61 @@ +package influxdb + +import ( + "encoding/json" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestSecretFieldJSON(t *testing.T) { + cases := []struct { + name string + fld *SecretField + json string + target SecretField + }{ + { + name: "regular", + fld: &SecretField{Key: "some key"}, + json: `"secret: some key"`, + target: SecretField{Key: "some key"}, + }, + {name: "blank", fld: &SecretField{}, json: `""`}, + { + name: "with value", + fld: &SecretField{ + Key: "some key", + Value: strPtr("some value"), + }, + json: `"secret: some key"`, + target: SecretField{ + Key: "some key", + }, + }, + { + name: "unmarshal a post", + json: `"some value"`, + target: SecretField{ + Value: strPtr("some value"), + }, + }, + } + for _, c := range cases { + if c.fld != nil { + serialized, err := json.Marshal(c.fld) + if err != nil { + t.Fatalf("%s failed, secret key marshal err: %q", c.name, err.Error()) + } + if string(serialized) != c.json { + t.Fatalf("%s failed, secret key marshal result is unexpected, got %q, want %q", c.name, string(serialized), c.json) + } + } + var deserialized SecretField + if err := json.Unmarshal([]byte(c.json), &deserialized); err != nil { + t.Fatalf("%s failed, secret key unmarshal err: %q", c.name, err.Error()) + } + if diff := cmp.Diff(deserialized, c.target); diff != "" { + t.Fatalf("%s failed, secret key unmarshal result is unexpected, diff %s", c.name, diff) + } + } +}