29 Commits

Author SHA1 Message Date
SpeckleDevBot 85f808a037 updating helm chart to version 0.3.2 2024-06-18 07:58:23 +00:00
SpeckleDevBot b63473f6fa updating helm chart to version 0.3.2-alpha.330 2024-06-18 07:57:47 +00:00
SpeckleDevBot 0b80ad109f updating helm chart to version 0.3.2-alpha.318 2024-05-28 15:16:42 +00:00
SpeckleDevBot bffe744de6 updating helm chart to version 0.3.2-alpha.307 2024-05-02 15:39:19 +00:00
SpeckleDevBot d365c026fe updating helm chart to version 0.3.1 2024-04-03 15:39:10 +00:00
SpeckleDevBot 812214c347 updating helm chart to version 0.3.1-alpha.293 2024-04-03 15:38:21 +00:00
SpeckleDevBot 528904d5b5 updating helm chart to version 0.3.1-alpha.281 2024-02-23 12:29:46 +00:00
SpeckleDevBot 1bdac9ac74 updating helm chart to version 0.3.0 2024-02-02 16:34:15 +00:00
SpeckleDevBot d3ef24606f updating helm chart to version 0.2.7-alpha.269 2024-02-02 16:33:32 +00:00
SpeckleDevBot d0dd302553 updating helm chart to version 0.2.6 2024-01-29 12:01:39 +00:00
SpeckleDevBot 52ff391d1e updating helm chart to version 0.2.6-alpha.256 2024-01-29 12:00:40 +00:00
SpeckleDevBot d666789f9d updating helm chart to version 0.2.5 2024-01-03 19:07:05 +00:00
SpeckleDevBot c0b26632cb updating helm chart to version 0.2.5-alpha.241 2024-01-03 19:06:01 +00:00
SpeckleDevBot 124538dfb7 updating helm chart to version 0.2.5-alpha.224 2023-08-08 06:09:12 +00:00
SpeckleDevBot b7ea2f3d0e updating helm chart to version 0.2.4 2023-03-16 18:46:12 +00:00
SpeckleDevBot 03d6f1abfa updating helm chart to version 0.2.4-alpha.204 2023-03-16 18:46:00 +00:00
Iain Sproat 8e10b37887 chore(artifacthub): include configuration for ArtifactHub 2023-02-09 16:24:53 +00:00
SpeckleDevBot 13ba0fc9fc updating helm chart to version 0.2.4-alpha.195 2023-02-07 08:53:11 +00:00
SpeckleDevBot 728f9cac50 updating helm chart to version 0.2.4-alpha.184 2023-01-05 16:49:07 +00:00
SpeckleDevBot 7beb4c0f3b updating helm chart to version 0.2.3 2022-11-25 11:04:57 +00:00
SpeckleDevBot 26bdb79b94 updating helm chart to version 0.2.3-alpha.162 2022-11-25 10:58:21 +00:00
Iain Sproat 735464ff73 Iain/gh pages clear all (#23)
* Delete all existing helm charts in gh-pages

* Adds an empty CircleCI config and a minimal pre-commit-config to gh-pages
2022-11-25 10:55:22 +00:00
SpeckleDevBot a172b84ef2 updating helm chart to version 0.2.2 2022-11-25 10:30:42 +00:00
SpeckleDevBot ecf4e163ac updating helm chart to version 0.2.2-alpha.148 2022-11-25 10:23:39 +00:00
Iain Sproat 56df7c26f5 Deletes Helm Charts released to date (#21) 2022-11-25 10:20:49 +00:00
SpeckleDevBot 07bdefe59d updating helm chart to version 0.2.2-alpha.140 2022-11-25 09:58:01 +00:00
SpeckleDevBot b655618d56 updating helm chart to version 0.2.1 2022-11-25 09:53:34 +00:00
SpeckleDevBot ba01ca707c updating helm chart to version 0.2.1-alpha.128 2022-11-24 17:29:10 +00:00
Iain Sproat 9c4a5c9e9e Initial empty commit 2022-11-18 15:43:23 +00:00
69 changed files with 214 additions and 2945 deletions
-12
View File
@@ -1,12 +0,0 @@
#!/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 .
+2 -158
View File
@@ -1,161 +1,5 @@
version: 2.1
workflows:
build-image:
jobs:
- get-version:
filters:
tags: &filter-allow-all
only: /.*/
workflows: {}
- 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
jobs: {}
-23
View File
@@ -1,23 +0,0 @@
#!/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
-57
View File
@@ -1,57 +0,0 @@
#!/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}"
-24
View File
@@ -1,24 +0,0 @@
#!/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.

Before

Width:  |  Height:  |  Size: 312 KiB

-10
View File
@@ -1,10 +0,0 @@
alertmanager-discord
alertmanager-discord.darwin
alertmanager-discord.linux
cover.out
# CircleCI orb Helm package & push
.cr-release-packages
# pre-commit
node_modules
+2 -52
View File
@@ -1,56 +1,6 @@
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.18"
rev: "v0.0.17"
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
View File
@@ -1,30 +0,0 @@
# 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"]
-201
View File
@@ -1,201 +0,0 @@
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.
-173
View File
@@ -1,173 +0,0 @@
# 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:
![](/.github/discord-screenshot.png)
## 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
View File
@@ -1,125 +0,0 @@
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
}
}
-23
View File
@@ -1,23 +0,0 @@
# 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/
-11
View File
@@ -1,11 +0,0 @@
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"
-41
View File
@@ -1,41 +0,0 @@
# alertmanager-discord
![Version: 0.0.0-local](https://img.shields.io/badge/Version-0.0.0--local-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.0.0-local](https://img.shields.io/badge/AppVersion-0.0.0--local-informational?style=flat-square)
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 | `[]` | |
-16
View File
@@ -1,16 +0,0 @@
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 }}
-62
View File
@@ -1,62 +0,0 @@
{{/*
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 }}
@@ -1,24 +0,0 @@
{{- 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 -}}
-74
View File
@@ -1,74 +0,0 @@
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
-15
View File
@@ -1,15 +0,0 @@
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 }}
-12
View File
@@ -1,12 +0,0 @@
{{- 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 }}
-12
View File
@@ -1,12 +0,0 @@
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
@@ -1,15 +0,0 @@
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
-218
View File
@@ -1,218 +0,0 @@
{
"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": {}
}
}
}
}
}
-68
View File
@@ -1,68 +0,0 @@
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: {}
-47
View File
@@ -1,47 +0,0 @@
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
)
-107
View File
@@ -1,107 +0,0 @@
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=
-9
View File
@@ -1,9 +0,0 @@
package main
import (
"github.com/specklesystems/alertmanager-discord/cmd"
)
func main() {
cmd.Execute()
}
-221
View File
@@ -1,221 +0,0 @@
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)
}
-366
View File
@@ -1,366 +0,0 @@
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
}
-41
View File
@@ -1,41 +0,0 @@
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
}
@@ -1,45 +0,0 @@
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")
}
-89
View File
@@ -1,89 +0,0 @@
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
}
-33
View File
@@ -1,33 +0,0 @@
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"`
}
-75
View File
@@ -1,75 +0,0 @@
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
}
-24
View File
@@ -1,24 +0,0 @@
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"})
)
-25
View File
@@ -1,25 +0,0 @@
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"`
}
-9
View File
@@ -1,9 +0,0 @@
package flags
const (
ConfigurationPathFlagKey = "configuration_file_path"
DiscordWebhookUrlFlagKey = "discord_webhook_url"
ListenAddressFlagKey = "listen_address"
MaxBackoffTimeSecondsFlagKey = "max_backoff_time_seconds"
LogLevelFlagKey = "log_level"
)
-9
View File
@@ -1,9 +0,0 @@
package logging
const (
// field values should be lowercase snake_case where possible
EventTypeRequestReceived = "received_request"
EventTypeRequestSending = "sending_request"
EventTypeResponseReceived = "received_response"
EventTypeResponseSending = "sending_response"
)
-12
View File
@@ -1,12 +0,0 @@
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"
)
-24
View File
@@ -1,24 +0,0 @@
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"})
)
-32
View File
@@ -1,32 +0,0 @@
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
}
-136
View File
@@ -1,136 +0,0 @@
//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)")
// }
-119
View File
@@ -1,119 +0,0 @@
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
}
-5
View File
@@ -1,5 +0,0 @@
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.
+6
View File
@@ -0,0 +1,6 @@
repositoryID: 970959ac-e2fa-44a7-955d-4fabe93fa253
owners:
- name: SpeckleDevops
ignore:
- name: alertmanager-discord
version: .*alpha.*
+204
View File
@@ -0,0 +1,204 @@
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"
-58
View File
@@ -1,58 +0,0 @@
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{}
})
}
-3
View File
@@ -1,3 +0,0 @@
discord_webhook_url: https://discordapp.com/api/webhooks/123456789123456789/abc
listen_address: "127.0.0.1:7073"
max_backoff_time_seconds: 1