diff --git a/.editorconfig b/.editorconfig index 2f1eafd3c..af3f62495 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,7 +13,7 @@ insert_final_newline = true indent_style = space indent_size = 4 -[*.{bash,sh}] +[*.{bash,py,sh}] indent_style = space indent_size = 4 diff --git a/kubernetes/apps/default/homelab/github-notifier/config/config.yaml b/kubernetes/apps/default/homelab/github-notifier/config/config.yaml new file mode 100644 index 000000000..5107849ba --- /dev/null +++ b/kubernetes/apps/default/homelab/github-notifier/config/config.yaml @@ -0,0 +1,15 @@ +repositories: + - name: CoreELEC/CoreELEC + check: releases + - name: fluxcd/flux2 + check: releases + - name: opnsense/core + check: tags + ignore_tags_containing: + - a + - b + - r + - name: siderolabs/talos + check: releases + - name: ublue-os/bluefin + check: releases diff --git a/kubernetes/apps/default/homelab/github-notifier/externalsecret.yaml b/kubernetes/apps/default/homelab/github-notifier/externalsecret.yaml new file mode 100644 index 000000000..c3b886e67 --- /dev/null +++ b/kubernetes/apps/default/homelab/github-notifier/externalsecret.yaml @@ -0,0 +1,47 @@ +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/external-secrets.io/externalsecret_v1beta1.json +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: homelab-github-notifier +spec: + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-connect + target: + name: homelab-github-notifier-secret + template: + engineVersion: v2 + data: + # pushover + PUSHOVER_USER_KEY: " {{ .PUSHOVER_USER_KEY }}" + # pushover-notifier + HEALTHCHECKS_ID: "{{ .GITHUB_RELEASES_HEALTHCHECKS_ID }}" + PUSHOVER_APP_TOKEN: "{{ .PUSHOVER_APP_TOKEN }}" + dataFrom: + - extract: + key: pushover + - extract: + key: pushover-notifier +--- +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: homelab-github-notifier-db +spec: + secretStoreRef: + kind: ClusterSecretStore + name: crunchy-pgo-secrets + target: + name: homelab-github-notifier-db-secret + template: + engineVersion: v2 + data: + DB_NAME: '{{ index . "dbname" }}' + DB_HOST: '{{ index . "host" }}' + DB_USER: '{{ index . "user" }}' + DB_PASSWORD: '{{ index . "password" }}' + DB_PORT: '{{ index . "port" }}' + dataFrom: + - extract: + key: postgres-pguser-pushover-notifier diff --git a/kubernetes/apps/default/homelab/github-notifier/helmrelease.yaml b/kubernetes/apps/default/homelab/github-notifier/helmrelease.yaml new file mode 100644 index 000000000..f5d00e073 --- /dev/null +++ b/kubernetes/apps/default/homelab/github-notifier/helmrelease.yaml @@ -0,0 +1,69 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/bjw-s/helm-charts/main/charts/other/app-template/schemas/helmrelease-helm-v2.schema.json +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: &app homelab-github-notifier +spec: + interval: 30m + chart: + spec: + chart: app-template + version: 3.6.1 + sourceRef: + kind: HelmRepository + name: bjw-s + namespace: flux-system + maxHistory: 2 + install: + createNamespace: true + remediation: + retries: 3 + upgrade: + cleanupOnFail: true + remediation: + strategy: rollback + retries: 3 + uninstall: + keepHistory: false + values: + controllers: + music-transcode: + type: cronjob + cronjob: + concurrencyPolicy: Forbid + schedule: 0 */3 * * * # Every 3 hours + containers: + app: + image: + repository: ghcr.io/auricom/github-notifier + tag: rolling@sha256:e940cef874e6aba265f07e7e9b205f4b7224d3ca5a7eedd5d3ed5767bbd63461 + command: + - python + - /app/github-notifier.py + envFrom: + - secretRef: + name: homelab-github-notifier-secret + - secretRef: + name: homelab-github-notifier-db-secret + service: + app: + controller: *app + enabled: false + persistence: + config: + type: configMap + name: homelab-github-notifier-configmap + defaultMode: 0644 # trunk-ignore(yamllint/octal-values) + globalMounts: + - path: /config/config.yaml + subPath: config.yaml + readOnly: true + script: + type: configMap + name: homelab-github-notifier-configmap + defaultMode: 0644 # trunk-ignore(yamllint/octal-values) + globalMounts: + - path: /app/github-notifier.py + subPath: github-notifier.py + readOnly: true diff --git a/kubernetes/apps/default/homelab/github-notifier/kustomization.yaml b/kubernetes/apps/default/homelab/github-notifier/kustomization.yaml new file mode 100644 index 000000000..6f6743ef9 --- /dev/null +++ b/kubernetes/apps/default/homelab/github-notifier/kustomization.yaml @@ -0,0 +1,16 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/kustomization.json +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./externalsecret.yaml + - ./helmrelease.yaml +configMapGenerator: + - name: homelab-github-notifier-configmap + files: + - ./config/config.yaml + - ./scripts/github-notifier.py +generatorOptions: + disableNameSuffixHash: true + annotations: + kustomize.toolkit.fluxcd.io/substitute: disabled diff --git a/kubernetes/apps/default/homelab/github-notifier/scripts/github-notifier.py b/kubernetes/apps/default/homelab/github-notifier/scripts/github-notifier.py new file mode 100644 index 000000000..d3ad82a62 --- /dev/null +++ b/kubernetes/apps/default/homelab/github-notifier/scripts/github-notifier.py @@ -0,0 +1,169 @@ +import logging +import os +from datetime import datetime +import requests +import yaml +from decouple import config +from requests.exceptions import HTTPError +import psycopg2 +from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT + +log_level = logging.DEBUG if config("LOG_LEVEL", "") == "DEBUG" else logging.INFO + +# Configuration +conf_path = "/config/config.yaml" +if not os.path.exists(conf_path): + raise FileNotFoundError(f"Config file not found: {conf_path}") + +with open(conf_path, "r") as file: + conf = yaml.safe_load(file) + +# Pushover credentials +PUSHOVER_API_URL = "https://api.pushover.net/1/messages.json" +PUSHOVER_APP_TOKEN = config("PUSHOVER_APP_TOKEN") +PUSHOVER_USER_KEY = config("PUSHOVER_USER_KEY") + +# PostgreSQL connection parameters +DB_HOST = config("DB_HOST") +DB_PORT = config("DB_PORT", "5432") +DB_NAME = config("DB_NAME") +DB_USER = config("DB_USER") +DB_PASSWORD = config("DB_PASSWORD") + +# Observability +healthchecks_id = config("HEALTHCHECKS_ID") + +def get_db_connection(): + return psycopg2.connect( + host=DB_HOST, + port=DB_PORT, + dbname=DB_NAME, + user=DB_USER, + password=DB_PASSWORD + ) + +# Create table if not exists +def create_table(): + conn = get_db_connection() + conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) + with conn.cursor() as cur: + cur.execute(""" + CREATE TABLE IF NOT EXISTS github_releases ( + repo_name TEXT PRIMARY KEY, + latest_release TEXT, + release_date TIMESTAMP + ) + """) + conn.close() + +# Check for new release +def check_new_release(repo_name): + try: + response = requests.get( + f"https://api.github.com/repos/{repo_name}/releases/latest", timeout=5 + ) + response.raise_for_status() + release_data = response.json() + return release_data["tag_name"], release_data["published_at"] + except HTTPError as e: + if e.response.status_code == 404: + return None, None + else: + raise + +def should_ignore_tag(tag_name, ignore_list): + return any(ignore_str in tag_name for ignore_str in ignore_list) + +def check_latest_tag(repo_name, ignore_list): + response = requests.get(f"https://api.github.com/repos/{repo_name}/tags", timeout=5) + response.raise_for_status() + tags = response.json() + + for tag in tags: + tag_name = tag["name"] + if should_ignore_tag(tag_name, ignore_list): + continue + + commit_url = tag["commit"]["url"] + commit_response = requests.get(commit_url, timeout=5) + commit_response.raise_for_status() + commit_data = commit_response.json() + commit_date = commit_data["commit"]["committer"]["date"] + + return tag_name, commit_date + + return None, None + +def send_pushover_notification(repo_name, tag_name): + payload = { + "token": PUSHOVER_APP_TOKEN, + "user": PUSHOVER_USER_KEY, + "html": "1", + "message": f'New stable release {tag_name} for repository {repo_name} is available.', + } + response = requests.post(PUSHOVER_API_URL, data=payload, timeout=5, headers={'User-Agent': 'Python'}) + response.raise_for_status() + +def main(): + create_table() + conn = get_db_connection() + + try: + for repo_config in conf["repositories"]: + repo_name = repo_config["name"] + check_type = repo_config.get("check", "releases") + ignore_list = repo_config.get("ignore_tags_containing", []) + + print(f"Checking {check_type} for repository: {repo_name}") + + if check_type == "releases": + latest_tag, release_date = check_new_release(repo_name) + if latest_tag is None or release_date is None: + print(f"No release found for {repo_name}") + continue + elif check_type == "tags": + latest_tag, release_date = check_latest_tag(repo_name, ignore_list) + if latest_tag is None or release_date is None: + print(f"No valid tags found for {repo_name}, moving to next repository.") + continue + else: + print(f"Invalid check type for {repo_name}: {check_type}") + continue + + print(f"Latest tag for {repo_name}: {latest_tag}, published at: {release_date}") + release_date = datetime.strptime(release_date, "%Y-%m-%dT%H:%M:%SZ") + + with conn.cursor() as cur: + print(f"Updating database for {repo_name}...") + cur.execute( + "SELECT release_date FROM github_releases WHERE repo_name = %s", + (repo_name,) + ) + current = cur.fetchone() + + if not current or release_date > current[0]: + cur.execute(""" + INSERT INTO github_releases (repo_name, latest_release, release_date) + VALUES (%s, %s, %s) + ON CONFLICT (repo_name) + DO UPDATE SET + latest_release = EXCLUDED.latest_release, + release_date = EXCLUDED.release_date + """, (repo_name, latest_tag, release_date)) + conn.commit() + + print(f"New release for {repo_name} found, sending notification.") + send_pushover_notification(repo_name, latest_tag) + else: + print(f"No new release to update for {repo_name}.") + + try: + requests.get(f"https://hc-ping.com/{healthchecks_id}", timeout=10) + except requests.RequestException as e: + print("Ping failed: %s" % e) + + finally: + conn.close() + +if __name__ == "__main__": + main() diff --git a/kubernetes/apps/default/homelab/ks.yaml b/kubernetes/apps/default/homelab/ks.yaml index 0e20c756c..fa7a3e48f 100644 --- a/kubernetes/apps/default/homelab/ks.yaml +++ b/kubernetes/apps/default/homelab/ks.yaml @@ -2,6 +2,30 @@ # yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization +metadata: + name: &app homelab-github-notifier + namespace: flux-system +spec: + targetNamespace: default + commonMetadata: + labels: + app.kubernetes.io/name: *app + path: ./kubernetes/apps/default/homelab/github-notifier + prune: true + sourceRef: + kind: GitRepository + name: home-ops-kubernetes + wait: false + interval: 30m + retryInterval: 1m + timeout: 5m + postBuild: + substitute: + APP: *app +--- +# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/kustomize.toolkit.fluxcd.io/kustomization_v1.json +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization metadata: name: &app homelab-music-transcode namespace: flux-system diff --git a/kubernetes/apps/default/homelab/music-transcode/helmrelease.yaml b/kubernetes/apps/default/homelab/music-transcode/helmrelease.yaml index d2873d165..80a941fc3 100644 --- a/kubernetes/apps/default/homelab/music-transcode/helmrelease.yaml +++ b/kubernetes/apps/default/homelab/music-transcode/helmrelease.yaml @@ -4,7 +4,6 @@ apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: name: &app homelab-music-transcode - namespace: default spec: interval: 30m chart: @@ -64,7 +63,7 @@ spec: scripts: type: configMap name: music-transcode-configmap - defaultMode: 0775 + defaultMode: 0775 # trunk-ignore(yamllint/octal-values) globalMounts: - path: /app/transcode.sh subPath: transcode.sh @@ -72,7 +71,7 @@ spec: exclude: type: configMap name: music-transcode-configmap - defaultMode: 0775 + defaultMode: 0644 # trunk-ignore(yamllint/octal-values) globalMounts: - path: /app/transcode_exclude.cfg subPath: transcode_exclude.cfg diff --git a/kubernetes/apps/default/radarr/app/externalsecret.yaml b/kubernetes/apps/default/radarr/app/externalsecret.yaml index ee48cb1a3..e04ba79de 100644 --- a/kubernetes/apps/default/radarr/app/externalsecret.yaml +++ b/kubernetes/apps/default/radarr/app/externalsecret.yaml @@ -13,6 +13,7 @@ spec: name: radarr-secret template: data: + RADARR__AUTH__APIKEY: "{{ .RADARR__API_KEY }}" PUSHOVER_API_TOKEN: "{{ .PUSHOVER_API_TOKEN }}" PUSHOVER_USER_KEY: "{{ .PUSHOVER_USER_KEY }}" dataFrom: