Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c67919d0a2 | |||
| 62bc6ef79e | |||
| 3b47e55e2a | |||
| a2555fcff1 | |||
| c6dd80140e | |||
| f2872d6dea |
+7
-2
@@ -1,7 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
DOCKER_IMAGE_TAG=speckle/alertmanager-discord
|
||||
if [[ -z "${VERSION}" ]]; then
|
||||
echo "VERSION environment variable should be set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DOCKER_IMAGE_TAG="${DOCKER_IMAGE_TAG:-"speckle/alertmanager-discord"}"
|
||||
export DOCKER_BUILDKIT=1
|
||||
|
||||
docker build --tag "${DOCKER_IMAGE_TAG}:${CIRCLE_SHA1}.${CIRCLE_BUILD_NUM}" --file ./Dockerfile .
|
||||
docker build --tag "${DOCKER_IMAGE_TAG}:${VERSION}" --build-arg="APPLICATION_VERSION=${VERSION}" --file ./Dockerfile .
|
||||
|
||||
+85
-9
@@ -1,33 +1,78 @@
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
helm: travelaudience/helm@0.2.13
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build-image:
|
||||
jobs:
|
||||
- get-version:
|
||||
filters:
|
||||
tags: &filter-allow-all
|
||||
only: /.*/
|
||||
|
||||
- pre-commit:
|
||||
filters:
|
||||
tags: &filter-all-tags # run for all tags
|
||||
only: /.*/
|
||||
tags: *filter-allow-all
|
||||
|
||||
- docker-build:
|
||||
filters:
|
||||
tags: *filter-all-tags
|
||||
tags: &filter-ignore-all
|
||||
ignore: /.*/
|
||||
branches:
|
||||
ignore:
|
||||
- main
|
||||
requires:
|
||||
- get-version
|
||||
|
||||
- docker-build-and-publish:
|
||||
context:
|
||||
- docker-hub
|
||||
filters:
|
||||
tags: *filter-all-tags
|
||||
tags: *filter-allow-all
|
||||
branches:
|
||||
only:
|
||||
- main
|
||||
requires:
|
||||
- get-version
|
||||
- pre-commit
|
||||
|
||||
- helm-package-and-publish:
|
||||
filters:
|
||||
tags: *filter-allow-all
|
||||
branches:
|
||||
only:
|
||||
- main
|
||||
requires:
|
||||
- docker-build
|
||||
- docker-build-and-publish
|
||||
|
||||
jobs:
|
||||
get-version:
|
||||
docker: &docker-image
|
||||
- image: cimg/base:2022.04
|
||||
working_directory: &workingdir /tmp/ci
|
||||
steps:
|
||||
- checkout
|
||||
- run: mkdir -p workspace
|
||||
- run:
|
||||
name: set version
|
||||
command: |
|
||||
echo "export VERSION=$(.circleci/get_version.sh)" >> workspace/env-vars
|
||||
- run:
|
||||
name: store version
|
||||
command: |
|
||||
cat workspace/env-vars >> $BASH_ENV
|
||||
- run:
|
||||
name: echo version
|
||||
command: |
|
||||
echo "VERSION=${VERSION}"
|
||||
- persist_to_workspace:
|
||||
root: workspace
|
||||
paths:
|
||||
- env-vars
|
||||
|
||||
pre-commit:
|
||||
parameters:
|
||||
config_file:
|
||||
@@ -41,8 +86,8 @@ jobs:
|
||||
type: string
|
||||
docker:
|
||||
- image: speckle/pre-commit-runner:latest
|
||||
resource_class: large
|
||||
working_directory: &workingdir /tmp/ci
|
||||
resource_class: &docker-resource-class medium
|
||||
working_directory: *workingdir
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
@@ -64,12 +109,17 @@ jobs:
|
||||
when: on_fail
|
||||
|
||||
docker-build-and-publish:
|
||||
docker: &docker-image
|
||||
- image: cimg/base:2022.04
|
||||
resource_class: &docker-resource-class large
|
||||
docker: *docker-image
|
||||
resource_class: *docker-resource-class
|
||||
working_directory: *workingdir
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: /tmp/ci/workspace
|
||||
- run:
|
||||
name: populate environment variables
|
||||
command: |
|
||||
cat workspace/env-vars >> $BASH_ENV
|
||||
- setup_remote_docker: &remote-docker
|
||||
# a weird issue with yarn installing packages throwing EPERM errors
|
||||
# this fixes it
|
||||
@@ -85,7 +135,33 @@ jobs:
|
||||
working_directory: *workingdir
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: /tmp/ci/workspace
|
||||
- run:
|
||||
name: populate environment variables
|
||||
command: |
|
||||
cat workspace/env-vars >> $BASH_ENV
|
||||
- setup_remote_docker: *remote-docker
|
||||
- run:
|
||||
name: Build
|
||||
command: ./.circleci/build.sh
|
||||
|
||||
helm-package-and-publish:
|
||||
docker:
|
||||
- image: quay.io/helmpack/chart-testing:v3.7.1-amd64
|
||||
resource_class: *docker-resource-class
|
||||
working_directory: *workingdir
|
||||
steps:
|
||||
- checkout
|
||||
- add_ssh_keys:
|
||||
fingerprints:
|
||||
- "ef:0c:a5:6a:ef:c5:81:43:8c:36:4f:37:40:5b:17:2d"
|
||||
- attach_workspace:
|
||||
at: /tmp/workspace
|
||||
- run:
|
||||
name: populate environment variables
|
||||
command: |
|
||||
cat /tmp/workspace/env-vars >> $BASH_ENV
|
||||
- run:
|
||||
name: Build and Publish
|
||||
command: ./.circleci/package_and_publish_helm.sh
|
||||
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
set -eo pipefail
|
||||
|
||||
if [[ "${CIRCLE_TAG}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "${CIRCLE_TAG}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2068,SC2046
|
||||
LAST_RELEASE="$(git describe --always --tags $(git rev-list --tags) | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1)"
|
||||
NEXT_RELEASE="$(echo "${LAST_RELEASE}" | awk -F. -v OFS=. '{$NF += 1 ; print}')"
|
||||
if [[ "${CIRCLE_BRANCH}" == "main" ]]; then
|
||||
echo "${NEXT_RELEASE}-alpha.${CIRCLE_BUILD_NUM}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# docker has a 128 character tag limit, so ensuring the branch name will be short enough
|
||||
# helm uses semver 2, only valid characters are a-zA-Z0-9 and hyphen '-'
|
||||
# shellcheck disable=SC2034
|
||||
BRANCH_NAME_TRUNCATED="$(echo "${CIRCLE_BRANCH}" | cut -c -50 | sed 's/[^a-zA-Z0-9.-]/-/g')"
|
||||
|
||||
echo "${NEXT_RELEASE}-branch.${BRANCH_NAME_TRUNCATED}.${CIRCLE_BUILD_NUM}"
|
||||
exit 0
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
TEMP_PACKAGE_DIR="${TEMP_PACKAGE_DIR:-"/tmp/.cr-release-packages"}"
|
||||
HELM_PACKAGE_BRANCH="${HELM_PACKAGE_BRANCH:-"gh-pages"}"
|
||||
HELM_STABLE_BRANCH="${HELM_STABLE_BRANCH:-"main"}"
|
||||
HELM_CHART_DIR_PATH="${HELM_CHART_DIR_PATH:-"deploy/helm"}"
|
||||
|
||||
if [[ -z "${VERSION}" ]]; then
|
||||
echo "VERSION environment variable should be set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${GIT_EMAIL}" ]]; then
|
||||
echo "GIT_EMAIL environment variable should be set"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "${GIT_USERNAME}" ]]; then
|
||||
echo "GIT_USERNAME environment variable should be set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🧹 cleaning temporary directory"
|
||||
rm -rf "${TEMP_PACKAGE_DIR}" || true
|
||||
mkdir "${TEMP_PACKAGE_DIR}"
|
||||
|
||||
helm version -c
|
||||
|
||||
echo "🏗️ building dependencies"
|
||||
helm dependency build "${HELM_CHART_DIR_PATH}"
|
||||
echo "🎁 packaging ${HELM_CHART_DIR_PATH} with version: ${VERSION}"
|
||||
helm package "${HELM_CHART_DIR_PATH}" --dependency-update --version "${VERSION}" --app-version "${VERSION}" --destination "${TEMP_PACKAGE_DIR}"
|
||||
|
||||
echo "⏬ checking out git branch '${HELM_PACKAGE_BRANCH}'"
|
||||
git config user.email "${GIT_EMAIL}"
|
||||
git config user.name "${GIT_USERNAME}"
|
||||
git fetch
|
||||
git switch "${HELM_PACKAGE_BRANCH}"
|
||||
if [[ -n "${CIRCLE_TAG}" || "${CIRCLE_BRANCH}" == "${HELM_STABLE_BRANCH}" ]]; then
|
||||
echo "🛻 copying packages to stable directory"
|
||||
cp -a "${TEMP_PACKAGE_DIR}" stable/
|
||||
pushd stable
|
||||
helm repo index .
|
||||
popd
|
||||
else
|
||||
cp -a "${TEMP_PACKAGE_DIR}/." incubator/
|
||||
echo "🛻 copying packages to incubator directory"
|
||||
pushd incubator
|
||||
helm repo index .
|
||||
popd
|
||||
fi
|
||||
|
||||
echo "⏫ adding, commiting, and pushing to git repository"
|
||||
git add .
|
||||
git commit -m "updating helm chart to version ${VERSION}"
|
||||
git push --set-upstream origin "${HELM_PACKAGE_BRANCH}"
|
||||
+17
-2
@@ -1,9 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
DOCKER_IMAGE_TAG=speckle/alertmanager-discord
|
||||
if [[ -z "${VERSION}" ]]; then
|
||||
echo "VERSION environment variable should be set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker tag "${DOCKER_IMAGE_TAG}:${CIRCLE_SHA1}.${CIRCLE_BUILD_NUM}" "${DOCKER_IMAGE_TAG}:latest"
|
||||
if [[ -z "${DOCKER_REG_PASS}" ]]; then
|
||||
echo "DOCKER_REG_PASS environment variable should be set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${DOCKER_REG_USER}" ]]; then
|
||||
echo "DOCKER_REG_USER environment variable should be set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DOCKER_IMAGE_TAG="${DOCKER_IMAGE_TAG:-"speckle/alertmanager-discord"}"
|
||||
|
||||
docker tag "${DOCKER_IMAGE_TAG}:${VERSION}" "${DOCKER_IMAGE_TAG}:latest"
|
||||
|
||||
echo "${DOCKER_REG_PASS}" | docker login -u "${DOCKER_REG_USER}" --password-stdin "${DOCKER_REG_URL}"
|
||||
docker push -a "${DOCKER_IMAGE_TAG}"
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 312 KiB |
@@ -2,3 +2,6 @@ alertmanager-discord
|
||||
alertmanager-discord.darwin
|
||||
alertmanager-discord.linux
|
||||
cover.out
|
||||
|
||||
# CircleCI orb Helm package & push
|
||||
.cr-release-packages
|
||||
|
||||
@@ -10,7 +10,7 @@ This program is not a replacement to alertmanager, it accepts webhooks from aler
|
||||
|
||||
The standard "dataflow" should be:
|
||||
|
||||
```
|
||||
```text
|
||||
Prometheus -------------> alertmanager -------------------> alertmanager-discord
|
||||
|
||||
alerting: receivers:
|
||||
@@ -18,11 +18,6 @@ alerting: receivers:
|
||||
- static_configs: webhook_configs: - DISCORD_WEBHOOK=https://discordapp.com/api/we...
|
||||
- targets: - url: 'http://localhost:9094'
|
||||
- 127.0.0.1:9093
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
```
|
||||
|
||||
## Features
|
||||
@@ -44,30 +39,30 @@ alerting: receivers:
|
||||
|
||||
## Example alertmanager config
|
||||
|
||||
```
|
||||
```yaml
|
||||
global:
|
||||
# The smarthost and SMTP sender used for mail notifications.
|
||||
smtp_smarthost: 'localhost:25'
|
||||
smtp_from: 'alertmanager@example.org'
|
||||
smtp_auth_username: 'alertmanager'
|
||||
smtp_auth_password: 'password'
|
||||
smtp_smarthost: "localhost:25"
|
||||
smtp_from: "alertmanager@example.org"
|
||||
smtp_auth_username: "alertmanager"
|
||||
smtp_auth_password: "password"
|
||||
|
||||
# The directory from which notification templates are read.
|
||||
templates:
|
||||
- '/etc/alertmanager/template/*.tmpl'
|
||||
- "/etc/alertmanager/template/*.tmpl"
|
||||
|
||||
# The root route on which each incoming alert enters.
|
||||
route:
|
||||
group_by: ['alertname']
|
||||
group_by: ["alertname"]
|
||||
group_wait: 20s
|
||||
group_interval: 5m
|
||||
repeat_interval: 3h
|
||||
receiver: discord_webhook
|
||||
|
||||
receivers:
|
||||
- name: 'discord_webhook'
|
||||
webhook_configs:
|
||||
- url: 'http://localhost:9094'
|
||||
- name: "discord_webhook"
|
||||
webhook_configs:
|
||||
- url: "http://localhost:9094"
|
||||
```
|
||||
|
||||
## Deployment
|
||||
@@ -88,9 +83,9 @@ discord_webhook_url: https://discord.com/api/webhooks/123456789123456789/abc
|
||||
go run . --configuration_file_path=/path/to/your/config.yaml
|
||||
```
|
||||
|
||||
### Docker
|
||||
### Docker or OCI-compatible container runtime
|
||||
|
||||
If you wish to deploy this to docker infra, you can find the docker hub repo here: https://hub.docker.com/r/speckle/alertmanager-discord/
|
||||
If you wish to deploy this to Docker, or similar OCI-compatible container runtime, you can pull the OCI image from the [Docker Hub repository](https://hub.docker.com/r/speckle/alertmanager-discord/).
|
||||
|
||||
### Kubernetes Helm Chart
|
||||
|
||||
@@ -169,4 +164,4 @@ go test ./... -v -cover -test.shuffle on
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
This repository is forked from https://github.com/benjojo/alertmanager-discord under the Apache 2.0 license
|
||||
This repository is forked from [benjojo/alertmanager-discord](https://github.com/benjojo/alertmanager-discord) under the Apache 2.0 license
|
||||
|
||||
+49
-26
@@ -5,7 +5,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
. "github.com/specklesystems/alertmanager-discord/pkg/flags"
|
||||
"github.com/specklesystems/alertmanager-discord/pkg/flags"
|
||||
"github.com/specklesystems/alertmanager-discord/pkg/server"
|
||||
"github.com/specklesystems/alertmanager-discord/pkg/version"
|
||||
|
||||
@@ -18,35 +18,30 @@ import (
|
||||
const (
|
||||
defaultConfigurationPath = "/etc/alertmanager-discord/config.yaml"
|
||||
defaultMaxBackoffTimeSeconds = 10
|
||||
defaultLogLevel = "info"
|
||||
)
|
||||
|
||||
var (
|
||||
configurationFilePath string
|
||||
webhookURL string
|
||||
listenAddress string
|
||||
logLevel string
|
||||
maximumBackoffTimeSeconds int
|
||||
)
|
||||
|
||||
func init() {
|
||||
viper.SetDefault(ConfigurationPathFlagKey, defaultConfigurationPath)
|
||||
defineConfigurationVariable(&configurationFilePath, rootCmd.Flags().StringVarP, flags.ConfigurationPathFlagKey, "c", defaultConfigurationPath, "Path to the configuration file.")
|
||||
defineConfigurationVariable(&webhookURL, rootCmd.Flags().StringVarP, flags.DiscordWebhookUrlFlagKey, "d", "", "Url to the Discord webhook API endpoint.")
|
||||
defineConfigurationVariable(&listenAddress, rootCmd.Flags().StringVarP, flags.ListenAddressFlagKey, "l", server.DefaultListenAddress, "The address (host:port) which the server will attempt to bind to and listen on.")
|
||||
defineConfigurationVariable(&logLevel, rootCmd.Flags().StringVarP, flags.LogLevelFlagKey, "", defaultLogLevel, "The minimum level of logging to be produced by the pod. Acceptable values, in ascending order, are 'trace', 'debug', 'info', 'warn', 'error', 'fatal', 'panic', or 'disabled'.")
|
||||
defineConfigurationVariable(&maximumBackoffTimeSeconds, rootCmd.Flags().IntVarP, flags.MaxBackoffTimeSecondsFlagKey, "", defaultMaxBackoffTimeSeconds, "The maximum elapsed duration (expressed as an integer number of seconds) to allow the Discord client to continue retrying to send messages to the Discord API.")
|
||||
}
|
||||
|
||||
viper.BindEnv(ConfigurationPathFlagKey, strings.ToUpper(ConfigurationPathFlagKey))
|
||||
rootCmd.Flags().StringVarP(&configurationFilePath, ConfigurationPathFlagKey, "c", defaultConfigurationPath, "Path to the configuration file.")
|
||||
viper.BindPFlag(ConfigurationPathFlagKey, rootCmd.Flags().Lookup(ConfigurationPathFlagKey))
|
||||
|
||||
viper.BindEnv(DiscordWebhookUrlFlagKey, strings.ToUpper(DiscordWebhookUrlFlagKey))
|
||||
rootCmd.Flags().StringVarP(&webhookURL, DiscordWebhookUrlFlagKey, "d", "", "Url to the Discord webhook API endpoint.")
|
||||
viper.BindPFlag(DiscordWebhookUrlFlagKey, rootCmd.Flags().Lookup(DiscordWebhookUrlFlagKey))
|
||||
|
||||
viper.SetDefault(ListenAddressFlagKey, server.DefaultListenAddress)
|
||||
viper.BindEnv(ListenAddressFlagKey, strings.ToUpper(ListenAddressFlagKey))
|
||||
rootCmd.Flags().StringVarP(&listenAddress, ListenAddressFlagKey, "l", "", "The address (host:port) which the server will attempt to bind to and listen on.")
|
||||
viper.BindPFlag(ListenAddressFlagKey, rootCmd.Flags().Lookup(ListenAddressFlagKey))
|
||||
|
||||
viper.SetDefault(MaxBackoffTimeSecondsFlagKey, defaultMaxBackoffTimeSeconds)
|
||||
viper.BindEnv(MaxBackoffTimeSecondsFlagKey, strings.ToUpper(MaxBackoffTimeSecondsFlagKey))
|
||||
rootCmd.Flags().IntVarP(&maximumBackoffTimeSeconds, MaxBackoffTimeSecondsFlagKey, "", defaultMaxBackoffTimeSeconds, "The maximum elapsed duration (expressed as an integer number of seconds) to allow the Discord client to continue retrying to send messages to the Discord API.")
|
||||
viper.BindPFlag(MaxBackoffTimeSecondsFlagKey, rootCmd.Flags().Lookup(MaxBackoffTimeSecondsFlagKey))
|
||||
func defineConfigurationVariable[K int | string](variable *K, flagParser func(*K, string, string, K, string), flagKey string, shorthand string, defaultValue K, description string) {
|
||||
viper.SetDefault(flagKey, defaultValue)
|
||||
viper.BindEnv(flagKey, strings.ToUpper(flagKey))
|
||||
flagParser(variable, flagKey, shorthand, defaultValue, description)
|
||||
viper.BindPFlag(flagKey, rootCmd.Flags().Lookup(flagKey))
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
@@ -57,22 +52,27 @@ var rootCmd = &cobra.Command{
|
||||
translates the data to match Discord's message specifications,
|
||||
and forwards that to Discord's message API endpoint.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
zerolog.TimeFieldFormat = time.RFC3339
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
|
||||
// these log messages are generated before the log level is set
|
||||
log.Debug().Msgf("Attempting to read from configuration file path: ('%s')", configurationFilePath)
|
||||
viper.SetConfigFile(configurationFilePath)
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
log.Info().Err(err).Msgf("Unable to read configuration file at path ('%s'). Attempting to parse command line arguments or environment variables, the command line argument has higher order of precedence.", configurationFilePath)
|
||||
}
|
||||
|
||||
if viper.GetString(DiscordWebhookUrlFlagKey) != "" {
|
||||
webhookURL = viper.GetString(DiscordWebhookUrlFlagKey)
|
||||
if viper.GetString(flags.DiscordWebhookUrlFlagKey) != "" {
|
||||
webhookURL = viper.GetString(flags.DiscordWebhookUrlFlagKey)
|
||||
}
|
||||
if viper.GetString(ListenAddressFlagKey) != "" {
|
||||
listenAddress = viper.GetString(ListenAddressFlagKey)
|
||||
if viper.GetString(flags.ListenAddressFlagKey) != "" {
|
||||
listenAddress = viper.GetString(flags.ListenAddressFlagKey)
|
||||
}
|
||||
if viper.GetString(MaxBackoffTimeSecondsFlagKey) != "" {
|
||||
maximumBackoffTimeSeconds = viper.GetInt(MaxBackoffTimeSecondsFlagKey)
|
||||
|
||||
setGlobalLogLevel(viper.GetString(flags.LogLevelFlagKey))
|
||||
|
||||
if viper.GetString(flags.MaxBackoffTimeSecondsFlagKey) != "" {
|
||||
maximumBackoffTimeSeconds = viper.GetInt(flags.MaxBackoffTimeSecondsFlagKey)
|
||||
}
|
||||
|
||||
amds := server.AlertManagerDiscordServer{
|
||||
@@ -100,3 +100,26 @@ func Execute() {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func setGlobalLogLevel(logLevel string) {
|
||||
switch logLevel {
|
||||
case "trace":
|
||||
zerolog.SetGlobalLevel(zerolog.TraceLevel)
|
||||
case "debug":
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
case "info":
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
case "warn":
|
||||
zerolog.SetGlobalLevel(zerolog.WarnLevel)
|
||||
case "error":
|
||||
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||
case "fatal":
|
||||
zerolog.SetGlobalLevel(zerolog.FatalLevel)
|
||||
case "panic":
|
||||
zerolog.SetGlobalLevel(zerolog.PanicLevel)
|
||||
case "disabled":
|
||||
zerolog.SetGlobalLevel(zerolog.Disabled)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@ type: application
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.1.0
|
||||
version: 0.0.0-local
|
||||
# This is the version number of the application being deployed.
|
||||
appVersion: "0.1.0"
|
||||
appVersion: "0.0.0-local"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# alertmanager-discord
|
||||
|
||||
  
|
||||
  
|
||||
|
||||
A Helm chart to deploy alertmanager-discord to Kubernetes
|
||||
|
||||
@@ -29,7 +29,7 @@ A Helm chart to deploy alertmanager-discord to Kubernetes
|
||||
| securityContext.readOnlyRootFilesystem | bool | `true` | |
|
||||
| securityContext.runAsNonRoot | bool | `true` | |
|
||||
| securityContext.runAsUser | int | `1000` | |
|
||||
| server.configuration.key | string | `"config.yaml"` | |
|
||||
| server.configuration.key | string | `"config.yaml"` | the key within the Kubernetes Secret. This key is expected to be a filename, as it will for the path for the configuration file when mounted to the container. |
|
||||
| server.configuration.name | string | `"discord-config"` | name of the Kubernetes Secret containing the configuration file, will be mounted to the container. Must be in the same namespace as this helm chart is deployed. |
|
||||
| service.port | int | `9094` | The port to which alertmanager should push alerts |
|
||||
| service.type | string | `"ClusterIP"` | |
|
||||
|
||||
@@ -55,6 +55,7 @@ server:
|
||||
configuration:
|
||||
# -- name of the Kubernetes Secret containing the configuration file, will be mounted to the container. Must be in the same namespace as this helm chart is deployed.
|
||||
name: discord-config
|
||||
# -- the key within the Kubernetes Secret. This key is expected to be a filename, as it will for the path for the configuration file when mounted to the container.
|
||||
key: config.yaml
|
||||
# within the config.yaml data, it should be yaml formatted with the key `discord_webhook_url`, and optionally keys `listen_address` & `max_backoff_time_seconds`. An example of the data expected can be found at ./test/test-config.yaml
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/specklesystems/alertmanager-discord/pkg/alertmanager"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"github.com/specklesystems/alertmanager-discord/pkg/prometheus"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -24,9 +26,9 @@ type AlertForwarderHandler struct {
|
||||
af AlertForwarder
|
||||
}
|
||||
|
||||
func NewAlertForwarderHandler(client *http.Client, webhookURL string, maximumBackoffTimeSeconds time.Duration) *AlertForwarderHandler {
|
||||
func NewAlertForwarderHandler(client *http.Client, webhookURL string, maximumBackoffElapsedTime time.Duration) *AlertForwarderHandler {
|
||||
return &AlertForwarderHandler{
|
||||
af: NewAlertForwarder(client, webhookURL, maximumBackoffTimeSeconds),
|
||||
af: NewAlertForwarder(client, webhookURL, maximumBackoffElapsedTime),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,12 +40,20 @@ type AlertForwarder struct {
|
||||
client *discord.Client
|
||||
}
|
||||
|
||||
func NewAlertForwarder(client *http.Client, webhookURL string, maximumBackoffTimeSeconds time.Duration) AlertForwarder {
|
||||
func NewAlertForwarder(client *http.Client, webhookURL string, maximumBackoffElapsedTime time.Duration) AlertForwarder {
|
||||
return AlertForwarder{
|
||||
client: discord.NewClient(client, webhookURL, maximumBackoffTimeSeconds),
|
||||
client: discord.NewClient(client, webhookURL, maximumBackoffElapsedTime),
|
||||
}
|
||||
}
|
||||
|
||||
func (af *AlertForwarder) groupAlerts(amo *alertmanager.Out) map[string][]alertmanager.Alert {
|
||||
groupedAlerts := make(map[string][]alertmanager.Alert)
|
||||
for _, alert := range amo.Alerts {
|
||||
groupedAlerts[alert.Status] = append(groupedAlerts[alert.Status], alert)
|
||||
}
|
||||
return groupedAlerts
|
||||
}
|
||||
|
||||
func (af *AlertForwarder) sendWebhook(correlationId string, amo *alertmanager.Out, w http.ResponseWriter) {
|
||||
if len(amo.Alerts) < 1 {
|
||||
log.Debug().
|
||||
@@ -53,31 +63,35 @@ func (af *AlertForwarder) sendWebhook(correlationId string, amo *alertmanager.Ou
|
||||
return
|
||||
}
|
||||
|
||||
groupedAlerts := make(map[string][]alertmanager.Alert)
|
||||
for _, alert := range amo.Alerts {
|
||||
groupedAlerts[alert.Status] = append(groupedAlerts[alert.Status], alert)
|
||||
logger := zerolog.New(os.Stderr).With().
|
||||
Timestamp().
|
||||
Str(logging.FieldKeyCorrelationId, correlationId).Logger()
|
||||
if amo.CommonLabels.Alertname != "" {
|
||||
logger = logger.With().Str(logging.FieldKeyAlertName, amo.CommonLabels.Alertname).Logger()
|
||||
} else if amo.GroupLabels.Alertname != "" {
|
||||
logger = logger.With().Str(logging.FieldKeyAlertName, amo.GroupLabels.Alertname).Logger()
|
||||
}
|
||||
|
||||
failedToPublishAtLeastOne := false
|
||||
for status, alerts := range groupedAlerts {
|
||||
for status, alerts := range af.groupAlerts(amo) {
|
||||
DO := TranslateAlertManagerToDiscord(status, amo, alerts)
|
||||
|
||||
log.Info().
|
||||
logger.Info().
|
||||
Str(logging.FieldKeyEventType, logging.EventTypeRequestSending).
|
||||
Str(logging.FieldKeyCorrelationId, correlationId).
|
||||
Msg("Sending HTTP request to Discord.")
|
||||
res, err := af.client.PublishMessage(DO)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error encountered when publishing message to discord: %w", err)
|
||||
log.Error().
|
||||
err = fmt.Errorf("failed to publish message to Discord: %w", err)
|
||||
logger.Error().
|
||||
Str(logging.FieldKeyCorrelationId, correlationId).
|
||||
Err(err).
|
||||
Msg("Error when attempting to publish message to discord.")
|
||||
Msg("Error when attempting to publish message to Discord.")
|
||||
failedToPublishAtLeastOne = true
|
||||
continue
|
||||
}
|
||||
|
||||
log.Info().
|
||||
logger.Info().
|
||||
Str(logging.FieldKeyEventType, logging.EventTypeResponseReceived).
|
||||
Str(logging.FieldKeyCorrelationId, correlationId).
|
||||
Msg("HTTP response received from Discord")
|
||||
@@ -124,7 +138,7 @@ or https://prometheus.io/docs/alerting/latest/configuration/#webhook_config`
|
||||
Msg("Sending HTTP request to Discord.")
|
||||
res, err := af.client.PublishMessage(DO)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error encountered when publishing message to discord: %w", err)
|
||||
return nil, fmt.Errorf("error encountered when publishing message to Discord: %w", err)
|
||||
}
|
||||
|
||||
log.Info().
|
||||
@@ -204,5 +218,4 @@ func (af *AlertForwarder) handleInvalidInput(correlationId string, b []byte, w h
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -17,13 +17,10 @@ func CheckWebhookURL(webhookURL string) (bool, *url.URL, error) {
|
||||
|
||||
parsedUrl, err := url.Parse(webhookURL)
|
||||
if err != nil {
|
||||
return false, &url.URL{}, fmt.Errorf("The Discord WebHook URL ('%s') cannot be parsed as a url: %w", webhookURL, err)
|
||||
return false, &url.URL{}, fmt.Errorf("the Discord WebHook URL ('%s') cannot be parsed as a url: %w", webhookURL, err)
|
||||
}
|
||||
|
||||
host, _, err := net.SplitHostPort(parsedUrl.Host)
|
||||
if err != nil {
|
||||
// return false, parsedUrl, fmt.Errorf("The Discord WebHook URL ('%s') host ('%s') cannot be separated into domain/ip and port components: %w", webhookURL, parsedUrl.Host, err)
|
||||
}
|
||||
host, _, _ := net.SplitHostPort(parsedUrl.Host)
|
||||
if host == "" {
|
||||
host = parsedUrl.Host
|
||||
}
|
||||
@@ -37,7 +34,7 @@ func CheckWebhookURL(webhookURL string) (bool, *url.URL, error) {
|
||||
|
||||
ok := re.Match([]byte(webhookURL))
|
||||
if !ok {
|
||||
return false, parsedUrl, fmt.Errorf("The Discord WebHook URL doesn't seem to be a valid Discord Webhook API url: '%s'", webhookURL)
|
||||
return false, parsedUrl, fmt.Errorf("the Discord WebHook URL doesn't seem to be a valid Discord Webhook API url: '%s'", webhookURL)
|
||||
}
|
||||
|
||||
return ok, parsedUrl, nil
|
||||
|
||||
@@ -8,17 +8,17 @@ import (
|
||||
var (
|
||||
RequestsToDiscordInFlight = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "discord_client_requests_in_flight",
|
||||
Help: "The current number of http requests being sent by the discord client.",
|
||||
Help: "The current number of http requests being sent by the Discord client.",
|
||||
})
|
||||
|
||||
RequestsToDiscordTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "discord_client_requests_total",
|
||||
Help: "The total number of http requests sent by the discord client.",
|
||||
Help: "The total number of http requests sent by the Discord client.",
|
||||
}, []string{"code", "method"})
|
||||
|
||||
RequestsToDiscordDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Name: "discord_client_request_duration_seconds",
|
||||
Help: "Duration of all http requests sent by the discord client.",
|
||||
Help: "Duration of all http requests sent by the Discord client.",
|
||||
Buckets: prometheus.DefBuckets,
|
||||
}, []string{"code"})
|
||||
)
|
||||
|
||||
@@ -5,4 +5,5 @@ const (
|
||||
DiscordWebhookUrlFlagKey = "discord_webhook_url"
|
||||
ListenAddressFlagKey = "listen_address"
|
||||
MaxBackoffTimeSecondsFlagKey = "max_backoff_time_seconds"
|
||||
LogLevelFlagKey = "log_level"
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ const (
|
||||
FieldKeyHttpMethod = "method"
|
||||
FieldKeyHttpPath = "path"
|
||||
FieldKeyEventType = "event_type"
|
||||
FieldKeyAlertName = "alert_name"
|
||||
FieldKeyCorrelationId = "correlation_id"
|
||||
FieldKeyStatusCode = "status_code"
|
||||
)
|
||||
|
||||
@@ -22,11 +22,11 @@ const (
|
||||
)
|
||||
|
||||
func Test_Serve_HappyPath(t *testing.T) {
|
||||
// create a mock discord server to respond to our request
|
||||
// create a mock Discord server to respond to our request
|
||||
receivedRequest := make(chan bool, 1)
|
||||
mockDiscordServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Discord mock server will always return with Status Code 200 OK
|
||||
receivedRequest <- true // notify the channel that the discord server received the request
|
||||
receivedRequest <- true // notify the channel that the Discord server received the request
|
||||
}))
|
||||
defer mockDiscordServer.Close()
|
||||
|
||||
@@ -44,19 +44,23 @@ func Test_Serve_HappyPath(t *testing.T) {
|
||||
}
|
||||
|
||||
res, err := client.Get(fmt.Sprintf("http://%s/liveness", serverListenAddress))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, res, "response to GET '/liveness' should not be nil")
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode, "GET liveness should return status code OK (200)")
|
||||
res, err = client.Get(fmt.Sprintf("http://%s/readiness", serverListenAddress))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, res, "response to GET '/readiness' should not be nil")
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode, "GET readiness should return status code OK (200)")
|
||||
res, err = client.Get(fmt.Sprintf("http://%s/favicon.ico", serverListenAddress))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, res, "response to GET '/favicon.ico' should not be nil")
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode, "GET favicon.ico should return status code OK (200)")
|
||||
res, err = client.Get(fmt.Sprintf("http://%s/metrics", serverListenAddress))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, res, "response to GET '/metrics' should not be nil")
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode, "GET favicon.ico should return status code OK (200)")
|
||||
|
||||
// assert mock discord server received expected json
|
||||
// assert mock Discord server received expected json
|
||||
ao := alertmanager.Out{
|
||||
Alerts: []alertmanager.Alert{
|
||||
{
|
||||
@@ -68,6 +72,11 @@ func Test_Serve_HappyPath(t *testing.T) {
|
||||
}{
|
||||
Summary: "a_common_annotation_summary",
|
||||
},
|
||||
GroupLabels: struct {
|
||||
Alertname string `json:"alertname"`
|
||||
}{
|
||||
Alertname: "testAlertName",
|
||||
},
|
||||
}
|
||||
|
||||
aoJson, err := json.Marshal(ao)
|
||||
@@ -86,7 +95,7 @@ func Test_Serve_HappyPath(t *testing.T) {
|
||||
assert.NoError(t, err, "sending request to alertmanager-discord server.")
|
||||
assert.NotNil(t, res, "response to POST '/' should not be nil")
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode, "sending valid alertmanager data should expect http response status code")
|
||||
assert.True(t, <-receivedRequest, "Mock discord server should have received response") // will wait until the request is received
|
||||
assert.True(t, <-receivedRequest, "Mock Discord server should have received response") // will wait until the request is received
|
||||
|
||||
// TODO assert log lines were generated
|
||||
|
||||
|
||||
@@ -15,7 +15,9 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const DefaultListenAddress = "0.0.0.0:9094"
|
||||
const (
|
||||
DefaultListenAddress = "0.0.0.0:9094"
|
||||
)
|
||||
const (
|
||||
FaviconPath = "/favicon.ico"
|
||||
LivenessPath = "/liveness"
|
||||
|
||||
Reference in New Issue
Block a user