Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 20b5b7fdc3 | |||
| f79a6754f5 | |||
| c2457d9061 | |||
| 6ff13d331f | |||
| c701667bf2 | |||
| faa6956b29 | |||
| a328544e11 | |||
| 035505cb0e | |||
| 5de0c11f44 | |||
| e7fcdec23d | |||
| 3cf3536256 | |||
| 1ee293fe6a | |||
| ad387e2a4d | |||
| d4de1e7f3d | |||
| c67919d0a2 | |||
| 62bc6ef79e | |||
| 3b47e55e2a | |||
| a2555fcff1 | |||
| c6dd80140e | |||
| f2872d6dea | |||
| 6c6eef2854 | |||
| 2d7a7522e0 | |||
| 95422cf410 | |||
| c1caf51eb9 | |||
| b7fc536214 | |||
| 838a1d5fdf | |||
| 5a070fed0e | |||
| 5193686610 | |||
| 0ee0afdf99 | |||
| b4a48fd928 | |||
| a15481f294 | |||
| 82e9275166 | |||
| 8f5aef469e | |||
| 89379c4700 | |||
| efb5ce6449 | |||
| 9826fef9e7 | |||
| 4ce14be96c | |||
| 2c0628507c | |||
| 89ef841a7e | |||
| 16dcdf656d | |||
| ceceb475c7 | |||
| 2bfb007781 | |||
| 3b8af1f970 | |||
| a47f4146f5 | |||
| 7a32366d51 | |||
| 49e3076d4e | |||
| b504e7f09c | |||
| e8c1bd3569 | |||
| df21563f32 | |||
| d9ee4ef581 | |||
| e9c2c3801e | |||
| 5fac7bac9d | |||
| c27d983dda | |||
| 99f5833aee | |||
| 13d3a39e11 | |||
| ff1f9273cb | |||
| 462a419eed | |||
| c650e0c78a | |||
| eac7521a45 | |||
| 03a58c8afd | |||
| 2e7369c932 | |||
| 3fdeed85bb | |||
| e7453d0523 | |||
| db70e42850 | |||
| 093b67f417 | |||
| 3b4fad8a09 | |||
| 45ea6ee661 | |||
| 85ff16f0f3 | |||
| f581cbcce5 | |||
| e6edc2294a | |||
| 34535fcb78 | |||
| ba34a16bd0 | |||
| 0b75833a50 | |||
| a95f814b81 | |||
| 0b06423b70 | |||
| 809cc41335 | |||
| 020c84419f | |||
| 2116bc8460 | |||
| 7704e73594 | |||
| 5da0dc9494 | |||
| a33608f222 | |||
| 19878e15d1 | |||
| 8685381e20 |
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
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}:${VERSION}" --build-arg="APPLICATION_VERSION=${VERSION}" --file ./Dockerfile .
|
||||
+158
-2
@@ -1,5 +1,161 @@
|
||||
version: 2.1
|
||||
|
||||
workflows: {}
|
||||
workflows:
|
||||
build-image:
|
||||
jobs:
|
||||
- get-version:
|
||||
filters:
|
||||
tags: &filter-allow-all
|
||||
only: /.*/
|
||||
|
||||
jobs: {}
|
||||
- pre-commit:
|
||||
filters:
|
||||
tags: *filter-allow-all
|
||||
|
||||
- docker-build:
|
||||
filters:
|
||||
tags: &filter-ignore-all
|
||||
ignore: /.*/
|
||||
branches:
|
||||
ignore:
|
||||
- main
|
||||
requires:
|
||||
- get-version
|
||||
|
||||
- docker-build-and-publish:
|
||||
context:
|
||||
- docker-hub
|
||||
filters:
|
||||
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:2024.01
|
||||
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:
|
||||
default: ./.pre-commit-config.yaml
|
||||
description: Optional, path to pre-commit config file.
|
||||
type: string
|
||||
cache_prefix:
|
||||
default: ""
|
||||
description: |
|
||||
Optional cache prefix to be used on CircleCI. Can be used for cache busting or to ensure multiple jobs use different caches.
|
||||
type: string
|
||||
docker:
|
||||
- image: speckle/pre-commit-runner:latest
|
||||
resource_class: &docker-resource-class medium
|
||||
working_directory: *workingdir
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- cache-pre-commit-<<parameters.cache_prefix>>-{{ checksum "<<parameters.config_file>>" }}
|
||||
- run:
|
||||
name: Install pre-commit hooks
|
||||
command: pre-commit install-hooks --config <<parameters.config_file>>
|
||||
- save_cache:
|
||||
key: cache-pre-commit-<<parameters.cache_prefix>>-{{ checksum "<<parameters.config_file>>" }}
|
||||
paths:
|
||||
- ~/.cache/pre-commit
|
||||
- run:
|
||||
name: Run pre-commit
|
||||
command: pre-commit run --all-files --config <<parameters.config_file>>
|
||||
- run:
|
||||
command: git --no-pager diff
|
||||
name: git diff
|
||||
when: on_fail
|
||||
|
||||
docker-build-and-publish:
|
||||
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
|
||||
version: default
|
||||
docker_layer_caching: true
|
||||
- run:
|
||||
name: Build and Publish
|
||||
command: ./.circleci/build.sh && ./.circleci/publish.sh
|
||||
|
||||
docker-build:
|
||||
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
|
||||
- run:
|
||||
name: Build
|
||||
command: ./.circleci/build.sh
|
||||
|
||||
helm-package-and-publish:
|
||||
docker:
|
||||
- image: quay.io/helmpack/chart-testing:v3.10.1-amd64
|
||||
resource_class: *docker-resource-class
|
||||
working_directory: *workingdir
|
||||
steps:
|
||||
- checkout
|
||||
- add_ssh_keys:
|
||||
fingerprints:
|
||||
- "30:cb:bf:0c:ec:3b:fe:88:6c:be:af:b3:d1:36:75:db"
|
||||
- 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}"
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
if [[ -z "${VERSION}" ]]; then
|
||||
echo "VERSION environment variable should be set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
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.
|
After Width: | Height: | Size: 312 KiB |
+10
@@ -0,0 +1,10 @@
|
||||
alertmanager-discord
|
||||
alertmanager-discord.darwin
|
||||
alertmanager-discord.linux
|
||||
cover.out
|
||||
|
||||
# CircleCI orb Helm package & push
|
||||
.cr-release-packages
|
||||
|
||||
# pre-commit
|
||||
node_modules
|
||||
+52
-2
@@ -1,6 +1,56 @@
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: "v3.1.0"
|
||||
hooks:
|
||||
- id: prettier
|
||||
exclude: "deploy/helm"
|
||||
|
||||
- repo: https://github.com/hadolint/hadolint
|
||||
rev: "v2.12.1-beta"
|
||||
hooks:
|
||||
- id: hadolint
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: "v4.5.0"
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
exclude: "deploy/helm"
|
||||
- id: check-merge-conflict
|
||||
- id: check-executables-have-shebangs
|
||||
- id: check-shebang-scripts-are-executable
|
||||
- id: check-symlinks
|
||||
- id: check-vcs-permalinks
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
exclude: "deploy/helm/README.md"
|
||||
|
||||
- repo: https://github.com/syntaqx/git-hooks
|
||||
rev: "v0.0.17"
|
||||
rev: "v0.0.18"
|
||||
hooks:
|
||||
- id: circleci-config-validate
|
||||
|
||||
- repo: https://github.com/Jarmos-san/shellcheck-precommit
|
||||
rev: "v0.2.0"
|
||||
hooks:
|
||||
- id: shellcheck-system
|
||||
|
||||
- repo: https://github.com/dnephin/pre-commit-golang
|
||||
rev: "v0.5.1"
|
||||
hooks:
|
||||
- id: go-fmt
|
||||
# - id: go-vet
|
||||
- id: no-go-testing
|
||||
- id: go-critic
|
||||
- id: go-unit-tests
|
||||
- id: go-build
|
||||
- id: go-mod-tidy
|
||||
|
||||
- repo: https://github.com/norwoodj/helm-docs
|
||||
rev: v1.12.0
|
||||
hooks:
|
||||
- id: helm-docs
|
||||
args:
|
||||
- --chart-search-root=deploy/helm
|
||||
|
||||
ci:
|
||||
autoupdate_schedule: quarterly
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
# Built following https://medium.com/@chemidy/create-the-smallest-and-secured-golang-docker-image-based-on-scratch-4752223b7324
|
||||
|
||||
# STEP 1 build executable binary
|
||||
FROM golang:alpine as builder
|
||||
# Install SSL ca certificates
|
||||
RUN apk update && apk add --no-cache \
|
||||
git=2.45.2-r0 \
|
||||
ca-certificates=20240226-r0
|
||||
# Create appuser
|
||||
RUN adduser -D -g '' appuser
|
||||
COPY . $GOPATH/src/alertmanager-discord/
|
||||
WORKDIR $GOPATH/src/alertmanager-discord/
|
||||
ARG APPLICATION_VERSION
|
||||
ENV APPLICATION_VERSION="${APPLICATION_VERSION:-"0.0.0"}"
|
||||
#get dependancies
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s -X 'github.com/specklesystems/alertmanager-discord/pkg/version.Version=${APPLICATION_VERSION}'" -o /go/bin/alertmanager-discord main.go
|
||||
|
||||
|
||||
# STEP 2 build a small image
|
||||
# start from scratch
|
||||
FROM scratch
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=builder /etc/passwd /etc/passwd
|
||||
# Copy our static executable
|
||||
COPY --from=builder /go/bin/alertmanager-discord /bin/alertmanager-discord
|
||||
|
||||
ENV LISTEN_ADDRESS=0.0.0.0:9094
|
||||
EXPOSE 9094
|
||||
USER appuser
|
||||
ENTRYPOINT ["/bin/alertmanager-discord"]
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,173 @@
|
||||
# alertmanager-discord
|
||||
|
||||
This is a webserver that accepts webhooks from AlertManager. It will post your Prometheus alert notifications into a Discord channel as they trigger:
|
||||
|
||||

