From 9bc26dd0168fa6a650d7257b5bc6bdb73fcd1482 Mon Sep 17 00:00:00 2001 From: Philipp Born Date: Thu, 16 Jan 2025 07:37:25 +0100 Subject: [PATCH] feat: add googlechat notifier Signed-off-by: Philipp Born --- asset/assets_vfsdata.go | 4 +- config/config.go | 9 ++ config/notifiers.go | 27 +++++ config/receiver/receiver.go | 4 + docs/configuration.md | 24 ++++ notify/googlechat/googlechat.go | 131 +++++++++++++++++++++ notify/googlechat/googlechat_test.go | 163 +++++++++++++++++++++++++++ notify/notify.go | 1 + template/default.tmpl | 12 ++ 9 files changed, 373 insertions(+), 2 deletions(-) create mode 100644 notify/googlechat/googlechat.go create mode 100644 notify/googlechat/googlechat_test.go diff --git a/asset/assets_vfsdata.go b/asset/assets_vfsdata.go index 3dd06e052b..175db03ebf 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: 8486, - 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\x4d\x6f\xe3\x36\x10\xbd\xfb\x57\x0c\xb4\x3d\xc4\x87\x68\x8b\x1e\x03\x04\xc5\xa2\xe8\xc7\x21\x2d\x8a\x2c\xd2\x4b\x51\x18\x8c\x34\x56\x98\x50\xa4\x42\x8e\xec\x18\x8e\xff\x7b\x41\xc9\x96\x29\x51\xb6\x29\xc5\x7b\x28\xea\x9b\x4d\xcd\xbc\x19\xbd\x37\x1a\x8e\xc4\xf5\x1a\x52\x9c\x73\x89\x10\xcd\x66\x4c\xa0\xa6\x9c\x49\x96\xa1\x8e\x60\xb3\xf9\xe2\xfc\x5f\xaf\x01\x65\x0a\x9b\xcd\xe4\xa0\xcb\xc3\xfd\x9d\xf5\x5a\xaf\x21\xfe\xf9\x8d\x50\x4b\x26\x1e\xee\xef\x60\xb3\xf9\xfc\xe9\x73\x65\x67\x7e\xd4\x98\x20\x5f\xa0\xbe\xb5\x46\xf7\xdb\x3f\xf0\x0e\xa5\x16\xaf\x25\xea\x55\xed\xbe\x0d\xd4\x8e\x64\xca\xc7\x67\x4c\xc8\x46\xf8\xdb\x7a\x7f\x25\x46\xa5\x81\x77\x20\xf5\x50\x14\xa8\x6b\x57\x3e\x07\x7c\x6d\x2e\x46\x73\xae\xb9\xcc\xac\xcf\x8d\xf5\xa9\x6e\xc8\xc4\xbf\x54\xab\xf0\x0e\x02\xa5\x1b\xf1\x1f\xb0\x46\xbf\x6a\x55\x16\x77\xec\x11\x85\x89\xbf\x2a\x4d\x98\xfe\xc9\xb8\x36\xf1\x5f\x4c\x94\x68\x03\x3e\x2b\x2e\x21\x02\x8b\x0a\x75\xc8\x8c\xe0\xca\x62\xc5\x3f\xa9\x3c\x57\xb2\x76\x9e\x6e\xd7\x1c\xbc\x29\x6c\x36\x57\xeb\x35\x2c\x39\x3d\xb5\x8d\xe3\x7b\xcc\xd5\x02\xdb\xd1\xff\x60\x39\x9a\x2d\xa3\x7d\xd1\x9b\xc4\xa7\xcd\xaf\x03\x32\xa5\x68\x12\xcd\x0b\xe2\x4a\x46\x47\x38\x26\x7c\xa3\x5a\xd2\x99\xe0\x86\xb6\xa6\x9a\xc9\x0c\x21\x86\xcd\xa6\xce\xeb\x66\xb2\x5f\xf4\x79\xb2\xac\x5c\x57\x44\xda\xf4\xed\xbf\x5b\x68\x6e\x60\x9b\x58\x1d\xfc\x8b\x94\x8a\x98\xcd\xa9\x05\xe9\x2c\x8f\xc3\xfd\xaa\x4a\x9d\xe0\x4d\x2d\x26\x4a\xd4\x8c\x94\xae\x2b\x71\xd2\x43\xd4\x51\x0a\x66\x39\xd3\x2f\xa9\x5a\x4a\x8f\x8b\x49\x28\x19\x81\x59\x4f\x86\xd3\x11\x8a\x1c\x44\xc8\xa4\x9f\x11\x23\x58\xf2\x12\xa7\x38\x67\xa5\xa0\x98\x38\x09\xdc\x52\x41\x98\x17\x82\x51\xfb\xe1\x8c\x0f\xd5\x60\x1b\xa7\x34\xb6\x3d\xe4\x7d\x50\xed\x26\x14\x88\x37\x67\x42\x3c\xb2\xe4\xc5\xc3\xeb\x4d\xdf\x82\xc2\x3b\x9c\x32\x14\x5c\xbe\x04\x67\x90\x6c\x33\xe0\x69\x14\xe6\x50\x68\xb4\xb5\x16\x68\xed\x24\x74\x94\xb1\xaa\x07\x07\xa6\xcc\x13\x25\x31\x57\xcf\x3c\x0a\xb7\x2f\xb5\x08\xcd\x38\xfc\xe6\xe6\x4a\x51\xbd\xe3\x38\x35\xe8\x9a\x17\xf6\xd6\xd2\x92\x56\x8d\x8b\xdf\xd0\x86\x95\xa3\x8f\x98\x08\x8e\x92\xc6\x17\xe4\x21\xc4\xfd\xae\x38\x4e\x33\x1f\x97\x4b\x43\x4c\x26\x68\x7a\x70\xbd\x0e\x1e\x1f\x66\x55\x15\x26\x43\xc9\xb1\x01\xce\xd1\x18\x96\x8d\x7b\xbe\x3d\x30\x5f\xa1\xed\x86\x77\xa0\xa1\xf5\xee\x70\x93\xce\xfe\xda\xda\xc0\xa7\xf0\x3d\x5c\xdb\xc6\x59\x2d\x42\xbd\x58\xb5\xce\xe3\x8c\xb4\xa7\x80\x2a\xc8\xb5\x73\x47\x3d\xf1\xee\xd1\x28\xb1\xc0\xb4\x13\x71\xb7\x1c\x1e\x73\xe7\xe1\x45\xbd\x0e\xa1\xd4\x54\x7d\x7c\x78\x35\xb5\x54\x5f\x62\xf2\xc4\x68\xa8\xe6\x93\x8b\x7e\x47\xf4\x73\x07\xe5\x07\x2d\x3c\xbc\x5e\x7d\x0e\xa8\xde\xd1\x87\xd4\xcc\x6e\x96\x07\x3b\xa9\x6f\x5e\x30\x4d\xab\x01\xf6\xc4\xb2\x50\x6b\x96\xa1\xa4\x59\x77\x8b\x6b\xd7\xd7\x82\x27\xa4\xb4\x2a\xcc\xbe\x6c\x89\x11\xce\xda\x85\x76\xa9\xa5\x61\xbd\xc0\x67\x15\x25\x71\x5a\xcd\x52\x6e\x0a\xc1\x56\xb3\x03\xd3\xd4\xe9\xc6\xed\x23\xe7\x4a\x72\x52\x96\x90\x19\x29\x25\x06\x6e\x89\xad\xbd\xab\x34\x4f\x6a\x81\xfa\x0c\xf3\xa3\x07\xf5\xed\xeb\xe9\x3c\xe5\x14\x5e\x4d\xe7\x2b\x26\x7f\xa4\x3f\xc6\xe4\x7e\xa6\x1b\xb2\xa7\xb8\xd3\x9c\x74\x1e\xf6\xfd\x6b\xfa\xf0\x77\x04\x07\xe7\x22\xef\x10\x79\x5d\x16\x09\x05\x66\x9a\xe5\x7d\x54\xfe\x6f\x49\x49\xb9\x49\x94\x4e\xf7\xb3\xb9\x92\xb4\x1f\xf7\xfd\x52\xec\xda\x8f\x6f\x5c\x5d\xa4\x8b\x1a\x76\xac\x78\xc4\xb7\xcb\xa3\xfe\x61\x1e\x73\x43\xc8\x72\xb7\xf9\xe6\x39\xd3\xab\x51\x75\xda\xc5\x1a\x5f\xf1\x1e\xd2\xf6\x4b\x40\x88\x4c\x9f\x60\x90\x50\xce\xe7\xb9\x0f\x2b\xd6\x84\x0e\xd5\xac\x27\xf8\x08\xf1\x16\x3f\x9c\x8f\x72\x17\xeb\x42\x7a\x1f\xe9\x99\x52\x99\xf8\x6f\xbf\x01\x9f\xa0\xff\xdb\xbe\xbe\x9c\xa4\xbf\xef\x3d\xc6\x15\xe0\x99\x6b\x76\x96\x7e\xd5\x02\xea\x7c\x6c\xba\x14\xfd\xa4\xe2\xbf\x97\xab\x42\x73\xa5\x39\xad\xa2\x4a\x7f\x6b\xf6\xdd\x6e\x09\x6e\x6e\x21\x8a\x76\xea\xed\x0e\x20\x5a\x77\x6b\x7d\x00\x00\x2a\x3f\x83\x0b\xdc\xf9\x71\x99\xe2\xdb\xee\x0c\x04\xa2\xdd\xa5\xa8\xe5\xc1\xe7\x70\x85\xaf\x8e\x63\x94\x68\x4e\x3c\x61\x22\x9a\x36\x86\x0d\x7c\x93\xd6\x2d\x44\xbf\xf1\xec\xa9\x8d\x85\xc2\x60\x05\xc8\x64\xda\x45\x5d\x32\x2d\xb9\xcc\xa2\x29\x5c\x49\x74\x80\x6a\x98\xe9\x89\x58\xbf\x63\xca\xcb\x3c\x3c\x1a\x97\x73\x65\x43\xd9\xd5\x7d\xa8\x93\x61\xee\xd4\xb2\x13\x43\xa6\x8d\x26\xee\xef\xfa\x50\xd3\x85\x6e\xb9\xb5\x75\x6a\x0a\xc3\x8b\x3d\x48\xad\xc1\x8a\x05\xa8\x76\x76\xe5\x82\xd4\x3b\x9f\x82\xa7\x55\xec\x2a\x79\x4a\xd9\x3d\x52\xf7\xaa\xdb\xea\xb4\x4a\x5e\x90\xda\xdf\xed\x46\x8f\x0a\x3d\x60\x4c\x70\x66\xc6\x9f\x7c\x1c\x4a\xef\xc3\xc7\x55\x3d\xc0\xc7\xcf\xab\x7a\x1c\x4e\x1d\x5a\xf5\x25\xef\x9d\x5c\xfd\x1b\x00\x00\xff\xff\xbb\xf2\x91\xb4\x26\x21\x00\x00"), }, "/templates/email.tmpl": &vfsgen۰CompressedFileInfo{ name: "email.tmpl", diff --git a/config/config.go b/config/config.go index b8e484130a..d3f749c4e2 100644 --- a/config/config.go +++ b/config/config.go @@ -292,6 +292,9 @@ func resolveFilepaths(baseDir string, cfg *Config) { for _, cfg := range receiver.MSTeamsV2Configs { cfg.HTTPConfig.SetDirectory(baseDir) } + for _, cfg := range receiver.GoogleChatConfigs { + cfg.HTTPConfig.SetDirectory(baseDir) + } for _, cfg := range receiver.JiraConfigs { cfg.HTTPConfig.SetDirectory(baseDir) } @@ -599,6 +602,11 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { return errors.New("no msteamsv2 webhook URL or URLFile provided") } } + for _, googlechat := range rcv.GoogleChatConfigs { + if googlechat.HTTPConfig == nil { + googlechat.HTTPConfig = c.Global.HTTPConfig + } + } for _, jira := range rcv.JiraConfigs { if jira.HTTPConfig == nil { jira.HTTPConfig = c.Global.HTTPConfig @@ -1024,6 +1032,7 @@ type Receiver struct { 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"` + GoogleChatConfigs []*GoogleChatConfig `yaml:"googlechat_configs,omitempty" json:"googlechat_configs,omitempty"` JiraConfigs []*JiraConfig `yaml:"jira_configs,omitempty" json:"jira_configs,omitempty"` RocketchatConfigs []*RocketchatConfig `yaml:"rocketchat_configs,omitempty" json:"rocketchat_configs,omitempty"` } diff --git a/config/notifiers.go b/config/notifiers.go index 87f806aa27..f53334939a 100644 --- a/config/notifiers.go +++ b/config/notifiers.go @@ -195,6 +195,14 @@ var ( Text: `{{ template "msteamsv2.default.text" . }}`, } + DefaultGoogleChatConfig = GoogleChatConfig{ + NotifierConfig: NotifierConfig{ + VSendResolved: true, + }, + Message: `{{ template "googlechat.default.message" . }}`, + Threading: true, + } + DefaultJiraConfig = JiraConfig{ NotifierConfig: NotifierConfig{ VSendResolved: true, @@ -892,6 +900,25 @@ func (c *MSTeamsV2Config) UnmarshalYAML(unmarshal func(interface{}) error) error return nil } +// GoogleChatConfig configures notifications via Discord. +type GoogleChatConfig struct { + NotifierConfig `yaml:",inline" json:",inline"` + + HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` + URL *SecretURL `yaml:"url,omitempty" json:"url,omitempty"` + URLFile string `yaml:"url_file" json:"url_file"` + + Message string `yaml:"message,omitempty" json:"message,omitempty"` + Threading bool `yaml:"threading,omitempty" json:"threading,omitempty"` +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (c *GoogleChatConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + *c = DefaultGoogleChatConfig + type plain GoogleChatConfig + return unmarshal((*plain)(c)) +} + type JiraConfig struct { NotifierConfig `yaml:",inline" json:",inline"` HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` diff --git a/config/receiver/receiver.go b/config/receiver/receiver.go index d92a19a4c5..d1c0ac5fb3 100644 --- a/config/receiver/receiver.go +++ b/config/receiver/receiver.go @@ -26,6 +26,7 @@ import ( "github.com/prometheus/alertmanager/notify/jira" "github.com/prometheus/alertmanager/notify/msteams" "github.com/prometheus/alertmanager/notify/msteamsv2" + "github.com/prometheus/alertmanager/notify/googlechat" "github.com/prometheus/alertmanager/notify/opsgenie" "github.com/prometheus/alertmanager/notify/pagerduty" "github.com/prometheus/alertmanager/notify/pushover" @@ -103,6 +104,9 @@ func BuildReceiverIntegrations(nc config.Receiver, tmpl *template.Template, logg for i, c := range nc.MSTeamsV2Configs { add("msteamsv2", i, c, func(l *slog.Logger) (notify.Notifier, error) { return msteamsv2.New(c, tmpl, l, httpOpts...) }) } + for i, c := range nc.GoogleChatConfigs { + add("googlechat", i, c, func(l log.Logger) (notify.Notifier, error) { return googlechat.New(c, tmpl, l) }) + } for i, c := range nc.JiraConfigs { add("jira", i, c, func(l *slog.Logger) (notify.Notifier, error) { return jira.New(c, tmpl, l, httpOpts...) }) } diff --git a/docs/configuration.md b/docs/configuration.md index f2a4f6b197..3a81f80002 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -731,6 +731,8 @@ webhook_configs: [ - , ... ] wechat_configs: [ - , ... ] +googlechat_configs: + [ - , ... ] ``` ### `` @@ -1684,3 +1686,25 @@ room_id: # The HTTP client's configuration. You must use this configuration to supply the bot token as part of the HTTP `Authorization` header. [ http_config: | default = global.http_config ] ``` + +### `` + + +```yaml +# Whether to notify about resolved alerts. +[ send_resolved: | default = true ] + +# Whether or not to enable threading. +# Enabling this, will cause resolved notifications to be posted in the same thread as the original message. +[ threading: | default = true ] + +# The Webhook URL for the Google Chat space. +# i.e. https://chat.googleapis.com/v1/spaces/XXXXXX/messages?key=YYYYYYY&token=ZZZZZZZ +[ url: ] + +# Alternative to `url`. Allows the URL to be read from a file instead. +[ url_file: ] + +# Message template. +[ message: | default = '{{ template "googlechat.default.message" . }}' ] +``` diff --git a/notify/googlechat/googlechat.go b/notify/googlechat/googlechat.go new file mode 100644 index 0000000000..d0feca954b --- /dev/null +++ b/notify/googlechat/googlechat.go @@ -0,0 +1,131 @@ +// 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 googlechat + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "os" + "strings" + + "github.com/go-kit/log" + commoncfg "github.com/prometheus/common/config" + + "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/template" + "github.com/prometheus/alertmanager/types" +) + +type Notifier struct { + conf *config.GoogleChatConfig + tmpl *template.Template + logger log.Logger + client *http.Client + retrier *notify.Retrier +} + +func New(conf *config.GoogleChatConfig, t *template.Template, l log.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) { + client, err := commoncfg.NewClientFromConfig(*conf.HTTPConfig, "googlechat", httpOpts...) + if err != nil { + return nil, err + } + return &Notifier{ + conf: conf, + tmpl: t, + logger: l, + client: client, + retrier: ¬ify.Retrier{}, + }, nil +} + +// Message represents the structure for sending a +// Text message in Google Chat Webhook endpoint. +// https://developers.google.com/chat/api/guides/message-formats/basic +type Message struct { + Text string `json:"text"` +} + +// Notify implements the Notifier interface. +func (n *Notifier) Notify(ctx context.Context, alert ...*types.Alert) (bool, error) { + key, err := notify.ExtractGroupKey(ctx) + if err != nil { + return false, err + } + + var ( + data = notify.GetTemplateData(ctx, n.tmpl, alert, n.logger) + tmpl = notify.TmplText(n.tmpl, data, &err) + ) + + message := tmpl(n.conf.Message) + if err != nil { + return false, err + } + + msg := &Message{ + Text: message, + } + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(msg); err != nil { + return false, err + } + + var webhookURL string + if n.conf.URL != nil { + webhookURL = n.conf.URL.String() + } else { + content, err := os.ReadFile(n.conf.URLFile) + if err != nil { + return false, fmt.Errorf("read url_file: %w", err) + } + webhookURL = strings.TrimSpace(string(content)) + } + + // https://developers.google.com/chat/how-tos/webhooks#start_a_message_thread + // To post the first message of a thread with a webhook, + // append the threadKey and messageReplyOption parameters to the webhook URL. + // Set the threadKey to an arbitrary string, but remember what it is; + // you'll need to specify it again to post a reply to the thread. + // https://chat.googleapis.com/v1/spaces/SPACE_ID/messages?threadKey=ARBITRARY_STRING&messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD + + u, err := url.Parse(webhookURL) + if err != nil { + return false, fmt.Errorf("unable to parse googlechat url: %w", err) + } + q := u.Query() + if n.conf.Threading { + q.Set("threadKey", key.Hash()) + q.Set("messageReplyOption", "REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD") + } + u.RawQuery = q.Encode() + webhookURL = u.String() + + resp, err := notify.PostJSON(ctx, n.client, webhookURL, &buf) + if err != nil { + return true, notify.RedactURL(err) + } + defer notify.Drain(resp) + + 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/googlechat/googlechat_test.go b/notify/googlechat/googlechat_test.go new file mode 100644 index 0000000000..1e40cb9b1a --- /dev/null +++ b/notify/googlechat/googlechat_test.go @@ -0,0 +1,163 @@ +// 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 googlechat + +import ( + "context" + "encoding/json" + "fmt" + "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 testGoogleChatURL, _ = url.Parse("https://chat.googleapis.com/v1/spaces/XXXXXX/messages?key=YYYYYYY&token=ZZZZZZZ") + +func TestGoogleChatRetry(t *testing.T) { + notifier, err := New( + &config.GoogleChatConfig{ + URL: &config.SecretURL{URL: testGoogleChatURL}, + HTTPConfig: &commoncfg.HTTPClientConfig{}, + }, + test.CreateTmpl(t), + log.NewNopLogger(), + ) + require.NoError(t, err) + + t.Run("test retry status code", func(t *testing.T) { + for statusCode, expected := range test.RetryTests(test.DefaultRetryCodes()) { + actual, _ := notifier.retrier.Check(statusCode, nil) + require.Equal(t, expected, actual, fmt.Sprintf("error on status %d", statusCode)) + } + }) +} + +func TestGoogleChatRedactedURL(t *testing.T) { + ctx, u, fn := test.GetContextWithCancelingURL() + defer fn() + + secret := "secret" + notifier, err := New( + &config.GoogleChatConfig{ + URL: &config.SecretURL{URL: u}, + HTTPConfig: &commoncfg.HTTPClientConfig{}, + }, + test.CreateTmpl(t), + log.NewNopLogger(), + ) + require.NoError(t, err) + + test.AssertNotifyLeaksNoSecret(ctx, t, notifier, secret) +} + +func TestGoogleChatReadingURLFromFile(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.GoogleChatConfig{ + URLFile: f.Name(), + HTTPConfig: &commoncfg.HTTPClientConfig{}, + }, + test.CreateTmpl(t), + log.NewNopLogger(), + ) + require.NoError(t, err) + + test.AssertNotifyLeaksNoSecret(ctx, t, notifier, u.String()) +} + +func TestGooglechatTemplating(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.GoogleChatConfig + + retry bool + errMsg string + }{ + { + title: "default message", + cfg: &config.GoogleChatConfig{ + Message: `{{ template "googlechat.default.message" . }}`, + }, + retry: false, + }, + { + title: "message with templating errors", + cfg: &config.GoogleChatConfig{ + Message: "{{ ", + }, + errMsg: "template: :1: unclosed action", + }, + } { + t.Run(tc.title, func(t *testing.T) { + tc.cfg.URL = &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) + }) + } +} diff --git a/notify/notify.go b/notify/notify.go index 8fa85c0a21..b0aaae9140 100644 --- a/notify/notify.go +++ b/notify/notify.go @@ -362,6 +362,7 @@ func (m *Metrics) InitializeFor(receiver map[string][]Integration) { "sns", "telegram", "discord", + "googlechat", "webex", "msteams", "msteamsv2", diff --git a/template/default.tmpl b/template/default.tmpl index 57e877c0c2..45309c3ed2 100644 --- a/template/default.tmpl +++ b/template/default.tmpl @@ -172,6 +172,18 @@ Alerts Resolved: {{ end }} {{ end }} +{{ define "googlechat.default.message" }}{{ template "__subject" . }} +{{ .CommonAnnotations.SortedPairs.Values | join " " }} +{{ 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 }}