Skip to content
This repository has been archived by the owner on Sep 25, 2024. It is now read-only.

Discord webhook support #6

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@

# velero-notifications

This is a simple Kubernetes controller written in Crystal that sends Email/Slack/webhook notifications when backups are performed by [Velero](https://velero.io/) in a [Kubernetes](https://kubernetes.io/) cluster.
This is a simple Kubernetes controller written in Crystal that sends Email/Slack/Discord/webhook notifications when backups are performed by [Velero](https://velero.io/) in a [Kubernetes](https://kubernetes.io/) cluster.

![Screenshot](slack.png?raw=true "Screenshot")

![Screenshot](discord.png?raw=true "Screenshot")

If you like this or any of my other projects and would like to help with their development, consider [becoming a sponsor](https://github.com/sponsors/vitobotta).

## Installation
Expand All @@ -35,6 +37,12 @@ helm upgrade --install \
--set slack.webhook=https://... \
--set slack.channel=velero \
--set slack.username=Velero \
--set discord.enabled=true \
--set discord.failures_only=false \
--set discord.webhook=https://... \
--set discord.mentions.enabled=false \
--set discord.mentions.failures_only=true \
--set discord.mentions.role_id="1234567890" \
--set email.enabled=true \
--set email.failures_only=true \
--set email.smtp.host=... \
Expand Down
4 changes: 2 additions & 2 deletions bin/build
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!/bin/bash

docker run --rm -it --platform=linux/amd64 -v $(pwd):/workspace -w /workspace crystallang/crystal:latest-alpine crystal build src/velero-notifications.cr --static
docker run --rm -it --platform=linux/amd64 -v $(pwd):/workspace -w /workspace crystallang/crystal:latest-alpine /bin/sh -c "shards install && crystal run ./lib/k8s/bin/gen_crd.cr -- ./crds.yaml ./src/crds && crystal build src/velero-notifications.cr --static"


IMAGE="vitobotta/velero-backup-notification"
IMAGE="woutthenines/velero-backup-notification"
VERSION="$(git describe --tags --abbrev=0)"

docker build --platform=linux -t ${IMAGE}:${VERSION} .
Expand Down
73 changes: 42 additions & 31 deletions crds.yaml

Large diffs are not rendered by default.

Binary file added discord.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions helm/Chart.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
apiVersion: v1
appVersion: "1.0"
appVersion: "v1.0.3"
description: A Helm chart to send email/Slack notifications for Velero backups/restores
name: velero-backup-notification
version: 0.1.0
version: 1.0.3
15 changes: 15 additions & 0 deletions helm/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@ spec:
secretKeyRef:
key: slack_channel
name: velero-backup-notification-secrets
- name: ENABLE_DISCORD_NOTIFICATIONS
value: {{ .Values.discord.enabled | quote }}
- name: DISCORD_FAILURES_ONLY
value: {{ .Values.discord.failures_only | quote }}
- name: DISCORD_WEBHOOK
valueFrom:
secretKeyRef:
key: discord_webhook
name: velero-backup-notification-secrets
- name: ENABLE_DISCORD_MENTIONS
value: {{ .Values.discord.mentions.enabled | quote }}
- name: DISCORD_MENTIONS_FAILURES_ONLY
value: {{ .Values.discord.mentions.failures_only | quote }}
- name: DISCORD_MENTIONS_ROLE_ID
value: {{ .Values.discord.mentions.role_id | quote }}
- name: ENABLE_EMAIL_NOTIFICATIONS
value: {{ .Values.email.enabled | quote }}
- name: EMAIL_FAILURES_ONLY
Expand Down
1 change: 1 addition & 0 deletions helm/templates/secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type: Opaque
stringData:
slack_webhook: {{ .Values.slack.webhook | quote }}
slack_channel: {{ .Values.slack.channel | quote }}
discord_webhook: {{ .Values.discord.webhook | quote }}
email_smtp_host: {{ .Values.email.smtp.host | quote }}
email_smtp_port: {{ .Values.email.smtp.port | quote }}
email_smtp_username: {{ .Values.email.smtp.username | quote }}
Expand Down
13 changes: 11 additions & 2 deletions helm/values.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
image:
repository: vitobotta/velero-backup-notification
tag: v1.0.0
repository: woutthenines/velero-backup-notification
tag: v1.0.3

slack:
enabled: false
Expand All @@ -9,6 +9,15 @@ slack:
channel: "stuff"
username: Velero

discord:
enabled: false
failures_only: true
webhook: "https://...."
mentions:
enabled: false
failures_only: true
role_id: "1234567890"

email:
enabled: false
failures_only: true
Expand Down
6 changes: 3 additions & 3 deletions shard.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: 2.0
shards:
db:
git: https://github.com/crystal-lang/crystal-db.git
version: 0.11.0
version: 0.12.0

email:
git: https://github.com/arcage/crystal-email.git
Expand All @@ -14,11 +14,11 @@ shards:

k8s:
git: https://github.com/spoved/k8s.cr.git
version: 0.1.11
version: 0.1.12

kube-client:
git: https://github.com/spoved/kube-client.cr.git
version: 0.4.6+git.commit.8778313a458f239376c7f05896a79fd8414e4139
version: 0.4.8

retriable:
git: https://github.com/sija/retriable.cr.git
Expand Down
4 changes: 2 additions & 2 deletions shard.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: velero-notifications
version: 0.1.0
version: 1.0.3

authors:
- Vito Botta <[email protected]>
Expand All @@ -15,7 +15,7 @@ license: MIT
dependencies:
kube-client:
github: spoved/kube-client.cr
branch: kalinon/issue12
version: 0.4.8
retriable:
git: https://github.com/sija/retriable.cr.git
version: 0.2.4
Expand Down
2 changes: 1 addition & 1 deletion src/controller.cr
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
require "log"
require "kube-client/v1.26"
require "kube-client/v1.28"
require "retriable"
require "./crds/velero/v1/backup_spec"
require "./crds/velero/v1/backup_status"
Expand Down
3 changes: 3 additions & 0 deletions src/crds/velero/v1/backup_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require "json"
properties: [

{name: "csi_snapshot_timeout", kind: String, key: "csiSnapshotTimeout", nilable: true, read_only: false, description: "CSISnapshotTimeout specifies the time used to wait for CSI VolumeSnapshot status turns to ReadyToUse during creation, before returning error as timeout. The default value is 10 minute."},
{name: "datamover", kind: String, key: "datamover", nilable: true, read_only: false, description: "DataMover specifies the data mover to be used by the backup. If DataMover is \"\" or \"velero\", the built-in data mover will be used."},
{name: "default_volumes_to_fs_backup", kind: ::Bool, key: "defaultVolumesToFsBackup", nilable: true, read_only: false, description: "DefaultVolumesToFsBackup specifies whether pod volume file system backup should be used for all volumes by default."},
{name: "default_volumes_to_restic", kind: ::Bool, key: "defaultVolumesToRestic", nilable: true, read_only: false, description: "DefaultVolumesToRestic specifies whether restic should be used to take a backup of all pod volumes by default. \n Deprecated: this field is no longer used and will be removed entirely in future. Use DefaultVolumesToFsBackup instead."},
{name: "excluded_cluster_scoped_resources", kind: ::Array(String), key: "excludedClusterScopedResources", nilable: true, read_only: false, description: "ExcludedClusterScopedResources is a slice of cluster-scoped resource type names to exclude from the backup. If set to \"*\", all cluster-scoped resource types are excluded. The default value is empty."},
Expand All @@ -26,9 +27,11 @@ require "json"
{name: "or_label_selectors", kind: Union(::Array(::Hash(String, ::Array(::Hash(String, String | ::Array(String))) | ::Hash(String, String)))), key: "orLabelSelectors", nilable: true, read_only: false, description: "OrLabelSelectors is list of metav1.LabelSelector to filter with when adding individual objects to the backup. If multiple provided they will be joined by the OR operator. LabelSelector as well as OrLabelSelectors cannot co-exist in backup request, only one of them can be used."},
{name: "ordered_resources", kind: ::Hash(String, String), key: "orderedResources", nilable: true, read_only: false, description: "OrderedResources specifies the backup order of resources of specific Kind. The map key is the resource name and value is a list of object names separated by commas. Each resource name has format [\"namespace/objectname\". For cluster resources, simply use \"objectname\".](\"namespace/objectname\". For cluster resources, simply use \"objectname\".)"},
{name: "resource_policy", kind: ::Hash(String, String), key: "resourcePolicy", nilable: true, read_only: false, description: "ResourcePolicy specifies the referenced resource policies that backup should follow"},
{name: "snapshot_move_data", kind: ::Bool, key: "snapshotMoveData", nilable: true, read_only: false, description: "SnapshotMoveData specifies whether snapshot data should be moved"},
{name: "snapshot_volumes", kind: ::Bool, key: "snapshotVolumes", nilable: true, read_only: false, description: "SnapshotVolumes specifies whether to take snapshots of any PV's referenced in the set of objects included in the Backup."},
{name: "storage_location", kind: String, key: "storageLocation", nilable: true, read_only: false, description: "StorageLocation is a string containing the name of a BackupStorageLocation where the backup should be stored."},
{name: "ttl", kind: String, key: "ttl", nilable: true, read_only: false, description: "TTL is a time.Duration-parseable string describing how long the Backup should be retained for."},
{name: "uploader_config", kind: ::Hash(String, Int32), key: "uploaderConfig", nilable: true, read_only: false, description: "UploaderConfig specifies the configuration for the uploader."},
{name: "volume_snapshot_locations", kind: ::Array(String), key: "volumeSnapshotLocations", nilable: true, read_only: false, description: "VolumeSnapshotLocations is a list containing names of VolumeSnapshotLocations associated with this backup."},

]
Expand Down
1 change: 1 addition & 0 deletions src/crds/velero/v1/backup_status.cr
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ require "json"
{name: "expiration", kind: String, key: "expiration", nilable: true, read_only: false, description: "Expiration is when this Backup is eligible for garbage-collection."},
{name: "failure_reason", kind: String, key: "failureReason", nilable: true, read_only: false, description: "FailureReason is an error that caused the entire backup to fail."},
{name: "format_version", kind: String, key: "formatVersion", nilable: true, read_only: false, description: "FormatVersion is the backup format version, including major, minor, and patch version."},
{name: "hook_status", kind: ::Hash(String, Int32), key: "hookStatus", nilable: true, read_only: false, description: "HookStatus contains information about the status of the hooks."},
{name: "phase", kind: String, key: "phase", nilable: true, read_only: false, description: "Phase is the current state of the Backup."},
{name: "progress", kind: ::Hash(String, Int32), key: "progress", nilable: true, read_only: false, description: "Progress contains information about the backup's execution progress. Note that this information is best-effort only -- if Velero fails to update it during a backup for any reason, it may be [inaccurate/stale.](inaccurate/stale.)"},
{name: "start_timestamp", kind: String, key: "startTimestamp", nilable: true, read_only: false, description: "StartTimestamp records the time a backup was started. Separate from CreationTimestamp, since that value changes on restores. The server's time is used for StartTimestamps"},
Expand Down
48 changes: 47 additions & 1 deletion src/event.cr
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ require "email"

class Event
SLACK_WEBHOOK = ENV.fetch("SLACK_WEBHOOK", "")
DISCORD_WEBHOOK = ENV.fetch("DISCORD_WEBHOOK", "")
ENABLE_DISCORD_MENTIONS = ENV.fetch("ENABLE_DISCORD_MENTIONS", "false").downcase
DISCORD_MENTIONS_FAILURES_ONLY = ENV.fetch("DISCORD_MENTIONS_FAILURES_ONLY", "false").downcase
DISCORD_MENTIONS_ROLE_ID = ENV.fetch("DISCORD_MENTIONS_ROLE_ID", "")
EMAIL_SMTP_HOST = ENV.fetch("EMAIL_SMTP_HOST", "")
EMAIL_SMTP_PORT = ENV.fetch("EMAIL_SMTP_PORT", "")
EMAIL_SMTP_USERNAME = ENV.fetch("EMAIL_SMTP_USERNAME", "")
Expand All @@ -25,6 +29,7 @@ class Event
Log.info { notification_subject }

send_slack_notification if send_slack_notification?
send_discord_notification if send_discord_notification?
send_email_notification if send_email_notification?
send_webhook_notification if send_webhook_notification?
end
Expand Down Expand Up @@ -83,6 +88,48 @@ class Event
)
end

def send_discord_notification?
send_notification?(:discord)
end

# Add a method to send notifications to Discord
def send_discord_notification
if DISCORD_WEBHOOK.blank?
Log.info { "Ensure the DISCORD_WEBHOOK environment variable is set" }
raise Exception.new("Discord configuration missing")
end

if ENABLE_DISCORD_MENTIONS == "true"
if DISCORD_MENTIONS_ROLE_ID.blank?
Log.info { "Ensure the DISCORD_MENTIONS_ROLE_ID environment variable is set" }
raise Exception.new("Discord mentions configuration missing")
end

failures_only = DISCORD_MENTIONS_FAILURES_ONLY == "true"
succeeded = phase == "Completed"

notification_mention = !failures_only || (failures_only && !succeeded) ? "<@&#{DISCORD_MENTIONS_ROLE_ID}>" : nil
end

color = phase == "Completed" ? 0x36a64f : 0xa30202
payload = {
"content" => notification_mention.nil? ? "" : notification_mention,
"embeds" => [
{
"title" => notification_subject,
"description" => notification_body,
"color" => color
}
]
}.to_json

HTTP::Client.post(
DISCORD_WEBHOOK,
headers: HTTP::Headers{"Content-type" => "application/json"},
body: payload
)
end

private def email_client : EMail::Client
@email_client ||= begin
if EMAIL_SMTP_HOST.blank? || EMAIL_SMTP_PORT.blank? || EMAIL_SMTP_USERNAME.blank? || EMAIL_SMTP_PASSWORD.blank? || EMAIL_FROM_ADDRESS.blank? || EMAIL_TO_ADDRESS.blank?
Expand Down Expand Up @@ -138,4 +185,3 @@ class Event
HTTP::Client.get(WEBHOOK_URL)
end
end

2 changes: 1 addition & 1 deletion src/velero-notifications.cr
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require "./controller"

module Velero::Notifications
VERSION = "1.0.0"
VERSION = "1.0.3"

end

Expand Down