|
||||
|
||||
## Warning
|
||||
|
||||
This program is not a replacement to alertmanager, it accepts webhooks from alertmanager, not Prometheus.
|
||||
|
||||
The standard "dataflow" should be:
|
||||
|
||||
```text
|
||||
Prometheus -------------> alertmanager -------------------> alertmanager-discord
|
||||
|
||||
alerting: receivers:
|
||||
alertmanagers: - name: 'discord_webhook' environment:
|
||||
- static_configs: webhook_configs: - DISCORD_WEBHOOK=https://discordapp.com/api/we...
|
||||
- targets: - url: 'http://localhost:9094'
|
||||
- 127.0.0.1:9093
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- REST API
|
||||
- Small, standalone binary ( less than 12 Mb)
|
||||
- Small Docker (OCI) Image (also less than 12 Mb) with minimal dependencies
|
||||
- Helm Chart for deployment to Kubernetes.
|
||||
- includes Cilium Network Policies which can be optionally enabled.
|
||||
- Liveness and Readiness probes, at `/liveness` and `/readiness`.
|
||||
- Unit and Integration tests, approx 90% coverage.
|
||||
- Structured Logging.
|
||||
- Prometheus metrics at `/metrics`.
|
||||
|
||||
### Roadmap
|
||||
|
||||
- Template Discord messages
|
||||
- REST API documented with OpenAPI (Swagger) specification.
|
||||
|
||||
## 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"
|
||||
|
||||
# The directory from which notification templates are read.
|
||||
templates:
|
||||
- "/etc/alertmanager/template/*.tmpl"
|
||||
|
||||
# The root route on which each incoming alert enters.
|
||||
route:
|
||||
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"
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Running binary
|
||||
|
||||
```shell
|
||||
go run . --discord_webhook_url=https://discord.com/api/webhooks/123456789123456789/abc
|
||||
```
|
||||
|
||||
You may instead provide the Discord webhook url by environment variable, `DISCORD_WEBHOOK_URL`, or via a configuration file:
|
||||
|
||||
```yaml
|
||||
discord_webhook_url: https://discord.com/api/webhooks/123456789123456789/abc
|
||||
```
|
||||
|
||||
```shell
|
||||
go run . --configuration_file_path=/path/to/your/config.yaml
|
||||
```
|
||||
|
||||
### Docker or OCI-compatible container runtime
|
||||
|
||||
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
|
||||
|
||||
If you wish to deploy this to Kubernetes, this repository contains a Helm Chart.
|
||||
|
||||
Firstly, please deploy a Secret with your configuration information, the discord webhook url is required.:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: discord-config
|
||||
data:
|
||||
"config.yaml": |
|
||||
discord_webhook_url: https://discord.com/api/webhooks/123456789123456789/abc
|
||||
```
|
||||
|
||||
```shell
|
||||
helm upgrade --install \
|
||||
--create-namespace \
|
||||
--namespace alertmanager-discord
|
||||
alertmanager-discord \
|
||||
./deploy/helm
|
||||
```
|
||||
|
||||
You can optionally also provide a values yaml file, `--values ./your-values.yaml`, to override the default values.
|
||||
|
||||
## Development
|
||||
|
||||
To build the binary locally:
|
||||
|
||||
```shell
|
||||
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s" -o /tmp/alertmanager-discord ./cmd/alertforward
|
||||
```
|
||||
|
||||
To build the Dockerfile locally:
|
||||
|
||||
```shell
|
||||
docker build . -t speckle/alertmanager-discord:local
|
||||
```
|
||||
|
||||
Or to build the Dockerfile on Apple Silicon (M1, M2 etc.):
|
||||
|
||||
```shell
|
||||
docker buildx build --platform=linux/amd64 . -t speckle/alertmanager-discord:local
|
||||
```
|
||||
|
||||
### Pre-commit
|
||||
|
||||
A pre-commit configuration is provided. With [pre-commit](https://pre-commit.com/) installed, run:
|
||||
|
||||
```shell
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
This should install hooks on git, which will cause pre-commit to run every time a git commit is created.
|
||||
|
||||
Alternatively, to run pre-commit on the entire repository:
|
||||
|
||||
```shell
|
||||
pre-commit run --all-files
|
||||
```
|
||||
|
||||
### Upgrading
|
||||
|
||||
```shell
|
||||
go get -u ./...
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```shell
|
||||
go test ./... -v -cover -test.shuffle on
|
||||
```
|
||||
|
||||
## Design philosophy
|
||||
|
||||
- small footprint
|
||||
- Minimal external dependencies
|
||||
- binary should be agnostic to deployment location or method.
|
||||
- synchronous; the connection to the server is kept open until the connection to Discord has responded (or errored). This allows the response code or error to be returned to the request - we can have more confidence that the message was sent, and have a better ability to quickly correlate which requests caused an error.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
This repository is forked from [benjojo/alertmanager-discord](https://github.com/benjojo/alertmanager-discord) under the Apache 2.0 license
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/specklesystems/alertmanager-discord/pkg/flags"
|
||||
"github.com/specklesystems/alertmanager-discord/pkg/server"
|
||||
"github.com/specklesystems/alertmanager-discord/pkg/version"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultConfigurationPath = "/etc/alertmanager-discord/config.yaml"
|
||||
defaultMaxBackoffTimeSeconds = 10
|
||||
defaultLogLevel = "info"
|
||||
)
|
||||
|
||||
var (
|
||||
configurationFilePath string
|
||||
webhookURL string
|
||||
listenAddress string
|
||||
logLevel string
|
||||
maximumBackoffTimeSeconds int
|
||||
)
|
||||
|
||||
func init() {
|
||||
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.")
|
||||
}
|
||||
|
||||
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{
|
||||
Use: "alertmanager-discord",
|
||||
Version: version.Version,
|
||||
Short: "Forwards AlertManager alerts to Discord.",
|
||||
Long: `A simple web server that accepts AlertManager webhooks,
|
||||
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 = 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(flags.DiscordWebhookUrlFlagKey) != "" {
|
||||
webhookURL = viper.GetString(flags.DiscordWebhookUrlFlagKey)
|
||||
}
|
||||
if viper.GetString(flags.ListenAddressFlagKey) != "" {
|
||||
listenAddress = viper.GetString(flags.ListenAddressFlagKey)
|
||||
}
|
||||
|
||||
setGlobalLogLevel(viper.GetString(flags.LogLevelFlagKey))
|
||||
|
||||
if viper.GetString(flags.MaxBackoffTimeSecondsFlagKey) != "" {
|
||||
maximumBackoffTimeSeconds = viper.GetInt(flags.MaxBackoffTimeSecondsFlagKey)
|
||||
}
|
||||
|
||||
amds := server.AlertManagerDiscordServer{
|
||||
MaximumBackoffTimeSeconds: time.Duration(maximumBackoffTimeSeconds) * time.Second,
|
||||
}
|
||||
stopCh, err := amds.ListenAndServe(webhookURL, listenAddress)
|
||||
defer func() {
|
||||
if err = amds.Shutdown(); err != nil {
|
||||
log.Fatal().Err(err).Msg("Error while shutting down server.")
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error in AlertManager-Discord server")
|
||||
close(stopCh)
|
||||
}
|
||||
|
||||
// Waits here for SIGINT (kill -2) or for channel to be closed (which can occur if there is an error in the server)
|
||||
<-stopCh
|
||||
},
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Error().Err(err).Msg("Error when executing command. Exiting program...")
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
@@ -0,0 +1,11 @@
|
||||
apiVersion: v2
|
||||
name: alertmanager-discord
|
||||
description: A Helm chart to deploy alertmanager-discord to Kubernetes
|
||||
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.0.0-local
|
||||
# This is the version number of the application being deployed.
|
||||
appVersion: "0.0.0-local"
|
||||
@@ -0,0 +1,41 @@
|
||||
# alertmanager-discord
|
||||
|
||||
  
|
||||
|
||||
A Helm chart to deploy alertmanager-discord to Kubernetes
|
||||
|
||||
## Values
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| affinity | object | `{}` | |
|
||||
| ciliumNetworkPolicy.alertManagerSelectorLabels | object | `{}` | the labels applied to the alertmanager which will send data to this service. If Cilium Network Policy is enabled, ingress to this service is only allowed from a pod matching these labels. |
|
||||
| ciliumNetworkPolicy.enabled | bool | `false` | |
|
||||
| fullnameOverride | string | `""` | |
|
||||
| image.pullPolicy | string | `"Always"` | |
|
||||
| image.repository | string | `"speckle/alertmanager-discord"` | |
|
||||
| image.tag | string | `"latest"` | Overrides the image tag whose default is the chart appVersion. |
|
||||
| imagePullSecrets | list | `[]` | |
|
||||
| nameOverride | string | `""` | |
|
||||
| nodeSelector | object | `{}` | |
|
||||
| podAnnotations | object | `{}` | |
|
||||
| podSecurityContext.fsGroup | int | `2000` | |
|
||||
| priorityClassName | string | `""` | https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/ |
|
||||
| replicaCount | int | `1` | |
|
||||
| resources.limits.cpu | string | `"100m"` | |
|
||||
| resources.limits.memory | string | `"128Mi"` | |
|
||||
| resources.requests.cpu | string | `"50m"` | |
|
||||
| resources.requests.memory | string | `"64Mi"` | |
|
||||
| securityContext.capabilities.drop[0] | string | `"ALL"` | |
|
||||
| securityContext.readOnlyRootFilesystem | bool | `true` | |
|
||||
| securityContext.runAsNonRoot | bool | `true` | |
|
||||
| securityContext.runAsUser | int | `1000` | |
|
||||
| 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"` | |
|
||||
| serviceAccount.annotations | object | `{}` | Annotations to add to the service account |
|
||||
| serviceAccount.create | bool | `true` | Specifies whether a service account should be created |
|
||||
| serviceAccount.name | string | `""` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template |
|
||||
| tolerations | list | `[]` | |
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
1. Get the application URL by running these commands:
|
||||
{{- if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "alertmanager-discord.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "alertmanager-discord.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "alertmanager-discord.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "alertmanager-discord.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||
{{- end }}
|
||||
@@ -0,0 +1,62 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "alertmanager-discord.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "alertmanager-discord.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "alertmanager-discord.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "alertmanager-discord.labels" -}}
|
||||
helm.sh/chart: {{ include "alertmanager-discord.chart" . }}
|
||||
{{ include "alertmanager-discord.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "alertmanager-discord.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "alertmanager-discord.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "alertmanager-discord.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "alertmanager-discord.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,24 @@
|
||||
{{- if .Values.ciliumNetworkPolicy.enabled -}}
|
||||
apiVersion: cilium.io/v2
|
||||
kind: CiliumNetworkPolicy
|
||||
metadata:
|
||||
name: alertmanager-discord
|
||||
spec:
|
||||
endpointSelector:
|
||||
matchLabels:
|
||||
{{- include "alertmanager-discord.selectorLabels" . | nindent 6 }}
|
||||
ingress:
|
||||
- fromEndpoints:
|
||||
- matchLabels:
|
||||
{{- .Values.ciliumNetworkPolicy.alertManagerSelectorLabels | toYaml | nindent 12 }}
|
||||
toPorts:
|
||||
- ports:
|
||||
- port: "{{ .Values.service.port }}"
|
||||
egress:
|
||||
- toFQDNs:
|
||||
- matchPattern: "discord.com"
|
||||
- matchPattern: "discordapp.com"
|
||||
toPorts:
|
||||
- ports:
|
||||
- port: "443"
|
||||
{{- end -}}
|
||||
@@ -0,0 +1,74 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "alertmanager-discord.fullname" . }}
|
||||
labels:
|
||||
{{- include "alertmanager-discord.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "alertmanager-discord.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "alertmanager-discord.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if .Values.priorityClassName }}
|
||||
priorityClassName: {{ .Values.priorityClassName }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "alertmanager-discord.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 9094
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /liveness
|
||||
port: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /readiness
|
||||
port: http
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
env:
|
||||
- name: CONFIGURATION_FILE_PATH
|
||||
value: {{ printf "/etc/alertmanager-discord/%s" .Values.server.configuration.key }}
|
||||
volumeMounts:
|
||||
- name: alertmanager-discord-config
|
||||
mountPath: /etc/alertmanager-discord
|
||||
readOnly: true
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: alertmanager-discord-config
|
||||
secret:
|
||||
secretName: {{ .Values.server.configuration.name }}
|
||||
optional: false
|
||||
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "alertmanager-discord.fullname" . }}
|
||||
labels:
|
||||
{{- include "alertmanager-discord.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "alertmanager-discord.selectorLabels" . | nindent 4 }}
|
||||
@@ -0,0 +1,12 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "alertmanager-discord.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "alertmanager-discord.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: {{ include "alertmanager-discord.fullname" . }}
|
||||
labels:
|
||||
{{- include "alertmanager-discord.labels" . | nindent 4 }}
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "alertmanager-discord.selectorLabels" . | nindent 6 }}
|
||||
endpoints:
|
||||
- port: http
|
||||
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "{{ include "alertmanager-discord.fullname" . }}-test-connection"
|
||||
labels:
|
||||
{{- include "alertmanager-discord.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": test
|
||||
spec:
|
||||
containers:
|
||||
- name: wget
|
||||
image: busybox
|
||||
command: ['wget']
|
||||
args: ['{{ include "alertmanager-discord.fullname" . }}:{{ .Values.service.port }}/readiness']
|
||||
restartPolicy: Never
|
||||
@@ -0,0 +1,218 @@
|
||||
{
|
||||
"title": "Chart Values",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"replicaCount": {
|
||||
"type": "number",
|
||||
"description": "The number of replicas of the alertmanager-discord pod to deploy",
|
||||
"default": 1
|
||||
},
|
||||
"image": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"repository": {
|
||||
"type": "string",
|
||||
"description": "The OCI compatible repository containing the image to deploy",
|
||||
"default": "speckle/alertmanager-discord"
|
||||
},
|
||||
"pullPolicy": {
|
||||
"type": "string",
|
||||
"description": "Controls when the image should be pulled from the repository.",
|
||||
"default": "Always"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"description": "Overrides the image tag whose default is the chart appVersion.",
|
||||
"default": "latest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"imagePullSecrets": {
|
||||
"type": "array",
|
||||
"description": "If the image is in a private repository, pull secrets are required.",
|
||||
"default": [],
|
||||
"items": {}
|
||||
},
|
||||
"nameOverride": {
|
||||
"type": "string",
|
||||
"description": "Override the name of this Helm Release",
|
||||
"default": ""
|
||||
},
|
||||
"fullnameOverride": {
|
||||
"type": "string",
|
||||
"description": "Override the full name generated for this Helm Release",
|
||||
"default": ""
|
||||
},
|
||||
"serviceAccount": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"create": {
|
||||
"type": "boolean",
|
||||
"description": "Specifies whether a service account should be created",
|
||||
"default": true
|
||||
},
|
||||
"annotations": {
|
||||
"type": "object",
|
||||
"description": "Annotations to add to the service account",
|
||||
"default": {}
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the service account to use. If not set and create is true, a name is generated using the fullname template",
|
||||
"default": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"podAnnotations": {
|
||||
"type": "object",
|
||||
"description": "Annotations to apply to pods generated by this Helm Chart",
|
||||
"default": {}
|
||||
},
|
||||
"podSecurityContext": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fsGroup": {
|
||||
"type": "number",
|
||||
"description": "The group number in which the pod user will operate",
|
||||
"default": 2000
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityContext": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"capabilities": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"drop": {
|
||||
"type": "array",
|
||||
"description": "The kernel capabilities to remove from this pod.",
|
||||
"default": [
|
||||
"ALL"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"readOnlyRootFilesystem": {
|
||||
"type": "boolean",
|
||||
"description": "Deteremines whether the root file system is readonly.",
|
||||
"default": true
|
||||
},
|
||||
"runAsNonRoot": {
|
||||
"type": "boolean",
|
||||
"description": "Deteremines whether the user is non-root.",
|
||||
"default": true
|
||||
},
|
||||
"runAsUser": {
|
||||
"type": "number",
|
||||
"description": "The uid of the user under which the pod is run.",
|
||||
"default": 1000
|
||||
}
|
||||
}
|
||||
},
|
||||
"service": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "The type of service (e.g. ClusterIP or Loadbalancer)",
|
||||
"default": "ClusterIP"
|
||||
},
|
||||
"port": {
|
||||
"type": "number",
|
||||
"description": "The port to which alertmanager should push alerts",
|
||||
"default": 9094
|
||||
}
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limits": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cpu": {
|
||||
"type": "string",
|
||||
"description": "The maximum CPU that will be made available to the pod",
|
||||
"default": "100m"
|
||||
},
|
||||
"memory": {
|
||||
"type": "string",
|
||||
"description": "The maximum memory that will be made available to the pod. Above this value the pod runs the risk of being OOMKilled",
|
||||
"default": "128Mi"
|
||||
}
|
||||
}
|
||||
},
|
||||
"requests": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cpu": {
|
||||
"type": "string",
|
||||
"description": "The CPU that is requested by this pod when deployed, used for space allocation on nodes.",
|
||||
"default": "50m"
|
||||
},
|
||||
"memory": {
|
||||
"type": "string",
|
||||
"description": "The memory that is requested by this pod when deployed, used for space allocation on nodes.",
|
||||
"default": "64Mi"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"nodeSelector": {
|
||||
"type": "object",
|
||||
"description": "Assign the pods deployed by this Helm Chart to a specific node which matches the selector.",
|
||||
"default": {}
|
||||
},
|
||||
"tolerations": {
|
||||
"type": "array",
|
||||
"description": "Allows the pods deployed by this Helm Chart to tolerate specific criteria which may be found applied to nodes.",
|
||||
"default": [],
|
||||
"items": {}
|
||||
},
|
||||
"affinity": {
|
||||
"type": "object",
|
||||
"description": "The pods deployed by this Helm Chart will tend to this specific criteria which may be found applied to nodes.",
|
||||
"default": {}
|
||||
},
|
||||
"server": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"configuration": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the Kubernetes Secret containing the Discord webhook url. Must be in the same namespace as this helm chart is deployed.",
|
||||
"default": "discord-webhook"
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "The key within the Opaque Kubernetes Secret containing the webhook value.",
|
||||
"default": "discord-uri"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ciliumNetworkPolicy": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "If enabled, deploys a Cilium Network Policy to restrict network ingress and egress to the pods deployed by this Helm Chart",
|
||||
"default": false
|
||||
},
|
||||
"alertManagerSelectorLabels": {
|
||||
"type": "object",
|
||||
"description": "The labels expected to be applied to the AlertManager pods which will send data to this service. If Cilium Network Policy is enabled, ingress to this service is only allowed from a pod matching these labels.",
|
||||
"default": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: speckle/alertmanager-discord
|
||||
pullPolicy: Always
|
||||
# -- Overrides the image tag whose default is the chart appVersion.
|
||||
tag: "latest"
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
serviceAccount:
|
||||
# -- Specifies whether a service account should be created
|
||||
create: true
|
||||
# -- Annotations to add to the service account
|
||||
annotations: {}
|
||||
# -- The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
name: ""
|
||||
|
||||
podAnnotations: {}
|
||||
|
||||
podSecurityContext:
|
||||
fsGroup: 2000
|
||||
|
||||
# -- https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/
|
||||
priorityClassName: ""
|
||||
|
||||
securityContext:
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
# -- The port to which alertmanager should push alerts
|
||||
port: 9094
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
|
||||
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
|
||||
|
||||
ciliumNetworkPolicy:
|
||||
enabled: false
|
||||
# -- the labels applied to the alertmanager which will send data to this service. If Cilium Network Policy is enabled, ingress to this service is only allowed from a pod matching these labels.
|
||||
alertManagerSelectorLabels: {}
|
||||
@@ -0,0 +1,47 @@
|
||||
module github.com/specklesystems/alertmanager-discord
|
||||
|
||||
go 1.21
|
||||
|
||||
toolchain go1.22.1
|
||||
|
||||
require (
|
||||
github.com/cenkalti/backoff/v4 v4.3.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/prometheus/client_golang v1.19.1
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/viper v1.19.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.54.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/sagikazarmark/locafero v0.6.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
@@ -0,0 +1,107 @@
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8=
|
||||
github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
|
||||
github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/specklesystems/alertmanager-discord/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package alertforwarder
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/specklesystems/alertmanager-discord/pkg/alertmanager"
|
||||
"github.com/specklesystems/alertmanager-discord/pkg/discord"
|
||||
"github.com/specklesystems/alertmanager-discord/pkg/logging"
|
||||
"github.com/specklesystems/alertmanager-discord/pkg/prometheus"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
maxLogLength = 1024
|
||||
)
|
||||
|
||||
type AlertForwarderHandler struct {
|
||||
af AlertForwarder
|
||||
}
|
||||
|
||||
func NewAlertForwarderHandler(client *http.Client, webhookURL string, maximumBackoffElapsedTime time.Duration) *AlertForwarderHandler {
|
||||
return &AlertForwarderHandler{
|
||||
af: NewAlertForwarder(client, webhookURL, maximumBackoffElapsedTime),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AlertForwarderHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.af.TransformAndForward(w, r)
|
||||
}
|
||||
|
||||
type AlertForwarder struct {
|
||||
client *discord.Client
|
||||
}
|
||||
|
||||
func NewAlertForwarder(client *http.Client, webhookURL string, maximumBackoffElapsedTime time.Duration) AlertForwarder {
|
||||
return AlertForwarder{
|
||||
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().
|
||||
Str(logging.FieldKeyCorrelationId, correlationId).
|
||||
Msg("There are no alerts within this notification. There is nothing to forward to Discord. Returning early...")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
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 af.groupAlerts(amo) {
|
||||
DO := TranslateAlertManagerToDiscord(status, amo, alerts)
|
||||
|
||||
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("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.")
|
||||
failedToPublishAtLeastOne = true
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Info().
|
||||
Str(logging.FieldKeyEventType, logging.EventTypeResponseReceived).
|
||||
Str(logging.FieldKeyCorrelationId, correlationId).
|
||||
Msg("HTTP response received from Discord")
|
||||
|
||||
if res.StatusCode < 200 || res.StatusCode > 399 {
|
||||
failedToPublishAtLeastOne = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if failedToPublishAtLeastOne {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (af *AlertForwarder) sendRawPromAlertWarn(correlationId string) (*http.Response, error) {
|
||||
|
||||
warningMessage := `You have probably misconfigured this software.
|
||||
We detected input in Prometheus Alert format but are expecting AlertManager format.
|
||||
This program is intended to ingest alerts from alertmanager.
|
||||
It is not a replacement for alertmanager, it is a
|
||||
webhook target for it. Please read the README.md
|
||||
for guidance on how to configure it for alertmanager
|
||||
or https://prometheus.io/docs/alerting/latest/configuration/#webhook_config`
|
||||
log.Warn().Msg(warningMessage)
|
||||
DO := discord.Out{
|
||||
Content: "",
|
||||
Embeds: []discord.Embed{
|
||||
{
|
||||
Title: "You have misconfigured this software",
|
||||
Description: warningMessage,
|
||||
Color: discord.ColorGrey,
|
||||
Fields: []discord.EmbedField{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
log.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 {
|
||||
return nil, fmt.Errorf("error encountered when publishing message to Discord: %w", err)
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str(logging.FieldKeyEventType, logging.EventTypeResponseReceived).
|
||||
Str(logging.FieldKeyCorrelationId, correlationId).
|
||||
Msg("HTTP response received from Discord")
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (af *AlertForwarder) TransformAndForward(w http.ResponseWriter, r *http.Request) {
|
||||
correlationId := uuid.New().String()
|
||||
log.Info().
|
||||
Str(logging.FieldKeyHttpHost, r.Host).
|
||||
Str(logging.FieldKeyHttpMethod, r.Method).
|
||||
Str(logging.FieldKeyHttpPath, r.URL.Path).
|
||||
Str(logging.FieldKeyEventType, logging.EventTypeRequestReceived).
|
||||
Str(logging.FieldKeyCorrelationId, correlationId).
|
||||
Msg("HTTP request received from AlertManager.")
|
||||
defer log.Info().
|
||||
Str(logging.FieldKeyEventType, logging.EventTypeResponseSending).
|
||||
Str(logging.FieldKeyCorrelationId, correlationId).
|
||||
Msg("Sending HTTP response to AlertManager.")
|
||||
|
||||
b, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Str(logging.FieldKeyCorrelationId, correlationId).
|
||||
Err(err).
|
||||
Msg("Unable to read request body.")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
amo := alertmanager.Out{}
|
||||
err = json.Unmarshal(b, &amo)
|
||||
if err != nil {
|
||||
af.handleInvalidInput(correlationId, b, w)
|
||||
return
|
||||
}
|
||||
|
||||
af.sendWebhook(correlationId, &amo, w)
|
||||
}
|
||||
|
||||
func (af *AlertForwarder) handleInvalidInput(correlationId string, b []byte, w http.ResponseWriter) {
|
||||
if prometheus.IsAlert(b) {
|
||||
log.Info().
|
||||
Str(logging.FieldKeyCorrelationId, correlationId).
|
||||
Msg("Detected a Prometheus Alert, and not an AlertManager alert, has been sent within the http request. This indicates a misconfiguration. Attempting to send a message to notify the Discord channel of the misconfiguration.")
|
||||
res, err := af.sendRawPromAlertWarn(correlationId)
|
||||
if err != nil || (res != nil && res.StatusCode < 200 || res.StatusCode > 399) {
|
||||
statusCode := 0
|
||||
if res != nil {
|
||||
statusCode = res.StatusCode
|
||||
}
|
||||
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str(logging.FieldKeyCorrelationId, correlationId).
|
||||
Int(logging.FieldKeyStatusCode, statusCode).
|
||||
Msg("Error when attempting to send a warning message to Discord.")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
if len(b) > maxLogLength-3 {
|
||||
log.Info().
|
||||
Str(logging.FieldKeyCorrelationId, correlationId).
|
||||
Msgf("Failed to unpack inbound alert request - %s...", string(b[:maxLogLength-3]))
|
||||
} else {
|
||||
log.Info().
|
||||
Str(logging.FieldKeyCorrelationId, correlationId).
|
||||
Msgf("Failed to unpack inbound alert request - %s", string(b))
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
package alertforwarder
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/specklesystems/alertmanager-discord/pkg/alertmanager"
|
||||
"github.com/specklesystems/alertmanager-discord/pkg/discord"
|
||||
"github.com/specklesystems/alertmanager-discord/pkg/prometheus"
|
||||
. "github.com/specklesystems/alertmanager-discord/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_TransformAndForward_HappyPath(t *testing.T) {
|
||||
ao := alertmanager.Out{
|
||||
Alerts: []alertmanager.Alert{
|
||||
{
|
||||
Status: alertmanager.StatusFiring,
|
||||
},
|
||||
},
|
||||
CommonAnnotations: struct {
|
||||
Summary string `json:"summary"`
|
||||
}{
|
||||
Summary: "a_common_annotation_summary",
|
||||
},
|
||||
}
|
||||
|
||||
mockClientRecorder, res := triggerAndRecordRequest(t, ao, http.StatusOK)
|
||||
defer res.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode, "http response status code")
|
||||
|
||||
assert.Equal(t, 1, len(mockClientRecorder.Requests), "Should have sent one request to Discord")
|
||||
assert.Equal(t, "application/json", mockClientRecorder.Requests[0].ContentType, "content type")
|
||||
|
||||
do := readerToDiscordOut(t, mockClientRecorder.Requests[0].Body)
|
||||
assert.Equal(t, 1, len(do.Embeds), "Discord message embed length")
|
||||
assert.Equal(t, 10038562, do.Embeds[0].Color, "Discord message embed color")
|
||||
assert.Contains(t, do.Content, "a_common_annotation_summary", "Discord message content")
|
||||
}
|
||||
|
||||
func Test_TransformAndForward_InvalidInput_NoValue_ReturnsErrorResponseCode(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(""))
|
||||
req.Host = "testing.localhost"
|
||||
|
||||
mockClientRecorder := MockClientRecorder{}
|
||||
mockClient := mockClientRecorder.NewMockClientWithResponse(http.StatusBadRequest)
|
||||
|
||||
SUT := NewAlertForwarder(mockClient, "https://discordapp.com/api/webhooks/123456789123456789/abc", 100*time.Millisecond)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
SUT.TransformAndForward(w, req)
|
||||
|
||||
res := w.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, res.StatusCode, "Should expect an http response status code indicating request was bad.")
|
||||
|
||||
assert.Equal(t, 0, len(mockClientRecorder.Requests), "should not have sent a request to Discord")
|
||||
}
|
||||
|
||||
func Test_TransformAndForward_InvalidInput_LongString_ReturnsErrorResponseCode(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(strings.Repeat("a", 1025)))
|
||||
req.Host = "testing.localhost"
|
||||
|
||||
mockClientRecorder := MockClientRecorder{}
|
||||
mockClient := mockClientRecorder.NewMockClientWithResponse(http.StatusBadRequest)
|
||||
|
||||
SUT := NewAlertForwarder(mockClient, "https://discordapp.com/api/webhooks/123456789123456789/abc", 100*time.Millisecond)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
SUT.TransformAndForward(w, req)
|
||||
|
||||
res := w.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, res.StatusCode, "Should expect an http response status code indicating request was bad.")
|
||||
|
||||
assert.Equal(t, 0, len(mockClientRecorder.Requests), "should not have sent a request to Discord")
|
||||
}
|
||||
|
||||
func Test_TransformAndForward_InvalidInput_PrometheusAlert_ReturnsErrorResponseCode(t *testing.T) {
|
||||
promAlert := []prometheus.Alert{
|
||||
{
|
||||
Status: "",
|
||||
},
|
||||
}
|
||||
promAlertJson, err := json.Marshal(promAlert)
|
||||
assert.NoError(t, err, "marshalling prometheus alert")
|
||||
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(promAlertJson))
|
||||
req.Host = "testing.localhost"
|
||||
|
||||
mockClientRecorder := MockClientRecorder{}
|
||||
mockClient := mockClientRecorder.NewMockClientWithResponse(http.StatusBadRequest)
|
||||
|
||||
SUT := NewAlertForwarder(mockClient, "https://discordapp.com/api/webhooks/123456789123456789/abc", 100*time.Millisecond)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
SUT.TransformAndForward(w, req)
|
||||
|
||||
res := w.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusInternalServerError, res.StatusCode, "Should expect an http response status code indicating server internal error.")
|
||||
|
||||
assert.Equal(t, 1, len(mockClientRecorder.Requests), "should have sent one request to Discord (with a message stating there is a problem)")
|
||||
// TODO test message content sent to Discord
|
||||
}
|
||||
|
||||
// FIXME may not be able to simulate error in http Client?
|
||||
func Test_TransformAndForward_PrometheusAlert_And_DiscordClientResponsdsWithError_RespondsWithErrorCode(t *testing.T) {
|
||||
promAlert := []prometheus.Alert{
|
||||
{
|
||||
Status: "",
|
||||
},
|
||||
}
|
||||
promAlertJson, err := json.Marshal(promAlert)
|
||||
assert.NoError(t, err, "marshalling prometheus alert")
|
||||
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(promAlertJson))
|
||||
req.Host = "testing.localhost"
|
||||
|
||||
mockClientRecorder := MockClientRecorder{}
|
||||
mockClient := mockClientRecorder.NewMockClientReturnsNil()
|
||||
|
||||
SUT := NewAlertForwarder(mockClient, "https://discordapp.com/api/webhooks/123456789123456789/abc", 100*time.Millisecond)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
SUT.TransformAndForward(w, req)
|
||||
|
||||
res := w.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusInternalServerError, res.StatusCode, "Should expect an http response status code indicating request was unprocessable.")
|
||||
|
||||
assert.GreaterOrEqual(t, len(mockClientRecorder.Requests), 1, "should have sent at least one request to Discord (with a message stating there is a problem)")
|
||||
// TODO test message content sent to Discord
|
||||
}
|
||||
|
||||
func Test_TransformAndForward_PrometheusAlert_And_DiscordClientResponsdsWithErrorStatusCode_RespondsWithErrorStatusCode(t *testing.T) {
|
||||
promAlert := []prometheus.Alert{
|
||||
{
|
||||
Status: "",
|
||||
},
|
||||
}
|
||||
promAlertJson, err := json.Marshal(promAlert)
|
||||
assert.NoError(t, err, "marshalling prometheus alert")
|
||||
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(promAlertJson))
|
||||
req.Host = "testing.localhost"
|
||||
|
||||
mockClientRecorder := MockClientRecorder{}
|
||||
mockClient := mockClientRecorder.NewMockClientWithResponse(http.StatusBadRequest)
|
||||
|
||||
SUT := NewAlertForwarder(mockClient, "https://discordapp.com/api/webhooks/123456789123456789/abc", 100*time.Millisecond)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
SUT.TransformAndForward(w, req)
|
||||
|
||||
res := w.Result()
|
||||
defer res.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusInternalServerError, res.StatusCode, "Should expect an http response status code indicating internal server error.")
|
||||
|
||||
assert.Equal(t, 1, len(mockClientRecorder.Requests), "should have sent a request to Discord (with a message stating there is a problem)")
|
||||
// TODO test message content sent to Discord
|
||||
}
|
||||
|
||||
func Test_TransformAndForward_NoAlerts_DoesNotSendToDiscord(t *testing.T) {
|
||||
ao := alertmanager.Out{}
|
||||
|
||||
mockClientRecorder, res := triggerAndRecordRequest(t, ao, http.StatusBadRequest)
|
||||
defer res.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode, "http response status code")
|
||||
|
||||
assert.Equal(t, 0, len(mockClientRecorder.Requests), "mock client should not be triggered")
|
||||
}
|
||||
|
||||
func Test_TransformAndForward_NoCommonAnnotationSummary_HappyPath(t *testing.T) {
|
||||
ao := alertmanager.Out{
|
||||
Alerts: []alertmanager.Alert{
|
||||
{
|
||||
Status: alertmanager.StatusFiring,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mockClientRecorder, res := triggerAndRecordRequest(t, ao, http.StatusOK)
|
||||
defer res.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode, "http response status code")
|
||||
|
||||
assert.Equal(t, 1, len(mockClientRecorder.Requests), "mock client should be triggered")
|
||||
assert.Equal(t, "application/json", mockClientRecorder.Requests[0].ContentType, "content type")
|
||||
|
||||
do := readerToDiscordOut(t, mockClientRecorder.Requests[0].Body)
|
||||
assert.Equal(t, 1, len(do.Embeds), "Discord message embed length")
|
||||
assert.Equal(t, 10038562, do.Embeds[0].Color, "Discord message embed color")
|
||||
assert.Equal(t, "", do.Content, "Discord message content")
|
||||
}
|
||||
|
||||
func Test_TransformAndForward_StatusResolved_HappyPath(t *testing.T) {
|
||||
ao := alertmanager.Out{
|
||||
Alerts: []alertmanager.Alert{
|
||||
{
|
||||
Status: alertmanager.StatusResolved,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mockClientRecorder, res := triggerAndRecordRequest(t, ao, http.StatusOK)
|
||||
defer res.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode, "http response status code")
|
||||
|
||||
assert.Equal(t, 1, len(mockClientRecorder.Requests), "mock client should be triggered")
|
||||
|
||||
do := readerToDiscordOut(t, mockClientRecorder.Requests[0].Body)
|
||||
assert.Equal(t, 1, len(do.Embeds), "Discord message embed length")
|
||||
assert.Equal(t, 3066993, do.Embeds[0].Color, "Discord message embed color")
|
||||
}
|
||||
|
||||
// alert with a label 'instance'='localhost' and 'exported_instance' label is set, should have the instance replaced by 'exported_instance'
|
||||
func Test_TransformAndForward_Annotations_AreAddedAsEmbedFields_HappyPath(t *testing.T) {
|
||||
ao := alertmanager.Out{
|
||||
Alerts: []alertmanager.Alert{
|
||||
{
|
||||
Status: alertmanager.StatusFiring,
|
||||
Annotations: map[string]string{
|
||||
"environment": "development",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mockClientRecorder, res := triggerAndRecordRequest(t, ao, http.StatusOK)
|
||||
defer res.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode, "http response status code")
|
||||
|
||||
assert.Equal(t, 1, len(mockClientRecorder.Requests), "mock client should be triggered")
|
||||
assert.Equal(t, "application/json", mockClientRecorder.Requests[0].ContentType, "content type")
|
||||
|
||||
do := readerToDiscordOut(t, mockClientRecorder.Requests[0].Body)
|
||||
assert.Equal(t, 1, len(do.Embeds), "Discord message embed length")
|
||||
assert.Equal(t, 10038562, do.Embeds[0].Color, "Discord message embed color")
|
||||
assert.Equal(t, 1, len(do.Embeds[0].Fields), "Discord message embed fields length")
|
||||
assert.Contains(t, do.Embeds[0].Fields[0].Value, "development", "Discord message embed field Name should contain attribute")
|
||||
assert.Equal(t, "", do.Content, "Discord message content")
|
||||
}
|
||||
|
||||
// FIXME may not be able to create an error in http.Client
|
||||
// Discord client returns an error (e.g. a closed connection, network outage or similar)
|
||||
func Test_TransformAndForward_DiscordClientReturnsError(t *testing.T) {
|
||||
ao := alertmanager.Out{
|
||||
Alerts: []alertmanager.Alert{
|
||||
{
|
||||
Status: alertmanager.StatusFiring,
|
||||
},
|
||||
},
|
||||
CommonAnnotations: struct {
|
||||
Summary string `json:"summary"`
|
||||
}{
|
||||
Summary: "a_common_annotation_summary",
|
||||
},
|
||||
}
|
||||
|
||||
mockClientRecorder, res := triggerAndRecordRequest(t, ao, http.StatusBadRequest)
|
||||
defer res.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusInternalServerError, res.StatusCode, "http response status code")
|
||||
|
||||
assert.GreaterOrEqual(t, len(mockClientRecorder.Requests), 1, "Should have sent a request to Discord")
|
||||
assert.Equal(t, "application/json", mockClientRecorder.Requests[0].ContentType, "content type")
|
||||
|
||||
do := readerToDiscordOut(t, mockClientRecorder.Requests[0].Body)
|
||||
assert.Equal(t, 1, len(do.Embeds), "Discord message embed length")
|
||||
assert.Equal(t, 10038562, do.Embeds[0].Color, "Discord message embed color")
|
||||
assert.Contains(t, do.Content, "a_common_annotation_summary", "Discord message content")
|
||||
}
|
||||
|
||||
func Test_TransformAndForward_DiscordReturnsWithErrorStatusCode_ReturnInternalServerErrorStatusCode(t *testing.T) {
|
||||
ao := alertmanager.Out{
|
||||
Alerts: []alertmanager.Alert{
|
||||
{
|
||||
Status: alertmanager.StatusFiring,
|
||||
},
|
||||
},
|
||||
CommonAnnotations: struct {
|
||||
Summary string `json:"summary"`
|
||||
}{
|
||||
Summary: "a_common_annotation_summary",
|
||||
},
|
||||
}
|
||||
|
||||
mockClientRecorder, res := triggerAndRecordRequest(t, ao, http.StatusUnauthorized)
|
||||
defer res.Body.Close()
|
||||
|
||||
assert.Equal(t, 1, len(mockClientRecorder.Requests), "Should have sent a request to Discord")
|
||||
assert.Equal(t, "application/json", mockClientRecorder.Requests[0].ContentType, "content type")
|
||||
|
||||
assert.Equal(t, http.StatusInternalServerError, res.StatusCode, "http response status code should be 500")
|
||||
}
|
||||
|
||||
// TODO Add a test for context with multiple alerts: if some are firing and some resolved we should publish two separate messages to Discord - alerts with matching statuses should be grouped together
|
||||
func Test_TransformAndForward_MultipleAlerts_DifferentStatus_HappyPath(t *testing.T) {
|
||||
ao := alertmanager.Out{
|
||||
Alerts: []alertmanager.Alert{
|
||||
{
|
||||
Status: alertmanager.StatusFiring,
|
||||
},
|
||||
{
|
||||
Status: alertmanager.StatusFiring,
|
||||
},
|
||||
{
|
||||
Status: alertmanager.StatusResolved,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mockClientRecorder, res := triggerAndRecordRequest(t, ao, http.StatusOK)
|
||||
defer res.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode, "http response status code")
|
||||
|
||||
assert.Equal(t, 2, len(mockClientRecorder.Requests), "Should have sent two requests to Discord")
|
||||
}
|
||||
|
||||
// HELPERS
|
||||
|
||||
func triggerAndRecordRequest(t *testing.T, request alertmanager.Out, discordStatusCode int) (mockClientRecorder MockClientRecorder, httpResponse *http.Response) {
|
||||
aoJson, err := json.Marshal(request)
|
||||
assert.NoError(t, err, "marshalling alertmanager out")
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(aoJson))
|
||||
req.Host = "testing.localhost"
|
||||
|
||||
mockClientRecorder = MockClientRecorder{}
|
||||
mockClient := mockClientRecorder.NewMockClientWithResponse(discordStatusCode)
|
||||
|
||||
SUT := NewAlertForwarder(mockClient, "https://discordapp.com/api/webhooks/123456789123456789/abc", 100*time.Millisecond)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
SUT.TransformAndForward(w, req)
|
||||
|
||||
httpResponse = w.Result()
|
||||
return mockClientRecorder, httpResponse
|
||||
}
|
||||
|
||||
func readerToDiscordOut(t *testing.T, reader io.Reader) discord.Out {
|
||||
buf := new(bytes.Buffer)
|
||||
buf.ReadFrom(reader)
|
||||
|
||||
do := discord.Out{}
|
||||
err := json.Unmarshal(buf.Bytes(), &do)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error marshalling to Discord Object from the Discord client request body.")
|
||||
}
|
||||
return do
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package alertforwarder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/specklesystems/alertmanager-discord/pkg/flags"
|
||||
)
|
||||
|
||||
func CheckWebhookURL(webhookURL string) (bool, *url.URL, error) {
|
||||
if webhookURL == "" {
|
||||
return false, &url.URL{}, fmt.Errorf("'%s' has not been set. This can be provided via configuration file, command line, or environment variable ('%s')", flags.DiscordWebhookUrlFlagKey, strings.ToUpper(flags.DiscordWebhookUrlFlagKey))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
host, _, _ := net.SplitHostPort(parsedUrl.Host)
|
||||
if host == "" {
|
||||
host = parsedUrl.Host
|
||||
}
|
||||
|
||||
// localhost is allowed, for testing or for proxied routes etc..
|
||||
if host == "127.0.0.1" || host == "::1" || host == "localhost" {
|
||||
return true, parsedUrl, nil
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`https://discord(?:app)?.com/api/webhooks/[0-9]{18,19}/[a-zA-Z0-9_-]+`)
|
||||
|
||||
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 ok, parsedUrl, nil
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package alertforwarder
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_WebhookUrl_HappyPath(t *testing.T) {
|
||||
ok, _, _ := CheckWebhookURL("https://discordapp.com/api/webhooks/123456789123456789/abc")
|
||||
assert.True(t, ok, "Should be a valid webhook url")
|
||||
|
||||
ok, _, _ = CheckWebhookURL("https://discord.com/api/webhooks/123456789123456789/abc")
|
||||
assert.True(t, ok, "Should be a valid webhook url")
|
||||
|
||||
ok, _, _ = CheckWebhookURL("http://localhost/")
|
||||
assert.True(t, ok, "Should be a valid webhook url")
|
||||
|
||||
ok, _, _ = CheckWebhookURL("http://127.0.0.1/")
|
||||
assert.True(t, ok, "Should be a valid webhook url")
|
||||
|
||||
ok, _, _ = CheckWebhookURL("http://::1/")
|
||||
assert.True(t, ok, "Should be a valid webhook url")
|
||||
}
|
||||
|
||||
func Test_WebhookUrl_EmptyUrl_ReturnsFalse(t *testing.T) {
|
||||
ok, _, err := CheckWebhookURL("")
|
||||
assert.False(t, ok, "Empty url should be identified as invalid")
|
||||
assert.Error(t, err, "Empty url should return an error message")
|
||||
}
|
||||
|
||||
func Test_WebhookUrl_InvalidUrl_ReturnsFalse(t *testing.T) {
|
||||
ok, _, err := CheckWebhookURL("::::::::::")
|
||||
assert.False(t, ok, "Malformed urls should be identified as invalid")
|
||||
assert.Error(t, err, "Invalid url should return an error message")
|
||||
}
|
||||
|
||||
func Test_WebhookUrl_InvalidAPIUrl_ReturnsFalse(t *testing.T) {
|
||||
ok, _, err := CheckWebhookURL("https://discordapp.com/api/webhooks/12/abc")
|
||||
assert.False(t, ok, "Malformed Discord API urls should be identified as invalid")
|
||||
assert.Error(t, err, "Malformed Discord API url should return an error message")
|
||||
|
||||
ok, _, err = CheckWebhookURL("https://example.org/api/webhooks/12/abc")
|
||||
assert.False(t, ok, "Non-Discord urls should be identified as invalid")
|
||||
assert.Error(t, err, "Non-Discord urls should return an error message")
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package alertforwarder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/specklesystems/alertmanager-discord/pkg/alertmanager"
|
||||
"github.com/specklesystems/alertmanager-discord/pkg/discord"
|
||||
)
|
||||
|
||||
const (
|
||||
keySummary = "summary"
|
||||
keyEnvironmentType = "source_environment_type"
|
||||
keyEnvironmentName = "source_environment_name"
|
||||
)
|
||||
|
||||
func TranslateAlertManagerToDiscord(status string, amo *alertmanager.Out, alerts []alertmanager.Alert) discord.Out {
|
||||
DO := discord.Out{}
|
||||
|
||||
if amo.CommonAnnotations.Summary != "" {
|
||||
DO.Content = fmt.Sprintf(" === %s === \n", amo.CommonAnnotations.Summary)
|
||||
}
|
||||
|
||||
RichEmbed := discord.Embed{
|
||||
Title: fmt.Sprintf("[%s: %d] %s", strings.ToUpper(status), len(alerts), amo.CommonLabels.Alertname),
|
||||
Description: amo.CommonAnnotations.Summary,
|
||||
Color: discord.ColorGrey,
|
||||
Fields: []discord.EmbedField{},
|
||||
}
|
||||
|
||||
switch status {
|
||||
case alertmanager.StatusFiring:
|
||||
RichEmbed.Color = discord.ColorRed
|
||||
case alertmanager.StatusResolved:
|
||||
RichEmbed.Color = discord.ColorGreen
|
||||
}
|
||||
|
||||
for _, alert := range alerts {
|
||||
fieldName := fmt.Sprintf("[%s/%s] Alert details", alert.Labels[keyEnvironmentType], alert.Labels[keyEnvironmentName])
|
||||
if summary, ok := alert.Annotations[keySummary]; ok {
|
||||
fieldName = fmt.Sprintf("[%s/%s] %s", alert.Labels[keyEnvironmentType], alert.Labels[keyEnvironmentName], summary)
|
||||
}
|
||||
|
||||
var details strings.Builder
|
||||
details.WriteString("Annotations:\n")
|
||||
|
||||
// sort into alphabetical order
|
||||
annotationKeys := make([]string, 0, len(alert.Annotations))
|
||||
for key := range alert.Annotations {
|
||||
annotationKeys = append(annotationKeys, key)
|
||||
}
|
||||
sort.Strings(annotationKeys)
|
||||
|
||||
for _, key := range annotationKeys {
|
||||
if key == keySummary {
|
||||
// if there is a summary, it is already the field name so no need to repeat it
|
||||
continue
|
||||
}
|
||||
details.WriteString(fmt.Sprintf("\t%s: %s\n", key, alert.Annotations[key]))
|
||||
}
|
||||
|
||||
details.WriteString("Labels:\n")
|
||||
|
||||
// sort into alphabetical order
|
||||
labelKeys := make([]string, 0, len(alert.Labels))
|
||||
for key := range alert.Labels {
|
||||
labelKeys = append(labelKeys, key)
|
||||
}
|
||||
sort.Strings(labelKeys)
|
||||
|
||||
for _, key := range labelKeys {
|
||||
if key == keyEnvironmentName || key == keyEnvironmentType {
|
||||
// if these keys exist, we have already added them to the field name
|
||||
continue
|
||||
}
|
||||
details.WriteString(fmt.Sprintf("\t%s: %s\n", key, alert.Labels[key]))
|
||||
}
|
||||
|
||||
RichEmbed.Fields = append(RichEmbed.Fields, discord.EmbedField{
|
||||
Name: fieldName,
|
||||
Value: details.String(),
|
||||
})
|
||||
}
|
||||
|
||||
DO.Embeds = []discord.Embed{RichEmbed}
|
||||
|
||||
return DO
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package alertmanager
|
||||
|
||||
const (
|
||||
StatusFiring = "firing"
|
||||
StatusResolved = "resolved"
|
||||
)
|
||||
|
||||
type Alert struct {
|
||||
Annotations map[string]string `json:"annotations"`
|
||||
EndsAt string `json:"endsAt"`
|
||||
GeneratorURL string `json:"generatorURL"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
StartsAt string `json:"startsAt"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type Out struct {
|
||||
Alerts []Alert `json:"alerts"`
|
||||
CommonAnnotations struct {
|
||||
Summary string `json:"summary"`
|
||||
} `json:"commonAnnotations"`
|
||||
CommonLabels struct {
|
||||
Alertname string `json:"alertname"`
|
||||
} `json:"commonLabels"`
|
||||
ExternalURL string `json:"externalURL"`
|
||||
GroupKey string `json:"groupKey"`
|
||||
GroupLabels struct {
|
||||
Alertname string `json:"alertname"`
|
||||
} `json:"groupLabels"`
|
||||
Receiver string `json:"receiver"`
|
||||
Status string `json:"status"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package discord
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
backoff "github.com/cenkalti/backoff/v4"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultMaximumBackoffElapsedTime = 10 * time.Second
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
httpClient *http.Client
|
||||
URL string
|
||||
maximumBackoffElapsedTime time.Duration
|
||||
}
|
||||
|
||||
func NewClient(client *http.Client, url string, maximumBackoffElapsedTime time.Duration) *Client {
|
||||
if maximumBackoffElapsedTime <= 0 {
|
||||
maximumBackoffElapsedTime = DefaultMaximumBackoffElapsedTime
|
||||
}
|
||||
|
||||
underlyingTransport := http.DefaultTransport
|
||||
if client.Transport != nil {
|
||||
underlyingTransport = client.Transport
|
||||
}
|
||||
|
||||
// wrap instrumentation around the existing http.Client transport
|
||||
client.Transport = promhttp.InstrumentRoundTripperInFlight(RequestsToDiscordInFlight,
|
||||
promhttp.InstrumentRoundTripperCounter(RequestsToDiscordTotal,
|
||||
promhttp.InstrumentRoundTripperDuration(RequestsToDiscordDuration,
|
||||
underlyingTransport,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
return &Client{
|
||||
httpClient: client,
|
||||
URL: url,
|
||||
maximumBackoffElapsedTime: maximumBackoffElapsedTime,
|
||||
}
|
||||
}
|
||||
|
||||
func (dc *Client) PublishMessage(message Out) (*http.Response, error) {
|
||||
DOD, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error encountered when marshalling object to json. We will not continue posting to Discord. Discord Out object: '%v+'. Error: %w", message, err)
|
||||
}
|
||||
|
||||
var response *http.Response
|
||||
|
||||
operation := func() error {
|
||||
res, err := dc.httpClient.Post(dc.URL, "application/json", bytes.NewReader(DOD))
|
||||
if err == nil {
|
||||
response = res
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
exponential := backoff.NewExponentialBackOff()
|
||||
exponential.MaxElapsedTime = dc.maximumBackoffElapsedTime
|
||||
err = backoff.Retry(operation, exponential)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error encountered sending POST to '%s'. Error: %w", dc.URL, err)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package discord
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
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.",
|
||||
})
|
||||
|
||||
RequestsToDiscordTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "discord_client_requests_total",
|
||||
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.",
|
||||
Buckets: prometheus.DefBuckets,
|
||||
}, []string{"code"})
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
package discord
|
||||
|
||||
// Discord color values
|
||||
const (
|
||||
ColorRed = 0x992D22
|
||||
ColorGreen = 0x2ECC71
|
||||
ColorGrey = 0x95A5A6
|
||||
)
|
||||
|
||||
type Out struct {
|
||||
Content string `json:"content"`
|
||||
Embeds []Embed `json:"embeds"`
|
||||
}
|
||||
|
||||
type Embed struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Color int `json:"color"`
|
||||
Fields []EmbedField `json:"fields"`
|
||||
}
|
||||
|
||||
type EmbedField struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package flags
|
||||
|
||||
const (
|
||||
ConfigurationPathFlagKey = "configuration_file_path"
|
||||
DiscordWebhookUrlFlagKey = "discord_webhook_url"
|
||||
ListenAddressFlagKey = "listen_address"
|
||||
MaxBackoffTimeSecondsFlagKey = "max_backoff_time_seconds"
|
||||
LogLevelFlagKey = "log_level"
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package logging
|
||||
|
||||
const (
|
||||
// field values should be lowercase snake_case where possible
|
||||
EventTypeRequestReceived = "received_request"
|
||||
EventTypeRequestSending = "sending_request"
|
||||
EventTypeResponseReceived = "received_response"
|
||||
EventTypeResponseSending = "sending_response"
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
package logging
|
||||
|
||||
const (
|
||||
// field keys should be lowercase snake_case.
|
||||
FieldKeyHttpHost = "host"
|
||||
FieldKeyHttpMethod = "method"
|
||||
FieldKeyHttpPath = "path"
|
||||
FieldKeyEventType = "event_type"
|
||||
FieldKeyAlertName = "alert_name"
|
||||
FieldKeyCorrelationId = "correlation_id"
|
||||
FieldKeyStatusCode = "status_code"
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
var (
|
||||
RequestsToAlertForwarderInFlight = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "alertmanager_discord_alert_forwarder_requests_in_flight",
|
||||
Help: "The current number of events being processed by alert forwarder.",
|
||||
})
|
||||
|
||||
RequestsToAlertForwarderTotal = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "alertmanager_discord_alert_forwarder_requests_total",
|
||||
Help: "The total number of http requests processed by alert forwarder.",
|
||||
}, []string{"code"})
|
||||
|
||||
RequestsToAlertForwarderDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Name: "alertmanager_discord_alert_forwarder_request_duration_seconds",
|
||||
Help: "Duration of all http requests processed by alert forwarder.",
|
||||
Buckets: prometheus.DefBuckets,
|
||||
}, []string{"code"})
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
package prometheus
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type Alert struct {
|
||||
Annotations struct {
|
||||
Description string `json:"description"`
|
||||
Summary string `json:"summary"`
|
||||
} `json:"annotations"`
|
||||
EndsAt string `json:"endsAt"`
|
||||
GeneratorURL string `json:"generatorURL"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
StartsAt string `json:"startsAt"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
func IsAlert(b []byte) bool {
|
||||
alertTest := make([]Alert, 0)
|
||||
err := json.Unmarshal(b, &alertTest)
|
||||
if err == nil {
|
||||
|
||||
if len(alertTest) != 0 {
|
||||
if alertTest[0].Status == "" {
|
||||
// Ok it's more than likely then
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
//go:build !unit
|
||||
// +build !unit
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/specklesystems/alertmanager-discord/pkg/alertmanager"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
serverListenAddress = "127.0.0.1:9096"
|
||||
)
|
||||
|
||||
func Test_Serve_HappyPath(t *testing.T) {
|
||||
// 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
|
||||
}))
|
||||
defer mockDiscordServer.Close()
|
||||
|
||||
amds := AlertManagerDiscordServer{}
|
||||
defer func() {
|
||||
err := amds.Shutdown()
|
||||
assert.NoError(t, err, "server shutdown should not error")
|
||||
}()
|
||||
|
||||
_, err := amds.ListenAndServe(mockDiscordServer.URL, serverListenAddress)
|
||||
assert.NoError(t, err, "server ListenAndServe should not error")
|
||||
|
||||
client := http.Client{
|
||||
Timeout: 500 * time.Millisecond,
|
||||
}
|
||||
|
||||
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
|
||||
ao := alertmanager.Out{
|
||||
Alerts: []alertmanager.Alert{
|
||||
{
|
||||
Status: alertmanager.StatusFiring,
|
||||
},
|
||||
},
|
||||
CommonAnnotations: struct {
|
||||
Summary string `json:"summary"`
|
||||
}{
|
||||
Summary: "a_common_annotation_summary",
|
||||
},
|
||||
GroupLabels: struct {
|
||||
Alertname string `json:"alertname"`
|
||||
}{
|
||||
Alertname: "testAlertName",
|
||||
},
|
||||
}
|
||||
|
||||
aoJson, err := json.Marshal(ao)
|
||||
assert.NoError(t, err, "marshalling alertmanager out")
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("http://%s/", serverListenAddress), bytes.NewReader(aoJson))
|
||||
assert.NoError(t, err, "creating http request")
|
||||
req.Host = mockDiscordServer.URL
|
||||
|
||||
res, err = client.Do(req)
|
||||
defer func() {
|
||||
if res != nil && res.Body != nil {
|
||||
res.Body.Close()
|
||||
}
|
||||
}()
|
||||
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
|
||||
|
||||
// TODO assert log lines were generated
|
||||
|
||||
// TODO assert prometheus metrics were generated
|
||||
}
|
||||
|
||||
// Test with invalid URL, throws an error
|
||||
func Test_Server_InvalidDiscordUrl(t *testing.T) {
|
||||
amds := AlertManagerDiscordServer{}
|
||||
defer func() {
|
||||
err := amds.Shutdown()
|
||||
assert.NoError(t, err, "server shutdown should not error")
|
||||
}()
|
||||
|
||||
_, err := amds.ListenAndServe("https://example.org/not/a/discord/webhook/api", "127.0.0.1:9095")
|
||||
assert.Error(t, err, "server ListenAndServe should return an error for an invalid url")
|
||||
}
|
||||
|
||||
// // Commented out as some interaction with the Server_HappyPath test causes that to fail ~5% of runs
|
||||
// func Test_Server_With_EmptyListenAddress_DefaultsToListenAddress(t *testing.T) {
|
||||
// amds := AlertManagerDiscordServer{}
|
||||
// defer func() {
|
||||
// err := amds.Shutdown()
|
||||
// NoError(t, err, "server shutdown should not error")
|
||||
// }()
|
||||
|
||||
// _, err := amds.ListenAndServe("http://localhost/", "")
|
||||
// NoError(t, err, "server ListenAndServe should not error")
|
||||
|
||||
// client := http.Client{
|
||||
// Timeout: 500 * time.Millisecond,
|
||||
// }
|
||||
|
||||
// // it should have defaulted to the default listen address
|
||||
// res, err := client.Get(fmt.Sprintf("http://%s/liveness", "127.0.0.1:9094"))
|
||||
// NotNil(t, res, "Response should not be nil")
|
||||
// EqualInt(t, http.StatusOK, res.StatusCode, "Liveness probe should return status code OK (200)")
|
||||
// }
|
||||
@@ -0,0 +1,119 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
"github.com/specklesystems/alertmanager-discord/pkg/alertforwarder"
|
||||
"github.com/specklesystems/alertmanager-discord/pkg/metrics"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultListenAddress = "0.0.0.0:9094"
|
||||
)
|
||||
const (
|
||||
FaviconPath = "/favicon.ico"
|
||||
LivenessPath = "/liveness"
|
||||
ReadinessPath = "/readiness"
|
||||
)
|
||||
|
||||
type AlertManagerDiscordServer struct {
|
||||
httpServer *http.Server
|
||||
MaximumBackoffTimeSeconds time.Duration
|
||||
}
|
||||
|
||||
func (amds *AlertManagerDiscordServer) ListenAndServe(webhookUrl, listenAddress string) (chan os.Signal, error) {
|
||||
stop := make(chan os.Signal, 1)
|
||||
mux := http.NewServeMux()
|
||||
|
||||
ok, _, err := alertforwarder.CheckWebhookURL(webhookUrl)
|
||||
if !ok {
|
||||
return stop, fmt.Errorf("url is invalid: %w", err)
|
||||
}
|
||||
|
||||
if listenAddress == "" {
|
||||
log.Info().Msgf("Listen address not provided. Using default: '%s'", DefaultListenAddress)
|
||||
listenAddress = DefaultListenAddress
|
||||
}
|
||||
log.Info().Msgf("Listening on: %s", listenAddress)
|
||||
|
||||
discordClient := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
transformAndForwardWithInstrumentation := promhttp.InstrumentHandlerDuration(metrics.RequestsToAlertForwarderDuration,
|
||||
promhttp.InstrumentHandlerCounter(metrics.RequestsToAlertForwarderTotal,
|
||||
promhttp.InstrumentHandlerInFlight(metrics.RequestsToAlertForwarderInFlight,
|
||||
alertforwarder.NewAlertForwarderHandler(discordClient,
|
||||
webhookUrl,
|
||||
amds.MaximumBackoffTimeSeconds,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
mux.HandleFunc("/", transformAndForwardWithInstrumentation)
|
||||
|
||||
mux.HandleFunc("/readiness", func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Debug().Msg("Readiness probe encountered.")
|
||||
})
|
||||
|
||||
mux.HandleFunc("/liveness", func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Debug().Msg("Liveness probe encountered.")
|
||||
})
|
||||
|
||||
mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
||||
// purposefully empty
|
||||
})
|
||||
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
amds.httpServer = &http.Server{
|
||||
Addr: listenAddress,
|
||||
Handler: mux,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
MaxHeaderBytes: 1 << 20,
|
||||
}
|
||||
|
||||
// Setting up signal capturing
|
||||
signal.Notify(stop, os.Interrupt)
|
||||
|
||||
go func() {
|
||||
// check for nil prevents race condition if we have already shutdown the server before this goroutine attempts to start
|
||||
if amds.httpServer != nil {
|
||||
if err := amds.httpServer.ListenAndServe(); err != nil {
|
||||
close(stop)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return stop, nil
|
||||
}
|
||||
|
||||
func (amds *AlertManagerDiscordServer) Shutdown() error {
|
||||
log.Info().Msg("Received signal to shut down server. Shutting down server...")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if amds.httpServer == nil {
|
||||
// http server is not referenced, or was never created, so we're unable to shut it down
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := amds.httpServer.Shutdown(ctx); err != nil {
|
||||
// prevent race condition if shutdown signal was sent prior to server starting, we remove server reference to prevent it starting
|
||||
amds.httpServer = nil
|
||||
return err
|
||||
}
|
||||
|
||||
// prevent race condition if shutdown signal was sent prior to server starting, we remove server remove to prevent it starting
|
||||
amds.httpServer = nil
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package version
|
||||
|
||||
var (
|
||||
Version = "0.0.0"
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,6 +0,0 @@
|
||||
repositoryID: 970959ac-e2fa-44a7-955d-4fabe93fa253
|
||||
owners:
|
||||
- name: SpeckleDevops
|
||||
ignore:
|
||||
- name: alertmanager-discord
|
||||
version: .*alpha.*
|
||||
@@ -1,204 +0,0 @@
|
||||
apiVersion: v1
|
||||
entries:
|
||||
alertmanager-discord:
|
||||
- apiVersion: v2
|
||||
appVersion: 0.3.2
|
||||
created: "2024-06-18T07:58:23.882557595Z"
|
||||
description: A Helm chart to deploy alertmanager-discord to Kubernetes
|
||||
digest: d68756563716ac4b50e31773e25d6f47cdd9c11b957873cdda295d7fcb47f7aa
|
||||
name: alertmanager-discord
|
||||
type: application
|
||||
urls:
|
||||
- alertmanager-discord-0.3.2.tgz
|
||||
version: 0.3.2
|
||||
- apiVersion: v2
|
||||
appVersion: 0.3.2-alpha.330
|
||||
created: "2024-06-18T07:58:23.881828426Z"
|
||||
description: A Helm chart to deploy alertmanager-discord to Kubernetes
|
||||
digest: 2e99ba537eab1e6768a15f145414058c18b79073ff4767edcd18201b7639ca87
|
||||
name: alertmanager-discord
|
||||
type: application
|
||||
urls:
|
||||
- alertmanager-discord-0.3.2-alpha.330.tgz
|
||||
version: 0.3.2-alpha.330
|
||||
- apiVersion: v2
|
||||
appVersion: 0.3.2-alpha.318
|
||||
created: "2024-06-18T07:58:23.881120375Z"
|
||||
description: A Helm chart to deploy alertmanager-discord to Kubernetes
|
||||
digest: e0964cdb347be771761d7132f5375525e33eef222d2ee1f1a32a0c6f1b6dfbf5
|
||||
name: alertmanager-discord
|
||||
type: application
|
||||
urls:
|
||||
- alertmanager-discord-0.3.2-alpha.318.tgz
|
||||
version: 0.3.2-alpha.318
|
||||
- apiVersion: v2
|
||||
appVersion: 0.3.2-alpha.307
|
||||
created: "2024-06-18T07:58:23.880315763Z"
|
||||
description: A Helm chart to deploy alertmanager-discord to Kubernetes
|
||||
digest: 10f27530cc06b8a8e3e576db3b668289606ebc17d675396f6e4f47eaa0814b30
|
||||
name: alertmanager-discord
|
||||
type: application
|
||||
urls:
|
||||
- alertmanager-discord-0.3.2-alpha.307.tgz
|
||||
version: 0.3.2-alpha.307
|
||||
- apiVersion: v2
|
||||
appVersion: 0.3.1
|
||||
created: "2024-06-18T07:58:23.879536262Z"
|
||||
description: A Helm chart to deploy alertmanager-discord to Kubernetes
|
||||
digest: a5017b6f1a7f8f204e0239b80c379bc756e636177eb89c00320b64235f2fff1e
|
||||
name: alertmanager-discord
|
||||
type: application
|
||||
urls:
|
||||
- alertmanager-discord-0.3.1.tgz
|
||||
version: 0.3.1
|
||||
- apiVersion: v2
|
||||
appVersion: 0.3.1-alpha.293
|
||||
created: "2024-06-18T07:58:23.878764926Z"
|
||||
description: A Helm chart to deploy alertmanager-discord to Kubernetes
|
||||
digest: effc1fb7ad6a775a5ec24925082debd9a8347fa347876ea6b06d1730b2043ef3
|
||||
name: alertmanager-discord
|
||||
type: application
|
||||
urls:
|
||||
- alertmanager-discord-0.3.1-alpha.293.tgz
|
||||
version: 0.3.1-alpha.293
|
||||
- apiVersion: v2
|
||||
appVersion: 0.3.1-alpha.281
|
||||
created: "2024-06-18T07:58:23.87807261Z"
|
||||
description: A Helm chart to deploy alertmanager-discord to Kubernetes
|
||||
digest: b3ead27ac560c2a0e1d15c44ff05e36f9ee524fdbed18a211032f46020ba630c
|
||||
name: alertmanager-discord
|
||||
type: application
|
||||
urls:
|
||||
- alertmanager-discord-0.3.1-alpha.281.tgz
|
||||
version: 0.3.1-alpha.281
|
||||
- apiVersion: v2
|
||||
appVersion: 0.3.0
|
||||
created: "2024-06-18T07:58:23.877341303Z"
|
||||
description: A Helm chart to deploy alertmanager-discord to Kubernetes
|
||||
digest: 56f217638a5b19f408a5f12b026aa0afaec1839feb5ec034e42d59a8c8937ca4
|
||||
name: alertmanager-discord
|
||||
type: application
|
||||
urls:
|
||||
- alertmanager-discord-0.3.0.tgz
|
||||
version: 0.3.0
|
||||
- apiVersion: v2
|
||||
appVersion: 0.2.7-alpha.269
|
||||
created: "2024-06-18T07:58:23.87656442Z"
|
||||
description: A Helm chart to deploy alertmanager-discord to Kubernetes
|
||||
digest: 4b59471bcfe728b3bac38ff2e7934c8763c6be17a934930cca69b77a3e458671
|
||||
name: alertmanager-discord
|
||||
type: application
|
||||
urls:
|
||||
- alertmanager-discord-0.2.7-alpha.269.tgz
|
||||
version: 0.2.7-alpha.269
|
||||
- apiVersion: v2
|
||||
appVersion: 0.2.6
|
||||
created: "2024-06-18T07:58:23.875842782Z"
|
||||
description: A Helm chart to deploy alertmanager-discord to Kubernetes
|
||||
digest: be8a9501623295ef3aae817e08d4030c3aa732b8132b2d516135a3c54ee66447
|
||||
name: alertmanager-discord
|
||||
type: application
|
||||
urls:
|
||||
- alertmanager-discord-0.2.6.tgz
|
||||
version: 0.2.6
|
||||
- apiVersion: v2
|
||||
appVersion: 0.2.6-alpha.256
|
||||
created: "2024-06-18T07:58:23.875050193Z"
|
||||
description: A Helm chart to deploy alertmanager-discord to Kubernetes
|
||||
digest: 6dba71521444d9015c70e9f587f21f0a65ecffedeac6f5d5db608d9bd0aacf45
|
||||
name: alertmanager-discord
|
||||
type: application
|
||||
urls:
|
||||
- alertmanager-discord-0.2.6-alpha.256.tgz
|
||||
version: 0.2.6-alpha.256
|
||||
- apiVersion: v2
|
||||
appVersion: 0.2.5
|
||||
created: "2024-06-18T07:58:23.87369352Z"
|
||||
description: A Helm chart to deploy alertmanager-discord to Kubernetes
|
||||
digest: f08ceb39fafb9c88c547db67877bc93db73cf7179fcdd9f90b7a2d249e979c50
|
||||
name: alertmanager-discord
|
||||
type: application
|
||||
urls:
|
||||
- alertmanager-discord-0.2.5.tgz
|
||||
version: 0.2.5
|
||||
- apiVersion: v2
|
||||
appVersion: 0.2.5-alpha.241
|
||||
created: "2024-06-18T07:58:23.873181011Z"
|
||||
description: A Helm chart to deploy alertmanager-discord to Kubernetes
|
||||
digest: bae9ba18ee9132d76826c75c8e37168d01122508df75281febb2dd446e3b24b1
|
||||
name: alertmanager-discord
|
||||
type: application
|
||||
urls:
|
||||
- alertmanager-discord-0.2.5-alpha.241.tgz
|
||||
version: 0.2.5-alpha.241
|
||||
- apiVersion: v2
|
||||
appVersion: 0.2.5-alpha.224
|
||||
created: "2024-06-18T07:58:23.872622486Z"
|
||||
description: A Helm chart to deploy alertmanager-discord to Kubernetes
|
||||
digest: 06bbdc0cf20a19fe9c01916fca66653d839b63345c87ec9f7ce804e95ff9f342
|
||||
name: alertmanager-discord
|
||||
type: application
|
||||
urls:
|
||||
- alertmanager-discord-0.2.5-alpha.224.tgz
|
||||
version: 0.2.5-alpha.224
|
||||
- apiVersion: v2
|
||||
appVersion: 0.2.4
|
||||
created: "2024-06-18T07:58:23.872077404Z"
|
||||
description: A Helm chart to deploy alertmanager-discord to Kubernetes
|
||||
digest: 3808bcd0b3497a706530832651609cea6790ca90c857d240b612dc1a5fdf4dbd
|
||||
name: alertmanager-discord
|
||||
type: application
|
||||
urls:
|
||||
- alertmanager-discord-0.2.4.tgz
|
||||
version: 0.2.4
|
||||
- apiVersion: v2
|
||||
appVersion: 0.2.4-alpha.204
|
||||
created: "2024-06-18T07:58:23.87153428Z"
|
||||
description: A Helm chart to deploy alertmanager-discord to Kubernetes
|
||||
digest: ec677263b8ec6e57c59141b109332a2658147b77205778f1616f030e84a7b5bd
|
||||
name: alertmanager-discord
|
||||
type: application
|
||||
urls:
|
||||
- alertmanager-discord-0.2.4-alpha.204.tgz
|
||||
version: 0.2.4-alpha.204
|
||||
- apiVersion: v2
|
||||
appVersion: 0.2.4-alpha.195
|
||||
created: "2024-06-18T07:58:23.871076127Z"
|
||||
description: A Helm chart to deploy alertmanager-discord to Kubernetes
|
||||
digest: bdb002e7a2c01319edfe799b0cb451d8b6b4b302feb7e2fcabc8af7bf60ae22a
|
||||
name: alertmanager-discord
|
||||
type: application
|
||||
urls:
|
||||
- alertmanager-discord-0.2.4-alpha.195.tgz
|
||||
version: 0.2.4-alpha.195
|
||||
- apiVersion: v2
|
||||
appVersion: 0.2.4-alpha.184
|
||||
created: "2024-06-18T07:58:23.870621323Z"
|
||||
description: A Helm chart to deploy alertmanager-discord to Kubernetes
|
||||
digest: e70856450013f1b20982852d7e4bfed64640e65ea54cfd6f51d09f05f55c68e3
|
||||
name: alertmanager-discord
|
||||
type: application
|
||||
urls:
|
||||
- alertmanager-discord-0.2.4-alpha.184.tgz
|
||||
version: 0.2.4-alpha.184
|
||||
- apiVersion: v2
|
||||
appVersion: 0.2.3
|
||||
created: "2024-06-18T07:58:23.870100053Z"
|
||||
description: A Helm chart to deploy alertmanager-discord to Kubernetes
|
||||
digest: e9f0615fff3299f25a364d81cbbbde1d071ed02a3e106a80529c54a7eff78486
|
||||
name: alertmanager-discord
|
||||
type: application
|
||||
urls:
|
||||
- alertmanager-discord-0.2.3.tgz
|
||||
version: 0.2.3
|
||||
- apiVersion: v2
|
||||
appVersion: 0.2.3-alpha.162
|
||||
created: "2024-06-18T07:58:23.869572322Z"
|
||||
description: A Helm chart to deploy alertmanager-discord to Kubernetes
|
||||
digest: 05d1b1bb438c53cfef25ab455f0b8936a61f367f44802926c7c906a34d77832f
|
||||
name: alertmanager-discord
|
||||
type: application
|
||||
urls:
|
||||
- alertmanager-discord-0.2.3-alpha.162.tgz
|
||||
version: 0.2.3-alpha.162
|
||||
generated: "2024-06-18T07:58:23.868856027Z"
|
||||
@@ -0,0 +1,58 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
// "bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type MockClientRequest struct {
|
||||
Url string
|
||||
ContentType string
|
||||
Body io.Reader
|
||||
}
|
||||
|
||||
type MockClientRecorder struct {
|
||||
Requests []MockClientRequest
|
||||
}
|
||||
|
||||
type RoundTripFunc func(req *http.Request) *http.Response
|
||||
|
||||
func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return f(req), nil
|
||||
}
|
||||
|
||||
func NewMockClient(fn RoundTripFunc) *http.Client {
|
||||
return &http.Client{
|
||||
Transport: RoundTripFunc(fn),
|
||||
}
|
||||
}
|
||||
|
||||
func (mc *MockClientRecorder) NewMockClientWithResponse(statusCode int) *http.Client {
|
||||
return NewMockClient(func(req *http.Request) *http.Response {
|
||||
mc.Requests = append(mc.Requests, MockClientRequest{
|
||||
Url: req.URL.String(),
|
||||
ContentType: req.Header.Get("content-type"),
|
||||
Body: req.Body,
|
||||
})
|
||||
|
||||
return &http.Response{
|
||||
StatusCode: statusCode,
|
||||
// Body: io.NopCloser(bytes.NewBufferString(responseBody)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// intended to cause errors in the client
|
||||
// to be used for tests of error handling
|
||||
func (mc *MockClientRecorder) NewMockClientReturnsNil() *http.Client {
|
||||
return NewMockClient(func(req *http.Request) *http.Response {
|
||||
mc.Requests = append(mc.Requests, MockClientRequest{
|
||||
Url: req.URL.String(),
|
||||
ContentType: req.Header.Get("content-type"),
|
||||
Body: req.Body,
|
||||
})
|
||||
|
||||
return &http.Response{}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
discord_webhook_url: https://discordapp.com/api/webhooks/123456789123456789/abc
|
||||
listen_address: "127.0.0.1:7073"
|
||||
max_backoff_time_seconds: 1
|
||||
Reference in New Issue
Block a user