From 520bc9a6abf578420beb84ed3ba071f05b126cb9 Mon Sep 17 00:00:00 2001 From: Santiago Date: Fri, 3 May 2024 17:58:40 +0200 Subject: [PATCH 01/17] Add promoted bool field to Grafana state and configuration --- pkg/alertmanager/alertspb/alerts.pb.go | 99 ++++++++++++++++++-------- pkg/alertmanager/alertspb/alerts.proto | 1 + pkg/alertmanager/alertspb/compat.go | 3 +- pkg/alertmanager/api_grafana.go | 14 ++-- 4 files changed, 84 insertions(+), 33 deletions(-) diff --git a/pkg/alertmanager/alertspb/alerts.pb.go b/pkg/alertmanager/alertspb/alerts.pb.go index 894710a8e30..0c0361a0219 100644 --- a/pkg/alertmanager/alertspb/alerts.pb.go +++ b/pkg/alertmanager/alertspb/alerts.pb.go @@ -185,6 +185,7 @@ type GrafanaAlertConfigDesc struct { Hash string `protobuf:"bytes,4,opt,name=hash,proto3" json:"hash,omitempty"` CreatedAtTimestamp int64 `protobuf:"varint,5,opt,name=created_at_timestamp,json=createdAtTimestamp,proto3" json:"created_at_timestamp,omitempty"` Default bool `protobuf:"varint,7,opt,name=default,proto3" json:"default,omitempty"` + Promoted bool `protobuf:"varint,8,opt,name=promoted,proto3" json:"promoted,omitempty"` } func (m *GrafanaAlertConfigDesc) Reset() { *m = GrafanaAlertConfigDesc{} } @@ -254,6 +255,13 @@ func (m *GrafanaAlertConfigDesc) GetDefault() bool { return false } +func (m *GrafanaAlertConfigDesc) GetPromoted() bool { + if m != nil { + return m.Promoted + } + return false +} + func init() { proto.RegisterType((*AlertConfigDesc)(nil), "alerts.AlertConfigDesc") proto.RegisterType((*TemplateDesc)(nil), "alerts.TemplateDesc") @@ -264,33 +272,33 @@ func init() { func init() { proto.RegisterFile("alerts.proto", fileDescriptor_20493709c38b81dc) } var fileDescriptor_20493709c38b81dc = []byte{ - // 403 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x51, 0xbd, 0xae, 0xd3, 0x30, - 0x18, 0x8d, 0x49, 0xee, 0xbd, 0xa9, 0xb9, 0x08, 0x64, 0x55, 0x28, 0xaa, 0x84, 0x89, 0x32, 0x45, - 0x0c, 0x09, 0xba, 0x6c, 0x0c, 0x57, 0x6a, 0x41, 0x20, 0x31, 0x86, 0x4e, 0x2c, 0x95, 0x93, 0x3a, - 0x3f, 0x52, 0x12, 0x47, 0xb6, 0xa3, 0xc2, 0xc6, 0x23, 0xf0, 0x08, 0x8c, 0xbc, 0x01, 0xaf, 0xc0, - 0xd8, 0xb1, 0x23, 0x4d, 0x97, 0x8e, 0x7d, 0x04, 0x14, 0x3b, 0x69, 0x79, 0x80, 0x3b, 0xe5, 0x9c, - 0x9c, 0x73, 0xbe, 0x1f, 0x7f, 0xf0, 0x96, 0x94, 0x94, 0x4b, 0x11, 0x34, 0x9c, 0x49, 0x86, 0xae, - 0x35, 0x9b, 0x4d, 0x33, 0x96, 0x31, 0xf5, 0x2b, 0xec, 0x91, 0x56, 0x67, 0x8b, 0xac, 0x90, 0x79, - 0x1b, 0x07, 0x09, 0xab, 0xc2, 0x86, 0xb3, 0x8a, 0xca, 0x9c, 0xb6, 0x22, 0x54, 0x99, 0x8a, 0xd4, - 0x24, 0xa3, 0x3c, 0x4c, 0xca, 0x56, 0xc8, 0xcb, 0xb7, 0x89, 0x47, 0xa4, 0x6b, 0x78, 0x5f, 0xe1, - 0xd3, 0x79, 0xef, 0x7f, 0xc7, 0xea, 0xb4, 0xc8, 0xde, 0x53, 0x91, 0x20, 0x04, 0xad, 0x56, 0x50, - 0xee, 0x00, 0x17, 0xf8, 0x93, 0x48, 0x61, 0xf4, 0x02, 0x42, 0x4e, 0x36, 0xab, 0x44, 0xb9, 0x9c, - 0x47, 0x4a, 0x99, 0x70, 0xb2, 0xd1, 0x31, 0x74, 0x07, 0x27, 0x92, 0x56, 0x4d, 0x49, 0x24, 0x15, - 0x8e, 0xe9, 0x9a, 0xfe, 0xe3, 0xbb, 0x69, 0x30, 0x6c, 0xb2, 0x1c, 0x84, 0xbe, 0x76, 0x74, 0xb1, - 0x79, 0xf7, 0xf0, 0xf6, 0x7f, 0x09, 0xcd, 0xa0, 0x9d, 0x16, 0x25, 0xad, 0x49, 0x45, 0x87, 0xd6, - 0x67, 0xde, 0x8f, 0x14, 0xb3, 0xf5, 0xb7, 0xa1, 0xb1, 0xc2, 0xde, 0x1c, 0x3e, 0xf9, 0xd0, 0x96, - 0xe5, 0x67, 0x39, 0x16, 0x78, 0x05, 0xaf, 0x44, 0x4f, 0x54, 0xba, 0x1f, 0xe0, 0xbc, 0x73, 0x70, - 0x36, 0x46, 0xda, 0xf2, 0xd6, 0x3a, 0xfe, 0x7c, 0x69, 0x78, 0xbf, 0x01, 0x7c, 0xfe, 0x91, 0x93, - 0x94, 0xd4, 0xe4, 0x01, 0x1e, 0x01, 0x41, 0x2b, 0x27, 0x22, 0x77, 0x2c, 0x1d, 0xe9, 0x31, 0x7a, - 0x0d, 0xa7, 0x09, 0xa7, 0x44, 0xd2, 0xf5, 0x8a, 0xc8, 0x95, 0x2c, 0x2a, 0x2a, 0x24, 0xa9, 0x1a, - 0xe7, 0xca, 0x05, 0xbe, 0x19, 0xa1, 0x41, 0x9b, 0xcb, 0xe5, 0xa8, 0x20, 0x07, 0xde, 0xac, 0x69, - 0x4a, 0xda, 0x52, 0x3a, 0x37, 0x2e, 0xf0, 0xed, 0x68, 0xa4, 0x7a, 0xe6, 0x4f, 0x96, 0x6d, 0x3e, - 0xb3, 0x16, 0xf7, 0xdb, 0x3d, 0x36, 0x76, 0x7b, 0x6c, 0x9c, 0xf6, 0x18, 0x7c, 0xef, 0x30, 0xf8, - 0xd5, 0x61, 0xf0, 0xa7, 0xc3, 0x60, 0xdb, 0x61, 0xf0, 0xb7, 0xc3, 0xe0, 0xd8, 0x61, 0xe3, 0xd4, - 0x61, 0xf0, 0xe3, 0x80, 0x8d, 0xed, 0x01, 0x1b, 0xbb, 0x03, 0x36, 0xbe, 0xd8, 0xfa, 0x24, 0x4d, - 0x1c, 0x5f, 0xab, 0xeb, 0xbf, 0xf9, 0x17, 0x00, 0x00, 0xff, 0xff, 0x71, 0xad, 0x28, 0x26, 0x6f, - 0x02, 0x00, 0x00, + // 416 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x52, 0x3d, 0x8e, 0xd4, 0x30, + 0x18, 0x8d, 0x99, 0xec, 0x6e, 0xc6, 0x2c, 0x02, 0x59, 0x23, 0x64, 0x8d, 0x84, 0x19, 0x4d, 0x15, + 0x51, 0x24, 0x68, 0xe9, 0x28, 0x56, 0x9a, 0x05, 0x81, 0x44, 0x19, 0xb6, 0xa2, 0x19, 0x39, 0x89, + 0xf3, 0x23, 0x25, 0x71, 0x64, 0x3b, 0x5a, 0xe8, 0x38, 0x02, 0x47, 0xa0, 0xe4, 0x28, 0x94, 0x23, + 0xd1, 0x6c, 0xc9, 0x64, 0x9a, 0x2d, 0xf7, 0x08, 0xc8, 0x76, 0x92, 0xe1, 0x00, 0x5b, 0xe5, 0xbd, + 0xbc, 0xf7, 0x3e, 0xfb, 0xfb, 0x3e, 0xc3, 0x73, 0x5a, 0x31, 0xa1, 0x64, 0xd0, 0x0a, 0xae, 0x38, + 0x3a, 0xb5, 0x6c, 0xb9, 0xc8, 0x79, 0xce, 0xcd, 0xaf, 0x50, 0x23, 0xab, 0x2e, 0xaf, 0xf2, 0x52, + 0x15, 0x5d, 0x1c, 0x24, 0xbc, 0x0e, 0x5b, 0xc1, 0x6b, 0xa6, 0x0a, 0xd6, 0xc9, 0xd0, 0x64, 0x6a, + 0xda, 0xd0, 0x9c, 0x89, 0x30, 0xa9, 0x3a, 0xa9, 0x8e, 0xdf, 0x36, 0x1e, 0x91, 0xad, 0xb1, 0xfe, + 0x0a, 0x9f, 0x6e, 0xb4, 0xff, 0x1d, 0x6f, 0xb2, 0x32, 0x7f, 0xcf, 0x64, 0x82, 0x10, 0x74, 0x3b, + 0xc9, 0x04, 0x06, 0x2b, 0xe0, 0xcf, 0x23, 0x83, 0xd1, 0x0b, 0x08, 0x05, 0xbd, 0xd9, 0x26, 0xc6, + 0x85, 0x1f, 0x19, 0x65, 0x2e, 0xe8, 0x8d, 0x8d, 0xa1, 0x0b, 0x38, 0x57, 0xac, 0x6e, 0x2b, 0xaa, + 0x98, 0xc4, 0xb3, 0xd5, 0xcc, 0x7f, 0x7c, 0xb1, 0x08, 0x86, 0x4e, 0xae, 0x07, 0x41, 0xd7, 0x8e, + 0x8e, 0xb6, 0xf5, 0x25, 0x3c, 0xff, 0x5f, 0x42, 0x4b, 0xe8, 0x65, 0x65, 0xc5, 0x1a, 0x5a, 0xb3, + 0xe1, 0xe8, 0x89, 0xeb, 0x2b, 0xc5, 0x3c, 0xfd, 0x36, 0x1c, 0x6c, 0xf0, 0x7a, 0x03, 0x9f, 0x7c, + 0xe8, 0xaa, 0xea, 0xb3, 0x1a, 0x0b, 0xbc, 0x82, 0x27, 0x52, 0x13, 0x93, 0xd6, 0x17, 0x98, 0x7a, + 0x0e, 0x26, 0x63, 0x64, 0x2d, 0x6f, 0xdd, 0xbb, 0x9f, 0x2f, 0x9d, 0xf5, 0x1f, 0x00, 0x9f, 0x7f, + 0x14, 0x34, 0xa3, 0x0d, 0x7d, 0x80, 0x21, 0x20, 0xe8, 0x16, 0x54, 0x16, 0xd8, 0xb5, 0x11, 0x8d, + 0xd1, 0x6b, 0xb8, 0x48, 0x04, 0xa3, 0x8a, 0xa5, 0x5b, 0xaa, 0xb6, 0xaa, 0xac, 0x99, 0x54, 0xb4, + 0x6e, 0xf1, 0xc9, 0x0a, 0xf8, 0xb3, 0x08, 0x0d, 0xda, 0x46, 0x5d, 0x8f, 0x0a, 0xc2, 0xf0, 0x2c, + 0x65, 0x19, 0xed, 0x2a, 0x85, 0xcf, 0x56, 0xc0, 0xf7, 0xa2, 0x91, 0xea, 0x01, 0xe9, 0x2d, 0x73, + 0xc5, 0x52, 0xec, 0x19, 0x69, 0xe2, 0xb6, 0x9f, 0x4f, 0xae, 0x37, 0x7b, 0xe6, 0x5e, 0x5d, 0xee, + 0xf6, 0xc4, 0xb9, 0xdd, 0x13, 0xe7, 0x7e, 0x4f, 0xc0, 0xf7, 0x9e, 0x80, 0x5f, 0x3d, 0x01, 0xbf, + 0x7b, 0x02, 0x76, 0x3d, 0x01, 0x7f, 0x7b, 0x02, 0xee, 0x7a, 0xe2, 0xdc, 0xf7, 0x04, 0xfc, 0x38, + 0x10, 0x67, 0x77, 0x20, 0xce, 0xed, 0x81, 0x38, 0x5f, 0x3c, 0xbb, 0xae, 0x36, 0x8e, 0x4f, 0xcd, + 0xcb, 0x78, 0xf3, 0x2f, 0x00, 0x00, 0xff, 0xff, 0x4c, 0x8a, 0x84, 0x8c, 0x8b, 0x02, 0x00, 0x00, } func (this *AlertConfigDesc) Equal(that interface{}) bool { @@ -396,13 +404,14 @@ func (this *GrafanaAlertConfigDesc) GoString() string { if this == nil { return "nil" } - s := make([]string, 0, 9) + s := make([]string, 0, 10) s = append(s, "&alertspb.GrafanaAlertConfigDesc{") s = append(s, "User: "+fmt.Sprintf("%#v", this.User)+",\n") s = append(s, "RawConfig: "+fmt.Sprintf("%#v", this.RawConfig)+",\n") s = append(s, "Hash: "+fmt.Sprintf("%#v", this.Hash)+",\n") s = append(s, "CreatedAtTimestamp: "+fmt.Sprintf("%#v", this.CreatedAtTimestamp)+",\n") s = append(s, "Default: "+fmt.Sprintf("%#v", this.Default)+",\n") + s = append(s, "Promoted: "+fmt.Sprintf("%#v", this.Promoted)+",\n") s = append(s, "}") return strings.Join(s, "") } @@ -557,6 +566,16 @@ func (m *GrafanaAlertConfigDesc) MarshalToSizedBuffer(dAtA []byte) (int, error) _ = i var l int _ = l + if m.Promoted { + i-- + if m.Promoted { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x40 + } if m.Default { i-- if m.Default { @@ -684,6 +703,9 @@ func (m *GrafanaAlertConfigDesc) Size() (n int) { if m.Default { n += 2 } + if m.Promoted { + n += 2 + } return n } @@ -741,6 +763,7 @@ func (this *GrafanaAlertConfigDesc) String() string { `Hash:` + fmt.Sprintf("%v", this.Hash) + `,`, `CreatedAtTimestamp:` + fmt.Sprintf("%v", this.CreatedAtTimestamp) + `,`, `Default:` + fmt.Sprintf("%v", this.Default) + `,`, + `Promoted:` + fmt.Sprintf("%v", this.Promoted) + `,`, `}`, }, "") return s @@ -1274,6 +1297,26 @@ func (m *GrafanaAlertConfigDesc) Unmarshal(dAtA []byte) error { } } m.Default = bool(v != 0) + case 8: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Promoted", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowAlerts + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.Promoted = bool(v != 0) default: iNdEx = preIndex skippy, err := skipAlerts(dAtA[iNdEx:]) diff --git a/pkg/alertmanager/alertspb/alerts.proto b/pkg/alertmanager/alertspb/alerts.proto index 4ea75398ad1..0248a38bc1a 100644 --- a/pkg/alertmanager/alertspb/alerts.proto +++ b/pkg/alertmanager/alertspb/alerts.proto @@ -43,4 +43,5 @@ message GrafanaAlertConfigDesc { string hash = 4; int64 created_at_timestamp = 5; bool default = 7; + bool promoted = 8; } diff --git a/pkg/alertmanager/alertspb/compat.go b/pkg/alertmanager/alertspb/compat.go index 77b427d485e..1a629f32b6a 100644 --- a/pkg/alertmanager/alertspb/compat.go +++ b/pkg/alertmanager/alertspb/compat.go @@ -34,13 +34,14 @@ func ToProto(cfg string, templates map[string]string, user string) AlertConfigDe } // ToGrafanaProto transforms a Grafana Alertmanager config to a GrafanaAlertConfigDesc. -func ToGrafanaProto(cfg, user, hash string, createdAtTimestamp int64, isDefault bool) GrafanaAlertConfigDesc { +func ToGrafanaProto(cfg, user, hash string, createdAtTimestamp int64, isDefault, isPromoted bool) GrafanaAlertConfigDesc { return GrafanaAlertConfigDesc{ User: user, RawConfig: cfg, Hash: hash, CreatedAtTimestamp: createdAtTimestamp, Default: isDefault, + Promoted: isPromoted, } } diff --git a/pkg/alertmanager/api_grafana.go b/pkg/alertmanager/api_grafana.go index 0cce20b6fcd..5a131b0aa9f 100644 --- a/pkg/alertmanager/api_grafana.go +++ b/pkg/alertmanager/api_grafana.go @@ -47,6 +47,7 @@ type UserGrafanaConfig struct { Hash string `json:"configuration_hash"` CreatedAt int64 `json:"created"` Default bool `json:"default"` + Promoted bool `json:"promoted"` } func (gc *UserGrafanaConfig) Validate() error { @@ -82,8 +83,13 @@ type UserGrafanaState struct { State string `json:"state"` } -func (gs *UserGrafanaState) UnmarshalJSON(data []byte) error { - type plain UserGrafanaState +type PostableUserGrafanaState struct { + UserGrafanaState + Promoted bool `json:"promoted"` +} + +func (gs *PostableUserGrafanaState) UnmarshalJSON(data []byte) error { + type plain PostableUserGrafanaState err := json.Unmarshal(data, (*plain)(gs)) if err != nil { return err @@ -172,7 +178,7 @@ func (am *MultitenantAlertmanager) SetUserGrafanaState(w http.ResponseWriter, r return } - st := &UserGrafanaState{} + st := &PostableUserGrafanaState{} err = json.Unmarshal(payload, st) if err != nil { level.Error(logger).Log("msg", errMarshallingStateJSON, "err", err.Error()) @@ -323,7 +329,7 @@ func (am *MultitenantAlertmanager) SetUserGrafanaConfig(w http.ResponseWriter, r return } - cfgDesc := alertspb.ToGrafanaProto(string(rawCfg), userID, cfg.Hash, cfg.CreatedAt, cfg.Default) + cfgDesc := alertspb.ToGrafanaProto(string(rawCfg), userID, cfg.Hash, cfg.CreatedAt, cfg.Default, cfg.Promoted) err = am.store.SetGrafanaAlertConfig(r.Context(), cfgDesc) if err != nil { level.Error(logger).Log("msg", errStoringGrafanaConfig, "err", err.Error()) From af5c9f92b58944edefdf92c08eb5dced495e83ce Mon Sep 17 00:00:00 2001 From: Santiago Date: Mon, 6 May 2024 11:19:21 +0200 Subject: [PATCH 02/17] fix tests --- pkg/alertmanager/api_grafana_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/alertmanager/api_grafana_test.go b/pkg/alertmanager/api_grafana_test.go index 62a346f1e87..afec2c2bb41 100644 --- a/pkg/alertmanager/api_grafana_test.go +++ b/pkg/alertmanager/api_grafana_test.go @@ -212,7 +212,8 @@ func TestMultitenantAlertmanager_GetUserGrafanaConfig(t *testing.T) { "configuration": %s, "configuration_hash": "bb788eaa294c05ec556c1ed87546b7a9", "created": %d, - "default": false + "default": false, + "promoted": false }, "status": "success" } @@ -326,7 +327,8 @@ func TestMultitenantAlertmanager_SetUserGrafanaConfig(t *testing.T) { "configuration": %s, "configuration_hash": "ChEKBW5mbG9nEghzb21lZGF0YQ==", "created": 12312414343, - "default": false + "default": false, + "promoted": true } `, testGrafanaConfig) req.Body = io.NopCloser(strings.NewReader(json)) From 8112ab89347edd18593713a0447c2b6bf44cd0dd Mon Sep 17 00:00:00 2001 From: Santiago Date: Mon, 6 May 2024 17:10:14 +0200 Subject: [PATCH 03/17] add function to compute final configuration from Mimir and Grafana configurations --- pkg/alertmanager/multitenant.go | 42 +++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/pkg/alertmanager/multitenant.go b/pkg/alertmanager/multitenant.go index e9e6d9342b4..6c7bef1c5d2 100644 --- a/pkg/alertmanager/multitenant.go +++ b/pkg/alertmanager/multitenant.go @@ -317,7 +317,7 @@ func NewMultitenantAlertmanager(cfg *MultitenantAlertmanagerConfig, store alerts return createMultitenantAlertmanager(cfg, fallbackConfig, store, ringStore, limits, features, logger, registerer) } -// ComputeFallbackConfig will load, vaildate and return the provided fallbackConfigFile +// ComputeFallbackConfig will load, validate and return the provided fallbackConfigFile // or return an valid empty default configuration if none is provided. func ComputeFallbackConfig(fallbackConfigFile string) ([]byte, error) { if fallbackConfigFile != "" { @@ -631,11 +631,10 @@ func (am *MultitenantAlertmanager) isUserOwned(userID string) bool { return alertmanagers.Includes(am.ringLifecycler.GetInstanceAddr()) } -func (am *MultitenantAlertmanager) syncConfigs(cfgs map[string]alertspb.AlertConfigDescs) { - level.Debug(am.logger).Log("msg", "adding configurations", "num_configs", len(cfgs)) - for user, cfg := range cfgs { - err := am.setConfig(cfg.Mimir) - if err != nil { +func (am *MultitenantAlertmanager) syncConfigs(cfgMap map[string]alertspb.AlertConfigDescs) { + level.Debug(am.logger).Log("msg", "adding configurations", "num_configs", len(cfgMap)) + for user, cfgs := range cfgMap { + if err := am.setConfig(am.computeConfig(cfgs)); err != nil { am.multitenantMetrics.lastReloadSuccessful.WithLabelValues(user).Set(float64(0)) level.Warn(am.logger).Log("msg", "error applying config", "err", err) continue @@ -649,7 +648,7 @@ func (am *MultitenantAlertmanager) syncConfigs(cfgs map[string]alertspb.AlertCon am.alertmanagersMtx.Lock() for userID, userAM := range am.alertmanagers { - if _, exists := cfgs[userID]; !exists { + if _, exists := cfgMap[userID]; !exists { userAlertmanagersToStop[userID] = userAM delete(am.alertmanagers, userID) delete(am.cfgs, userID) @@ -668,6 +667,35 @@ func (am *MultitenantAlertmanager) syncConfigs(cfgs map[string]alertspb.AlertCon } } +// computeConfig takes an AlertConfigDescs struct containing Mimir and Grafana configurations. +// It returns the final configuration the Alertmanager will use, merging both configs if necessary. +func (am *MultitenantAlertmanager) computeConfig(cfgs alertspb.AlertConfigDescs) alertspb.AlertConfigDesc { + var cfg alertspb.AlertConfigDesc + switch { + case !cfgs.Grafana.Promoted: + level.Debug(am.logger).Log("msg", "grafana configuration not promoted, using mimir config", "user", cfgs.Mimir.User) + cfg = cfgs.Mimir + case cfgs.Grafana.Default: + level.Debug(am.logger).Log("msg", "grafana configuration is default, using mimir config", "user", cfgs.Mimir.User) + cfg = cfgs.Mimir + case cfgs.Grafana.RawConfig == "": + level.Debug(am.logger).Log("msg", "grafana configuration is empty, using mimir config", "user", cfgs.Mimir.User) + cfg = cfgs.Mimir + + case cfgs.Mimir.RawConfig == am.fallbackConfig: + level.Debug(am.logger).Log("msg", "mimir configuration is default, using grafana config", "user", cfgs.Mimir.User) + // TODO: parse Grafana config. + case cfgs.Mimir.RawConfig == "": + level.Debug(am.logger).Log("msg", "mimir configuration is empty, using grafana config", "user", cfgs.Grafana.User) + // TODO: parse Grafana config. + + default: + level.Debug(am.logger).Log("msg", "merging configurations not implemented, using mimir config", "user", cfgs.Mimir.User) + } + + return cfg +} + // setConfig applies the given configuration to the alertmanager for `userID`, // creating an alertmanager if it doesn't already exist. func (am *MultitenantAlertmanager) setConfig(cfg alertspb.AlertConfigDesc) error { From 24bd0f34cf95a83a126c8f71c261bb92e6bc1232 Mon Sep 17 00:00:00 2001 From: Santiago Date: Tue, 7 May 2024 11:53:17 +0200 Subject: [PATCH 04/17] parse grafana configuration into an AlertConfigDesc --- pkg/alertmanager/alertspb/compat.go | 2 +- pkg/alertmanager/multitenant.go | 36 +++++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/pkg/alertmanager/alertspb/compat.go b/pkg/alertmanager/alertspb/compat.go index 1a629f32b6a..06e4505cf10 100644 --- a/pkg/alertmanager/alertspb/compat.go +++ b/pkg/alertmanager/alertspb/compat.go @@ -19,7 +19,7 @@ type AlertConfigDescs struct { // ToProto transforms a yaml Alertmanager config and map of template files to an AlertConfigDesc. func ToProto(cfg string, templates map[string]string, user string) AlertConfigDesc { - tmpls := []*TemplateDesc{} + tmpls := make([]*TemplateDesc, 0, len(templates)) for fn, body := range templates { tmpls = append(tmpls, &TemplateDesc{ Body: body, diff --git a/pkg/alertmanager/multitenant.go b/pkg/alertmanager/multitenant.go index 6c7bef1c5d2..3bbff76dc59 100644 --- a/pkg/alertmanager/multitenant.go +++ b/pkg/alertmanager/multitenant.go @@ -7,6 +7,7 @@ package alertmanager import ( "context" + "encoding/json" "flag" "fmt" "net/http" @@ -634,7 +635,14 @@ func (am *MultitenantAlertmanager) isUserOwned(userID string) bool { func (am *MultitenantAlertmanager) syncConfigs(cfgMap map[string]alertspb.AlertConfigDescs) { level.Debug(am.logger).Log("msg", "adding configurations", "num_configs", len(cfgMap)) for user, cfgs := range cfgMap { - if err := am.setConfig(am.computeConfig(cfgs)); err != nil { + cfg, err := am.computeConfig(cfgs) + if err != nil { + am.multitenantMetrics.lastReloadSuccessful.WithLabelValues(user).Set(float64(0)) + level.Warn(am.logger).Log("msg", "error computing config", "err", err) + continue + } + + if err := am.setConfig(cfg); err != nil { am.multitenantMetrics.lastReloadSuccessful.WithLabelValues(user).Set(float64(0)) level.Warn(am.logger).Log("msg", "error applying config", "err", err) continue @@ -669,9 +677,10 @@ func (am *MultitenantAlertmanager) syncConfigs(cfgMap map[string]alertspb.AlertC // computeConfig takes an AlertConfigDescs struct containing Mimir and Grafana configurations. // It returns the final configuration the Alertmanager will use, merging both configs if necessary. -func (am *MultitenantAlertmanager) computeConfig(cfgs alertspb.AlertConfigDescs) alertspb.AlertConfigDesc { +func (am *MultitenantAlertmanager) computeConfig(cfgs alertspb.AlertConfigDescs) (alertspb.AlertConfigDesc, error) { var cfg alertspb.AlertConfigDesc switch { + // Mimir configuration. case !cfgs.Grafana.Promoted: level.Debug(am.logger).Log("msg", "grafana configuration not promoted, using mimir config", "user", cfgs.Mimir.User) cfg = cfgs.Mimir @@ -682,18 +691,35 @@ func (am *MultitenantAlertmanager) computeConfig(cfgs alertspb.AlertConfigDescs) level.Debug(am.logger).Log("msg", "grafana configuration is empty, using mimir config", "user", cfgs.Mimir.User) cfg = cfgs.Mimir + // Grafana configuration. case cfgs.Mimir.RawConfig == am.fallbackConfig: level.Debug(am.logger).Log("msg", "mimir configuration is default, using grafana config", "user", cfgs.Mimir.User) - // TODO: parse Grafana config. + return parseGrafanaConfig(cfgs.Grafana) case cfgs.Mimir.RawConfig == "": level.Debug(am.logger).Log("msg", "mimir configuration is empty, using grafana config", "user", cfgs.Grafana.User) - // TODO: parse Grafana config. + return parseGrafanaConfig(cfgs.Grafana) + // Both configurations. default: level.Debug(am.logger).Log("msg", "merging configurations not implemented, using mimir config", "user", cfgs.Mimir.User) + return cfgs.Mimir, nil + } + + return cfg, nil +} + +func parseGrafanaConfig(cfg alertspb.GrafanaAlertConfigDesc) (alertspb.AlertConfigDesc, error) { + var amCfg GrafanaAlertmanagerConfig + if err := json.Unmarshal([]byte(cfg.RawConfig), &amCfg); err != nil { + return alertspb.AlertConfigDesc{}, fmt.Errorf("failed to unmarshal Grafana Alertmanager configuration %w", err) + } + + rawCfg, err := json.Marshal(amCfg.AlertmanagerConfig) + if err != nil { + return alertspb.AlertConfigDesc{}, fmt.Errorf("failed to marshal Grafana Alertmanager configuration %w", err) } - return cfg + return alertspb.ToProto(string(rawCfg), amCfg.Templates, cfg.User), nil } // setConfig applies the given configuration to the alertmanager for `userID`, From 8ecff4196f542c5359cc4641fb624eb09d56e964 Mon Sep 17 00:00:00 2001 From: Santiago Date: Tue, 7 May 2024 12:58:27 +0200 Subject: [PATCH 05/17] apply grafana configuration to the alertmanager ignoring grafana receivers --- pkg/alertmanager/alertmanager.go | 13 +++++--- pkg/alertmanager/config.go | 57 ++++++++++++++++++++++++++++++++ pkg/alertmanager/multitenant.go | 24 +++----------- 3 files changed, 70 insertions(+), 24 deletions(-) create mode 100644 pkg/alertmanager/config.go diff --git a/pkg/alertmanager/alertmanager.go b/pkg/alertmanager/alertmanager.go index 8372f39b536..f5ac68c3d28 100644 --- a/pkg/alertmanager/alertmanager.go +++ b/pkg/alertmanager/alertmanager.go @@ -20,6 +20,7 @@ import ( "github.com/go-kit/log" "github.com/go-kit/log/level" + "github.com/grafana/alerting/definition" "github.com/grafana/dskit/flagext" "github.com/pkg/errors" "github.com/prometheus/alertmanager/api" @@ -313,7 +314,7 @@ func clusterWait(position func() int, timeout time.Duration) func() time.Duratio } // ApplyConfig applies a new configuration to an Alertmanager. -func (am *Alertmanager) ApplyConfig(userID string, conf *config.Config, rawCfg string) error { +func (am *Alertmanager) ApplyConfig(userID string, conf *definition.PostableApiAlertingConfig, rawCfg string) error { templateFiles := make([]string, len(conf.Templates)) for i, t := range conf.Templates { templateFilepath, err := safeTemplateFilepath(filepath.Join(am.cfg.TenantDataDir, templatesDir), t) @@ -330,7 +331,8 @@ func (am *Alertmanager) ApplyConfig(userID string, conf *config.Config, rawCfg s } tmpl.ExternalURL = am.cfg.ExternalURL - am.api.Update(conf, func(_ model.LabelSet) {}) + cfg := grafanaToUpstreamConfig(conf) + am.api.Update(&cfg, func(_ model.LabelSet) {}) // Ensure inhibitor is set before being called if am.inhibitor != nil { @@ -394,7 +396,7 @@ func (am *Alertmanager) ApplyConfig(userID string, conf *config.Config, rawCfg s am.lastPipeline = pipeline am.dispatcher = dispatch.NewDispatcher( am.alerts, - dispatch.NewRoute(conf.Route, nil), + dispatch.NewRoute(cfg.Route, nil), pipeline, am.marker, timeoutFunc, @@ -451,10 +453,11 @@ func (am *Alertmanager) getFullState() (*clusterpb.FullState, error) { // buildIntegrationsMap builds a map of name to the list of integration notifiers off of a // list of receiver config. -func buildIntegrationsMap(nc []config.Receiver, tmpl *template.Template, firewallDialer *util_net.FirewallDialer, logger log.Logger, notifierWrapper func(string, notify.Notifier) notify.Notifier) (map[string][]notify.Integration, error) { +func buildIntegrationsMap(nc []*definition.PostableApiReceiver, tmpl *template.Template, firewallDialer *util_net.FirewallDialer, logger log.Logger, notifierWrapper func(string, notify.Notifier) notify.Notifier) (map[string][]notify.Integration, error) { integrationsMap := make(map[string][]notify.Integration, len(nc)) for _, rcv := range nc { - integrations, err := buildReceiverIntegrations(rcv, tmpl, firewallDialer, logger, notifierWrapper) + // TODO: We're currently passing only the upstream receivers, we need to use the Grafana receivers too. + integrations, err := buildReceiverIntegrations(rcv.Receiver, tmpl, firewallDialer, logger, notifierWrapper) if err != nil { return nil, err } diff --git a/pkg/alertmanager/config.go b/pkg/alertmanager/config.go new file mode 100644 index 00000000000..0a9eb57bd05 --- /dev/null +++ b/pkg/alertmanager/config.go @@ -0,0 +1,57 @@ +package alertmanager + +import ( + "encoding/json" + "fmt" + + "github.com/grafana/alerting/definition" + "github.com/grafana/mimir/pkg/alertmanager/alertspb" + amconfig "github.com/prometheus/alertmanager/config" + "gopkg.in/yaml.v3" +) + +func parseGrafanaConfig(cfg alertspb.GrafanaAlertConfigDesc) (alertspb.AlertConfigDesc, error) { + var amCfg GrafanaAlertmanagerConfig + if err := json.Unmarshal([]byte(cfg.RawConfig), &amCfg); err != nil { + return alertspb.AlertConfigDesc{}, fmt.Errorf("failed to unmarshal Grafana Alertmanager configuration %w", err) + } + + rawCfg, err := json.Marshal(amCfg.AlertmanagerConfig) + if err != nil { + return alertspb.AlertConfigDesc{}, fmt.Errorf("failed to marshal Grafana Alertmanager configuration %w", err) + } + + return alertspb.ToProto(string(rawCfg), amCfg.Templates, cfg.User), nil +} + +func loadConfig(rawCfg []byte) (*definition.PostableApiAlertingConfig, error) { + var cfg definition.PostableApiAlertingConfig + if err := yaml.Unmarshal(rawCfg, &cfg); err != nil { + return nil, err + } + + if err := cfg.Validate(); err != nil { + return nil, err + } + return &cfg, nil +} + +// Note: This could be a method in the alerting package. +func grafanaToUpstreamConfig(cfg *definition.PostableApiAlertingConfig) amconfig.Config { + // Note: We're ignoring the Grafana part of the receivers. + // TODO: Use it. + rcvs := make([]amconfig.Receiver, 0, len(cfg.Receivers)) + for _, r := range cfg.Receivers { + rcvs = append(rcvs, r.Receiver) + } + + return amconfig.Config{ + Global: cfg.Config.Global, + Route: cfg.Config.Route.AsAMRoute(), + InhibitRules: cfg.Config.InhibitRules, + Receivers: rcvs, + Templates: cfg.Config.Templates, + MuteTimeIntervals: cfg.Config.MuteTimeIntervals, + TimeIntervals: cfg.Config.TimeIntervals, + } +} diff --git a/pkg/alertmanager/multitenant.go b/pkg/alertmanager/multitenant.go index 3bbff76dc59..ed398bcc9f2 100644 --- a/pkg/alertmanager/multitenant.go +++ b/pkg/alertmanager/multitenant.go @@ -7,7 +7,6 @@ package alertmanager import ( "context" - "encoding/json" "flag" "fmt" "net/http" @@ -19,6 +18,7 @@ import ( "github.com/go-kit/log" "github.com/go-kit/log/level" + "github.com/grafana/alerting/definition" "github.com/grafana/dskit/concurrency" "github.com/grafana/dskit/flagext" "github.com/grafana/dskit/httpgrpc" @@ -708,24 +708,10 @@ func (am *MultitenantAlertmanager) computeConfig(cfgs alertspb.AlertConfigDescs) return cfg, nil } -func parseGrafanaConfig(cfg alertspb.GrafanaAlertConfigDesc) (alertspb.AlertConfigDesc, error) { - var amCfg GrafanaAlertmanagerConfig - if err := json.Unmarshal([]byte(cfg.RawConfig), &amCfg); err != nil { - return alertspb.AlertConfigDesc{}, fmt.Errorf("failed to unmarshal Grafana Alertmanager configuration %w", err) - } - - rawCfg, err := json.Marshal(amCfg.AlertmanagerConfig) - if err != nil { - return alertspb.AlertConfigDesc{}, fmt.Errorf("failed to marshal Grafana Alertmanager configuration %w", err) - } - - return alertspb.ToProto(string(rawCfg), amCfg.Templates, cfg.User), nil -} - // setConfig applies the given configuration to the alertmanager for `userID`, // creating an alertmanager if it doesn't already exist. func (am *MultitenantAlertmanager) setConfig(cfg alertspb.AlertConfigDesc) error { - var userAmConfig *amconfig.Config + var userAmConfig *definition.PostableApiAlertingConfig var err error var hasTemplateChanges bool var userTemplateDir = filepath.Join(am.getTenantDirectory(cfg.User), templatesDir) @@ -787,13 +773,13 @@ func (am *MultitenantAlertmanager) setConfig(cfg alertspb.AlertConfigDesc) error return fmt.Errorf("blank Alertmanager configuration for %v", cfg.User) } level.Debug(am.logger).Log("msg", "blank Alertmanager configuration; using fallback", "user", cfg.User) - userAmConfig, err = amconfig.Load(am.fallbackConfig) + userAmConfig, err = loadConfig([]byte(am.fallbackConfig)) if err != nil { return fmt.Errorf("unable to load fallback configuration for %v: %v", cfg.User, err) } rawCfg = am.fallbackConfig } else { - userAmConfig, err = amconfig.Load(cfg.RawConfig) + userAmConfig, err = loadConfig([]byte(cfg.RawConfig)) if err != nil && hasExisting { // This means that if a user has a working config and // they submit a broken one, the Manager will keep running the last known @@ -835,7 +821,7 @@ func (am *MultitenantAlertmanager) getTenantDirectory(userID string) string { return filepath.Join(am.cfg.DataDir, userID) } -func (am *MultitenantAlertmanager) newAlertmanager(userID string, amConfig *amconfig.Config, rawCfg string) (*Alertmanager, error) { +func (am *MultitenantAlertmanager) newAlertmanager(userID string, amConfig *definition.PostableApiAlertingConfig, rawCfg string) (*Alertmanager, error) { reg := prometheus.NewRegistry() tenantDir := am.getTenantDirectory(userID) From 38a3fb3e0367e7f08ca7d3d297c0321706089070 Mon Sep 17 00:00:00 2001 From: Santiago Date: Wed, 8 May 2024 12:26:46 +0200 Subject: [PATCH 06/17] include grafana receivers --- go.mod | 3 + go.sum | 4 +- pkg/alertmanager/alertmanager.go | 76 ++- pkg/alertmanager/config.go | 38 ++ pkg/alertmanager/sender.go | 21 + .../grafana/alerting/cluster/cluster.go | 23 + .../grafana/alerting/images/images.go | 77 +++ .../grafana/alerting/images/testing.go | 149 +++++ .../grafana/alerting/images/utils.go | 77 +++ .../grafana/alerting/logging/log.go | 51 ++ .../grafana/alerting/models/labels.go | 29 + .../grafana/alerting/notify/alerts.go | 242 +++++++ .../grafana/alerting/notify/crypto.go | 1 + .../grafana/alerting/notify/factory.go | 141 ++++ .../alerting/notify/grafana_alertmanager.go | 618 ++++++++++++++++++ .../notify/grafana_alertmanager_metrics.go | 46 ++ .../alerting/notify/mimir_alertmanager.go | 1 + .../alerting/notify/multiorg_alertmanager.go | 21 + .../grafana/alerting/notify/receivers.go | 564 ++++++++++++++++ .../grafana/alerting/notify/silences.go | 208 ++++++ .../grafana/alerting/notify/status.go | 13 + .../grafana/alerting/notify/templates.go | 147 +++++ .../grafana/alerting/notify/testing.go | 220 +++++++ .../receivers/alertmanager/alertmanager.go | 82 +++ .../alerting/receivers/alertmanager/config.go | 52 ++ .../receivers/alertmanager/testing.go | 13 + .../grafana/alerting/receivers/base.go | 30 + .../grafana/alerting/receivers/config_util.go | 63 ++ .../alerting/receivers/dinding/config.go | 39 ++ .../alerting/receivers/dinding/dingding.go | 115 ++++ .../alerting/receivers/dinding/testing.go | 9 + .../alerting/receivers/discord/config.go | 37 ++ .../alerting/receivers/discord/discord.go | 324 +++++++++ .../alerting/receivers/discord/testing.go | 10 + .../grafana/alerting/receivers/email.go | 26 + .../alerting/receivers/email/config.go | 58 ++ .../grafana/alerting/receivers/email/email.go | 110 ++++ .../alerting/receivers/email/testing.go | 9 + .../alerting/receivers/googlechat/config.go | 34 + .../receivers/googlechat/googlechat.go | 252 +++++++ .../alerting/receivers/googlechat/testing.go | 10 + .../alerting/receivers/kafka/config.go | 66 ++ .../grafana/alerting/receivers/kafka/kafka.go | 284 ++++++++ .../alerting/receivers/kafka/testing.go | 18 + .../grafana/alerting/receivers/line/config.go | 35 + .../grafana/alerting/receivers/line/line.go | 88 +++ .../alerting/receivers/line/testing.go | 13 + .../grafana/alerting/receivers/number.go | 30 + .../alerting/receivers/oncall/config.go | 81 +++ .../alerting/receivers/oncall/oncall.go | 149 +++++ .../alerting/receivers/oncall/testing.go | 20 + .../alerting/receivers/opsgenie/config.go | 126 ++++ .../alerting/receivers/opsgenie/opsgenie.go | 273 ++++++++ .../alerting/receivers/opsgenie/testing.go | 31 + .../alerting/receivers/pagerduty/config.go | 100 +++ .../alerting/receivers/pagerduty/pagerduty.go | 219 +++++++ .../alerting/receivers/pagerduty/testing.go | 19 + .../alerting/receivers/pushover/config.go | 93 +++ .../alerting/receivers/pushover/pushover.go | 252 +++++++ .../alerting/receivers/pushover/testing.go | 23 + .../alerting/receivers/sensugo/config.go | 39 ++ .../alerting/receivers/sensugo/sensugo.go | 147 +++++ .../alerting/receivers/sensugo/testing.go | 17 + .../alerting/receivers/slack/config.go | 72 ++ .../grafana/alerting/receivers/slack/slack.go | 551 ++++++++++++++++ .../alerting/receivers/slack/testing.go | 23 + .../alerting/receivers/teams/config.go | 35 + .../grafana/alerting/receivers/teams/teams.go | 343 ++++++++++ .../alerting/receivers/teams/testing.go | 9 + .../alerting/receivers/telegram/config.go | 75 +++ .../alerting/receivers/telegram/telegram.go | 189 ++++++ .../alerting/receivers/telegram/testing.go | 18 + .../grafana/alerting/receivers/testing.go | 23 + .../alerting/receivers/testing/testing.go | 40 ++ .../alerting/receivers/threema/config.go | 58 ++ .../alerting/receivers/threema/testing.go | 15 + .../alerting/receivers/threema/threema.go | 107 +++ .../grafana/alerting/receivers/util.go | 168 +++++ .../alerting/receivers/victorops/config.go | 42 ++ .../alerting/receivers/victorops/testing.go | 9 + .../alerting/receivers/victorops/victorops.go | 137 ++++ .../alerting/receivers/webex/config.go | 51 ++ .../alerting/receivers/webex/testing.go | 14 + .../grafana/alerting/receivers/webex/webex.go | 111 ++++ .../grafana/alerting/receivers/webhook.go | 18 + .../alerting/receivers/webhook/config.go | 81 +++ .../alerting/receivers/webhook/testing.go | 20 + .../alerting/receivers/webhook/webhook.go | 140 ++++ .../alerting/receivers/wecom/config.go | 87 +++ .../alerting/receivers/wecom/testing.go | 19 + .../grafana/alerting/receivers/wecom/wecom.go | 162 +++++ .../alerting/templates/default_template.go | 106 +++ .../grafana/alerting/templates/funcs.go | 9 + .../alerting/templates/template_data.go | 265 ++++++++ .../grafana/alerting/templates/util.go | 108 +++ .../prometheus/alertmanager/api/api.go | 5 +- .../alertmanager/api/v1_deprecation_router.go | 1 - .../prometheus/alertmanager/api/v2/api.go | 55 +- .../alertmanager/api/v2/models/integration.go | 128 ++++ .../alertmanager/api/v2/models/receiver.go | 89 ++- .../alertmanager/api/v2/openapi.yaml | 28 + .../api/v2/restapi/embedded_spec.go | 82 ++- .../alertmanager/cluster/channel.go | 10 +- .../alertmanager/cluster/cluster.go | 6 +- .../alertmanager/cluster/delegate.go | 9 +- .../prometheus/alertmanager/config/config.go | 10 +- .../alertmanager/config/notifiers.go | 4 +- .../prometheus/alertmanager/dispatch/route.go | 22 +- .../alertmanager/matchers/parse/lexer.go | 2 +- .../alertmanager/matchers/parse/parse.go | 8 +- .../prometheus/alertmanager/nflog/nflog.go | 10 +- .../alertmanager/notify/email/email.go | 2 +- .../prometheus/alertmanager/notify/notify.go | 103 +-- .../alertmanager/notify/receiver.go | 42 ++ .../prometheus/alertmanager/notify/util.go | 4 +- .../alertmanager/notify/webex/webex.go | 1 + .../alertmanager/notify/webhook/webhook.go | 18 +- .../alertmanager/pkg/labels/parse.go | 2 +- .../alertmanager/provider/mem/mem.go | 5 +- .../alertmanager/provider/provider.go | 2 +- .../alertmanager/silence/silence.go | 31 +- .../prometheus/alertmanager/store/store.go | 23 +- .../alertmanager/template/template.go | 12 + .../alertmanager/timeinterval/timeinterval.go | 4 +- .../prometheus/alertmanager/types/types.go | 2 +- .../x/sync/singleflight/singleflight.go | 214 ++++++ vendor/modules.txt | 32 +- 127 files changed, 10097 insertions(+), 140 deletions(-) create mode 100644 pkg/alertmanager/sender.go create mode 100644 vendor/github.com/grafana/alerting/cluster/cluster.go create mode 100644 vendor/github.com/grafana/alerting/images/images.go create mode 100644 vendor/github.com/grafana/alerting/images/testing.go create mode 100644 vendor/github.com/grafana/alerting/images/utils.go create mode 100644 vendor/github.com/grafana/alerting/logging/log.go create mode 100644 vendor/github.com/grafana/alerting/models/labels.go create mode 100644 vendor/github.com/grafana/alerting/notify/alerts.go create mode 100644 vendor/github.com/grafana/alerting/notify/crypto.go create mode 100644 vendor/github.com/grafana/alerting/notify/factory.go create mode 100644 vendor/github.com/grafana/alerting/notify/grafana_alertmanager.go create mode 100644 vendor/github.com/grafana/alerting/notify/grafana_alertmanager_metrics.go create mode 100644 vendor/github.com/grafana/alerting/notify/mimir_alertmanager.go create mode 100644 vendor/github.com/grafana/alerting/notify/multiorg_alertmanager.go create mode 100644 vendor/github.com/grafana/alerting/notify/receivers.go create mode 100644 vendor/github.com/grafana/alerting/notify/silences.go create mode 100644 vendor/github.com/grafana/alerting/notify/status.go create mode 100644 vendor/github.com/grafana/alerting/notify/templates.go create mode 100644 vendor/github.com/grafana/alerting/notify/testing.go create mode 100644 vendor/github.com/grafana/alerting/receivers/alertmanager/alertmanager.go create mode 100644 vendor/github.com/grafana/alerting/receivers/alertmanager/config.go create mode 100644 vendor/github.com/grafana/alerting/receivers/alertmanager/testing.go create mode 100644 vendor/github.com/grafana/alerting/receivers/base.go create mode 100644 vendor/github.com/grafana/alerting/receivers/config_util.go create mode 100644 vendor/github.com/grafana/alerting/receivers/dinding/config.go create mode 100644 vendor/github.com/grafana/alerting/receivers/dinding/dingding.go create mode 100644 vendor/github.com/grafana/alerting/receivers/dinding/testing.go create mode 100644 vendor/github.com/grafana/alerting/receivers/discord/config.go create mode 100644 vendor/github.com/grafana/alerting/receivers/discord/discord.go create mode 100644 vendor/github.com/grafana/alerting/receivers/discord/testing.go create mode 100644 vendor/github.com/grafana/alerting/receivers/email.go create mode 100644 vendor/github.com/grafana/alerting/receivers/email/config.go create mode 100644 vendor/github.com/grafana/alerting/receivers/email/email.go create mode 100644 vendor/github.com/grafana/alerting/receivers/email/testing.go create mode 100644 vendor/github.com/grafana/alerting/receivers/googlechat/config.go create mode 100644 vendor/github.com/grafana/alerting/receivers/googlechat/googlechat.go create mode 100644 vendor/github.com/grafana/alerting/receivers/googlechat/testing.go create mode 100644 vendor/github.com/grafana/alerting/receivers/kafka/config.go create mode 100644 vendor/github.com/grafana/alerting/receivers/kafka/kafka.go create mode 100644 vendor/github.com/grafana/alerting/receivers/kafka/testing.go create mode 100644 vendor/github.com/grafana/alerting/receivers/line/config.go create mode 100644 vendor/github.com/grafana/alerting/receivers/line/line.go create mode 100644 vendor/github.com/grafana/alerting/receivers/line/testing.go create mode 100644 vendor/github.com/grafana/alerting/receivers/number.go create mode 100644 vendor/github.com/grafana/alerting/receivers/oncall/config.go create mode 100644 vendor/github.com/grafana/alerting/receivers/oncall/oncall.go create mode 100644 vendor/github.com/grafana/alerting/receivers/oncall/testing.go create mode 100644 vendor/github.com/grafana/alerting/receivers/opsgenie/config.go create mode 100644 vendor/github.com/grafana/alerting/receivers/opsgenie/opsgenie.go create mode 100644 vendor/github.com/grafana/alerting/receivers/opsgenie/testing.go create mode 100644 vendor/github.com/grafana/alerting/receivers/pagerduty/config.go create mode 100644 vendor/github.com/grafana/alerting/receivers/pagerduty/pagerduty.go create mode 100644 vendor/github.com/grafana/alerting/receivers/pagerduty/testing.go create mode 100644 vendor/github.com/grafana/alerting/receivers/pushover/config.go create mode 100644 vendor/github.com/grafana/alerting/receivers/pushover/pushover.go create mode 100644 vendor/github.com/grafana/alerting/receivers/pushover/testing.go create mode 100644 vendor/github.com/grafana/alerting/receivers/sensugo/config.go create mode 100644 vendor/github.com/grafana/alerting/receivers/sensugo/sensugo.go create mode 100644 vendor/github.com/grafana/alerting/receivers/sensugo/testing.go create mode 100644 vendor/github.com/grafana/alerting/receivers/slack/config.go create mode 100644 vendor/github.com/grafana/alerting/receivers/slack/slack.go create mode 100644 vendor/github.com/grafana/alerting/receivers/slack/testing.go create mode 100644 vendor/github.com/grafana/alerting/receivers/teams/config.go create mode 100644 vendor/github.com/grafana/alerting/receivers/teams/teams.go create mode 100644 vendor/github.com/grafana/alerting/receivers/teams/testing.go create mode 100644 vendor/github.com/grafana/alerting/receivers/telegram/config.go create mode 100644 vendor/github.com/grafana/alerting/receivers/telegram/telegram.go create mode 100644 vendor/github.com/grafana/alerting/receivers/telegram/testing.go create mode 100644 vendor/github.com/grafana/alerting/receivers/testing.go create mode 100644 vendor/github.com/grafana/alerting/receivers/testing/testing.go create mode 100644 vendor/github.com/grafana/alerting/receivers/threema/config.go create mode 100644 vendor/github.com/grafana/alerting/receivers/threema/testing.go create mode 100644 vendor/github.com/grafana/alerting/receivers/threema/threema.go create mode 100644 vendor/github.com/grafana/alerting/receivers/util.go create mode 100644 vendor/github.com/grafana/alerting/receivers/victorops/config.go create mode 100644 vendor/github.com/grafana/alerting/receivers/victorops/testing.go create mode 100644 vendor/github.com/grafana/alerting/receivers/victorops/victorops.go create mode 100644 vendor/github.com/grafana/alerting/receivers/webex/config.go create mode 100644 vendor/github.com/grafana/alerting/receivers/webex/testing.go create mode 100644 vendor/github.com/grafana/alerting/receivers/webex/webex.go create mode 100644 vendor/github.com/grafana/alerting/receivers/webhook.go create mode 100644 vendor/github.com/grafana/alerting/receivers/webhook/config.go create mode 100644 vendor/github.com/grafana/alerting/receivers/webhook/testing.go create mode 100644 vendor/github.com/grafana/alerting/receivers/webhook/webhook.go create mode 100644 vendor/github.com/grafana/alerting/receivers/wecom/config.go create mode 100644 vendor/github.com/grafana/alerting/receivers/wecom/testing.go create mode 100644 vendor/github.com/grafana/alerting/receivers/wecom/wecom.go create mode 100644 vendor/github.com/grafana/alerting/templates/default_template.go create mode 100644 vendor/github.com/grafana/alerting/templates/funcs.go create mode 100644 vendor/github.com/grafana/alerting/templates/template_data.go create mode 100644 vendor/github.com/grafana/alerting/templates/util.go create mode 100644 vendor/github.com/prometheus/alertmanager/api/v2/models/integration.go create mode 100644 vendor/github.com/prometheus/alertmanager/notify/receiver.go create mode 100644 vendor/golang.org/x/sync/singleflight/singleflight.go diff --git a/go.mod b/go.mod index d0f846edee8..dfe9b3c57f3 100644 --- a/go.mod +++ b/go.mod @@ -284,3 +284,6 @@ replace github.com/opentracing-contrib/go-stdlib => github.com/grafana/opentraci // Replace opentracing-contrib/go-grpc with a fork until https://github.com/opentracing-contrib/go-grpc/pull/16 is merged. replace github.com/opentracing-contrib/go-grpc => github.com/charleskorn/go-grpc v0.0.0-20231024023642-e9298576254f + +// Replacing prometheus/alertmanager with our fork. +replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20240422145632-c33c6b5b6e6b diff --git a/go.sum b/go.sum index 99808aba899..b428ebc3e7e 100644 --- a/go.sum +++ b/go.sum @@ -521,6 +521,8 @@ github.com/grafana/mimir-prometheus v0.0.0-20240430115734-de14b0ea393d h1:Om2Xlh github.com/grafana/mimir-prometheus v0.0.0-20240430115734-de14b0ea393d/go.mod h1:aHDiVcHIagfJ2pA67wkdb+PZloT0qmhnz354giFb3AQ= github.com/grafana/opentracing-contrib-go-stdlib v0.0.0-20230509071955-f410e79da956 h1:em1oddjXL8c1tL0iFdtVtPloq2hRPen2MJQKoAWpxu0= github.com/grafana/opentracing-contrib-go-stdlib v0.0.0-20230509071955-f410e79da956/go.mod h1:qtI1ogk+2JhVPIXVc6q+NHziSmy2W5GbdQZFUHADCBU= +github.com/grafana/prometheus-alertmanager v0.25.1-0.20240422145632-c33c6b5b6e6b h1:HCbWyVL6vi7gxyO76gQksSPH203oBJ1MJ3JcG1OQlsg= +github.com/grafana/prometheus-alertmanager v0.25.1-0.20240422145632-c33c6b5b6e6b/go.mod h1:01sXtHoRwI8W324IPAzuxDFOmALqYLCOhvSC2fUHWXc= github.com/grafana/pyroscope-go/godeltaprof v0.1.6 h1:nEdZ8louGAplSvIJi1HVp7kWvFvdiiYg3COLlTwJiFo= github.com/grafana/pyroscope-go/godeltaprof v0.1.6/go.mod h1:Tk376Nbldo4Cha9RgiU7ik8WKFkNpfds98aUzS8omLE= github.com/grafana/regexp v0.0.0-20221005093135-b4c2bcb0a4b6 h1:A3dhViTeFDSQcGOXuUi6ukCQSMyDtDISBp2z6OOo2YM= @@ -783,8 +785,6 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/prometheus/alertmanager v0.27.0 h1:V6nTa2J5V4s8TG4C4HtrBP/WNSebCCTYGGv4qecA/+I= -github.com/prometheus/alertmanager v0.27.0/go.mod h1:8Ia/R3urPmbzJ8OsdvmZvIprDwvwmYCmUbwBL+jlPOE= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= diff --git a/pkg/alertmanager/alertmanager.go b/pkg/alertmanager/alertmanager.go index f5ac68c3d28..7cbe4183c96 100644 --- a/pkg/alertmanager/alertmanager.go +++ b/pkg/alertmanager/alertmanager.go @@ -21,6 +21,10 @@ import ( "github.com/go-kit/log" "github.com/go-kit/log/level" "github.com/grafana/alerting/definition" + "github.com/grafana/alerting/images" + alertingLogging "github.com/grafana/alerting/logging" + alertingNotify "github.com/grafana/alerting/notify" + alertingReceivers "github.com/grafana/alerting/receivers" "github.com/grafana/dskit/flagext" "github.com/pkg/errors" "github.com/prometheus/alertmanager/api" @@ -331,9 +335,6 @@ func (am *Alertmanager) ApplyConfig(userID string, conf *definition.PostableApiA } tmpl.ExternalURL = am.cfg.ExternalURL - cfg := grafanaToUpstreamConfig(conf) - am.api.Update(&cfg, func(_ model.LabelSet) {}) - // Ensure inhibitor is set before being called if am.inhibitor != nil { am.inhibitor.Stop() @@ -374,6 +375,11 @@ func (am *Alertmanager) ApplyConfig(userID string, conf *definition.PostableApiA return err } + cfg := grafanaToUpstreamConfig(conf) + activeReceivers := getActiveReceiversMap(dispatch.NewRoute(cfg.Route, nil)) + receivers := buildReceivers(integrationsMap, activeReceivers) + am.api.Update(&cfg, receivers, func(_ model.LabelSet) {}) + timeIntervals := make(map[string][]timeinterval.TimeInterval, len(conf.MuteTimeIntervals)+len(conf.TimeIntervals)) for _, ti := range conf.MuteTimeIntervals { timeIntervals[ti.Name] = ti.TimeIntervals @@ -385,7 +391,7 @@ func (am *Alertmanager) ApplyConfig(userID string, conf *definition.PostableApiA intervener := timeinterval.NewIntervener(timeIntervals) pipeline := am.pipelineBuilder.New( - integrationsMap, + receivers, waitFunc, am.inhibitor, silence.NewSilencer(am.silences, am.marker, am.logger), @@ -453,26 +459,52 @@ func (am *Alertmanager) getFullState() (*clusterpb.FullState, error) { // buildIntegrationsMap builds a map of name to the list of integration notifiers off of a // list of receiver config. -func buildIntegrationsMap(nc []*definition.PostableApiReceiver, tmpl *template.Template, firewallDialer *util_net.FirewallDialer, logger log.Logger, notifierWrapper func(string, notify.Notifier) notify.Notifier) (map[string][]notify.Integration, error) { - integrationsMap := make(map[string][]notify.Integration, len(nc)) +func buildIntegrationsMap(nc []*definition.PostableApiReceiver, tmpl *template.Template, firewallDialer *util_net.FirewallDialer, logger log.Logger, notifierWrapper func(string, notify.Notifier) notify.Notifier) (map[string][]*notify.Integration, error) { + integrationsMap := make(map[string][]*notify.Integration, len(nc)) for _, rcv := range nc { - // TODO: We're currently passing only the upstream receivers, we need to use the Grafana receivers too. - integrations, err := buildReceiverIntegrations(rcv.Receiver, tmpl, firewallDialer, logger, notifierWrapper) - if err != nil { - return nil, err + // Check if it's a Grafana or an upstream receiver and build the integrations accordingly. + if rcv.Type() == definition.GrafanaReceiverType { + // The decrypt functions and the context are used to decrypt the configuration. + // We don't need to decrypt anything, so we can pass a no-op decrypt func and a context.Background(). + rCfg, err := alertingNotify.BuildReceiverConfiguration(context.Background(), postableApiReceiverToApiReceiver(rcv), noopDecryptFn) + if err != nil { + return nil, err + } + + // TODO: logger factory, webhook sender and email sender are no-ops for now. + // TODO: orgID, version. + integrations, err := alertingNotify.BuildReceiverIntegrations(rCfg, tmpl, &images.UnavailableProvider{}, loggerFactory, whSenderFn, emailSenderFn, 1, "") + if err != nil { + return nil, err + } + integrationsMap[rcv.Name] = integrations + } else { + integrations, err := buildReceiverIntegrations(rcv.Receiver, tmpl, firewallDialer, logger, notifierWrapper) + if err != nil { + return nil, err + } + integrationsMap[rcv.Name] = integrations } - integrationsMap[rcv.Name] = integrations } return integrationsMap, nil } +func buildReceivers(integrationsMap map[string][]*notify.Integration, activeReceivers map[string]struct{}) []*notify.Receiver { + var receivers []*notify.Receiver + for k, v := range integrationsMap { + _, active := activeReceivers[k] + receivers = append(receivers, notify.NewReceiver(k, active, v)) + } + return receivers +} + // buildReceiverIntegrations builds a list of integration notifiers off of a // receiver config. // Taken from https://github.com/prometheus/alertmanager/blob/94d875f1227b29abece661db1a68c001122d1da5/cmd/alertmanager/main.go#L112-L159. -func buildReceiverIntegrations(nc config.Receiver, tmpl *template.Template, firewallDialer *util_net.FirewallDialer, logger log.Logger, wrapper func(string, notify.Notifier) notify.Notifier) ([]notify.Integration, error) { +func buildReceiverIntegrations(nc config.Receiver, tmpl *template.Template, firewallDialer *util_net.FirewallDialer, logger log.Logger, wrapper func(string, notify.Notifier) notify.Notifier) ([]*notify.Integration, error) { var ( errs types.MultiError - integrations []notify.Integration + integrations []*notify.Integration add = func(name string, i int, rs notify.ResolvedSender, f func(l log.Logger) (notify.Notifier, error)) { n, err := f(log.With(logger, "integration", name)) if err != nil { @@ -535,6 +567,24 @@ func buildReceiverIntegrations(nc config.Receiver, tmpl *template.Template, fire return integrations, nil } +// noopDecryptFn implements alertingNotify.DecryptFn. +// TODO: make part of alerting package. +func noopDecryptFn(_ context.Context, sjd map[string][]byte, key string, fallback string) string { + if v, ok := sjd[key]; ok { + return string(v) + } + return fallback +} +func loggerFactory(logger string, ctx ...interface{}) alertingLogging.Logger { + return alertingLogging.FakeLogger{} +} +func whSenderFn(n alertingReceivers.Metadata) (alertingReceivers.WebhookSender, error) { + return &sender{}, nil +} +func emailSenderFn(n alertingReceivers.Metadata) (alertingReceivers.EmailSender, error) { + return &sender{}, nil +} + func md5HashAsMetricValue(data []byte) float64 { sum := md5.Sum(data) // We only want 48 bits as a float64 only has a 53 bit mantissa. diff --git a/pkg/alertmanager/config.go b/pkg/alertmanager/config.go index 0a9eb57bd05..327bcc67fe7 100644 --- a/pkg/alertmanager/config.go +++ b/pkg/alertmanager/config.go @@ -5,8 +5,10 @@ import ( "fmt" "github.com/grafana/alerting/definition" + alertingNotify "github.com/grafana/alerting/notify" "github.com/grafana/mimir/pkg/alertmanager/alertspb" amconfig "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/dispatch" "gopkg.in/yaml.v3" ) @@ -55,3 +57,39 @@ func grafanaToUpstreamConfig(cfg *definition.PostableApiAlertingConfig) amconfig TimeIntervals: cfg.Config.TimeIntervals, } } + +// TODO: move to alerting package. +func postableApiReceiverToApiReceiver(r *definition.PostableApiReceiver) *alertingNotify.APIReceiver { + integrations := alertingNotify.GrafanaIntegrations{ + Integrations: make([]*alertingNotify.GrafanaIntegrationConfig, 0, len(r.GrafanaManagedReceivers)), + } + for _, cfg := range r.GrafanaManagedReceivers { + integrations.Integrations = append(integrations.Integrations, postableGrafanaReceiverToGrafanaIntegrationConfig(cfg)) + } + + return &alertingNotify.APIReceiver{ + ConfigReceiver: r.Receiver, + GrafanaIntegrations: integrations, + } +} + +// TODO: move to alerting package. +func postableGrafanaReceiverToGrafanaIntegrationConfig(p *definition.PostableGrafanaReceiver) *alertingNotify.GrafanaIntegrationConfig { + return &alertingNotify.GrafanaIntegrationConfig{ + UID: p.UID, + Name: p.Name, + Type: p.Type, + DisableResolveMessage: p.DisableResolveMessage, + Settings: json.RawMessage(p.Settings), + SecureSettings: p.SecureSettings, + } +} + +// TODO: move to alerting package. +func getActiveReceiversMap(r *dispatch.Route) map[string]struct{} { + activeReceivers := make(map[string]struct{}) + r.Walk(func(r *dispatch.Route) { + activeReceivers[r.RouteOpts.Receiver] = struct{}{} + }) + return activeReceivers +} diff --git a/pkg/alertmanager/sender.go b/pkg/alertmanager/sender.go new file mode 100644 index 00000000000..000f6e2b2ba --- /dev/null +++ b/pkg/alertmanager/sender.go @@ -0,0 +1,21 @@ +package alertmanager + +import ( + "context" + + alertingReceivers "github.com/grafana/alerting/receivers" +) + +// sender is a no-op webhook and email sender. +// TODO: make it work. +type sender struct{} + +// SendWebhook implements alertingReceivers.WebhookSender. +func (s *sender) SendWebhook(ctx context.Context, cmd *alertingReceivers.SendWebhookSettings) error { + return nil +} + +// SendEmail implements alertingReceivers.EmailSender. +func (s *sender) SendEmail(ctx context.Context, cmd *alertingReceivers.SendEmailSettings) error { + return nil +} diff --git a/vendor/github.com/grafana/alerting/cluster/cluster.go b/vendor/github.com/grafana/alerting/cluster/cluster.go new file mode 100644 index 00000000000..434c3bbae13 --- /dev/null +++ b/vendor/github.com/grafana/alerting/cluster/cluster.go @@ -0,0 +1,23 @@ +package cluster + +import ( + "github.com/prometheus/alertmanager/cluster" +) + +const ( + DefaultGossipInterval = cluster.DefaultGossipInterval + DefaultPushPullInterval = cluster.DefaultPushPullInterval + DefaultProbeInterval = cluster.DefaultProbeInterval + DefaultProbeTimeout = cluster.DefaultProbeTimeout + DefaultReconnectInterval = cluster.DefaultReconnectInterval + DefaultReconnectTimeout = cluster.DefaultReconnectTimeout + DefaultTCPTimeout = cluster.DefaultTCPTimeout +) + +var ( + Create = cluster.Create +) + +type ClusterChannel = cluster.ClusterChannel //nolint:revive +type Peer = cluster.Peer +type State = cluster.State diff --git a/vendor/github.com/grafana/alerting/images/images.go b/vendor/github.com/grafana/alerting/images/images.go new file mode 100644 index 00000000000..9b9bafee1af --- /dev/null +++ b/vendor/github.com/grafana/alerting/images/images.go @@ -0,0 +1,77 @@ +package images + +import ( + "context" + "errors" + "io" + "time" + + "github.com/prometheus/alertmanager/types" +) + +var ( + ErrImageNotFound = errors.New("image not found") + + // ErrImagesDone is used to stop iteration of subsequent images. It should be + // returned from forEachFunc when either the intended image has been found or + // the maximum number of images has been iterated. + ErrImagesDone = errors.New("images done") + + // ErrImagesNoPath is returned whenever an image is found but has no path on disk. + ErrImagesNoPath = errors.New("no path for image") + + // ErrImagesNoURL is returned whenever an image is found but has no URL. + ErrImagesNoURL = errors.New("no URL for image") + + ErrImagesUnavailable = errors.New("alert screenshots are unavailable") + + // ErrNoImageForAlert is returned when no image is associated to a given alert. + ErrNoImageForAlert = errors.New("no image for alert") +) + +type Image struct { + Token string + Path string + URL string + CreatedAt time.Time +} + +func (i Image) HasURL() bool { + return i.URL != "" +} + +type Provider interface { + // GetImage takes a token (identifier) and returns the image that token belongs to. + // Returns `ErrImageNotFound` if there's no image for said token. + // + // Deprecated: This method will be removed when all integrations use GetImageURL and/or GetRawImage, + // which allow integrations to get just the data they need for adding images to notifications. + // Use any of those two methods instead. + GetImage(ctx context.Context, token string) (*Image, error) + + // GetImageURL returns the URL of an image associated with a given alert. + // - Returns `ErrImageNotFound` if no image is found. + // - Returns `ErrImagesNoURL` if the image doesn't have a URL. + GetImageURL(ctx context.Context, alert *types.Alert) (string, error) + + // GetRawImage returns an io.Reader to read the bytes of an image associated with a given alert + // and a string representing the filename. + // - Returns `ErrImageNotFound` if no image is found. + // - Returns `ErrImagesNoPath` if the image doesn't have a path on disk. + GetRawImage(ctx context.Context, alert *types.Alert) (io.ReadCloser, string, error) +} + +type UnavailableProvider struct{} + +// GetImage returns the image with the corresponding token, or ErrImageNotFound. +func (u *UnavailableProvider) GetImage(context.Context, string) (*Image, error) { + return nil, ErrImagesUnavailable +} + +func (u *UnavailableProvider) GetImageURL(context.Context, *types.Alert) (string, error) { + return "", ErrImagesUnavailable +} + +func (u *UnavailableProvider) GetRawImage(context.Context, *types.Alert) (io.ReadCloser, string, error) { + return nil, "", ErrImagesUnavailable +} diff --git a/vendor/github.com/grafana/alerting/images/testing.go b/vendor/github.com/grafana/alerting/images/testing.go new file mode 100644 index 00000000000..c537f6f3f36 --- /dev/null +++ b/vendor/github.com/grafana/alerting/images/testing.go @@ -0,0 +1,149 @@ +package images + +import ( + "bytes" + "context" + "encoding/base64" + "fmt" + "io" + "os" + "path/filepath" + "testing" + "time" + + "github.com/grafana/alerting/models" + "github.com/prometheus/alertmanager/types" +) + +type FakeProvider struct { + Images []*Image + Bytes []byte +} + +// GetImage returns an image with the same token. +func (f *FakeProvider) GetImage(_ context.Context, token string) (*Image, error) { + for _, img := range f.Images { + if img.Token == token { + return img, nil + } + } + return nil, ErrImageNotFound +} + +// GetImageURL returns the URL of the image associated with a given alert. +func (f *FakeProvider) GetImageURL(_ context.Context, alert *types.Alert) (string, error) { + uri, err := getImageURI(alert) + if err != nil { + return "", err + } + + for _, img := range f.Images { + if img.Token == uri || img.URL == uri { + if !img.HasURL() { + return "", ErrImagesNoURL + } + return img.URL, nil + } + } + return "", ErrImageNotFound +} + +// GetRawImage returns an io.Reader to read the bytes of the image associated with a given alert. +func (f *FakeProvider) GetRawImage(_ context.Context, alert *types.Alert) (io.ReadCloser, string, error) { + uri, err := getImageURI(alert) + if err != nil { + return nil, "", err + } + + uriString := string(uri) + for _, img := range f.Images { + if img.Token == uriString || img.URL == uriString { + filename := filepath.Base(img.Path) + return io.NopCloser(bytes.NewReader(f.Bytes)), filename, nil + } + } + return nil, "", ErrImageNotFound +} + +// getImageURI is a helper function to retrieve the image URI from the alert annotations as a string. +func getImageURI(alert *types.Alert) (string, error) { + uri, ok := alert.Annotations[models.ImageTokenAnnotation] + if !ok { + return "", ErrNoImageForAlert + } + return string(uri), nil +} + +// NewFakeProvider returns an image provider with N test images. +// Each image has a token and a URL, but does not have a file on disk. +func NewFakeProvider(n int) Provider { + p := FakeProvider{} + for i := 1; i <= n; i++ { + p.Images = append(p.Images, &Image{ + Token: fmt.Sprintf("test-image-%d", i), + URL: fmt.Sprintf("https://www.example.com/test-image-%d.jpg", i), + CreatedAt: time.Now().UTC(), + }) + } + return &p +} + +// NewFakeProviderWithFile returns an image provider with N test images. +// Each image has a token, path and a URL, where the path is 1x1 transparent +// PNG on disk. The test should call deleteFunc to delete the images from disk +// at the end of the test. +// nolint:deadcode,unused +func NewFakeProviderWithFile(t *testing.T, n int) Provider { + var ( + files []string + p FakeProvider + ) + + t.Cleanup(func() { + // remove all files from disk + for _, f := range files { + if err := os.Remove(f); err != nil { + t.Logf("failed to delete file: %s", err) + } + } + }) + + for i := 1; i <= n; i++ { + file, err := newTestImage() + if err != nil { + t.Fatalf("failed to create test image: %s", err) + } + files = append(files, file) + p.Images = append(p.Images, &Image{ + Token: fmt.Sprintf("test-image-%d", i), + Path: file, + URL: fmt.Sprintf("https://www.example.com/test-image-%d", i), + CreatedAt: time.Now().UTC(), + }) + } + + return &p +} + +func newTestImage() (string, error) { + f, err := os.CreateTemp("", "test-image-*.png") + if err != nil { + return "", fmt.Errorf("failed to create temp image: %s", err) + } + + // 1x1 transparent PNG + b, err := base64.StdEncoding.DecodeString("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=") + if err != nil { + return f.Name(), fmt.Errorf("failed to decode PNG data: %s", err) + } + + if _, err := f.Write(b); err != nil { + return f.Name(), fmt.Errorf("failed to write to file: %s", err) + } + + if err := f.Close(); err != nil { + return f.Name(), fmt.Errorf("failed to close file: %s", err) + } + + return f.Name(), nil +} diff --git a/vendor/github.com/grafana/alerting/images/utils.go b/vendor/github.com/grafana/alerting/images/utils.go new file mode 100644 index 00000000000..042735a1580 --- /dev/null +++ b/vendor/github.com/grafana/alerting/images/utils.go @@ -0,0 +1,77 @@ +package images + +import ( + "context" + "errors" + "time" + + "github.com/prometheus/alertmanager/types" + "github.com/prometheus/common/model" + + "github.com/grafana/alerting/logging" + "github.com/grafana/alerting/models" +) + +const ( + // ProviderTimeout should be used by all callers for calles to `Images` + ProviderTimeout = 500 * time.Millisecond +) + +type forEachImageFunc func(index int, image Image) error + +// getImage returns the image for the alert or an error. It returns a nil +// image if the alert does not have an image token or the image does not exist. +// +//nolint:revive +func getImage(ctx context.Context, l logging.Logger, imageProvider Provider, alert types.Alert) (*Image, error) { + token := getTokenFromAnnotations(alert.Annotations) + if token == "" { + return nil, nil + } + + ctx, cancelFunc := context.WithTimeout(ctx, ProviderTimeout) + defer cancelFunc() + + img, err := imageProvider.GetImage(ctx, token) + if errors.Is(err, ErrImageNotFound) || errors.Is(err, ErrImagesUnavailable) { + return nil, nil + } else if err != nil { + l.Warn("failed to get image with token", "token", token, "error", err) + return nil, err + } else { + return img, nil + } +} + +// WithStoredImages retrieves the image for each alert and then calls forEachFunc +// with the index of the alert and the retrieved image struct. If the alert does +// not have an image token, or the image does not exist then forEachFunc will not be +// called for that alert. If forEachFunc returns an error, WithStoredImages will return +// the error and not iterate the remaining alerts. A forEachFunc can return ErrImagesDone +// to stop the iteration of remaining alerts if the intended image or maximum number of +// images have been found. +func WithStoredImages(ctx context.Context, l logging.Logger, imageProvider Provider, forEachFunc forEachImageFunc, alerts ...*types.Alert) error { + for index, alert := range alerts { + logger := l.New("alert", alert.String()) + img, err := getImage(ctx, logger, imageProvider, *alert) + if err != nil { + return err + } else if img != nil { + if err := forEachFunc(index, *img); err != nil { + if errors.Is(err, ErrImagesDone) { + return nil + } + logger.Error("Failed to attach image to notification", "error", err) + return err + } + } + } + return nil +} + +func getTokenFromAnnotations(annotations model.LabelSet) string { + if value, ok := annotations[models.ImageTokenAnnotation]; ok { + return string(value) + } + return "" +} diff --git a/vendor/github.com/grafana/alerting/logging/log.go b/vendor/github.com/grafana/alerting/logging/log.go new file mode 100644 index 00000000000..ec972484484 --- /dev/null +++ b/vendor/github.com/grafana/alerting/logging/log.go @@ -0,0 +1,51 @@ +package logging + +type LoggerFactory func(loggerName string, ctx ...interface{}) Logger + +type Logger interface { + // New returns a new contextual Logger that has this logger's context plus the given context. + New(ctx ...interface{}) Logger + + Log(keyvals ...interface{}) error + + // Debug logs a message with debug level and key/value pairs, if any. + Debug(msg string, ctx ...interface{}) + + // Info logs a message with info level and key/value pairs, if any. + Info(msg string, ctx ...interface{}) + + // Warn logs a message with warning level and key/value pairs, if any. + Warn(msg string, ctx ...interface{}) + + // Error logs a message with error level and key/value pairs, if any. + Error(msg string, ctx ...interface{}) +} + +type FakeLogger struct { +} + +//nolint:revive +func (f FakeLogger) New(ctx ...interface{}) Logger { + return f +} + +//nolint:revive +func (f FakeLogger) Log(keyvals ...interface{}) error { + return nil +} + +//nolint:revive +func (f FakeLogger) Debug(msg string, ctx ...interface{}) { +} + +//nolint:revive +func (f FakeLogger) Info(msg string, ctx ...interface{}) { +} + +//nolint:revive +func (f FakeLogger) Warn(msg string, ctx ...interface{}) { +} + +//nolint:revive +func (f FakeLogger) Error(msg string, ctx ...interface{}) { +} diff --git a/vendor/github.com/grafana/alerting/models/labels.go b/vendor/github.com/grafana/alerting/models/labels.go new file mode 100644 index 00000000000..dbd6f247ba7 --- /dev/null +++ b/vendor/github.com/grafana/alerting/models/labels.go @@ -0,0 +1,29 @@ +package models + +const ( + RuleUIDLabel = "__alert_rule_uid__" + NamespaceUIDLabel = "__alert_rule_namespace_uid__" + + // Annotations are actually a set of labels, so technically this is the label name of an annotation. + DashboardUIDAnnotation = "__dashboardUid__" + PanelIDAnnotation = "__panelId__" + OrgIDAnnotation = "__orgId__" + + // This isn't a hard-coded secret token, hence the nolint. + //nolint:gosec + ImageTokenAnnotation = "__alertImageToken__" + + // GrafanaReservedLabelPrefix contains the prefix for Grafana reserved labels. These differ from "__