diff --git a/asset/assets_vfsdata.go b/asset/assets_vfsdata.go index 16ab608e..eeacb03a 100644 --- a/asset/assets_vfsdata.go +++ b/asset/assets_vfsdata.go @@ -163,9 +163,9 @@ var Assets = func() http.FileSystem { "/templates/default.tmpl": &vfsgen۰CompressedFileInfo{ name: "default.tmpl", modTime: time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC), - uncompressedSize: 7283, + uncompressedSize: 7656, - compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\xec\x59\xcd\x6e\xeb\x36\x13\xdd\xfb\x29\x06\xba\xdf\x22\x5e\x44\xf7\x5b\x07\x08\x8a\x8b\xa2\x3f\x8b\xb4\x28\x1c\xa4\x9b\xa2\x10\x18\x69\xac\x30\xa1\x48\x85\x1c\xd9\x31\x1c\xbd\x7b\x41\x49\x96\x45\x49\xb6\x29\x5f\x77\x55\xef\x62\x7a\xe6\x9c\xe1\x39\xa3\x21\xe5\x6c\xb7\x90\xe0\x92\x4b\x84\x20\x8a\x98\x40\x4d\x19\x93\x2c\x45\x1d\x40\x59\x7e\xeb\x7c\xde\x6e\x01\x65\x02\x65\x39\x3b\x98\xf2\xb4\x78\xb0\x59\xdb\x2d\x84\x3f\x7d\x10\x6a\xc9\xc4\xd3\xe2\x01\xca\xf2\xeb\x97\xaf\x55\x9c\xf9\x41\x63\x8c\x7c\x85\xfa\xde\x06\x2d\x9a\x0f\xf0\x09\x85\x16\xef\x05\xea\x4d\x9d\xde\x10\xb9\x4c\xa6\x78\x7e\xc5\x98\x2c\xc3\x5f\x36\xfb\x91\x18\x15\x06\x3e\x81\xd4\x53\x9e\xa3\xae\x53\xf9\x12\xf0\xbd\xfd\x32\x58\x72\xcd\x65\x6a\x73\xee\x6c\x4e\xb5\x21\x13\xfe\x5c\xad\xc2\x27\x08\x94\x5d\xc6\xbf\xc1\x06\xfd\xa2\x55\x91\x3f\xb0\x67\x14\x26\x7c\x54\x9a\x30\xf9\x83\x71\x6d\xc2\x3f\x99\x28\xd0\x12\xbe\x2a\x2e\x21\x00\x8b\x0a\x35\x65\x4a\x70\x63\xb1\xc2\x1f\x55\x96\x29\x59\x27\xcf\x9b\xb5\x0e\xde\x1c\xca\xf2\x66\xbb\x85\x35\xa7\x17\x37\x38\x5c\x60\xa6\x56\xe8\xb2\xff\xce\x32\x34\x8d\xa2\x63\xec\x6d\xe1\xf3\xf6\xaf\x03\x36\x25\x68\x62\xcd\x73\xe2\x4a\x06\x47\x34\x26\xfc\xa0\xda\xd2\x48\x70\x43\x4d\xa8\x66\x32\x45\x08\xa1\x2c\xeb\xba\xee\x66\xfb\xc5\xa1\x4e\x56\x95\xdb\x4a\x48\x5b\xbe\xfd\x74\x0f\xed\x06\x9a\xc2\x6a\xf2\x6f\x52\x2a\x62\xb6\x26\x07\xb2\xb3\x7c\x1e\xee\xa3\x2a\x74\x8c\x77\xb5\x99\x28\x51\x33\x52\xba\xee\xc4\xd9\x88\x50\x47\x25\x88\x32\xa6\xdf\x12\xb5\x96\x03\x2d\x66\xbe\x62\x78\x56\x3d\x9b\x2e\x87\x2f\xb2\x97\x20\xb3\x71\x45\x8c\x60\xf1\x5b\x98\xe0\x92\x15\x82\x42\xe2\x24\xb0\x91\x82\x30\xcb\x05\x23\xf7\xe1\x0c\x0f\xf5\xa0\x8b\x53\x18\x3b\x1e\xb2\x31\x28\x77\x08\x79\xe2\x2d\x99\x10\xcf\x2c\x7e\x1b\xe0\x8d\x96\x6f\x41\xe1\x13\x4e\x05\x0a\x2e\xdf\xbc\x2b\x88\x9b\x0a\x78\x12\xf8\x25\xe4\x1a\x6d\xaf\x79\x46\x77\x0a\x3a\xaa\x58\x35\x83\x3d\x4b\xe6\xb1\x92\x98\xa9\x57\x1e\xf8\xc7\x17\x5a\xf8\x56\xec\xbf\xb9\xa5\x52\x54\x9f\x38\x9d\x1e\xec\x86\xe7\x76\x6b\x49\x41\x9b\x36\x65\x38\xd0\xa6\xb5\xe3\x10\x31\x16\x1c\x25\x9d\xdf\x90\x87\x10\xf7\xa7\xe2\x79\x9e\x0d\x71\xb9\x34\xc4\x64\x8c\x66\x04\x77\x30\xc1\xc3\xc3\xaa\xaa\xdc\xa4\x28\x39\xb6\xc0\x19\x1a\xc3\xd2\xf3\x9e\xef\x01\xd8\xd0\xa1\xe6\xc0\x3b\x30\xd0\x46\x4f\xb8\x59\xef\x7c\x75\x0e\xf0\x39\xfc\x1f\x6e\xed\xe0\xac\x16\xa1\x5e\xac\x46\xe7\x71\x45\xdc\x5b\x40\x45\x72\xdb\xd9\xd1\x08\xdf\x02\x8d\x12\x2b\x4c\x7a\x8c\xbb\x65\x7f\xce\x5d\xc6\x80\xf5\xd6\x47\x52\x53\xcd\xf1\xe9\xdd\xe4\xb8\xbe\xc6\xf8\x85\xd1\x54\xcf\x67\x57\xff\x8e\xf8\xd7\xbd\x28\x3f\x69\x31\xc0\x1b\xf5\xe7\x80\xeb\x3d\x7f\x48\x45\xf6\xb0\x3c\x38\x49\x87\xe1\x39\xd3\xb4\x99\x10\x4f\x2c\xf5\x8d\x66\x29\x4a\x8a\xfa\x47\x9c\xdb\x5f\x2b\x1e\x93\xd2\x2a\x37\xfb\xb6\x25\x46\x18\xb9\x8d\x76\xed\xa5\x69\xb3\x60\xa8\x2a\x4a\xe2\xb4\x89\x12\x6e\x72\xc1\x36\xd1\x81\xdb\xd4\xe9\xc1\x3d\x44\xce\x94\xe4\xa4\xac\x20\x11\x29\x25\x26\x1e\x89\xce\xd9\x55\x98\x17\xb5\x42\x7d\x81\xfb\xe3\x00\xea\xdf\xef\xa7\xcb\xb4\x93\x7f\x37\x5d\xae\x99\x86\x57\xfa\x63\x4a\xee\xef\x74\x53\xce\x94\xee\x6d\x4e\x76\x1e\xf6\xfd\x6b\xfa\xf4\x77\x84\x0e\xce\xd5\xde\x29\xf6\x76\x55\x24\x14\x98\x6a\x96\x8d\x49\xf9\x9f\x15\x25\xe1\x26\x56\x3a\xb9\xc0\x20\xea\x23\x5d\xd5\xb5\xd7\x84\x67\xfc\xb8\x3e\xba\xdf\xad\x63\x66\x08\x59\xd6\x1d\xa6\x59\xc6\xf4\xe6\xac\x3e\xed\x63\x9d\xdf\xf1\x03\xa4\xe6\xcd\xde\xc7\xa6\x2f\x30\xc9\xa8\xce\xcf\x6d\xdf\xed\x58\x4b\xed\xeb\xd9\x08\xf9\x14\xf3\x5e\xb9\x66\x17\x71\xce\x01\xea\xbd\x46\x5f\x35\x9f\x55\x37\xe4\x51\xad\x72\xcd\x95\xe6\xf6\xe5\xe7\xb6\xb9\x48\xff\x6f\xb7\x04\x77\xf7\x10\x04\xbb\xfb\xf5\xee\xa7\x55\x67\xb7\x36\x07\x00\xa0\xca\x33\xb8\xc2\x5d\x1e\x97\x09\x7e\xec\x7e\xdd\x85\x60\xf7\x55\xe0\x64\xf0\x25\xdc\xe0\x7b\x27\x31\x88\x35\x27\x1e\x33\x11\xcc\xdb\xc0\x16\xbe\x2d\xeb\x1e\x82\x5f\x79\xfa\xe2\x62\xa1\x30\x58\x01\x32\x99\xf4\x51\xd7\x4c\x4b\x2e\xd3\x60\x0e\x37\x12\x3b\x40\x35\xcc\xfc\x04\xd7\x6f\x98\xf0\x22\xf3\x67\xe3\x72\xa9\x2c\x95\x5d\xdd\x53\x9d\xa4\x79\x50\xeb\x1e\x87\x4c\x5a\x4f\xba\x7f\xd7\xff\xae\xe9\x42\x3b\x69\xae\x4f\x6d\x63\x0c\xb8\x27\xb9\x35\xd9\x31\x0f\xd7\x2e\xee\x9c\x97\x7b\x97\x73\xf0\xb4\x8b\x7d\x27\x4f\x39\xbb\x47\xea\x7f\xfb\x4f\x00\x00\x00\xff\xff\x30\xb3\x3d\xcd\x73\x1c\x00\x00"), + compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\xec\x59\x4d\x6f\xe3\x36\x10\xbd\xfb\x57\x0c\xb4\x3d\xc4\x87\x68\x8b\x1e\x03\x04\xc5\xa2\xe8\xc7\x21\x2d\x0a\x2f\xd2\x4b\x51\x08\x8c\x34\x56\x98\x50\xa4\x42\x8e\xec\x18\x8e\xfe\x7b\x41\x49\x96\xa9\x0f\xdb\x94\xd7\x3d\xd5\xb7\x98\x9a\x79\x33\x7c\x6f\x34\x43\x2a\xdb\x2d\x24\xb8\xe4\x12\x21\x88\x22\x26\x50\x53\xc6\x24\x4b\x51\x07\x50\x96\x5f\x9c\xdf\xdb\x2d\xa0\x4c\xa0\x2c\x67\x07\x5d\x1e\x17\x0f\xd6\x6b\xbb\x85\xf0\xe7\x77\x42\x2d\x99\x78\x5c\x3c\x40\x59\x7e\xfe\xf4\xb9\xb2\x33\x3f\x6a\x8c\x91\xaf\x50\xdf\x5b\xa3\x45\xf3\x03\x3e\xa0\xd0\xe2\xad\x40\xbd\xa9\xdd\x9b\x40\xdd\x48\xa6\x78\x7a\xc1\x98\x6c\x84\xbf\xad\xf7\x57\x62\x54\x18\xf8\x00\x52\x8f\x79\x8e\xba\x76\xe5\x4b\xc0\xb7\xf6\x61\xb0\xe4\x9a\xcb\xd4\xfa\xdc\x59\x9f\x6a\x43\x26\xfc\xa5\x5a\x85\x0f\x10\x28\xdd\x88\xff\x80\x35\xfa\x55\xab\x22\x7f\x60\x4f\x28\x4c\xf8\x55\x69\xc2\xe4\x4f\xc6\xb5\x09\xff\x62\xa2\x40\x1b\xf0\x45\x71\x09\x01\x58\x54\xa8\x43\xa6\x04\x37\x16\x2b\xfc\x49\x65\x99\x92\xb5\xf3\xbc\x59\x73\xf0\xe6\x50\x96\x37\xdb\x2d\xac\x39\x3d\x77\x8d\xc3\x05\x66\x6a\x85\xdd\xe8\x7f\xb0\x0c\x4d\xc3\xe8\x58\xf4\x36\xf1\x79\xfb\xd7\x01\x99\x12\x34\xb1\xe6\x39\x71\x25\x83\x23\x1c\x13\xbe\x53\x2d\x69\x24\xb8\xa1\xc6\x54\x33\x99\x22\x84\x50\x96\x75\x5e\x77\xb3\xfd\xe2\x90\x27\xcb\xca\x6d\x45\xa4\x4d\xdf\xfe\xba\x87\x76\x03\x4d\x62\x75\xf0\x2f\x52\x2a\x62\x36\xa7\x0e\xa4\xb3\x7c\x1e\xee\x57\x55\xe8\x18\xef\x6a\x31\x51\xa2\x66\xa4\x74\x5d\x89\xb3\x11\xa2\x8e\x52\x10\x65\x4c\xbf\x26\x6a\x2d\x07\x5c\xcc\x7c\xc9\xf0\xcc\x7a\x36\x9d\x0e\x5f\x64\x2f\x42\x66\xe3\x8c\x18\xc1\xe2\xd7\x30\xc1\x25\x2b\x04\x85\xc4\x49\x60\x43\x05\x61\x96\x0b\x46\xdd\x97\x33\x3c\x54\x83\x5d\x9c\xc2\xd8\xf6\x90\x8d\x41\x75\x9b\x90\x27\xde\x92\x09\xf1\xc4\xe2\xd7\x01\xde\x68\xfa\x16\x14\x3e\xe0\x94\xa1\xe0\xf2\xd5\x3b\x83\xb8\xc9\x80\x27\x81\x9f\x43\xae\xd1\xd6\x9a\xa7\xb5\x93\xd0\x51\xc6\xaa\x1e\xec\x99\x32\x8f\x95\xc4\x4c\xbd\xf0\xc0\xdf\xbe\xd0\xc2\x37\x63\xff\xcd\x2d\x95\xa2\x7a\xe2\x38\x35\xe8\x9a\xe7\x76\x6b\x49\x41\x9b\xd6\x65\xd8\xd0\xa6\x95\xe3\x10\x31\x16\x1c\x25\x9d\x5f\x90\x87\x10\xf7\x53\xf1\x3c\xcd\x86\xb8\x5c\x1a\x62\x32\x46\x33\x82\x3b\xe8\xe0\xe1\x61\x56\x55\x6e\x52\x94\x1c\x5b\xe0\x0c\x8d\x61\xe9\x79\xef\xf7\x00\x6c\xa8\x50\x33\xf0\x0e\x34\xb4\xd1\x09\x37\xeb\xcd\xd7\xce\x00\x9f\xc3\xf7\x70\x6b\x1b\x67\xb5\x08\xf5\x62\xd5\x3a\x8f\x33\xd2\x3d\x05\x54\x41\x6e\x9d\x1d\x8d\xc4\x5b\xa0\x51\x62\x85\x49\x2f\xe2\x6e\xd9\x3f\xe6\xce\x63\x10\xf5\xd6\x87\x52\x53\xf5\xf1\xe9\xd5\xd4\x51\x7d\x8d\xf1\x33\xa3\xa9\x9a\xcf\xae\xfa\x1d\xd1\xcf\x3d\x28\x3f\x6a\x31\xc0\x1b\xd5\xe7\x80\xea\x3d\x7d\x48\x45\x76\x58\x1e\xec\xa4\x43\xf3\x9c\x69\xda\x4c\xb0\x27\x96\xfa\x5a\xb3\x14\x25\x45\xfd\x11\xd7\xad\xaf\x15\x8f\x49\x69\x95\x9b\x7d\xd9\x12\x23\x8c\xba\x85\x76\xad\xa5\x69\xbd\x60\xc8\x2a\x4a\xe2\xb4\x89\x12\x6e\x72\xc1\x36\xd1\x81\xd3\xd4\xe9\xc6\x3d\x44\xce\x94\xe4\xa4\x2c\x21\x11\x29\x25\x26\x8e\xc4\xce\xec\x2a\xcc\xb3\x5a\xa1\xbe\xc0\xf9\x71\x00\xf5\xdf\xd7\xd3\x65\xca\xc9\xbf\x9a\x2e\x57\x4c\xc3\x23\xfd\x31\x26\xf7\x67\xba\x29\x33\xc5\x3d\xcd\x49\xe7\x65\xdf\x5f\xd3\xa7\xdf\x11\x1c\x9c\xab\xbc\x53\xe4\x75\x59\x24\x14\x98\x6a\x96\x8d\x51\xf9\xbf\x25\x25\xe1\x26\x56\x3a\xb9\x40\x23\xea\x23\x5d\xd9\xb5\xc7\x84\x27\x7c\xbf\xbe\xba\xdf\xcc\x63\x66\x08\x59\xe6\x36\xd3\x2c\x63\x7a\x73\x56\x9d\xf6\xb1\xce\xaf\xf8\x01\x52\x73\xb3\xf7\x91\xe9\x13\x4c\x12\xca\xf9\xdc\xf6\xcd\x8a\xb5\xa1\x7d\x35\x1b\x09\x7e\x86\x78\xab\x1f\x2e\x47\xb9\x8b\x75\x25\x7d\x8c\xf4\x17\xae\xd9\x45\x5e\x97\x0e\x50\xef\xdb\xc5\x95\xf3\x59\x75\x2d\x19\xe5\x2a\xd7\x5c\x69\x6e\x6f\x9c\xb7\xcd\xed\xe5\xbb\xdd\x12\xdc\xdd\x43\x10\xec\x2e\x35\xbb\xef\xd9\x9d\xdd\x5a\x1f\x00\x80\xca\xcf\xe0\x0a\x77\x7e\x5c\x26\xf8\xbe\xfb\xa4\x0e\xc1\xee\x51\xd0\xf1\xe0\x4b\xb8\xc1\x37\xc7\x31\x88\x35\x27\x1e\x33\x11\xcc\x5b\xc3\x16\xbe\x4d\xeb\x1e\x82\xdf\x78\xfa\xdc\xc5\x42\x61\xb0\x02\x64\x32\xe9\xa3\xae\x99\x96\x5c\xa6\xc1\x1c\x6e\x24\x3a\x40\x35\xcc\xfc\x44\xac\xdf\x31\xe1\x45\xe6\x1f\x8d\xcb\xa5\xb2\xa1\xec\xea\x3e\xd4\xc9\x30\x0f\x6a\xdd\x8b\x21\x93\x56\x13\xf7\xef\xfa\x7f\x64\x2e\x74\xc7\xad\xab\x53\x5b\x18\x83\xd8\x93\xd4\x9a\xac\x98\x87\x6a\x17\x57\xce\x4b\xbd\xcb\x29\x78\x5a\xc5\xbe\x92\xa7\x94\xdd\x23\xf5\x9f\xfe\x1b\x00\x00\xff\xff\x7a\xa6\xa9\x68\xe8\x1d\x00\x00"), }, "/templates/email.tmpl": &vfsgen۰CompressedFileInfo{ name: "email.tmpl", diff --git a/config/config.go b/config/config.go index 4d694eef..5b8c7074 100644 --- a/config/config.go +++ b/config/config.go @@ -263,6 +263,9 @@ func resolveFilepaths(baseDir string, cfg *Config) { for _, cfg := range receiver.MSTeamsConfigs { cfg.HTTPConfig.SetDirectory(baseDir) } + for _, cfg := range receiver.MSTeamsV2Configs { + cfg.HTTPConfig.SetDirectory(baseDir) + } for _, cfg := range receiver.JiraConfigs { cfg.HTTPConfig.SetDirectory(baseDir) } @@ -282,7 +285,7 @@ func (mt *MuteTimeInterval) UnmarshalYAML(unmarshal func(interface{}) error) err return err } if mt.Name == "" { - return fmt.Errorf("missing name in mute time interval") + return errors.New("missing name in mute time interval") } return nil } @@ -300,7 +303,7 @@ func (ti *TimeInterval) UnmarshalYAML(unmarshal func(interface{}) error) error { return err } if ti.Name == "" { - return fmt.Errorf("missing name in time interval") + return errors.New("missing name in time interval") } return nil } @@ -346,19 +349,19 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { } if c.Global.SlackAPIURL != nil && len(c.Global.SlackAPIURLFile) > 0 { - return fmt.Errorf("at most one of slack_api_url & slack_api_url_file must be configured") + return errors.New("at most one of slack_api_url & slack_api_url_file must be configured") } if c.Global.OpsGenieAPIKey != "" && len(c.Global.OpsGenieAPIKeyFile) > 0 { - return fmt.Errorf("at most one of opsgenie_api_key & opsgenie_api_key_file must be configured") + return errors.New("at most one of opsgenie_api_key & opsgenie_api_key_file must be configured") } if c.Global.VictorOpsAPIKey != "" && len(c.Global.VictorOpsAPIKeyFile) > 0 { - return fmt.Errorf("at most one of victorops_api_key & victorops_api_key_file must be configured") + return errors.New("at most one of victorops_api_key & victorops_api_key_file must be configured") } if len(c.Global.SMTPAuthPassword) > 0 && len(c.Global.SMTPAuthPasswordFile) > 0 { - return fmt.Errorf("at most one of smtp_auth_password & smtp_auth_password_file must be configured") + return errors.New("at most one of smtp_auth_password & smtp_auth_password_file must be configured") } names := map[string]struct{}{} @@ -378,13 +381,13 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { } if ec.Smarthost.String() == "" { if c.Global.SMTPSmarthost.String() == "" { - return fmt.Errorf("no global SMTP smarthost set") + return errors.New("no global SMTP smarthost set") } ec.Smarthost = c.Global.SMTPSmarthost } if ec.From == "" { if c.Global.SMTPFrom == "" { - return fmt.Errorf("no global SMTP from set") + return errors.New("no global SMTP from set") } ec.From = c.Global.SMTPFrom } @@ -415,7 +418,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { } if sc.APIURL == nil && len(sc.APIURLFile) == 0 { if c.Global.SlackAPIURL == nil && len(c.Global.SlackAPIURLFile) == 0 { - return fmt.Errorf("no global Slack API URL set either inline or in a file") + return errors.New("no global Slack API URL set either inline or in a file") } sc.APIURL = c.Global.SlackAPIURL sc.APIURLFile = c.Global.SlackAPIURLFile @@ -432,7 +435,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { } if pdc.URL == nil { if c.Global.PagerdutyURL == nil { - return fmt.Errorf("no global PagerDuty URL set") + return errors.New("no global PagerDuty URL set") } pdc.URL = c.Global.PagerdutyURL } @@ -443,7 +446,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { } if ogc.APIURL == nil { if c.Global.OpsGenieAPIURL == nil { - return fmt.Errorf("no global OpsGenie URL set") + return errors.New("no global OpsGenie URL set") } ogc.APIURL = c.Global.OpsGenieAPIURL } @@ -452,7 +455,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { } if ogc.APIKey == "" && len(ogc.APIKeyFile) == 0 { if c.Global.OpsGenieAPIKey == "" && len(c.Global.OpsGenieAPIKeyFile) == 0 { - return fmt.Errorf("no global OpsGenie API Key set either inline or in a file") + return errors.New("no global OpsGenie API Key set either inline or in a file") } ogc.APIKey = c.Global.OpsGenieAPIKey ogc.APIKeyFile = c.Global.OpsGenieAPIKeyFile @@ -465,21 +468,21 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { if wcc.APIURL == nil { if c.Global.WeChatAPIURL == nil { - return fmt.Errorf("no global Wechat URL set") + return errors.New("no global Wechat URL set") } wcc.APIURL = c.Global.WeChatAPIURL } if wcc.APISecret == "" { if c.Global.WeChatAPISecret == "" { - return fmt.Errorf("no global Wechat ApiSecret set") + return errors.New("no global Wechat ApiSecret set") } wcc.APISecret = c.Global.WeChatAPISecret } if wcc.CorpID == "" { if c.Global.WeChatAPICorpID == "" { - return fmt.Errorf("no global Wechat CorpID set") + return errors.New("no global Wechat CorpID set") } wcc.CorpID = c.Global.WeChatAPICorpID } @@ -494,7 +497,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { } if voc.APIURL == nil { if c.Global.VictorOpsAPIURL == nil { - return fmt.Errorf("no global VictorOps URL set") + return errors.New("no global VictorOps URL set") } voc.APIURL = c.Global.VictorOpsAPIURL } @@ -503,7 +506,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { } if voc.APIKey == "" && len(voc.APIKeyFile) == 0 { if c.Global.VictorOpsAPIKey == "" && len(c.Global.VictorOpsAPIKeyFile) == 0 { - return fmt.Errorf("no global VictorOps API Key set") + return errors.New("no global VictorOps API Key set") } voc.APIKey = c.Global.VictorOpsAPIKey voc.APIKeyFile = c.Global.VictorOpsAPIKeyFile @@ -528,7 +531,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { discord.HTTPConfig = c.Global.HTTPConfig } if discord.WebhookURL == nil && len(discord.WebhookURLFile) == 0 { - return fmt.Errorf("no discord webhook URL or URLFile provided") + return errors.New("no discord webhook URL or URLFile provided") } } for _, webex := range rcv.WebexConfigs { @@ -537,7 +540,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { } if webex.APIURL == nil { if c.Global.WebexAPIURL == nil { - return fmt.Errorf("no global Webex URL set") + return errors.New("no global Webex URL set") } webex.APIURL = c.Global.WebexAPIURL @@ -548,7 +551,15 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { msteams.HTTPConfig = c.Global.HTTPConfig } if msteams.WebhookURL == nil && len(msteams.WebhookURLFile) == 0 { - return fmt.Errorf("no msteams webhook URL or URLFile provided") + return errors.New("no msteams webhook URL or URLFile provided") + } + } + for _, msteamsv2 := range rcv.MSTeamsV2Configs { + if msteamsv2.HTTPConfig == nil { + msteamsv2.HTTPConfig = c.Global.HTTPConfig + } + if msteamsv2.WebhookURL == nil && len(msteamsv2.WebhookURLFile) == 0 { + return errors.New("no msteamsv2 webhook URL or URLFile provided") } } for _, jira := range rcv.JiraConfigs { @@ -557,7 +568,7 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { } if jira.APIURL == nil { if c.Global.JiraAPIURL == nil { - return fmt.Errorf("no global Jira Cloud URL set") + return errors.New("no global Jira Cloud URL set") } jira.APIURL = c.Global.JiraAPIURL } @@ -569,20 +580,20 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { // The root route must not have any matchers as it is the fallback node // for all alerts. if c.Route == nil { - return fmt.Errorf("no routes provided") + return errors.New("no routes provided") } if len(c.Route.Receiver) == 0 { - return fmt.Errorf("root route must specify a default receiver") + return errors.New("root route must specify a default receiver") } if len(c.Route.Match) > 0 || len(c.Route.MatchRE) > 0 || len(c.Route.Matchers) > 0 { - return fmt.Errorf("root route must not have any matchers") + return errors.New("root route must not have any matchers") } if len(c.Route.MuteTimeIntervals) > 0 { - return fmt.Errorf("root route must not have any mute time intervals") + return errors.New("root route must not have any mute time intervals") } if len(c.Route.ActiveTimeIntervals) > 0 { - return fmt.Errorf("root route must not have any active time intervals") + return errors.New("root route must not have any active time intervals") } // Validate that all receivers used in the routing tree are defined. @@ -685,7 +696,7 @@ func parseURL(s string) (*URL, error) { return nil, fmt.Errorf("unsupported scheme %q for URL", u.Scheme) } if u.Host == "" { - return nil, fmt.Errorf("missing host for URL") + return nil, errors.New("missing host for URL") } return &URL{u}, nil } @@ -848,7 +859,7 @@ func (r *Route) UnmarshalYAML(unmarshal func(interface{}) error) error { } if len(r.GroupBy) > 0 && r.GroupByAll { - return fmt.Errorf("cannot have wildcard group_by (`...`) and other other labels at the same time") + return errors.New("cannot have wildcard group_by (`...`) and other other labels at the same time") } groupBy := map[model.LabelName]struct{}{} @@ -861,10 +872,10 @@ func (r *Route) UnmarshalYAML(unmarshal func(interface{}) error) error { } if r.GroupInterval != nil && time.Duration(*r.GroupInterval) == time.Duration(0) { - return fmt.Errorf("group_interval cannot be zero") + return errors.New("group_interval cannot be zero") } if r.RepeatInterval != nil && time.Duration(*r.RepeatInterval) == time.Duration(0) { - return fmt.Errorf("repeat_interval cannot be zero") + return errors.New("repeat_interval cannot be zero") } return nil @@ -935,6 +946,7 @@ type Receiver struct { TelegramConfigs []*TelegramConfig `yaml:"telegram_configs,omitempty" json:"telegram_configs,omitempty"` WebexConfigs []*WebexConfig `yaml:"webex_configs,omitempty" json:"webex_configs,omitempty"` MSTeamsConfigs []*MSTeamsConfig `yaml:"msteams_configs,omitempty" json:"msteams_configs,omitempty"` + MSTeamsV2Configs []*MSTeamsV2Config `yaml:"msteamsv2_configs,omitempty" json:"msteamsv2_configs,omitempty"` JiraConfigs []*JiraConfig `yaml:"jira_configs,omitempty" json:"jira_configs,omitempty"` } @@ -945,7 +957,7 @@ func (c *Receiver) UnmarshalYAML(unmarshal func(interface{}) error) error { return err } if c.Name == "" { - return fmt.Errorf("missing name in receiver") + return errors.New("missing name in receiver") } return nil } diff --git a/config/notifiers.go b/config/notifiers.go index 69841661..519e5efb 100644 --- a/config/notifiers.go +++ b/config/notifiers.go @@ -14,6 +14,7 @@ package config import ( + "errors" "fmt" "net/textproto" "regexp" @@ -174,6 +175,14 @@ var ( Text: `{{ template "msteams.default.text" . }}`, } + DefaultMSTeamsV2Config = MSTeamsV2Config{ + NotifierConfig: NotifierConfig{ + VSendResolved: true, + }, + Title: `{{ template "msteamsv2.default.title" . }}`, + Text: `{{ template "msteamsv2.default.text" . }}`, + } + DefaultJiraConfig = JiraConfig{ NotifierConfig: NotifierConfig{ VSendResolved: true, @@ -212,11 +221,11 @@ func (c *WebexConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { } if c.RoomID == "" { - return fmt.Errorf("missing room_id on webex_config") + return errors.New("missing room_id on webex_config") } if c.HTTPConfig == nil || c.HTTPConfig.Authorization == nil { - return fmt.Errorf("missing webex_configs.http_config.authorization") + return errors.New("missing webex_configs.http_config.authorization") } return nil @@ -243,11 +252,11 @@ func (c *DiscordConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { } if c.WebhookURL == nil && c.WebhookURLFile == "" { - return fmt.Errorf("one of webhook_url or webhook_url_file must be configured") + return errors.New("one of webhook_url or webhook_url_file must be configured") } if c.WebhookURL != nil && len(c.WebhookURLFile) > 0 { - return fmt.Errorf("at most one of webhook_url & webhook_url_file must be configured") + return errors.New("at most one of webhook_url & webhook_url_file must be configured") } return nil @@ -282,7 +291,7 @@ func (c *EmailConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { return err } if c.To == "" { - return fmt.Errorf("missing to address in email config") + return errors.New("missing to address in email config") } // Header names are case-insensitive, check for collisions. normalizedHeaders := map[string]string{} @@ -343,13 +352,13 @@ func (c *PagerdutyConfig) UnmarshalYAML(unmarshal func(interface{}) error) error return err } if c.RoutingKey == "" && c.ServiceKey == "" && c.RoutingKeyFile == "" && c.ServiceKeyFile == "" { - return fmt.Errorf("missing service or routing key in PagerDuty config") + return errors.New("missing service or routing key in PagerDuty config") } if len(c.RoutingKey) > 0 && len(c.RoutingKeyFile) > 0 { - return fmt.Errorf("at most one of routing_key & routing_key_file must be configured") + return errors.New("at most one of routing_key & routing_key_file must be configured") } if len(c.ServiceKey) > 0 && len(c.ServiceKeyFile) > 0 { - return fmt.Errorf("at most one of service_key & service_key_file must be configured") + return errors.New("at most one of service_key & service_key_file must be configured") } if c.Details == nil { c.Details = make(map[string]string) @@ -385,10 +394,10 @@ func (c *SlackAction) UnmarshalYAML(unmarshal func(interface{}) error) error { return err } if c.Type == "" { - return fmt.Errorf("missing type in Slack action configuration") + return errors.New("missing type in Slack action configuration") } if c.Text == "" { - return fmt.Errorf("missing text in Slack action configuration") + return errors.New("missing text in Slack action configuration") } if c.URL != "" { // Clear all message action fields. @@ -398,7 +407,7 @@ func (c *SlackAction) UnmarshalYAML(unmarshal func(interface{}) error) error { } else if c.Name != "" { c.URL = "" } else { - return fmt.Errorf("missing name or url in Slack action configuration") + return errors.New("missing name or url in Slack action configuration") } return nil } @@ -420,7 +429,7 @@ func (c *SlackConfirmationField) UnmarshalYAML(unmarshal func(interface{}) error return err } if c.Text == "" { - return fmt.Errorf("missing text in Slack confirmation configuration") + return errors.New("missing text in Slack confirmation configuration") } return nil } @@ -442,10 +451,10 @@ func (c *SlackField) UnmarshalYAML(unmarshal func(interface{}) error) error { return err } if c.Title == "" { - return fmt.Errorf("missing title in Slack field configuration") + return errors.New("missing title in Slack field configuration") } if c.Value == "" { - return fmt.Errorf("missing value in Slack field configuration") + return errors.New("missing value in Slack field configuration") } return nil } @@ -491,7 +500,7 @@ func (c *SlackConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { } if c.APIURL != nil && len(c.APIURLFile) > 0 { - return fmt.Errorf("at most one of api_url & api_url_file must be configured") + return errors.New("at most one of api_url & api_url_file must be configured") } return nil @@ -521,10 +530,10 @@ func (c *WebhookConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { return err } if c.URL == nil && c.URLFile == "" { - return fmt.Errorf("one of url or url_file must be configured") + return errors.New("one of url or url_file must be configured") } if c.URL != nil && c.URLFile != "" { - return fmt.Errorf("at most one of url & url_file must be configured") + return errors.New("at most one of url & url_file must be configured") } return nil } @@ -604,7 +613,7 @@ func (c *OpsGenieConfig) UnmarshalYAML(unmarshal func(interface{}) error) error } if c.APIKey != "" && len(c.APIKeyFile) > 0 { - return fmt.Errorf("at most one of api_key & api_key_file must be configured") + return errors.New("at most one of api_key & api_key_file must be configured") } for _, r := range c.Responders { @@ -663,10 +672,10 @@ func (c *VictorOpsConfig) UnmarshalYAML(unmarshal func(interface{}) error) error return err } if c.RoutingKey == "" { - return fmt.Errorf("missing Routing key in VictorOps config") + return errors.New("missing Routing key in VictorOps config") } if c.APIKey != "" && len(c.APIKeyFile) > 0 { - return fmt.Errorf("at most one of api_key & api_key_file must be configured") + return errors.New("at most one of api_key & api_key_file must be configured") } reservedFields := []string{"routing_key", "message_type", "state_message", "entity_display_name", "monitoring_tool", "entity_id", "entity_state"} @@ -724,16 +733,16 @@ func (c *PushoverConfig) UnmarshalYAML(unmarshal func(interface{}) error) error return err } if c.UserKey == "" && c.UserKeyFile == "" { - return fmt.Errorf("one of user_key or user_key_file must be configured") + return errors.New("one of user_key or user_key_file must be configured") } if c.UserKey != "" && c.UserKeyFile != "" { - return fmt.Errorf("at most one of user_key & user_key_file must be configured") + return errors.New("at most one of user_key & user_key_file must be configured") } if c.Token == "" && c.TokenFile == "" { - return fmt.Errorf("one of token or token_file must be configured") + return errors.New("one of token or token_file must be configured") } if c.Token != "" && c.TokenFile != "" { - return fmt.Errorf("at most one of token & token_file must be configured") + return errors.New("at most one of token & token_file must be configured") } return nil } @@ -761,7 +770,7 @@ func (c *SNSConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { return err } if (c.TargetARN == "") != (c.TopicARN == "") != (c.PhoneNumber == "") { - return fmt.Errorf("must provide either a Target ARN, Topic ARN, or Phone Number for SNS config") + return errors.New("must provide either a Target ARN, Topic ARN, or Phone Number for SNS config") } return nil } @@ -790,19 +799,19 @@ func (c *TelegramConfig) UnmarshalYAML(unmarshal func(interface{}) error) error return err } if c.BotToken == "" && c.BotTokenFile == "" { - return fmt.Errorf("missing bot_token or bot_token_file on telegram_config") + return errors.New("missing bot_token or bot_token_file on telegram_config") } if c.BotToken != "" && c.BotTokenFile != "" { - return fmt.Errorf("at most one of bot_token & bot_token_file must be configured") + return errors.New("at most one of bot_token & bot_token_file must be configured") } if c.ChatID == 0 { - return fmt.Errorf("missing chat_id on telegram_config") + return errors.New("missing chat_id on telegram_config") } if c.ParseMode != "" && c.ParseMode != "Markdown" && c.ParseMode != "MarkdownV2" && c.ParseMode != "HTML" { - return fmt.Errorf("unknown parse_mode on telegram_config, must be Markdown, MarkdownV2, HTML or empty string") + return errors.New("unknown parse_mode on telegram_config, must be Markdown, MarkdownV2, HTML or empty string") } return nil } @@ -826,11 +835,39 @@ func (c *MSTeamsConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { } if c.WebhookURL == nil && c.WebhookURLFile == "" { - return fmt.Errorf("one of webhook_url or webhook_url_file must be configured") + return errors.New("one of webhook_url or webhook_url_file must be configured") } if c.WebhookURL != nil && len(c.WebhookURLFile) > 0 { - return fmt.Errorf("at most one of webhook_url & webhook_url_file must be configured") + return errors.New("at most one of webhook_url & webhook_url_file must be configured") + } + + return nil +} + +type MSTeamsV2Config struct { + NotifierConfig `yaml:",inline" json:",inline"` + HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` + WebhookURL *SecretURL `yaml:"webhook_url,omitempty" json:"webhook_url,omitempty"` + WebhookURLFile string `yaml:"webhook_url_file,omitempty" json:"webhook_url_file,omitempty"` + + Title string `yaml:"title,omitempty" json:"title,omitempty"` + Text string `yaml:"text,omitempty" json:"text,omitempty"` +} + +func (c *MSTeamsV2Config) UnmarshalYAML(unmarshal func(interface{}) error) error { + *c = DefaultMSTeamsV2Config + type plain MSTeamsV2Config + if err := unmarshal((*plain)(c)); err != nil { + return err + } + + if c.WebhookURL == nil && c.WebhookURLFile == "" { + return errors.New("one of webhook_url or webhook_url_file must be configured") + } + + if c.WebhookURL != nil && len(c.WebhookURLFile) > 0 { + return errors.New("at most one of webhook_url & webhook_url_file must be configured") } return nil @@ -865,10 +902,10 @@ func (c *JiraConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { } if c.Project == "" { - return fmt.Errorf("missing project in jira_config") + return errors.New("missing project in jira_config") } if c.IssueType == "" { - return fmt.Errorf("missing issue_type in jira_config") + return errors.New("missing issue_type in jira_config") } return nil diff --git a/config/receiver/receiver.go b/config/receiver/receiver.go index 040d7534..7edc1775 100644 --- a/config/receiver/receiver.go +++ b/config/receiver/receiver.go @@ -24,6 +24,7 @@ import ( "github.com/prometheus/alertmanager/notify/email" "github.com/prometheus/alertmanager/notify/jira" "github.com/prometheus/alertmanager/notify/msteams" + "github.com/prometheus/alertmanager/notify/msteamsv2" "github.com/prometheus/alertmanager/notify/opsgenie" "github.com/prometheus/alertmanager/notify/pagerduty" "github.com/prometheus/alertmanager/notify/pushover" @@ -93,6 +94,9 @@ func BuildReceiverIntegrations(nc config.Receiver, tmpl *template.Template, logg for i, c := range nc.MSTeamsConfigs { add("msteams", i, c, func(l log.Logger) (notify.Notifier, error) { return msteams.New(c, tmpl, l, httpOpts...) }) } + for i, c := range nc.MSTeamsV2Configs { + add("msteamsv2", i, c, func(l log.Logger) (notify.Notifier, error) { return msteamsv2.New(c, tmpl, l, httpOpts...) }) + } for i, c := range nc.JiraConfigs { add("jira", i, c, func(l log.Logger) (notify.Notifier, error) { return jira.New(c, tmpl, l, httpOpts...) }) } diff --git a/docs/configuration.md b/docs/configuration.md index ee012b4f..7db87af5 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -700,6 +700,8 @@ email_configs: [ - , ... ] msteams_configs: [ - , ... ] +msteamsv2_configs: + [ - , ... ] jira_configs: [ - , ... ] opsgenie_configs: @@ -927,6 +929,8 @@ tls_config: Microsoft Teams notifications are sent via the [Incoming Webhooks](https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/what-are-webhooks-and-connectors) API endpoint. +DEPRECATION NOTICE: Microsoft is deprecating the creation and usage of [Microsoft 365 connectors via Microsoft Teams](https://devblogs.microsoft.com/microsoft365dev/retirement-of-office-365-connectors-within-microsoft-teams/). Consider migrating to using [Workflows](https://learn.microsoft.com/en-us/power-automate/teams/send-a-message-in-teams) with the msteamsv2 config. + ```yaml # Whether to notify about resolved alerts. [ send_resolved: | default = true ] @@ -949,6 +953,29 @@ Microsoft Teams notifications are sent via the [Incoming Webhooks](https://learn [ http_config: | default = global.http_config ] ``` +### `` + +Microsoft Teams v2 notifications using the new message format with adaptive cards as required by [flows](https://learn.microsoft.com/en-us/power-automate/teams/overview). Please follow [the documentation](https://support.microsoft.com/en-gb/office/create-incoming-webhooks-with-workflows-for-microsoft-teams-8ae491c7-0394-4861-ba59-055e33f75498) for more information on how to set up this integration. + +```yaml +# Whether to notify about resolved alerts. +[ send_resolved: | default = true ] + +# The incoming webhook URL. +# webhook_url and webhook_url_file are mutually exclusive. +[ webhook_url: ] +[ webhook_url_file: ] + +# Message title template. +[ title: | default = '{{ template "msteamsv2.default.title" . }}' ] + +# Message body template. +[ text: | default = '{{ template "msteamsv2.default.text" . }}' ] + +# The HTTP client's configuration. +[ http_config: | default = global.http_config ] +``` + ### `` JIRA notifications are sent via [JIRA Rest API v2](https://developer.atlassian.com/cloud/jira/platform/rest/v2/intro/) diff --git a/notify/msteamsv2/msteamsv2.go b/notify/msteamsv2/msteamsv2.go new file mode 100644 index 00000000..d3e7c123 --- /dev/null +++ b/notify/msteamsv2/msteamsv2.go @@ -0,0 +1,193 @@ +// Copyright 2024 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package msteamsv2 + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + + "github.com/go-kit/log" + "github.com/go-kit/log/level" + commoncfg "github.com/prometheus/common/config" + "github.com/prometheus/common/model" + + "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/template" + "github.com/prometheus/alertmanager/types" +) + +const ( + colorRed = "Attention" + colorGreen = "Good" + colorGrey = "Warning" +) + +type Notifier struct { + conf *config.MSTeamsV2Config + tmpl *template.Template + logger log.Logger + client *http.Client + retrier *notify.Retrier + webhookURL *config.SecretURL + postJSONFunc func(ctx context.Context, client *http.Client, url string, body io.Reader) (*http.Response, error) +} + +// https://learn.microsoft.com/en-us/connectors/teams/?tabs=text1#adaptivecarditemschema +type Content struct { + Schema string `json:"$schema"` + Type string `json:"type"` + Version string `json:"version"` + Body []Body `json:"body"` +} + +type Body struct { + Type string `json:"type"` + Text string `json:"text"` + Weight string `json:"weigth,omitempty"` + Size string `json:"size,omitempty"` + Wrap bool `json:"wrap,omitempty"` + Style string `json:"style,omitempty"` + Color string `json:"color,omitempty"` +} + +type Attachment struct { + ContentType string `json:"contentType"` + ContentURL *string `json:"contentUrl"` // Use a pointer to handle null values + Content Content `json:"content"` +} + +type teamsMessage struct { + Type string `json:"type"` + Attachments []Attachment `json:"attachments"` +} + +// New returns a new notifier that uses the Microsoft Teams Power Platform connector. +func New(c *config.MSTeamsV2Config, t *template.Template, l log.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) { + client, err := commoncfg.NewClientFromConfig(*c.HTTPConfig, "msteamsv2", httpOpts...) + if err != nil { + return nil, err + } + + n := &Notifier{ + conf: c, + tmpl: t, + logger: l, + client: client, + retrier: ¬ify.Retrier{}, + webhookURL: c.WebhookURL, + postJSONFunc: notify.PostJSON, + } + + return n, nil +} + +func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { + key, err := notify.ExtractGroupKey(ctx) + if err != nil { + return false, err + } + + level.Debug(n.logger).Log("incident", key) + + data := notify.GetTemplateData(ctx, n.tmpl, as, n.logger) + tmpl := notify.TmplText(n.tmpl, data, &err) + if err != nil { + return false, err + } + + title := tmpl(n.conf.Title) + if err != nil { + return false, err + } + text := tmpl(n.conf.Text) + if err != nil { + return false, err + } + + alerts := types.Alerts(as...) + color := colorGrey + switch alerts.Status() { + case model.AlertFiring: + color = colorRed + case model.AlertResolved: + color = colorGreen + } + + var url string + if n.conf.WebhookURL != nil { + url = n.conf.WebhookURL.String() + } else { + content, err := os.ReadFile(n.conf.WebhookURLFile) + if err != nil { + return false, fmt.Errorf("read webhook_url_file: %w", err) + } + url = strings.TrimSpace(string(content)) + } + + // A message as referenced in https://learn.microsoft.com/en-us/connectors/teams/?tabs=text1%2Cdotnet#request-body-schema + t := teamsMessage{ + Type: "message", + Attachments: []Attachment{ + { + ContentType: "application/vnd.microsoft.card.adaptive", + ContentURL: nil, + Content: Content{ + Schema: "http://adaptivecards.io/schemas/adaptive-card.json", + Type: "AdaptiveCard", + Version: "1.2", + Body: []Body{ + { + Type: "TextBlock", + Text: title, + Weight: "Bolder", + Size: "Medium", + Wrap: true, + Style: "heading", + Color: color, + }, + { + Type: "TextBlock", + Text: text, + }, + }, + }, + }, + }, + } + + var payload bytes.Buffer + if err = json.NewEncoder(&payload).Encode(t); err != nil { + return false, err + } + + resp, err := n.postJSONFunc(ctx, n.client, url, &payload) + if err != nil { + return true, notify.RedactURL(err) + } + defer notify.Drain(resp) + + // https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using?tabs=cURL#rate-limiting-for-connectors + shouldRetry, err := n.retrier.Check(resp.StatusCode, resp.Body) + if err != nil { + return shouldRetry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err) + } + return shouldRetry, err +} diff --git a/notify/msteamsv2/msteamsv2_test.go b/notify/msteamsv2/msteamsv2_test.go new file mode 100644 index 00000000..ec325ca3 --- /dev/null +++ b/notify/msteamsv2/msteamsv2_test.go @@ -0,0 +1,225 @@ +// Copyright 2024 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package msteamsv2 + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + "time" + + "github.com/go-kit/log" + commoncfg "github.com/prometheus/common/config" + "github.com/prometheus/common/model" + "github.com/stretchr/testify/require" + + "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/notify/test" + "github.com/prometheus/alertmanager/types" +) + +// This is a test URL that has been modified to not be valid. +var testWebhookURL, _ = url.Parse("https://example.westeurope.logic.azure.com:443/workflows/xxx/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=xxx") + +func TestMSTeamsV2Retry(t *testing.T) { + notifier, err := New( + &config.MSTeamsV2Config{ + WebhookURL: &config.SecretURL{URL: testWebhookURL}, + HTTPConfig: &commoncfg.HTTPClientConfig{}, + }, + test.CreateTmpl(t), + log.NewNopLogger(), + ) + require.NoError(t, err) + + for statusCode, expected := range test.RetryTests(test.DefaultRetryCodes()) { + actual, _ := notifier.retrier.Check(statusCode, nil) + require.Equal(t, expected, actual, fmt.Sprintf("retry - error on status %d", statusCode)) + } +} + +func TestNotifier_Notify_WithReason(t *testing.T) { + tests := []struct { + name string + statusCode int + responseContent string + expectedReason notify.Reason + noError bool + }{ + { + name: "with a 2xx status code and response 1", + statusCode: http.StatusOK, + responseContent: "1", + noError: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + notifier, err := New( + &config.MSTeamsV2Config{ + WebhookURL: &config.SecretURL{URL: testWebhookURL}, + HTTPConfig: &commoncfg.HTTPClientConfig{}, + }, + test.CreateTmpl(t), + log.NewNopLogger(), + ) + require.NoError(t, err) + + notifier.postJSONFunc = func(ctx context.Context, client *http.Client, url string, body io.Reader) (*http.Response, error) { + resp := httptest.NewRecorder() + resp.WriteString(tt.responseContent) + resp.WriteHeader(tt.statusCode) + return resp.Result(), nil + } + ctx := context.Background() + ctx = notify.WithGroupKey(ctx, "1") + + alert1 := &types.Alert{ + Alert: model.Alert{ + StartsAt: time.Now(), + EndsAt: time.Now().Add(time.Hour), + }, + } + _, err = notifier.Notify(ctx, alert1) + if tt.noError { + require.NoError(t, err) + } else { + var reasonError *notify.ErrorWithReason + require.ErrorAs(t, err, &reasonError) + require.Equal(t, tt.expectedReason, reasonError.Reason) + } + }) + } +} + +func TestMSTeamsV2Templating(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + dec := json.NewDecoder(r.Body) + out := make(map[string]interface{}) + err := dec.Decode(&out) + if err != nil { + panic(err) + } + })) + defer srv.Close() + u, _ := url.Parse(srv.URL) + + for _, tc := range []struct { + title string + cfg *config.MSTeamsV2Config + + retry bool + errMsg string + }{ + { + title: "full-blown message", + cfg: &config.MSTeamsV2Config{ + Title: `{{ template "msteams.default.title" . }}`, + Text: `{{ template "msteams.default.text" . }}`, + }, + retry: false, + }, + { + title: "title with templating errors", + cfg: &config.MSTeamsV2Config{ + Title: "{{ ", + }, + errMsg: "template: :1: unclosed action", + }, + { + title: "message with templating errors", + cfg: &config.MSTeamsV2Config{ + Title: `{{ template "msteams.default.title" . }}`, + Text: "{{ ", + }, + errMsg: "template: :1: unclosed action", + }, + } { + t.Run(tc.title, func(t *testing.T) { + tc.cfg.WebhookURL = &config.SecretURL{URL: u} + tc.cfg.HTTPConfig = &commoncfg.HTTPClientConfig{} + pd, err := New(tc.cfg, test.CreateTmpl(t), log.NewNopLogger()) + require.NoError(t, err) + + ctx := context.Background() + ctx = notify.WithGroupKey(ctx, "1") + + ok, err := pd.Notify(ctx, []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{ + "lbl1": "val1", + }, + StartsAt: time.Now(), + EndsAt: time.Now().Add(time.Hour), + }, + }, + }...) + if tc.errMsg == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errMsg) + } + require.Equal(t, tc.retry, ok) + }) + } +} + +func TestMSTeamsV2RedactedURL(t *testing.T) { + ctx, u, fn := test.GetContextWithCancelingURL() + defer fn() + + secret := "secret" + notifier, err := New( + &config.MSTeamsV2Config{ + WebhookURL: &config.SecretURL{URL: u}, + HTTPConfig: &commoncfg.HTTPClientConfig{}, + }, + test.CreateTmpl(t), + log.NewNopLogger(), + ) + require.NoError(t, err) + + test.AssertNotifyLeaksNoSecret(ctx, t, notifier, secret) +} + +func TestMSTeamsV2ReadingURLFromFile(t *testing.T) { + ctx, u, fn := test.GetContextWithCancelingURL() + defer fn() + + f, err := os.CreateTemp("", "webhook_url") + require.NoError(t, err, "creating temp file failed") + _, err = f.WriteString(u.String() + "\n") + require.NoError(t, err, "writing to temp file failed") + + notifier, err := New( + &config.MSTeamsV2Config{ + WebhookURLFile: f.Name(), + HTTPConfig: &commoncfg.HTTPClientConfig{}, + }, + test.CreateTmpl(t), + log.NewNopLogger(), + ) + require.NoError(t, err) + + test.AssertNotifyLeaksNoSecret(ctx, t, notifier, u.String()) +} diff --git a/notify/notify.go b/notify/notify.go index b5e1ae91..25d04cb2 100644 --- a/notify/notify.go +++ b/notify/notify.go @@ -365,6 +365,7 @@ func (m *Metrics) InitializeFor(receiver map[string][]Integration) { "discord", "webex", "msteams", + "msteamsv2", "jira", } { m.numNotifications.WithLabelValues(integration) diff --git a/template/default.tmpl b/template/default.tmpl index 299bdecb..6475e0a2 100644 --- a/template/default.tmpl +++ b/template/default.tmpl @@ -159,6 +159,18 @@ Alerts Resolved: {{ end }} {{ end }} +{{ define "msteamsv2.default.title" }}{{ template "__subject" . }}{{ end }} +{{ define "msteamsv2.default.text" }} +{{ if gt (len .Alerts.Firing) 0 }} +# Alerts Firing: +{{ template "__text_alert_list_markdown" .Alerts.Firing }} +{{ end }} +{{ if gt (len .Alerts.Resolved) 0 }} +# Alerts Resolved: +{{ template "__text_alert_list_markdown" .Alerts.Resolved }} +{{ end }} +{{ end }} + {{ define "jira.default.summary" }}{{ template "__subject" . }}{{ end }} {{ define "jira.default.description" }} {{ if gt (len .Alerts.Firing) 0 }}