diff --git a/asset/assets_vfsdata.go b/asset/assets_vfsdata.go index 3dd06e052b..f1ef5bd2d4 100644 --- a/asset/assets_vfsdata.go +++ b/asset/assets_vfsdata.go @@ -161,9 +161,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: 8101, + uncompressedSize: 8600, - compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\xec\x59\xcf\x6f\xeb\x36\x0c\xbe\xe7\xaf\x20\xfc\x76\x68\x0e\xf5\x1b\x76\x2c\x50\x0c\x0f\xc3\x7e\x1c\xba\x61\x68\xd1\x5d\x86\x21\x50\x6d\xc6\x55\x2b\x4b\xae\x44\x27\x0d\xd2\xfc\xef\x83\x6c\xc7\x91\x2d\x27\x91\xd3\xec\xb4\xdc\x12\x99\xfc\x48\x7f\x1f\x4d\xca\xf2\x7a\x0d\x29\xce\xb9\x44\x88\x66\x33\x26\x50\x53\xce\x24\xcb\x50\x47\xb0\xd9\x7c\x73\xfe\xaf\xd7\x80\x32\x85\xcd\x66\xb2\xd7\xe5\xf1\xfe\xce\x7a\xad\xd7\x10\xff\xfc\x4e\xa8\x25\x13\x8f\xf7\x77\xb0\xd9\x7c\xfd\xf2\xb5\xb2\x33\x3f\x6a\x4c\x90\x2f\x50\xdf\x5a\xa3\xfb\xe6\x0f\x7c\x40\xa9\xc5\x5b\x89\x7a\x55\xbb\x37\x81\xba\x91\x4c\xf9\xf4\x82\x09\xd9\x08\x7f\x5b\xef\x07\x62\x54\x1a\xf8\x00\x52\x8f\x45\x81\xba\x76\xe5\x73\xc0\xb7\xf6\x62\x34\xe7\x9a\xcb\xcc\xfa\xdc\x58\x9f\xea\x86\x4c\xfc\x4b\xb5\x0a\x1f\x20\x50\xba\x11\xff\x01\x6b\xf4\xab\x56\x65\x71\xc7\x9e\x50\x98\xf8\x41\x69\xc2\xf4\x4f\xc6\xb5\x89\xff\x62\xa2\x44\x1b\xf0\x45\x71\x09\x11\x58\x54\xa8\x43\x66\x04\x57\x16\x2b\xfe\x49\xe5\xb9\x92\xb5\xf3\xb4\x59\x73\xf0\xa6\xb0\xd9\x5c\xad\xd7\xb0\xe4\xf4\xdc\x35\x8e\xef\x31\x57\x0b\xec\x46\xff\x83\xe5\x68\x1a\x46\x87\xa2\xb7\x89\x4f\xdb\x5f\x7b\x64\x4a\xd1\x24\x9a\x17\xc4\x95\x8c\x0e\x70\x4c\xf8\x4e\xb5\xa4\x33\xc1\x0d\x35\xa6\x9a\xc9\x0c\x21\x86\xcd\xa6\xce\xeb\x66\xb2\x5b\xf4\x79\xb2\xac\x5c\x57\x44\xda\xf4\xed\xbf\x5b\x68\x6f\xa0\x49\xac\x0e\xfe\x4d\x4a\x45\xcc\xe6\xd4\x81\x74\x96\x4f\xc3\x7d\x50\xa5\x4e\xf0\xa6\x16\x13\x25\x6a\x46\x4a\xd7\x95\x38\x19\x20\xea\x20\x05\xb3\x9c\xe9\xd7\x54\x2d\xa5\xc7\xc5\x24\x94\x8c\xc0\xac\x27\xe3\xe9\x08\x45\x0e\x22\x64\x32\xcc\x88\x11\x2c\x79\x8d\x53\x9c\xb3\x52\x50\x4c\x9c\x04\x36\x54\x10\xe6\x85\x60\xd4\x7d\x38\xe3\x7d\x35\xd8\xc5\x29\x8d\x6d\x0f\xf9\x10\x54\xb7\x09\x05\xe2\xcd\x99\x10\x4f\x2c\x79\xf5\xf0\x06\xd3\xb7\xa0\xf0\x01\xc7\x0c\x05\x97\xaf\xc1\x19\x24\x4d\x06\x3c\x8d\xc2\x1c\x0a\x8d\xb6\xd6\x02\xad\x9d\x84\x0e\x32\x56\xf5\xe0\xc0\x94\x79\xa2\x24\xe6\xea\x85\x47\xe1\xf6\xa5\x16\xa1\x19\x87\xdf\xdc\x5c\x29\xaa\x27\x8e\x53\x83\xae\x79\x61\x6f\x2d\x2d\x69\xd5\xba\xf8\x0d\x6d\x5c\x39\xfa\x88\x89\xe0\x28\xe9\xf4\x82\xdc\x87\xb8\x9b\x8a\xa7\x69\xe6\xe3\x72\x69\x88\xc9\x04\xcd\x00\xae\xd7\xc1\xe3\xfd\xac\xaa\xc2\x64\x28\x39\xb6\xc0\x39\x1a\xc3\xb2\xd3\x9e\x6f\x0f\xcc\x57\xa8\x19\x78\x7b\x1a\xda\xe0\x84\x9b\xf4\xe6\x6b\x67\x80\x4f\xe1\x7b\xb8\xb6\x8d\xb3\x5a\x84\x7a\xb1\x6a\x9d\x87\x19\xe9\xee\x02\xaa\x20\xd7\xce\x1d\x0d\xc4\xbb\x47\xa3\xc4\x02\xd3\x5e\xc4\xed\x72\x78\xcc\xad\x87\x17\xf5\x3a\x84\x52\x53\xf5\xf1\xf1\xd5\xd4\x51\x7d\x89\xc9\x33\xa3\xb1\x9a\x4f\x2e\xfa\x1d\xd0\xcf\xdd\x28\x3f\x6a\xe1\xe1\x0d\xea\xb3\x47\xf5\x9e\x3e\xa4\x66\x76\x58\xee\xed\xa4\xbe\x79\xc1\x34\xad\x46\xd8\x13\xcb\x42\xad\x59\x86\x92\x66\xfd\x11\xd7\xad\xaf\x05\x4f\x48\x69\x55\x98\x5d\xd9\x12\x23\x9c\x75\x0b\xed\x52\x4b\xe3\x7a\x81\xcf\x2a\x4a\xe2\xb4\x9a\xa5\xdc\x14\x82\xad\x66\x7b\x76\x53\xc7\x1b\xb7\x8f\x9c\x2b\xc9\x49\x59\x42\x66\xa4\x94\x18\x39\x12\x3b\xb3\xab\x34\xcf\x6a\x81\xfa\x0c\xfb\x47\x0f\xea\xbf\xaf\xa7\xf3\x94\x53\x78\x35\x9d\xaf\x98\xfc\x2d\xfd\x21\x26\x77\x7b\xba\x31\x33\xc5\xdd\xcd\x49\xe7\x61\xdf\xbd\xa6\x8f\x7f\x47\x70\x70\x2e\xf2\x8e\x91\xd7\x65\x91\x50\x60\xa6\x59\x3e\x44\xe5\xff\x96\x94\x94\x9b\x44\xe9\x74\xb7\x37\x57\x92\x76\xdb\x7d\xbf\x14\xfb\xf6\xa7\x37\xae\x3e\xd2\x45\x0d\xbb\xad\x78\xc2\xf7\xcb\xa3\xfe\x69\x1e\x73\x43\xc8\x72\xb7\xf9\xe6\x39\xd3\xab\x93\xea\xb4\x8f\x75\x7a\xc5\x7b\x48\xcd\x49\x40\x88\x4c\x5f\x60\x94\x50\xce\xf1\xdc\xa7\x15\x6b\x43\x87\x6a\x36\x10\xfc\x04\xf1\x16\x3f\x9c\x8f\x72\x17\xeb\x42\xfa\x10\xe9\x2f\x5c\xb3\xb3\x3c\x2e\x1d\xa0\xde\x59\xc7\x85\xf3\x49\xf5\x1a\x33\xc8\x55\xa1\xb9\xd2\xdc\xbe\xa1\x5e\x37\x6f\x3b\xdf\x6d\x97\xe0\xe6\x16\xa2\x68\xfb\x12\xb4\x3d\xff\xee\xdc\xad\xf5\x01\x00\xa8\xfc\x0c\x2e\x70\xeb\xc7\x65\x8a\xef\xdb\x23\x78\x88\xb6\x97\xa2\x8e\x07\x9f\xc3\x15\xbe\x39\x8e\x51\xa2\x39\xf1\x84\x89\x68\xda\x1a\xb6\xf0\x6d\x5a\xb7\x10\xfd\xc6\xb3\xe7\x2e\x16\x0a\x83\x15\x20\x93\x69\x1f\x75\xc9\xb4\xe4\x32\x8b\xa6\x70\x25\xd1\x01\xaa\x61\xa6\x47\x62\xfd\x8e\x29\x2f\xf3\xf0\x68\x5c\xce\x95\x0d\x65\x57\x77\xa1\x8e\x86\xb9\x53\xcb\x5e\x0c\x99\xb6\x9a\xb8\xbf\xeb\x6f\x6a\x2e\x74\xc7\xad\xab\x53\x5b\x18\x5e\xec\x51\x6a\x8d\x56\x2c\x40\xb5\xb3\x2b\x17\xa4\xde\xf9\x14\x3c\xae\x62\x5f\xc9\x63\xca\xee\x90\xfa\x57\xdd\x56\xa7\x55\xf2\x8a\xd4\x3d\x36\x3a\x79\x52\x0d\x80\x31\xc1\x99\x39\xfd\xe0\x7d\x5f\x7a\x9f\xfe\x5a\x32\x00\x7c\xf8\x73\xc9\x80\xc3\xb1\x6f\x26\x43\xc9\x7b\x1f\x4e\xfe\x0d\x00\x00\xff\xff\x74\x5d\xc4\xb5\xa5\x1f\x00\x00"), + compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\xec\x59\xcb\x6e\xeb\x36\x10\xdd\xfb\x2b\x06\xba\x5d\xc4\x8b\xe8\x16\x5d\x06\x08\x8a\x8b\xa2\x8f\x45\x5a\x14\x09\xd2\x4d\x51\x18\x8c\x34\x56\x98\x50\xa4\x42\x8e\xec\x18\x8e\xff\xbd\xa0\x24\xcb\xd4\xcb\xa6\x14\x77\x55\xef\x6c\x6a\xe6\xcc\xf0\x9c\xd1\x90\x14\xb7\x5b\x88\x71\xc9\x25\x42\xb0\x58\x30\x81\x9a\x52\x26\x59\x82\x3a\x80\xdd\xee\x9b\xf3\x7f\xbb\x05\x94\x31\xec\x76\xb3\x41\x97\xc7\xfb\x3b\xeb\xb5\xdd\x42\xf8\xf3\x3b\xa1\x96\x4c\x3c\xde\xdf\xc1\x6e\xf7\xf5\xcb\xd7\xc2\xce\xfc\xa8\x31\x42\xbe\x42\x7d\x6b\x8d\xee\xab\x3f\xf0\x01\xb9\x16\x6f\x39\xea\x4d\xe9\x5e\x05\x6a\x46\x32\xf9\xd3\x0b\x46\x64\x23\xfc\x6d\xbd\x1f\x88\x51\x6e\xe0\x03\x48\x3d\x66\x19\xea\xd2\x95\x2f\x01\xdf\xea\x87\xc1\x92\x6b\x2e\x13\xeb\x73\x63\x7d\x8a\x09\x99\xf0\x97\x62\x14\x3e\x40\xa0\x74\x23\xfe\x03\xd6\xe8\x57\xad\xf2\xec\x8e\x3d\xa1\x30\xe1\x83\xd2\x84\xf1\x9f\x8c\x6b\x13\xfe\xc5\x44\x8e\x36\xe0\x8b\xe2\x12\x02\xb0\xa8\x50\x86\x4c\x08\xae\x2c\x56\xf8\x93\x4a\x53\x25\x4b\xe7\x79\x35\xe6\xe0\xcd\x61\xb7\xbb\xda\x6e\x61\xcd\xe9\xb9\x69\x1c\xde\x63\xaa\x56\xd8\x8c\xfe\x07\x4b\xd1\x54\x8c\xf6\x45\xaf\x13\x9f\xd7\xbf\x06\x64\x8a\xd1\x44\x9a\x67\xc4\x95\x0c\x8e\x70\x4c\xf8\x4e\xa5\xa4\x0b\xc1\x0d\x55\xa6\x9a\xc9\x04\x21\x84\xdd\xae\xcc\xeb\x66\x76\x18\xec\xf2\x64\x59\xb9\x2e\x88\xb4\xe9\xdb\x7f\xb7\x50\x4f\xa0\x4a\xac\x0c\xfe\x4d\x4a\x45\xcc\xe6\xd4\x80\x74\x86\xa7\xe1\x3e\xa8\x5c\x47\x78\x53\x8a\x89\x12\x35\x23\xa5\xcb\x4a\x9c\xf5\x10\x75\x94\x82\x45\xca\xf4\x6b\xac\xd6\xb2\xc3\xc5\xcc\x97\x0c\xcf\xac\x67\xe3\xe9\xf0\x45\xf6\x22\x64\xd6\xcf\x88\x11\x2c\x7a\x0d\x63\x5c\xb2\x5c\x50\x48\x9c\x04\x56\x54\x10\xa6\x99\x60\xd4\x7c\x39\xc3\xa1\x1a\x6c\xe2\xe4\xc6\xb6\x87\xb4\x0f\xaa\xd9\x84\x3c\xf1\x96\x4c\x88\x27\x16\xbd\x76\xf0\x7a\xd3\xb7\xa0\xf0\x01\xa7\x0c\x05\x97\xaf\xde\x19\x44\x55\x06\x3c\x0e\xfc\x1c\x32\x8d\xb6\xd6\x3c\xad\x9d\x84\x8e\x32\x56\xf4\x60\xcf\x94\x79\xa4\x24\xa6\xea\x85\x07\xfe\xf6\xb9\x16\xbe\x19\xfb\x4f\x6e\xa9\x14\x95\x2b\x8e\x53\x83\xae\x79\x66\xa7\x16\xe7\xb4\xa9\x5d\xba\x0d\x6d\x5c\x39\x76\x11\x23\xc1\x51\xd2\xf4\x82\x1c\x42\x3c\xac\x8a\xd3\x34\xeb\xe2\x72\x69\x88\xc9\x08\x4d\x0f\x6e\xa7\x83\x87\xc3\xac\xaa\xcc\x24\x28\x39\xd6\xc0\x29\x1a\xc3\x92\x69\xef\x77\x07\xac\xab\x50\xb5\xe0\x0d\x34\xb4\xde\x15\x6e\xd6\x5a\x5f\x1b\x0b\xf8\x1c\xbe\x87\x6b\xdb\x38\x8b\x41\x28\x07\x8b\xd6\x79\x9c\x91\xe6\x2e\xa0\x08\x72\xed\xcc\xa8\x27\xde\x3d\x1a\x25\x56\x18\xb7\x22\xee\x87\xfd\x63\xee\x3d\x3a\x51\xaf\x7d\x28\x35\x45\x1f\x1f\x5f\x4d\x0d\xd5\x23\x95\x66\xcc\x98\xb3\x88\xde\xc6\xba\x68\xfe\x59\xcd\xdb\x8c\x9e\x45\xf2\x35\x46\xcf\x8c\xc6\x2a\x3e\xbb\xc8\x77\x44\x3e\xf7\x6c\xf4\xa8\x45\x07\xaf\x57\x9f\x01\xd1\x5b\xfa\x90\x5a\xd8\xfd\xd1\xe0\xe2\xd9\x35\xcf\x98\xa6\xcd\x08\x7b\x62\x89\xaf\x35\x4b\x50\xd2\xa2\xbd\xab\x69\xd6\xd7\x8a\x47\xa4\xb4\xca\x9c\xb2\x25\x46\xb8\x68\x16\xda\xa5\x96\xc6\xb5\x82\x2e\xab\x28\x89\xd3\x66\x11\x73\x93\x09\xb6\x59\x0c\x6c\xa0\x4f\xb7\xed\x2e\x72\xaa\x24\x27\x65\x09\x59\x90\x52\x62\xe4\x2e\xa8\xb1\x5d\xc9\xcd\xb3\x5a\xa1\x3e\xc3\x91\xa1\x03\xf5\xdf\xd7\xd3\x79\xca\xc9\xbf\x9a\xce\x57\x4c\xdd\x53\xdc\x31\x26\x0f\xdb\xf8\x31\x6b\x8a\xbb\x81\x97\xce\xcb\x7e\xf8\x32\x33\xfe\x58\x28\xcd\x45\xde\x49\xf2\xba\x2c\x12\x0a\x4c\x34\x4b\xfb\xa8\xfc\xdf\x92\x12\x73\x13\x29\x1d\x1f\x8e\x63\x4a\xd2\xe1\x84\xd7\x2d\xc5\xb6\xfd\xf4\xc6\xd5\x46\xba\xa8\x61\xb7\x15\x4f\xf8\x7e\x79\xd5\x3f\xcd\x63\x6a\x08\x59\xea\x36\xdf\x34\x65\x7a\x33\xa9\x4e\xdb\x58\xd3\x2b\xbe\x83\x54\x7d\xfc\xf1\x91\xe9\x0b\x8c\x12\xca\xf9\x22\xfb\x69\xc5\xea\xd0\xbe\x9a\xf5\x04\x9f\x20\xde\xea\x87\xf3\x51\xee\x62\x5d\x48\xef\x23\xfd\x85\x6b\x76\x96\xd7\xa5\x01\xd4\xfa\xd4\x71\xe1\x7c\x56\x1c\x63\x7a\xb9\xca\x34\x57\x9a\xdb\x13\xea\x75\x75\xda\xf9\x6e\x3f\x04\x37\xb7\x10\x04\xfb\x43\xd0\xfe\xca\xa3\x31\x5b\xeb\x03\x00\x50\xf8\x19\x5c\xe1\xde\x8f\xcb\x18\xdf\xf7\xb7\x2e\x10\xec\x1f\x05\x0d\x0f\xbe\x84\x2b\x7c\x73\x1c\x83\x48\x73\xe2\x11\x13\xc1\xbc\x36\xac\xe1\xeb\xb4\x6e\x21\xf8\x8d\x27\xcf\x4d\x2c\x14\x06\x0b\x40\x26\xe3\x36\xea\x9a\x69\xc9\x65\x12\xcc\xe1\x4a\xa2\x03\x54\xc2\xcc\x4f\xc4\xfa\x1d\x63\x9e\xa7\xfe\xd1\xb8\x5c\x2a\x1b\xca\x8e\x1e\x42\x9d\x0c\x73\xa7\xd6\xad\x18\x32\xae\x35\x71\x7f\x97\xd7\xa8\x2e\x74\xc3\xad\xa9\x53\x5d\x18\x9d\xd8\xa3\xd4\x1a\xad\x98\x87\x6a\x67\x57\xce\x4b\xbd\xf3\x29\x78\x5a\xc5\xb6\x92\xa7\x94\x3d\x20\xb5\x9f\xba\xad\x4e\xab\xe8\x15\xa9\xf9\xd9\x68\xf2\x4a\xd5\x03\xc6\x04\x67\x66\xfa\x5d\xcb\x50\x7a\x9f\xbe\x20\xeb\x01\x3e\x7e\x43\xd6\xe3\x70\xea\x9a\xac\x2f\xf9\xce\x5d\xd9\xbf\x01\x00\x00\xff\xff\x6d\x80\x2c\x33\x98\x21\x00\x00"), }, "/templates/email.tmpl": &vfsgen۰CompressedFileInfo{ name: "email.tmpl", diff --git a/config/config.go b/config/config.go index 6f54c52368..4740d76c5b 100644 --- a/config/config.go +++ b/config/config.go @@ -1011,6 +1011,7 @@ type Receiver struct { SlackConfigs []*SlackConfig `yaml:"slack_configs,omitempty" json:"slack_configs,omitempty"` WebhookConfigs []*WebhookConfig `yaml:"webhook_configs,omitempty" json:"webhook_configs,omitempty"` OpsGenieConfigs []*OpsGenieConfig `yaml:"opsgenie_configs,omitempty" json:"opsgenie_configs,omitempty"` + CompassConfig []*CompassConfig `yaml:"compass_configs,omitempty" json:"compass_configs,omitempty"` WechatConfigs []*WechatConfig `yaml:"wechat_configs,omitempty" json:"wechat_configs,omitempty"` PushoverConfigs []*PushoverConfig `yaml:"pushover_configs,omitempty" json:"pushover_configs,omitempty"` VictorOpsConfigs []*VictorOpsConfig `yaml:"victorops_configs,omitempty" json:"victorops_configs,omitempty"` diff --git a/config/notifiers.go b/config/notifiers.go index 87f806aa27..a22722005a 100644 --- a/config/notifiers.go +++ b/config/notifiers.go @@ -123,6 +123,17 @@ var ( // TODO: Add a details field with all the alerts. } + // DefaultCompassConfig defines default values for Compass configurations. + DefaultCompassConfig = CompassConfig{ + NotifierConfig: NotifierConfig{ + VSendResolved: true, + }, + Message: `{{ template "compass.default.message" . }}`, + Description: `{{ template "compass.default.description" . }}`, + Source: `{{ template "compass.default.source" . }}`, + // TODO: Add a details field with all the alerts. + } + // DefaultWechatConfig defines default values for wechat configurations. DefaultWechatConfig = WechatConfig{ NotifierConfig: NotifierConfig{ @@ -666,6 +677,103 @@ type OpsGenieConfigResponder struct { Type string `yaml:"type,omitempty" json:"type,omitempty"` } +// CompassConfig configures notifications via Compass. +type CompassConfig struct { + NotifierConfig `yaml:",inline" json:",inline"` + + HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` + + APIUser string `yaml:"api_user,omitempty" json:"api_user,omitempty"` + APIKey Secret `yaml:"api_key,omitempty" json:"api_key,omitempty"` + APIKeyFile string `yaml:"api_key_file,omitempty" json:"api_key_file,omitempty"` + APIURL *URL `yaml:"api_url,omitempty" json:"api_url,omitempty"` + Message string `yaml:"message,omitempty" json:"message,omitempty"` + Description string `yaml:"description,omitempty" json:"description,omitempty"` + Source string `yaml:"source,omitempty" json:"source,omitempty"` + Entity string `yaml:"entity,omitempty" json:"entity,omitempty"` + Responders []CompassConfigConfigResponder `yaml:"responders,omitempty" json:"responders,omitempty"` + VisibleTo []CompassConfigConfigVisibleTo `yaml:"visible_to,omitempty" json:"visible_to,omitempty"` + Actions string `yaml:"actions,omitempty" json:"actions,omitempty"` + Tags string `yaml:"tags,omitempty" json:"tags,omitempty"` + Note string `yaml:"note,omitempty" json:"note,omitempty"` + Priority string `yaml:"priority,omitempty" json:"priority,omitempty"` + ExtraProperties map[string]string `yaml:"extra_properties,omitempty" json:"extra_properties,omitempty"` + UpdateAlerts bool `yaml:"update_alerts,omitempty" json:"update_alerts,omitempty"` +} + +const compassValidResponderTypesRe = `^(team|user|escalation|schedule)$` +const compassValidVisibleToTypesRe = `^(team|user)$` + +var compassResponderTypeMatcher = regexp.MustCompile(compassValidResponderTypesRe) +var compassVisibleToTypeMatcher = regexp.MustCompile(compassValidVisibleToTypesRe) + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (c *CompassConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + *c = DefaultCompassConfig + type plain CompassConfig + if err := unmarshal((*plain)(c)); err != nil { + return err + } + + if c.APIUser == "" { + return errors.New("api_user must be configured") + } + + if c.APIKey != "" && len(c.APIKeyFile) > 0 { + return errors.New("at most one of api_key & api_key_file must be configured") + } + + for _, responder := range c.Responders { + if responder.ID == "" { + return fmt.Errorf("compassConfig responder id must be configured") + } + + if strings.Contains(responder.Type, "{{") { + _, err := template.New("").Parse(responder.Type) + if err != nil { + return fmt.Errorf("compassConfig responder %v type is not a valid template: %w", responder, err) + } + } else { + responder.Type = strings.ToLower(responder.Type) + if !compassResponderTypeMatcher.MatchString(responder.Type) { + return fmt.Errorf("compassConfig responder %v type does not match valid options %s", responder, compassValidResponderTypesRe) + } + } + } + + for _, visibleTo := range c.VisibleTo { + if visibleTo.ID == "" { + return fmt.Errorf("compassConfig visibleTo id must be configured") + } + + if strings.Contains(visibleTo.Type, "{{") { + _, err := template.New("").Parse(visibleTo.Type) + if err != nil { + return fmt.Errorf("compassConfig visibleTo %v type is not a valid template: %w", visibleTo, err) + } + } else { + visibleTo.Type = strings.ToLower(visibleTo.Type) + if !compassVisibleToTypeMatcher.MatchString(visibleTo.Type) { + return fmt.Errorf("compassConfig visibleTo %v type does not match valid options %s", visibleTo, compassValidResponderTypesRe) + } + } + } + + return nil +} + +type CompassConfigConfigResponder struct { + ID string `yaml:"id,omitempty" json:"id,omitempty"` + // team, user, escalation, schedule etc. + Type string `yaml:"type,omitempty" json:"type,omitempty"` +} + +type CompassConfigConfigVisibleTo struct { + ID string `yaml:"id,omitempty" json:"id,omitempty"` + // team, user, escalation, schedule etc. + Type string `yaml:"type,omitempty" json:"type,omitempty"` +} + // VictorOpsConfig configures notifications via VictorOps. type VictorOpsConfig struct { NotifierConfig `yaml:",inline" json:",inline"` diff --git a/config/notifiers_test.go b/config/notifiers_test.go index af348b9ff5..26d4c5842d 100644 --- a/config/notifiers_test.go +++ b/config/notifiers_test.go @@ -882,6 +882,100 @@ api_url: http://example.com } } +func TestCompassConfiguration(t *testing.T) { + for _, tc := range []struct { + name string + in string + + err bool + }{ + { + name: "valid configuration", + in: `api_key: xyz +api_user: abc +responders: +- id: foo + type: schedule +api_url: http://example.com +`, + }, + { + name: "api_user is missing", + in: `api_key: xyz +api_url: http://example.com +`, + err: true, + }, + { + name: "api_key and api_key_file both defined", + in: `api_key: xyz +api_key_file: xyz +api_user: abc +api_url: http://example.com +`, + err: true, + }, + { + name: "invalid responder type", + in: `api_key: xyz +api_user: abc +responders: +- id: foo + type: wrong +api_url: http://example.com +`, + err: true, + }, + { + name: "missing responder field", + in: `api_key: xyz +api_user: abc +responders: +- type: schedule +api_url: http://example.com +`, + err: true, + }, + { + name: "valid responder type template", + in: `api_key: xyz +api_user: abc +responders: +- id: foo + type: "{{/* valid comment */}}team" +api_url: http://example.com +`, + }, + { + name: "invalid responder type template", + in: `api_key: xyz +api_user: abc +responders: +- id: foo + type: "{{/* invalid comment }}team" +api_url: http://example.com +`, + err: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + var cfg CompassConfig + + err := yaml.UnmarshalStrict([]byte(tc.in), &cfg) + if tc.err { + if err == nil { + t.Fatalf("expected error but got none") + } + return + } + + if err != nil { + t.Errorf("expected no error, got %v", err) + } + }) + } +} + func TestSNS(t *testing.T) { for _, tc := range []struct { in string diff --git a/config/receiver/receiver.go b/config/receiver/receiver.go index d92a19a4c5..ff3402b8ee 100644 --- a/config/receiver/receiver.go +++ b/config/receiver/receiver.go @@ -21,6 +21,7 @@ import ( "github.com/prometheus/alertmanager/config" "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/notify/compass" "github.com/prometheus/alertmanager/notify/discord" "github.com/prometheus/alertmanager/notify/email" "github.com/prometheus/alertmanager/notify/jira" @@ -73,6 +74,9 @@ func BuildReceiverIntegrations(nc config.Receiver, tmpl *template.Template, logg for i, c := range nc.OpsGenieConfigs { add("opsgenie", i, c, func(l *slog.Logger) (notify.Notifier, error) { return opsgenie.New(c, tmpl, l, httpOpts...) }) } + for i, c := range nc.CompassConfig { + add("compass", i, c, func(l *slog.Logger) (notify.Notifier, error) { return compass.New(c, tmpl, l, httpOpts...) }) + } for i, c := range nc.WechatConfigs { add("wechat", i, c, func(l *slog.Logger) (notify.Notifier, error) { return wechat.New(c, tmpl, l, httpOpts...) }) } diff --git a/notify/compass/api_key_file b/notify/compass/api_key_file new file mode 100644 index 0000000000..90d2bb3b06 --- /dev/null +++ b/notify/compass/api_key_file @@ -0,0 +1,2 @@ +my_secret_api_key + diff --git a/notify/compass/compass.go b/notify/compass/compass.go new file mode 100644 index 0000000000..0a37abd113 --- /dev/null +++ b/notify/compass/compass.go @@ -0,0 +1,310 @@ +// Copyright 2025 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 compass + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "os" + "strings" + + 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" +) + +// https://developer.atlassian.com/cloud/compass/rest/v1/api-group-alerts/#api-v1-alerts-post - 130 characters meaning runes. +const maxMessageLenRunes = 130 + +// Notifier implements a Notifier for Compass notifications. +type Notifier struct { + conf *config.CompassConfig + tmpl *template.Template + logger *slog.Logger + client *http.Client + retrier *notify.Retrier +} + +// New returns a new Compass notifier. +func New(c *config.CompassConfig, t *template.Template, l *slog.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) { + client, err := commoncfg.NewClientFromConfig(*c.HTTPConfig, "compass", httpOpts...) + if err != nil { + return nil, err + } + return &Notifier{ + conf: c, + tmpl: t, + logger: l, + client: client, + retrier: ¬ify.Retrier{RetryCodes: []int{http.StatusTooManyRequests}}, + }, nil +} + +type compassCreateMessage struct { + Alias string `json:"alias"` + Message string `json:"message"` + Description string `json:"description,omitempty"` + Source string `json:"source"` + Responders []compassCreateMessageResponder `json:"responders,omitempty"` + VisibleTo []compassCreateMessageResponder `json:"visibleTo,omitempty"` + Tags []string `json:"tags,omitempty"` + Note string `json:"note,omitempty"` + Priority string `json:"priority,omitempty"` + Entity string `json:"entity,omitempty"` + Actions []string `json:"actions,omitempty"` + ExtraProperties map[string]string `json:"extraProperties,omitempty"` +} + +type compassCreateMessageResponder struct { + ID string `json:"id,omitempty"` + Type string `json:"type"` // team, user, escalation, schedule etc. +} + +type compassCloseMessage struct { + Source string `json:"source"` +} + +type compassUpdateMessageMessage struct { + Message string `json:"message,omitempty"` +} + +type compassUpdateDescriptionMessage struct { + Description string `json:"description,omitempty"` +} + +// Notify implements the Notifier interface. +func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { + requests, retry, err := n.createRequests(ctx, as...) + if err != nil { + return retry, err + } + + for _, req := range requests { + req.Header.Set("User-Agent", notify.UserAgentHeader) + resp, err := n.client.Do(req) + if err != nil { + return true, err + } + shouldRetry, err := n.retrier.Check(resp.StatusCode, resp.Body) + notify.Drain(resp) + if err != nil { + return shouldRetry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err) + } + } + return true, nil +} + +// Like Split but filter out empty strings. +func safeSplit(s, sep string) []string { + a := strings.Split(strings.TrimSpace(s), sep) + b := a[:0] + for _, x := range a { + if x != "" { + b = append(b, x) + } + } + return b +} + +// Create requests for a list of alerts. +func (n *Notifier) createRequests(ctx context.Context, as ...*types.Alert) ([]*http.Request, bool, error) { + key, err := notify.ExtractGroupKey(ctx) + if err != nil { + return nil, false, err + } + data := notify.GetTemplateData(ctx, n.tmpl, as, n.logger) + + n.logger.Debug("extracted group key", "key", key) + + tmpl := notify.TmplText(n.tmpl, data, &err) + + extraProperties := make(map[string]string) + + for k, v := range data.CommonLabels { + extraProperties[k] = v + } + + for k, v := range n.conf.ExtraProperties { + extraProperties[k] = tmpl(v) + } + + requests := []*http.Request{} + + var ( + alias = key.Hash() + alerts = types.Alerts(as...) + ) + switch alerts.Status() { + case model.AlertResolved: + resolvedEndpointURL := n.conf.APIURL.Copy() + resolvedEndpointURL.Path += fmt.Sprintf("v1/alerts/%s/close", alias) + q := resolvedEndpointURL.Query() + q.Set("identifierType", "alias") + resolvedEndpointURL.RawQuery = q.Encode() + msg := &compassCloseMessage{Source: tmpl(n.conf.Source)} + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(msg); err != nil { + return nil, false, err + } + req, err := http.NewRequest("POST", resolvedEndpointURL.String(), &buf) + if err != nil { + return nil, true, err + } + requests = append(requests, req.WithContext(ctx)) + default: + message, truncated := notify.TruncateInRunes(tmpl(n.conf.Message), maxMessageLenRunes) + if truncated { + n.logger.Warn("Truncated message", "alert", key, "max_runes", maxMessageLenRunes) + } + + createEndpointURL := n.conf.APIURL.Copy() + createEndpointURL.Path += "v1/alerts" + + var responders []compassCreateMessageResponder + for _, r := range n.conf.Responders { + responder := compassCreateMessageResponder{ + ID: tmpl(r.ID), + Type: tmpl(r.Type), + } + + if responder == (compassCreateMessageResponder{}) { + // Filter out empty responders. This is useful if you want to fill + // responders dynamically from alert's common labels. + continue + } + + responders = append(responders, responder) + } + + var visibleTo []compassCreateMessageResponder + for _, r := range n.conf.VisibleTo { + responder := compassCreateMessageResponder{ + ID: tmpl(r.ID), + Type: tmpl(r.Type), + } + + if responder == (compassCreateMessageResponder{}) { + // Filter out empty responders. This is useful if you want to fill + // responders dynamically from alert's common labels. + continue + } + + visibleTo = append(visibleTo, responder) + } + + msg := &compassCreateMessage{ + Alias: alias, + Message: message, + Description: tmpl(n.conf.Description), + Source: tmpl(n.conf.Source), + Responders: responders, + VisibleTo: visibleTo, + Tags: safeSplit(tmpl(n.conf.Tags), ","), + Note: tmpl(n.conf.Note), + Priority: tmpl(n.conf.Priority), + Entity: tmpl(n.conf.Entity), + Actions: safeSplit(tmpl(n.conf.Actions), ","), + ExtraProperties: extraProperties, + } + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(msg); err != nil { + return nil, false, err + } + req, err := http.NewRequest("POST", createEndpointURL.String(), &buf) + if err != nil { + return nil, true, err + } + requests = append(requests, req.WithContext(ctx)) + + if n.conf.UpdateAlerts { + updateMessageEndpointURL := n.conf.APIURL.Copy() + updateMessageEndpointURL.Path += fmt.Sprintf("v1/alerts/%s/message", alias) + q := updateMessageEndpointURL.Query() + q.Set("identifierType", "alias") + updateMessageEndpointURL.RawQuery = q.Encode() + updateMsgMsg := &compassUpdateMessageMessage{ + Message: msg.Message, + } + var updateMessageBuf bytes.Buffer + if err := json.NewEncoder(&updateMessageBuf).Encode(updateMsgMsg); err != nil { + return nil, false, err + } + req, err := http.NewRequest("PATCH", updateMessageEndpointURL.String(), &updateMessageBuf) + if err != nil { + return nil, true, err + } + requests = append(requests, req) + + updateDescriptionEndpointURL := n.conf.APIURL.Copy() + updateDescriptionEndpointURL.Path += fmt.Sprintf("v1/alerts/%s/description", alias) + q = updateDescriptionEndpointURL.Query() + q.Set("identifierType", "alias") + updateDescriptionEndpointURL.RawQuery = q.Encode() + updateDescMsg := &compassUpdateDescriptionMessage{ + Description: msg.Description, + } + + var updateDescriptionBuf bytes.Buffer + if err := json.NewEncoder(&updateDescriptionBuf).Encode(updateDescMsg); err != nil { + return nil, false, err + } + req, err = http.NewRequest("PATCH", updateDescriptionEndpointURL.String(), &updateDescriptionBuf) + if err != nil { + return nil, true, err + } + requests = append(requests, req.WithContext(ctx)) + } + } + + var apiUser string + if n.conf.APIUser != "" { + apiUser = tmpl(string(n.conf.APIUser)) + } else { + return nil, false, fmt.Errorf("api_user is required") + } + + var apiKey string + if n.conf.APIKey != "" { + apiKey = tmpl(string(n.conf.APIKey)) + } else { + content, err := os.ReadFile(n.conf.APIKeyFile) + if err != nil { + return nil, false, fmt.Errorf("read key_file error: %w", err) + } + apiKey = tmpl(string(content)) + apiKey = strings.TrimSpace(string(apiKey)) + } + + basicAuth := base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%s:%s", apiUser, apiKey)) + + if err != nil { + return nil, false, fmt.Errorf("templating error: %w", err) + } + + for _, req := range requests { + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Basic %s", basicAuth)) + } + + return requests, true, nil +} diff --git a/notify/compass/compass_test.go b/notify/compass/compass_test.go new file mode 100644 index 0000000000..2778bde596 --- /dev/null +++ b/notify/compass/compass_test.go @@ -0,0 +1,357 @@ +// Copyright 2019 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 compass + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "os" + "testing" + "time" + + commoncfg "github.com/prometheus/common/config" + "github.com/prometheus/common/model" + "github.com/prometheus/common/promslog" + "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" +) + +func TestCompassRetry(t *testing.T) { + notifier, err := New( + &config.CompassConfig{ + HTTPConfig: &commoncfg.HTTPClientConfig{}, + }, + test.CreateTmpl(t), + promslog.NewNopLogger(), + ) + require.NoError(t, err) + + retryCodes := append(test.DefaultRetryCodes(), http.StatusTooManyRequests) + for statusCode, expected := range test.RetryTests(retryCodes) { + actual, _ := notifier.retrier.Check(statusCode, nil) + require.Equal(t, expected, actual, fmt.Sprintf("error on status %d", statusCode)) + } +} + +func TestCompassRedactedURL(t *testing.T) { + ctx, u, fn := test.GetContextWithCancelingURL() + defer fn() + + user := "user" + key := "key" + notifier, err := New( + &config.CompassConfig{ + APIURL: &config.URL{URL: u}, + APIUser: user, + APIKey: config.Secret(key), + HTTPConfig: &commoncfg.HTTPClientConfig{}, + }, + test.CreateTmpl(t), + promslog.NewNopLogger(), + ) + require.NoError(t, err) + + test.AssertNotifyLeaksNoSecret(ctx, t, notifier, key) +} + +func TestGettingCompassApikeyFromFile(t *testing.T) { + ctx, u, fn := test.GetContextWithCancelingURL() + defer fn() + + user := "user" + key := "key" + + f, err := os.CreateTemp("", "compass_test") + require.NoError(t, err, "creating temp file failed") + _, err = f.WriteString(key) + require.NoError(t, err, "writing to temp file failed") + + notifier, err := New( + &config.CompassConfig{ + APIURL: &config.URL{URL: u}, + APIUser: user, + APIKeyFile: f.Name(), + HTTPConfig: &commoncfg.HTTPClientConfig{}, + }, + test.CreateTmpl(t), + promslog.NewNopLogger(), + ) + require.NoError(t, err) + + test.AssertNotifyLeaksNoSecret(ctx, t, notifier, key) +} + +func TestCompass(t *testing.T) { + u, err := url.Parse("https://compass/api/") + if err != nil { + t.Fatalf("failed to parse URL: %v", err) + } + logger := promslog.NewNopLogger() + tmpl := test.CreateTmpl(t) + + for _, tc := range []struct { + title string + cfg *config.CompassConfig + + expectedEmptyAlertBody string + expectedBody string + }{ + { + title: "config without details", + cfg: &config.CompassConfig{ + NotifierConfig: config.NotifierConfig{ + VSendResolved: true, + }, + Message: `{{ .CommonLabels.Message }}`, + Description: `{{ .CommonLabels.Description }}`, + Source: `{{ .CommonLabels.Source }}`, + Responders: []config.CompassConfigConfigResponder{ + { + ID: `{{ .CommonLabels.ResponderID1 }}`, + Type: `{{ .CommonLabels.ResponderType1 }}`, + }, + { + ID: `{{ .CommonLabels.ResponderID2 }}`, + Type: `{{ .CommonLabels.ResponderType2 }}`, + }, + }, + Tags: `{{ .CommonLabels.Tags }}`, + Note: `{{ .CommonLabels.Note }}`, + Priority: `{{ .CommonLabels.Priority }}`, + Entity: `{{ .CommonLabels.Entity }}`, + Actions: `{{ .CommonLabels.Actions }}`, + APIUser: `{{ .ExternalURL }}`, + APIKey: `{{ .ExternalURL }}`, + APIURL: &config.URL{URL: u}, + HTTPConfig: &commoncfg.HTTPClientConfig{}, + }, + expectedEmptyAlertBody: `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"","source":""} +`, + expectedBody: `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"message","description":"description","source":"http://prometheus","responders":[{"id":"Schedule1","type":"schedule"},{"id":"Escalation2","type":"escalation"}],"tags":["tag1","tag2"],"note":"this is a note","priority":"P1","entity":"test-domain","actions":["doThis","doThat"],"extraProperties":{"Actions":"doThis,doThat","Description":"description","Entity":"test-domain","Message":"message","Note":"this is a note","Priority":"P1","ResponderID1":"Schedule1","ResponderID2":"Escalation2","ResponderID3":"Team3","ResponderType1":"schedule","ResponderType2":"escalation","ResponderType3":"team","Source":"http://prometheus","Tags":"tag1,tag2"}} +`, + }, + { + title: "config with details", + cfg: &config.CompassConfig{ + NotifierConfig: config.NotifierConfig{ + VSendResolved: true, + }, + Message: `{{ .CommonLabels.Message }}`, + Description: `{{ .CommonLabels.Description }}`, + Source: `{{ .CommonLabels.Source }}`, + Responders: []config.CompassConfigConfigResponder{ + { + ID: `{{ .CommonLabels.ResponderID1 }}`, + Type: `{{ .CommonLabels.ResponderType1 }}`, + }, + { + ID: `{{ .CommonLabels.ResponderID2 }}`, + Type: `{{ .CommonLabels.ResponderType2 }}`, + }, + }, + Tags: `{{ .CommonLabels.Tags }}`, + Note: `{{ .CommonLabels.Note }}`, + Priority: `{{ .CommonLabels.Priority }}`, + Entity: `{{ .CommonLabels.Entity }}`, + Actions: `{{ .CommonLabels.Actions }}`, + ExtraProperties: map[string]string{ + "Description": `adjusted {{ .CommonLabels.Description }}`, + }, + APIUser: `{{ .ExternalURL }}`, + APIKey: `{{ .ExternalURL }}`, + APIURL: &config.URL{URL: u}, + HTTPConfig: &commoncfg.HTTPClientConfig{}, + }, + expectedEmptyAlertBody: `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"","source":"","extraProperties":{"Description":"adjusted "}} +`, + expectedBody: `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"message","description":"description","source":"http://prometheus","responders":[{"id":"Schedule1","type":"schedule"},{"id":"Escalation2","type":"escalation"}],"tags":["tag1","tag2"],"note":"this is a note","priority":"P1","entity":"test-domain","actions":["doThis","doThat"],"extraProperties":{"Actions":"doThis,doThat","Description":"adjusted description","Entity":"test-domain","Message":"message","Note":"this is a note","Priority":"P1","ResponderID1":"Schedule1","ResponderID2":"Escalation2","ResponderID3":"Team3","ResponderType1":"schedule","ResponderType2":"escalation","ResponderType3":"team","Source":"http://prometheus","Tags":"tag1,tag2"}} +`, + }, + { + title: "config with multiple teams", + cfg: &config.CompassConfig{ + NotifierConfig: config.NotifierConfig{ + VSendResolved: true, + }, + Message: `{{ .CommonLabels.Message }}`, + Description: `{{ .CommonLabels.Description }}`, + Source: `{{ .CommonLabels.Source }}`, + Responders: []config.CompassConfigConfigResponder{ + { + ID: `{{ .CommonLabels.ResponderID3 }}`, + Type: `{{ .CommonLabels.ResponderType3 }}`, + }, + }, + Tags: `{{ .CommonLabels.Tags }}`, + Note: `{{ .CommonLabels.Note }}`, + Priority: `{{ .CommonLabels.Priority }}`, + ExtraProperties: map[string]string{ + "Description": `adjusted {{ .CommonLabels.Description }}`, + }, + APIUser: `{{ .ExternalURL }}`, + APIKey: `{{ .ExternalURL }}`, + APIURL: &config.URL{URL: u}, + HTTPConfig: &commoncfg.HTTPClientConfig{}, + }, + expectedEmptyAlertBody: `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"","source":"","extraProperties":{"Description":"adjusted "}} +`, + expectedBody: `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"message","description":"description","source":"http://prometheus","responders":[{"id":"Team3","type":"team"}],"tags":["tag1","tag2"],"note":"this is a note","priority":"P1","extraProperties":{"Actions":"doThis,doThat","Description":"adjusted description","Entity":"test-domain","Message":"message","Note":"this is a note","Priority":"P1","ResponderID1":"Schedule1","ResponderID2":"Escalation2","ResponderID3":"Team3","ResponderType1":"schedule","ResponderType2":"escalation","ResponderType3":"team","Source":"http://prometheus","Tags":"tag1,tag2"}} +`, + }, + } { + t.Run(tc.title, func(t *testing.T) { + notifier, err := New(tc.cfg, tmpl, logger) + require.NoError(t, err) + + ctx := context.Background() + ctx = notify.WithGroupKey(ctx, "1") + + expectedURL, _ := url.Parse("https://compass/api/v1/alerts") + + // Empty alert. + alert1 := &types.Alert{ + Alert: model.Alert{ + StartsAt: time.Now(), + EndsAt: time.Now().Add(time.Hour), + }, + } + + req, retry, err := notifier.createRequests(ctx, alert1) + require.NoError(t, err) + require.Len(t, req, 1) + require.True(t, retry) + require.Equal(t, expectedURL, req[0].URL) + require.Equal(t, "Basic aHR0cDovL2FtOmh0dHA6Ly9hbQ==", req[0].Header.Get("Authorization")) + require.Equal(t, tc.expectedEmptyAlertBody, readBody(t, req[0])) + + // Fully defined alert. + alert2 := &types.Alert{ + Alert: model.Alert{ + Labels: model.LabelSet{ + "Message": "message", + "Description": "description", + "Source": "http://prometheus", + "ResponderID1": "Schedule1", + "ResponderType1": "schedule", + "ResponderID2": "Escalation2", + "ResponderType2": "escalation", + "ResponderID3": "Team3", + "ResponderType3": "team", + "Tags": "tag1,tag2", + "Note": "this is a note", + "Priority": "P1", + "Entity": "test-domain", + "Actions": "doThis,doThat", + }, + StartsAt: time.Now(), + EndsAt: time.Now().Add(time.Hour), + }, + } + req, retry, err = notifier.createRequests(ctx, alert2) + require.NoError(t, err) + require.True(t, retry) + require.Len(t, req, 1) + require.Equal(t, tc.expectedBody, readBody(t, req[0])) + + // Broken API Key Template. + tc.cfg.APIKey = "{{ kaput " + _, _, err = notifier.createRequests(ctx, alert2) + require.Error(t, err) + require.Equal(t, "templating error: template: :1: function \"kaput\" not defined", err.Error()) + }) + } +} + +func TestCompassWithUpdate(t *testing.T) { + u, err := url.Parse("https://test-compass-url") + require.NoError(t, err) + tmpl := test.CreateTmpl(t) + ctx := context.Background() + ctx = notify.WithGroupKey(ctx, "1") + compassConfigWithUpdate := config.CompassConfig{ + Message: `{{ .CommonLabels.Message }}`, + Description: `{{ .CommonLabels.Description }}`, + UpdateAlerts: true, + APIUser: "test-api-user", + APIKey: "test-api-key", + APIURL: &config.URL{URL: u}, + HTTPConfig: &commoncfg.HTTPClientConfig{}, + } + notifierWithUpdate, err := New(&compassConfigWithUpdate, tmpl, promslog.NewNopLogger()) + alert := &types.Alert{ + Alert: model.Alert{ + StartsAt: time.Now(), + EndsAt: time.Now().Add(time.Hour), + Labels: model.LabelSet{ + "Message": "new message", + "Description": "new description", + }, + }, + } + require.NoError(t, err) + requests, retry, err := notifierWithUpdate.createRequests(ctx, alert) + require.NoError(t, err) + require.True(t, retry) + require.Len(t, requests, 3) + + body0 := readBody(t, requests[0]) + body1 := readBody(t, requests[1]) + body2 := readBody(t, requests[2]) + key, _ := notify.ExtractGroupKey(ctx) + alias := key.Hash() + + require.Equal(t, "https://test-compass-url/v1/alerts", requests[0].URL.String()) + require.NotEmpty(t, body0) + + require.Equal(t, requests[1].URL.String(), fmt.Sprintf("https://test-compass-url/v1/alerts/%s/message?identifierType=alias", alias)) + require.Equal(t, `{"message":"new message"} +`, body1) + require.Equal(t, requests[2].URL.String(), fmt.Sprintf("https://test-compass-url/v1/alerts/%s/description?identifierType=alias", alias)) + require.Equal(t, `{"description":"new description"} +`, body2) +} + +func TestCompassApiKeyFile(t *testing.T) { + u, err := url.Parse("https://test-compass-url") + require.NoError(t, err) + tmpl := test.CreateTmpl(t) + ctx := context.Background() + ctx = notify.WithGroupKey(ctx, "1") + compassConfigWithUpdate := config.CompassConfig{ + APIUser: "test-api-user", + APIKeyFile: `./api_key_file`, + APIURL: &config.URL{URL: u}, + HTTPConfig: &commoncfg.HTTPClientConfig{}, + } + notifierWithUpdate, err := New(&compassConfigWithUpdate, tmpl, promslog.NewNopLogger()) + + require.NoError(t, err) + requests, _, err := notifierWithUpdate.createRequests(ctx) + require.NoError(t, err) + require.Equal(t, "Basic dGVzdC1hcGktdXNlcjpteV9zZWNyZXRfYXBpX2tleQ==", requests[0].Header.Get("Authorization")) +} + +func readBody(t *testing.T, r *http.Request) string { + t.Helper() + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + return string(body) +} diff --git a/template/default.tmpl b/template/default.tmpl index 57e877c0c2..235053dd68 100644 --- a/template/default.tmpl +++ b/template/default.tmpl @@ -54,6 +54,20 @@ Alerts Resolved: {{ define "opsgenie.default.source" }}{{ template "__alertmanagerURL" . }}{{ end }} +{{ define "compass.default.message" }}{{ template "__subject" . }}{{ end }} +{{ define "compass.default.description" }}{{ .CommonAnnotations.SortedPairs.Values | join " " }} +{{ if gt (len .Alerts.Firing) 0 -}} +Alerts Firing: +{{ template "__text_alert_list" .Alerts.Firing }} +{{- end }} +{{ if gt (len .Alerts.Resolved) 0 -}} +Alerts Resolved: +{{ template "__text_alert_list" .Alerts.Resolved }} +{{- end }} +{{- end }} +{{ define "compass.default.source" }}{{ template "__alertmanagerURL" . }}{{ end }} + + {{ define "wechat.default.message" }}{{ template "__subject" . }} {{ .CommonAnnotations.SortedPairs.Values | join " " }} {{ if gt (len .Alerts.Firing) 0 -}}