Skip to content

Commit 64cb5f0

Browse files
feat: support global PagerDuty details template overrides
Signed-off-by: Mihir Dixit <dixitmihir1@gmail.com>
1 parent 7aa1b9f commit 64cb5f0

5 files changed

Lines changed: 631 additions & 0 deletions

File tree

config/config.go

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,223 @@ func (s *SecretTemplateURL) UnmarshalJSON(data []byte) error {
114114
return nil
115115
}
116116

117+
// applyGlobalReceiverTemplates merges global template defaults into each
118+
// receiver config, using the 3-tier priority:
119+
//
120+
// built-in default < global template override < per-receiver explicit value
121+
func applyGlobalReceiverTemplates(receivers []Receiver, global *GlobalConfig) {
122+
var gt *GlobalReceiverTemplates
123+
if global != nil {
124+
gt = global.ReceiverTemplates
125+
}
126+
127+
for i := range receivers {
128+
r := &receivers[i]
129+
130+
for j := range r.SlackConfigs {
131+
var slackGt *GlobalSlackTemplates
132+
if gt != nil {
133+
slackGt = gt.Slack
134+
}
135+
applySlackTemplates(r.SlackConfigs[j], slackGt)
136+
}
137+
138+
for j := range r.EmailConfigs {
139+
var emailGt *GlobalEmailTemplates
140+
if gt != nil {
141+
emailGt = gt.Email
142+
}
143+
applyEmailTemplates(r.EmailConfigs[j], emailGt)
144+
}
145+
146+
for j := range r.PagerdutyConfigs {
147+
var pdGt *GlobalPagerDutyTemplates
148+
if gt != nil {
149+
pdGt = gt.PagerDuty
150+
}
151+
applyPagerDutyTemplates(r.PagerdutyConfigs[j], pdGt)
152+
}
153+
154+
for j := range r.OpsGenieConfigs {
155+
var ogGt *GlobalOpsGenieTemplates
156+
if gt != nil {
157+
ogGt = gt.OpsGenie
158+
}
159+
applyOpsGenieTemplates(r.OpsGenieConfigs[j], ogGt)
160+
}
161+
162+
for j := range r.VictorOpsConfigs {
163+
var voGt *GlobalVictorOpsTemplates
164+
if gt != nil {
165+
voGt = gt.VictorOps
166+
}
167+
applyVictorOpsTemplates(r.VictorOpsConfigs[j], voGt)
168+
}
169+
170+
for j := range r.PushoverConfigs {
171+
var poGt *GlobalPushoverTemplates
172+
if gt != nil {
173+
poGt = gt.Pushover
174+
}
175+
applyPushoverTemplates(r.PushoverConfigs[j], poGt)
176+
}
177+
178+
for j := range r.WechatConfigs {
179+
var wcGt *GlobalWeChatTemplates
180+
if gt != nil {
181+
wcGt = gt.WeChat
182+
}
183+
applyWeChatTemplates(r.WechatConfigs[j], wcGt)
184+
}
185+
186+
for j := range r.TelegramConfigs {
187+
var tcGt *GlobalTelegramTemplates
188+
if gt != nil {
189+
tcGt = gt.Telegram
190+
}
191+
applyTelegramTemplates(r.TelegramConfigs[j], tcGt)
192+
}
193+
}
194+
}
195+
196+
func applySlackTemplates(sc *SlackConfig, gt *GlobalSlackTemplates) {
197+
if gt != nil {
198+
applyGlobalTemplateOverride(&sc.Username, gt.Username, DefaultSlackConfig.Username)
199+
applyGlobalTemplateOverride(&sc.IconEmoji, gt.IconEmoji, DefaultSlackConfig.IconEmoji)
200+
applyGlobalTemplateOverride(&sc.IconURL, gt.IconURL, DefaultSlackConfig.IconURL)
201+
applyGlobalTemplateOverride(&sc.Pretext, gt.Pretext, DefaultSlackConfig.Pretext)
202+
applyGlobalTemplateOverride(&sc.Title, gt.Title, DefaultSlackConfig.Title)
203+
applyGlobalTemplateOverride(&sc.TitleLink, gt.TitleLink, DefaultSlackConfig.TitleLink)
204+
applyGlobalTemplateOverride(&sc.Text, gt.Text, DefaultSlackConfig.Text)
205+
applyGlobalTemplateOverride(&sc.Fallback, gt.Fallback, DefaultSlackConfig.Fallback)
206+
applyGlobalTemplateOverride(&sc.Footer, gt.Footer, DefaultSlackConfig.Footer)
207+
applyGlobalTemplateOverride(&sc.Color, gt.Color, DefaultSlackConfig.Color)
208+
}
209+
}
210+
211+
func applyEmailTemplates(ec *EmailConfig, gt *GlobalEmailTemplates) {
212+
if gt != nil {
213+
if gt.Subject != "" {
214+
if ec.Headers == nil {
215+
ec.Headers = make(map[string]string)
216+
}
217+
if val, ok := ec.Headers["Subject"]; !ok || val == DefaultEmailSubject {
218+
ec.Headers["Subject"] = gt.Subject
219+
}
220+
}
221+
applyGlobalTemplateOverride(&ec.HTML, gt.HTML, DefaultEmailConfig.HTML)
222+
applyGlobalTemplateOverride(&ec.Text, gt.Text, DefaultEmailConfig.Text)
223+
}
224+
}
225+
226+
func applyPagerDutyTemplates(pc *PagerdutyConfig, gt *GlobalPagerDutyTemplates) {
227+
if gt == nil {
228+
return
229+
}
230+
231+
applyGlobalTemplateOverride(&pc.Description, gt.Description, DefaultPagerdutyConfig.Description)
232+
applyGlobalTemplateOverride(&pc.Client, gt.Client, DefaultPagerdutyConfig.Client)
233+
applyGlobalTemplateOverride(&pc.ClientURL, gt.ClientURL, DefaultPagerdutyConfig.ClientURL)
234+
235+
// Merge global template overrides into the receiver `details` map.
236+
//
237+
// Precedence model:
238+
// - built-in defaults < global receiver_templates < per-receiver explicit value
239+
//
240+
// For "standard" keys present in DefaultPagerdutyDetails, we only override
241+
// receiver values when they are still set to the built-in default template
242+
// string (so explicit empty-string overrides like `firing: ""` are preserved).
243+
//
244+
// For arbitrary/custom keys not present in DefaultPagerdutyDetails, we never
245+
// override existing per-receiver values; we only add the key if it's absent.
246+
if gt.Details == nil {
247+
return
248+
}
249+
if pc.Details == nil {
250+
pc.Details = make(map[string]any)
251+
}
252+
253+
for k, gv := range gt.Details {
254+
if gv == "" {
255+
// Treat global empty string as "not set" (consistent with applyGlobalTemplateOverride).
256+
continue
257+
}
258+
259+
receiverVal, receiverHasKey := pc.Details[k]
260+
if !receiverHasKey {
261+
// Key absent on receiver: global value becomes the default.
262+
pc.Details[k] = gv
263+
continue
264+
}
265+
266+
// Key exists on receiver:
267+
// - if it's a standard key, override only when receiver still equals the built-in default
268+
// - if it's a custom key, never override an existing per-receiver value
269+
builtInVal, builtInHasKey := DefaultPagerdutyDetails[k]
270+
if !builtInHasKey {
271+
continue
272+
}
273+
builtInStr, builtInOK := builtInVal.(string)
274+
receiverStr, receiverOK := receiverVal.(string)
275+
if !builtInOK || !receiverOK {
276+
continue
277+
}
278+
if receiverStr == builtInStr {
279+
pc.Details[k] = gv
280+
}
281+
}
282+
}
283+
284+
func applyOpsGenieTemplates(oc *OpsGenieConfig, gt *GlobalOpsGenieTemplates) {
285+
if gt != nil {
286+
applyGlobalTemplateOverride(&oc.Message, gt.Message, DefaultOpsGenieConfig.Message)
287+
applyGlobalTemplateOverride(&oc.Description, gt.Description, DefaultOpsGenieConfig.Description)
288+
applyGlobalTemplateOverride(&oc.Source, gt.Source, DefaultOpsGenieConfig.Source)
289+
applyGlobalTemplateOverride(&oc.Note, gt.Note, DefaultOpsGenieConfig.Note)
290+
}
291+
}
292+
293+
func applyVictorOpsTemplates(vc *VictorOpsConfig, gt *GlobalVictorOpsTemplates) {
294+
if gt != nil {
295+
applyGlobalTemplateOverride(&vc.MessageType, gt.MessageType, DefaultVictorOpsConfig.MessageType)
296+
applyGlobalTemplateOverride(&vc.EntityDisplayName, gt.EntityDisplayName, DefaultVictorOpsConfig.EntityDisplayName)
297+
applyGlobalTemplateOverride(&vc.StateMessage, gt.StateMessage, DefaultVictorOpsConfig.StateMessage)
298+
}
299+
}
300+
301+
func applyPushoverTemplates(pc *PushoverConfig, gt *GlobalPushoverTemplates) {
302+
if gt != nil {
303+
applyGlobalTemplateOverride(&pc.Title, gt.Title, DefaultPushoverConfig.Title)
304+
applyGlobalTemplateOverride(&pc.Message, gt.Message, DefaultPushoverConfig.Message)
305+
applyGlobalTemplateOverride(&pc.URL, gt.URL, DefaultPushoverConfig.URL)
306+
}
307+
}
308+
309+
func applyWeChatTemplates(wc *WechatConfig, gt *GlobalWeChatTemplates) {
310+
if gt != nil {
311+
applyGlobalTemplateOverride(&wc.Message, gt.Message, DefaultWechatConfig.Message)
312+
applyGlobalTemplateOverride(&wc.MessageType, gt.MessageType, DefaultWechatConfig.MessageType)
313+
}
314+
}
315+
316+
func applyTelegramTemplates(tc *TelegramConfig, gt *GlobalTelegramTemplates) {
317+
if gt != nil {
318+
applyGlobalTemplateOverride(&tc.Message, gt.Message, DefaultTelegramConfig.Message)
319+
}
320+
}
321+
322+
// applyGlobalTemplateOverride sets *dst to gtValue only if *dst is currently exactly equal to defaultVal.
323+
// This properly distinguishes between an omitted field (which defaults to defaultVal) and an explicit
324+
// empty string override (like `title: ""`), assuming defaultVal is not literally `""`.
325+
func applyGlobalTemplateOverride(dst *string, gtValue string, defaultVal string) {
326+
if gtValue == "" {
327+
return
328+
}
329+
if *dst == defaultVal {
330+
*dst = gtValue
331+
}
332+
}
333+
117334
// Load parses the YAML input s into a Config.
118335
func Load(s string) (*Config, error) {
119336
cfg := &Config{}
@@ -350,6 +567,8 @@ func (c *Config) UnmarshalYAML(unmarshal func(any) error) error {
350567
return errors.New("at most one of mattermost_webhook_url & mattermost_webhook_url_file must be configured")
351568
}
352569

570+
applyGlobalReceiverTemplates(c.Receivers, c.Global)
571+
353572
names := map[string]struct{}{}
354573

355574
for _, rcv := range c.Receivers {
@@ -838,6 +1057,9 @@ type GlobalConfig struct {
8381057
RocketchatTokenIDFile string `yaml:"rocketchat_token_id_file,omitempty" json:"rocketchat_token_id_file,omitempty"`
8391058
MattermostWebhookURL *amcommoncfg.SecretURL `yaml:"mattermost_webhook_url,omitempty" json:"mattermost_webhook_url,omitempty"`
8401059
MattermostWebhookURLFile string `yaml:"mattermost_webhook_url_file,omitempty" json:"mattermost_webhook_url_file,omitempty"`
1060+
1061+
// per-receiver-type global template overrides
1062+
ReceiverTemplates *GlobalReceiverTemplates `yaml:"receiver_templates,omitempty" json:"receiver_templates,omitempty"`
8411063
}
8421064

8431065
// UnmarshalYAML implements the yaml.Unmarshaler interface for GlobalConfig.

0 commit comments

Comments
 (0)