Compare commits
40
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d4bc326d8 | ||
|
|
6a62bf54e4 | ||
|
|
27a34fa5f5 | ||
|
|
e633b1a0c5 | ||
|
|
da60eba710 | ||
|
|
ed59b8a32a | ||
|
|
46d91c8fad | ||
|
|
c7e018e7b6 | ||
|
|
cd5e947baf | ||
|
|
ce292d629b | ||
|
|
e5d0ffc3e5 | ||
|
|
dae7550e6f | ||
|
|
1177bb839d | ||
|
|
3c9c001685 | ||
|
|
c68befa4a4 | ||
|
|
68e4d42f77 | ||
|
|
eb035613ab | ||
|
|
e8645d11ed | ||
|
|
f3c9188807 | ||
|
|
c68b9ad7c2 | ||
|
|
59c9cc29fe | ||
|
|
3501586841 | ||
|
|
9e1452cfe7 | ||
|
|
fe397f906d | ||
|
|
48a28d1c8f | ||
|
|
a05dcfdc94 | ||
|
|
9616ee2ec6 | ||
|
|
e0e07c0327 | ||
|
|
50307fe748 | ||
|
|
fd0d2ef301 | ||
|
|
141b11a9ad | ||
|
|
03cf6a0e45 | ||
|
|
88576f7022 | ||
|
|
8c85a49ae5 | ||
|
|
8fa68bc9cd | ||
|
|
20d928aeba | ||
|
|
60dfcf5382 | ||
|
|
5e814c525a | ||
|
|
1a99c02996 | ||
|
|
09a03dfd76 |
@@ -47,7 +47,7 @@ when:
|
||||
|
||||
steps:
|
||||
- name: build-ansible
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.19-debug@sha256:7795cec509a50a71378cc2c365f55c555ee3a05834d8eb1cd2552df5e66d44d8
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.20-debug@sha256:28af5a3bd9c51fedb353ad91de735eda53592b5c06a34e7f5a9550334725d7f7
|
||||
directory: ci/images/ansible
|
||||
# Only rebuild when THIS image's source (or the pipeline file) changed, or
|
||||
# on a manual run. Without this every step ran on any matching push, so a
|
||||
@@ -74,7 +74,7 @@ steps:
|
||||
--destination gitea.renner.uno/gitea_admin/ci-ansible:latest
|
||||
|
||||
- name: build-k8s
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.19-debug@sha256:7795cec509a50a71378cc2c365f55c555ee3a05834d8eb1cd2552df5e66d44d8
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.20-debug@sha256:28af5a3bd9c51fedb353ad91de735eda53592b5c06a34e7f5a9550334725d7f7
|
||||
directory: ci/images/k8s
|
||||
when:
|
||||
- event: deployment
|
||||
@@ -97,7 +97,7 @@ steps:
|
||||
--destination gitea.renner.uno/gitea_admin/ci-k8s:latest
|
||||
|
||||
- name: build-vmtest
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.19-debug@sha256:7795cec509a50a71378cc2c365f55c555ee3a05834d8eb1cd2552df5e66d44d8
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.20-debug@sha256:28af5a3bd9c51fedb353ad91de735eda53592b5c06a34e7f5a9550334725d7f7
|
||||
directory: ci/images/vmtest
|
||||
when:
|
||||
- event: deployment
|
||||
@@ -120,7 +120,7 @@ steps:
|
||||
--destination gitea.renner.uno/gitea_admin/ci-vmtest:latest
|
||||
|
||||
- name: build-tofu
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.19-debug@sha256:7795cec509a50a71378cc2c365f55c555ee3a05834d8eb1cd2552df5e66d44d8
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.20-debug@sha256:28af5a3bd9c51fedb353ad91de735eda53592b5c06a34e7f5a9550334725d7f7
|
||||
directory: ci/images/tofu
|
||||
when:
|
||||
- event: deployment
|
||||
@@ -150,7 +150,7 @@ steps:
|
||||
# Runtime image, not CI tooling: a static musl binary in a FROM-scratch image,
|
||||
# used as the base for secret-rotation CronJobs. Replaces per-job inline Python.
|
||||
- name: build-secret-rotator
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.19-debug@sha256:7795cec509a50a71378cc2c365f55c555ee3a05834d8eb1cd2552df5e66d44d8
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.20-debug@sha256:28af5a3bd9c51fedb353ad91de735eda53592b5c06a34e7f5a9550334725d7f7
|
||||
directory: secret-rotator
|
||||
when:
|
||||
- event: deployment
|
||||
@@ -175,7 +175,7 @@ steps:
|
||||
# review-bot: the Go service that reviews pull requests with a Mistral agent (webhook plus sweep).
|
||||
# Runs after Gitea is up, so no ghcr mirror.
|
||||
- name: build-review-bot
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.19-debug@sha256:7795cec509a50a71378cc2c365f55c555ee3a05834d8eb1cd2552df5e66d44d8
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.20-debug@sha256:28af5a3bd9c51fedb353ad91de735eda53592b5c06a34e7f5a9550334725d7f7
|
||||
directory: review-bot
|
||||
when:
|
||||
- event: deployment
|
||||
@@ -225,7 +225,7 @@ steps:
|
||||
#
|
||||
# The gitea_admin/ copy stays until nothing references it any more.
|
||||
- name: build-alpine-jq
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.19-debug@sha256:7795cec509a50a71378cc2c365f55c555ee3a05834d8eb1cd2552df5e66d44d8
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.20-debug@sha256:28af5a3bd9c51fedb353ad91de735eda53592b5c06a34e7f5a9550334725d7f7
|
||||
directory: ci/images/alpine-jq
|
||||
when:
|
||||
- event: deployment
|
||||
@@ -276,7 +276,7 @@ steps:
|
||||
# note on build-alpine-jq. Anything an app needs while the cluster is still
|
||||
# being built cannot live only in that cluster's own registry.
|
||||
- name: build-cni-plugins
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.19-debug@sha256:7795cec509a50a71378cc2c365f55c555ee3a05834d8eb1cd2552df5e66d44d8
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.20-debug@sha256:28af5a3bd9c51fedb353ad91de735eda53592b5c06a34e7f5a9550334725d7f7
|
||||
directory: ci/images/cni-plugins
|
||||
when:
|
||||
- event: deployment
|
||||
@@ -316,7 +316,7 @@ steps:
|
||||
# gitea.renner.uno is not there to serve it. A cold-rebuild rehearsal left
|
||||
# paperless in Init:ErrImagePull on exactly this image.
|
||||
- name: build-alpine-openssl
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.19-debug@sha256:7795cec509a50a71378cc2c365f55c555ee3a05834d8eb1cd2552df5e66d44d8
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.20-debug@sha256:28af5a3bd9c51fedb353ad91de735eda53592b5c06a34e7f5a9550334725d7f7
|
||||
directory: ci/images/alpine-openssl
|
||||
when:
|
||||
- event: deployment
|
||||
@@ -351,7 +351,7 @@ steps:
|
||||
# Ansible converge. Not a ci-* image -- it is a runtime artifact -- but it
|
||||
# builds the same way and lands in the same registry so the audit sees it.
|
||||
- name: build-heating-can
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.19-debug@sha256:7795cec509a50a71378cc2c365f55c555ee3a05834d8eb1cd2552df5e66d44d8
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.20-debug@sha256:28af5a3bd9c51fedb353ad91de735eda53592b5c06a34e7f5a9550334725d7f7
|
||||
directory: ci/images/heating-can
|
||||
when:
|
||||
- event: deployment
|
||||
@@ -377,7 +377,7 @@ steps:
|
||||
# services (public Ingress hosts -> TLS passthrough -> Traefik ClusterIP).
|
||||
# Runtime artifact like heating-can; same registry so the audit tracks it.
|
||||
- name: build-netbird-service-sync
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.19-debug@sha256:7795cec509a50a71378cc2c365f55c555ee3a05834d8eb1cd2552df5e66d44d8
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.20-debug@sha256:28af5a3bd9c51fedb353ad91de735eda53592b5c06a34e7f5a9550334725d7f7
|
||||
directory: ci/images/netbird-service-sync
|
||||
when:
|
||||
- event: deployment
|
||||
@@ -402,7 +402,7 @@ steps:
|
||||
# authentik-gate-sync: the authentik-gate ingress class (Ingress -> Authentik
|
||||
# proxy gate + Traefik route). Its unit tests run as a stage of the image build.
|
||||
- name: build-authentik-gate-sync
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.19-debug@sha256:7795cec509a50a71378cc2c365f55c555ee3a05834d8eb1cd2552df5e66d44d8
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.20-debug@sha256:28af5a3bd9c51fedb353ad91de735eda53592b5c06a34e7f5a9550334725d7f7
|
||||
directory: ci/images/authentik-gate-sync
|
||||
when:
|
||||
- event: deployment
|
||||
@@ -428,7 +428,7 @@ steps:
|
||||
# kuma-sync: reconciles KumaMonitor resources into Uptime Kuma. Its unit
|
||||
# tests run as a stage of the image build.
|
||||
- name: build-kuma-sync
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.19-debug@sha256:7795cec509a50a71378cc2c365f55c555ee3a05834d8eb1cd2552df5e66d44d8
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.20-debug@sha256:28af5a3bd9c51fedb353ad91de735eda53592b5c06a34e7f5a9550334725d7f7
|
||||
directory: ci/images/kuma-sync
|
||||
when:
|
||||
- event: deployment
|
||||
@@ -454,7 +454,7 @@ steps:
|
||||
# cloudflare-lb-sync: reconciles CloudflareLoadBalancer resources into
|
||||
# Cloudflare. Its unit tests run as a stage of the image build.
|
||||
- name: build-cloudflare-lb-sync
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.19-debug@sha256:7795cec509a50a71378cc2c365f55c555ee3a05834d8eb1cd2552df5e66d44d8
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.20-debug@sha256:28af5a3bd9c51fedb353ad91de735eda53592b5c06a34e7f5a9550334725d7f7
|
||||
directory: ci/images/cloudflare-lb-sync
|
||||
when:
|
||||
- event: deployment
|
||||
@@ -480,7 +480,7 @@ steps:
|
||||
# bird: BGP daemon for the pod-network mesh over pod0 (#576). Runtime
|
||||
# artifact like heating-can; same registry so the audit tracks it.
|
||||
- name: build-bird
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.19-debug@sha256:7795cec509a50a71378cc2c365f55c555ee3a05834d8eb1cd2552df5e66d44d8
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.20-debug@sha256:28af5a3bd9c51fedb353ad91de735eda53592b5c06a34e7f5a9550334725d7f7
|
||||
directory: ci/images/bird
|
||||
when:
|
||||
- event: deployment
|
||||
@@ -504,7 +504,7 @@ steps:
|
||||
|
||||
# Orchestrator for the monthly backup-verify CronJob (velero ns).
|
||||
- name: build-backup-verify
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.19-debug@sha256:7795cec509a50a71378cc2c365f55c555ee3a05834d8eb1cd2552df5e66d44d8
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.20-debug@sha256:28af5a3bd9c51fedb353ad91de735eda53592b5c06a34e7f5a9550334725d7f7
|
||||
directory: ci/images/backup-verify
|
||||
when:
|
||||
- event: deployment
|
||||
|
||||
@@ -131,7 +131,7 @@ steps:
|
||||
# --build-arg to the resolved version (not "latest") guarantees the tag and
|
||||
# the image content agree even if a new release lands mid-run.
|
||||
- name: build
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.19-debug@sha256:7795cec509a50a71378cc2c365f55c555ee3a05834d8eb1cd2552df5e66d44d8
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.20-debug@sha256:28af5a3bd9c51fedb353ad91de735eda53592b5c06a34e7f5a9550334725d7f7
|
||||
directory: ci/images/claude
|
||||
depends_on: [decide]
|
||||
environment:
|
||||
|
||||
@@ -172,7 +172,7 @@ steps:
|
||||
# whenever it trails the deployed server. If you bump the server, bump here.
|
||||
- name: woodpecker-lint
|
||||
depends_on: [] # see note above `steps:`
|
||||
image: woodpeckerci/woodpecker-cli:v3.19.0-alpine@sha256:27a77117e38d7bbdc380f0e90ff3ee4871ab1eefa78b5d9136a2c969fbecd9f4
|
||||
image: woodpeckerci/woodpecker-cli:v3.20.1-alpine@sha256:74c576faf49cce699825d11c7d7bd38ab5f007dd4f57cdbdd55099d2c0f2621a
|
||||
commands:
|
||||
- |
|
||||
set -eu
|
||||
|
||||
@@ -63,7 +63,7 @@ steps:
|
||||
# bridge, and the secret guard.
|
||||
- name: hermes-skill-tests
|
||||
depends_on: []
|
||||
image: python:3.15-alpine@sha256:d44b1254cb92329418d164b22b20d86bbd3869d48e80e09e476531a409a728bb
|
||||
image: python:3.15-alpine@sha256:20a82dbc2783112ddb91d1534603f802d222a3e63e593cd002eb2ac534e70211
|
||||
environment:
|
||||
PYTHONDONTWRITEBYTECODE: "1"
|
||||
commands:
|
||||
|
||||
@@ -10,7 +10,7 @@ when:
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.19-debug@sha256:7795cec509a50a71378cc2c365f55c555ee3a05834d8eb1cd2552df5e66d44d8
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.20-debug@sha256:28af5a3bd9c51fedb353ad91de735eda53592b5c06a34e7f5a9550334725d7f7
|
||||
directory: ci/images/hermes
|
||||
environment:
|
||||
# registry-bot write:package PAT (konairius org secret).
|
||||
|
||||
@@ -6,7 +6,7 @@ when:
|
||||
|
||||
steps:
|
||||
- name: go-test
|
||||
image: golang:1.27.3-bookworm@sha256:972b4110ba0f47539a975c31e52c64765e1ff19f2d74469ae4a595c6676e80a5
|
||||
image: golang:1.27.3-bookworm@sha256:d9b6a1beb9474adc2a1ce842573639b44b2a0dd49b6669f51d3821baf9ec37c9
|
||||
commands:
|
||||
- test -z "$(gofmt -l review-bot)"
|
||||
- go -C review-bot vet ./...
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
|
||||
# The unit tests gate the image: the final stage copies the controller out of
|
||||
# this stage, so a failing test fails the build.
|
||||
FROM python:3.15-slim@sha256:533b0b2f9b5d46fd097091cf826f27902f1d6c3bca714b6dc74551356aa97a51 AS test
|
||||
FROM python:3.15-slim@sha256:94d3426f1def7db4a106a232ac121bfd975393e5dfd40a619b955fae45fe7913 AS test
|
||||
WORKDIR /src
|
||||
COPY controller.py test_controller.py ./
|
||||
RUN python3 -B -m unittest -v
|
||||
|
||||
FROM python:3.15-slim@sha256:533b0b2f9b5d46fd097091cf826f27902f1d6c3bca714b6dc74551356aa97a51
|
||||
FROM python:3.15-slim@sha256:94d3426f1def7db4a106a232ac121bfd975393e5dfd40a619b955fae45fe7913
|
||||
|
||||
RUN useradd --system --no-create-home --shell /usr/sbin/nologin controller
|
||||
COPY --from=test /src/controller.py /opt/controller.py
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""authentik-gate-sync: the Authentik side of the gate Middleware and of OIDC clients.
|
||||
"""authentik-gate-sync: the Authentik side of the gate Middleware.
|
||||
|
||||
An Ingress is gated when its traefik.ingress.kubernetes.io/router.middlewares
|
||||
annotation names the gate Middleware (authentik/gate, a ForwardAuth against the
|
||||
@@ -18,16 +18,6 @@ Annotations (all optional):
|
||||
auth.renner.uno/authorization-flow authorization flow slug (default below)
|
||||
auth.renner.uno/skip-path provider skip_path_regex
|
||||
|
||||
OIDC clients: each OIDCClient (auth.renner.uno/v1alpha1, CRD in
|
||||
infrastructure/authentik-gate-sync-crds) gets a confidential OAuth2/OpenID
|
||||
provider oidc-<slug> (client ID = slug, secret generated by Authentik) and an
|
||||
application with that slug, so the issuer is <authentik>/application/o/<slug>/;
|
||||
with spec.group the same group policy as gates. Its status carries Ready and
|
||||
the issuer. OIDC slugs and gate names are kept apart: a slug that is a gate's
|
||||
name, or an existing application without this controller's OAuth2 provider,
|
||||
is refused. An unassigned provider named oidc-<slug> is adopted (the remains
|
||||
of an interrupted teardown, which deletes the application first).
|
||||
|
||||
Fail closed: until the provider exists, or after it is removed (e.g. the named
|
||||
group is gone), the embedded outpost has no provider for the host and rejects
|
||||
the request; Traefik answers 500 when the outpost is unreachable.
|
||||
@@ -40,7 +30,6 @@ import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
@@ -57,10 +46,6 @@ DEFAULT_AUTHZ_FLOW = os.environ.get("DEFAULT_AUTHORIZATION_FLOW", "default-provi
|
||||
INVALIDATION_FLOW = os.environ.get("INVALIDATION_FLOW", "default-provider-invalidation-flow")
|
||||
COOKIE_DOMAIN = os.environ.get("COOKIE_DOMAIN", "renner.uno")
|
||||
AK_BROWSER_URL = os.environ.get("AUTHENTIK_HOST_BROWSER", "https://auth.renner.uno/")
|
||||
SIGNING_KEY = os.environ.get("OIDC_SIGNING_KEY", "authentik Self-signed Certificate")
|
||||
OIDC_SCOPES = tuple(f"goauthentik.io/providers/oauth2/scope-{s}" for s in ("openid", "profile", "email"))
|
||||
OIDC_GRANTS = ["authorization_code", "refresh_token"]
|
||||
SLUG_RE = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,48}[a-z0-9])?$")
|
||||
INTERVAL = int(os.environ.get("INTERVAL_SECONDS", "60"))
|
||||
HTTP_TIMEOUT = 20
|
||||
|
||||
@@ -249,74 +234,6 @@ def provider_body(gate, flows):
|
||||
}
|
||||
|
||||
|
||||
OIDC_API = "/apis/auth.renner.uno/v1alpha1"
|
||||
|
||||
|
||||
def oidc_slug(cr):
|
||||
return ((cr.get("spec") or {}).get("slug") or cr["metadata"]["name"]).strip()
|
||||
|
||||
|
||||
def oidc_spec(cr):
|
||||
"""Translate an OIDCClient into the client it asks for. Raises GateError."""
|
||||
meta, spec = cr["metadata"], cr.get("spec") or {}
|
||||
slug = oidc_slug(cr)
|
||||
if not SLUG_RE.match(slug):
|
||||
raise GateError(f"slug {slug!r} is not a slug (a-z, 0-9, -; max 50)")
|
||||
launch = spec.get("launchURL", "")
|
||||
if not launch.startswith(("https://", "http://")):
|
||||
raise GateError(f"launchURL {launch!r} is not an http(s) URL")
|
||||
redirects = [{"matching_mode": r.get("matchingMode", "strict"), "url": r.get("url", ""),
|
||||
"redirect_uri_type": "authorization"}
|
||||
for r in spec.get("redirectURIs") or []]
|
||||
if not redirects or any(not r["url"] or r["matching_mode"] not in ("strict", "regex") for r in redirects):
|
||||
raise GateError("redirectURIs needs at least one entry with url and matchingMode strict|regex")
|
||||
for r in redirects:
|
||||
if r["matching_mode"] == "regex":
|
||||
try:
|
||||
re.compile(r["url"])
|
||||
except re.error as e:
|
||||
raise GateError(f"redirect regex {r['url']!r}: {e}") from None
|
||||
return {
|
||||
# name: provider and policy name; slug: application slug and client ID.
|
||||
"name": f"oidc-{slug}",
|
||||
"slug": slug,
|
||||
"namespace": meta["namespace"],
|
||||
"resource": meta["name"],
|
||||
"launch_url": launch,
|
||||
"group": spec.get("group") or None,
|
||||
"authorization_flow": spec.get("authorizationFlow") or DEFAULT_AUTHZ_FLOW,
|
||||
"redirect_uris": redirects,
|
||||
}
|
||||
|
||||
|
||||
def oidc_status(cr, ok, message):
|
||||
cond = {"type": "Ready", "status": "True" if ok else "False",
|
||||
"reason": "Reconciled" if ok else "Failed", "message": message[:1000],
|
||||
"observedGeneration": cr["metadata"].get("generation", 0)}
|
||||
old = next((c for c in (cr.get("status") or {}).get("conditions") or [] if c.get("type") == "Ready"), {})
|
||||
same = all(old.get(k) == cond[k] for k in ("status", "reason", "message", "observedGeneration"))
|
||||
cond["lastTransitionTime"] = old.get("lastTransitionTime") if same and old.get("lastTransitionTime") \
|
||||
else time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
status = {"conditions": [cond], "observedGeneration": cond["observedGeneration"]}
|
||||
if ok:
|
||||
status["issuer"] = f"{AK_BROWSER_URL}application/o/{oidc_slug(cr)}/"
|
||||
return status
|
||||
|
||||
|
||||
def oauth2_body(spec, flows, refs):
|
||||
return {
|
||||
"name": spec["name"],
|
||||
"authorization_flow": flows[spec["authorization_flow"]],
|
||||
"invalidation_flow": flows[INVALIDATION_FLOW],
|
||||
"client_type": "confidential",
|
||||
"client_id": spec["slug"],
|
||||
"grant_types": OIDC_GRANTS,
|
||||
"signing_key": refs["signing_key"],
|
||||
"property_mappings": sorted(refs["scopes"]),
|
||||
"redirect_uris": spec["redirect_uris"],
|
||||
}
|
||||
|
||||
|
||||
def drift(current, desired):
|
||||
return {k: v for k, v in desired.items() if current.get(k) != v}
|
||||
|
||||
@@ -347,18 +264,6 @@ class K8s:
|
||||
return http("PATCH", K8S_API + path, obj, headers=self._h(), cafile=K8S_CA_FILE,
|
||||
content_type="application/apply-patch+yaml")
|
||||
|
||||
def patch_status(self, path, status):
|
||||
return http("PATCH", K8S_API + path + "/status", {"status": status}, headers=self._h(),
|
||||
cafile=K8S_CA_FILE, content_type="application/merge-patch+json")
|
||||
|
||||
def oidc_clients(self):
|
||||
try:
|
||||
return self.get(f"{OIDC_API}/oidcclients").get("items", [])
|
||||
except ApiError as e:
|
||||
if e.code == 404: # CRD not installed: no clients, gates unaffected
|
||||
return []
|
||||
raise
|
||||
|
||||
def delete(self, obj):
|
||||
try:
|
||||
http("DELETE", K8S_API + object_path(obj), headers=self._h(), cafile=K8S_CA_FILE)
|
||||
@@ -542,111 +447,42 @@ def ensure_access(ak, gate, target):
|
||||
ak.req("DELETE", f"policies/expression/{stale['pk']}/")
|
||||
|
||||
|
||||
def oidc_refs(ak):
|
||||
"""Signing key and scope mappings every OIDC client uses; matched here, not
|
||||
by server-side filters, so an ignored filter cannot pick another object."""
|
||||
keys = [k["pk"] for k in ak.list("crypto/certificatekeypairs/") if k.get("name") == SIGNING_KEY]
|
||||
if len(keys) != 1:
|
||||
raise ApiError(f"expected one certificate {SIGNING_KEY!r}, found {len(keys)}")
|
||||
scopes = {m.get("managed"): m["pk"] for m in ak.list("propertymappings/provider/scope/")
|
||||
if m.get("managed") in OIDC_SCOPES}
|
||||
missing = [s for s in OIDC_SCOPES if s not in scopes]
|
||||
if missing:
|
||||
raise ApiError(f"scope mappings not found: {missing}")
|
||||
return {"signing_key": keys[0], "scopes": list(scopes.values())}
|
||||
|
||||
|
||||
def ensure_oidc(ak, spec, flows, refs):
|
||||
name, slug = spec["name"], spec["slug"]
|
||||
if spec["group"] and ak.one("core/groups/", name=spec["group"]) is None:
|
||||
raise GateError(f"group {spec['group']!r} not found in Authentik")
|
||||
|
||||
app = get_application(ak, slug)
|
||||
if app is not None and app.get("meta_publisher") != MANAGER:
|
||||
raise GateError(f"application {slug} exists and is not managed by {MANAGER}")
|
||||
|
||||
want = oauth2_body(spec, flows, refs)
|
||||
provider = next((p for p in ak.list("providers/oauth2/") if p.get("name", "").lower() == name), None)
|
||||
if app is not None and (provider is None or app.get("provider") != provider["pk"]):
|
||||
# E.g. a gate's application: never re-point it at an OAuth2 provider.
|
||||
raise GateError(f"application {slug} exists without OAuth2 provider {name}")
|
||||
if provider is None:
|
||||
# No client_secret: Authentik generates it (generate_client_secret).
|
||||
provider = ak.req("POST", "providers/oauth2/", want)
|
||||
LOG.info("%s: created OAuth2 provider %s", name, provider["pk"])
|
||||
elif provider.get("assigned_application_slug") not in (None, slug) or \
|
||||
(app is not None and app.get("provider") != provider["pk"]):
|
||||
raise GateError(f"OAuth2 provider {name} belongs to another application")
|
||||
else:
|
||||
current = dict(provider, property_mappings=sorted(provider.get("property_mappings") or []))
|
||||
if drift(current, want):
|
||||
ak.req("PATCH", f"providers/oauth2/{provider['pk']}/", drift(current, want))
|
||||
LOG.info("%s: updated OAuth2 provider %s", name, sorted(drift(current, want)))
|
||||
|
||||
want_app = {"name": f"{slug} (oidc)", "slug": slug, "provider": provider["pk"],
|
||||
"meta_launch_url": spec["launch_url"], "meta_publisher": MANAGER,
|
||||
"meta_description": f"OIDCClient {spec['namespace']}/{spec['resource']}"}
|
||||
if app is None:
|
||||
app = ak.req("POST", "core/applications/", want_app)
|
||||
LOG.info("%s: created application %s", name, slug)
|
||||
elif drift(app, want_app):
|
||||
ak.req("PATCH", f"core/applications/{slug}/", drift(app, want_app))
|
||||
LOG.info("%s: updated application %s", name, sorted(drift(app, want_app)))
|
||||
|
||||
ensure_access(ak, spec, app["pbm_uuid"])
|
||||
|
||||
|
||||
PROVIDER_PATHS = ("providers/proxy/", "providers/oauth2/")
|
||||
|
||||
|
||||
def policy_name(app):
|
||||
return f"oidc-{app['slug']}" if app.get("_provider_path") == "providers/oauth2/" else app["slug"]
|
||||
|
||||
|
||||
def remove_gate(ak, app):
|
||||
"""Delete an owned gate or OIDC client: the application (bindings cascade;
|
||||
its provider is PROTECTed while referenced), then the provider (a proxy
|
||||
provider leaves the embedded outpost with it), the policy. Ownership is
|
||||
found through the application, so until it is gone a failed step is
|
||||
retried on the next pass."""
|
||||
"""Delete an owned gate: the application (bindings cascade; its provider is
|
||||
PROTECTed while referenced), then the provider (which leaves the embedded
|
||||
outpost with it), the policy. Ownership is found through the application,
|
||||
so until it is gone a failed step is retried on the next pass."""
|
||||
name, provider = app["slug"], app.get("provider")
|
||||
path = app.get("_provider_path", "providers/proxy/")
|
||||
if path == "providers/proxy/":
|
||||
remove_dedicated_outposts(ak, name, provider)
|
||||
remove_dedicated_outposts(ak, name, provider)
|
||||
ak.req("DELETE", f"core/applications/{name}/")
|
||||
if provider:
|
||||
ak.req("DELETE", f"{path}{provider}/")
|
||||
policy = ak.one("policies/expression/", name=policy_name(app))
|
||||
ak.req("DELETE", f"providers/proxy/{provider}/")
|
||||
policy = ak.one("policies/expression/", name=name)
|
||||
if policy is not None:
|
||||
ak.req("DELETE", f"policies/expression/{policy['pk']}/")
|
||||
LOG.info("%s: removed %s from Authentik", name,
|
||||
"OIDC client" if path == "providers/oauth2/" else "gate")
|
||||
LOG.info("%s: removed gate from Authentik", name)
|
||||
|
||||
|
||||
def owned_applications(ak):
|
||||
"""Applications this controller owns, found through the (unfiltered) proxy
|
||||
and OAuth2 provider lists and confirmed one by one."""
|
||||
for path in PROVIDER_PATHS:
|
||||
for p in ak.list(path):
|
||||
slug = p.get("assigned_application_slug")
|
||||
if not slug:
|
||||
continue
|
||||
app = get_application(ak, slug)
|
||||
if app and app.get("meta_publisher") == MANAGER and app.get("provider") == p["pk"]:
|
||||
yield dict(app, _provider_path=path)
|
||||
provider list and confirmed one by one."""
|
||||
for p in ak.list("providers/proxy/"):
|
||||
slug = p.get("assigned_application_slug")
|
||||
if not slug:
|
||||
continue
|
||||
app = get_application(ak, slug)
|
||||
if app and app.get("meta_publisher") == MANAGER and app.get("provider") == p["pk"]:
|
||||
yield app
|
||||
|
||||
|
||||
# --- reconcile --------------------------------------------------------------
|
||||
|
||||
def reconcile(k8s=None, ak=None):
|
||||
k8s = k8s or K8s()
|
||||
if ak is None:
|
||||
with open(AK_TOKEN_FILE) as f:
|
||||
ak = Authentik(f.read().strip())
|
||||
def reconcile():
|
||||
k8s = K8s()
|
||||
with open(AK_TOKEN_FILE) as f:
|
||||
ak = Authentik(f.read().strip())
|
||||
|
||||
ingresses = k8s.get("/apis/networking.k8s.io/v1/ingresses").get("items", [])
|
||||
sources = [i for i in ingresses if is_gated(i)]
|
||||
clients = k8s.oidc_clients()
|
||||
sources = [i for i in k8s.get("/apis/networking.k8s.io/v1/ingresses").get("items", []) if is_gated(i)]
|
||||
shared = shared_hosts(sources)
|
||||
flows = {f["slug"]: f["pk"] for f in ak.list("flows/instances/")}
|
||||
ensure_embedded_config(ak)
|
||||
@@ -690,44 +526,6 @@ def reconcile(k8s=None, ak=None):
|
||||
held.add(gate_name(ing))
|
||||
failed += 1
|
||||
|
||||
gate_names = {gate_name(i) for i in sources}
|
||||
keep_clients = set() # OIDC slugs whose Authentik side should exist
|
||||
slugs = [oidc_slug(i) for i in clients]
|
||||
refs = None
|
||||
for cr in sorted(clients, key=oidc_slug):
|
||||
ref, slug = f'{cr["metadata"]["namespace"]}/{cr["metadata"]["name"]}', oidc_slug(cr)
|
||||
ok, message = False, ""
|
||||
try:
|
||||
spec = oidc_spec(cr)
|
||||
if slugs.count(slug) > 1:
|
||||
raise GateError(f"slug {slug} is claimed by more than one OIDCClient")
|
||||
if slug in gate_names:
|
||||
raise GateError(f"slug {slug} is the name of a gate")
|
||||
for flow in (spec["authorization_flow"], INVALIDATION_FLOW):
|
||||
if flow not in flows:
|
||||
raise GateError(f"flow {flow!r} not found in Authentik")
|
||||
keep_clients.add(slug)
|
||||
refs = refs or oidc_refs(ak)
|
||||
ensure_oidc(ak, spec, flows, refs)
|
||||
ok, message = True, f"provider {spec['name']}, application {slug}"
|
||||
except GateError as e:
|
||||
LOG.error("OIDCClient %s: %s", ref, e)
|
||||
failed += 1
|
||||
keep_clients.discard(slug)
|
||||
message = str(e)
|
||||
except Exception as e:
|
||||
LOG.error("OIDCClient %s: %s", ref, e)
|
||||
keep_clients.add(slug)
|
||||
failed += 1
|
||||
message = f"transient: {e}"
|
||||
try:
|
||||
path = f'{OIDC_API}/namespaces/{cr["metadata"]["namespace"]}/oidcclients/{cr["metadata"]["name"]}'
|
||||
status = oidc_status(cr, ok, message)
|
||||
if status != cr.get("status"):
|
||||
k8s.patch_status(path, status)
|
||||
except ApiError as e:
|
||||
LOG.error("OIDCClient %s: status: %s", ref, e)
|
||||
|
||||
removed = 0
|
||||
for obj in managed_objects(k8s):
|
||||
key = (obj["kind"], obj["metadata"]["namespace"], obj["metadata"]["name"])
|
||||
@@ -738,13 +536,11 @@ def reconcile(k8s=None, ak=None):
|
||||
removed += 1
|
||||
|
||||
for app in list(owned_applications(ak)):
|
||||
keep = keep_clients if app["_provider_path"] == "providers/oauth2/" else gates
|
||||
if app["slug"] not in keep:
|
||||
if app["slug"] not in gates:
|
||||
remove_gate(ak, app)
|
||||
removed += 1
|
||||
|
||||
LOG.info("reconciled: %d gates, %d OIDC clients, %d failed, %d objects removed",
|
||||
len(sources), len(clients), failed, removed)
|
||||
LOG.info("reconciled: %d gates, %d failed, %d objects removed", len(sources), failed, removed)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -176,10 +176,8 @@ class FakeAuthentik:
|
||||
"""In-memory stand-in for the endpoints the controller uses; records calls."""
|
||||
|
||||
def __init__(self):
|
||||
self.db = {"providers/proxy/": {}, "providers/oauth2/": {}, "core/applications/": {},
|
||||
"outposts/instances/": {}, "policies/bindings/": {}, "policies/expression/": {},
|
||||
"core/groups/": {}, "crypto/certificatekeypairs/": {},
|
||||
"propertymappings/provider/scope/": {}}
|
||||
self.db = {"providers/proxy/": {}, "core/applications/": {}, "outposts/instances/": {},
|
||||
"policies/bindings/": {}, "policies/expression/": {}, "core/groups/": {}}
|
||||
self.calls = []
|
||||
self.next = 1
|
||||
|
||||
@@ -196,9 +194,6 @@ class FakeAuthentik:
|
||||
return coll, rest or None
|
||||
raise AssertionError(path)
|
||||
|
||||
def _provider(self, pk):
|
||||
return self.db["providers/proxy/"].get(pk) or self.db["providers/oauth2/"][pk]
|
||||
|
||||
def _find(self, coll, key):
|
||||
if coll == "core/applications/":
|
||||
return next((a for a in self.db[coll].values() if a["slug"] == key), None)
|
||||
@@ -231,7 +226,7 @@ class FakeAuthentik:
|
||||
obj = self.add(coll, dict(body))
|
||||
if coll == "core/applications/":
|
||||
obj["pbm_uuid"] = f"pbm-{obj['pk']}"
|
||||
self._provider(obj["provider"])["assigned_application_slug"] = obj["slug"]
|
||||
self.db["providers/proxy/"][obj["provider"]]["assigned_application_slug"] = obj["slug"]
|
||||
return copy.deepcopy(obj)
|
||||
obj = self._find(coll, key)
|
||||
if method == "PATCH":
|
||||
@@ -244,7 +239,7 @@ class FakeAuthentik:
|
||||
if method == "DELETE":
|
||||
del self.db[coll][obj["pk"]]
|
||||
if coll == "core/applications/":
|
||||
self._provider(obj["provider"])["assigned_application_slug"] = None
|
||||
self.db["providers/proxy/"][obj["provider"]]["assigned_application_slug"] = None
|
||||
for b in [b for b in self.db["policies/bindings/"].values() if b["target"] == obj["pbm_uuid"]]:
|
||||
del self.db["policies/bindings/"][b["pk"]]
|
||||
return None
|
||||
@@ -387,234 +382,5 @@ class Reconcile(unittest.TestCase):
|
||||
self.assertEqual(self.ak.get(f"core/applications/{self.g['name']}/")["provider"], app["provider"])
|
||||
|
||||
|
||||
YOUTRACK = {
|
||||
"metadata": {"name": "youtrack", "namespace": "youtrack", "generation": 3},
|
||||
"spec": {
|
||||
"launchURL": "https://youtrack.renner.uno",
|
||||
"group": "YouTrack Users",
|
||||
"redirectURIs": [{"matchingMode": "regex", "url": r"^https://youtrack\.renner\.uno/.*$"}],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class OidcSpec(unittest.TestCase):
|
||||
def test_youtrack(self):
|
||||
s = c.oidc_spec(YOUTRACK)
|
||||
self.assertEqual((s["name"], s["slug"], s["launch_url"], s["group"]),
|
||||
("oidc-youtrack", "youtrack", "https://youtrack.renner.uno", "YouTrack Users"))
|
||||
self.assertEqual(s["redirect_uris"],
|
||||
[{"matching_mode": "regex", "url": r"^https://youtrack\.renner\.uno/.*$",
|
||||
"redirect_uri_type": "authorization"}])
|
||||
|
||||
def test_slug_overrides_name(self):
|
||||
cr = copy.deepcopy(YOUTRACK)
|
||||
cr["spec"]["slug"] = "yt"
|
||||
self.assertEqual(c.oidc_spec(cr)["slug"], "yt")
|
||||
|
||||
def test_strict_is_default(self):
|
||||
cr = copy.deepcopy(YOUTRACK)
|
||||
cr["spec"]["redirectURIs"] = [{"url": "https://youtrack.renner.uno/cb"}]
|
||||
self.assertEqual(c.oidc_spec(cr)["redirect_uris"][0]["matching_mode"], "strict")
|
||||
|
||||
def test_rejects_bad_slug(self):
|
||||
cr = copy.deepcopy(YOUTRACK)
|
||||
cr["spec"]["slug"] = "You Track"
|
||||
with self.assertRaises(c.GateError):
|
||||
c.oidc_spec(cr)
|
||||
|
||||
def test_rejects_missing_redirects(self):
|
||||
cr = copy.deepcopy(YOUTRACK)
|
||||
cr["spec"]["redirectURIs"] = []
|
||||
with self.assertRaises(c.GateError):
|
||||
c.oidc_spec(cr)
|
||||
|
||||
def test_rejects_bad_regex(self):
|
||||
cr = copy.deepcopy(YOUTRACK)
|
||||
cr["spec"]["redirectURIs"] = [{"matchingMode": "regex", "url": "("}]
|
||||
with self.assertRaises(c.GateError):
|
||||
c.oidc_spec(cr)
|
||||
|
||||
def test_status(self):
|
||||
st = c.oidc_status(YOUTRACK, True, "ok")
|
||||
self.assertEqual(st["issuer"], c.AK_BROWSER_URL + "application/o/youtrack/")
|
||||
self.assertEqual((st["conditions"][0]["status"], st["observedGeneration"]), ("True", 3))
|
||||
cr = dict(YOUTRACK, status=st)
|
||||
self.assertEqual(c.oidc_status(cr, True, "ok")["conditions"][0]["lastTransitionTime"],
|
||||
st["conditions"][0]["lastTransitionTime"])
|
||||
self.assertNotIn("issuer", c.oidc_status(cr, False, "group missing"))
|
||||
|
||||
|
||||
class ReconcileOidc(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.ak = FakeAuthentik()
|
||||
self.ak.add("crypto/certificatekeypairs/", {"name": c.SIGNING_KEY, "pk": "key-1"})
|
||||
self.ak.add("crypto/certificatekeypairs/", {"name": "other", "pk": "key-2"})
|
||||
for i, scope in enumerate(c.OIDC_SCOPES):
|
||||
self.ak.add("propertymappings/provider/scope/", {"pk": f"m{i}", "managed": scope})
|
||||
self.ak.add("propertymappings/provider/scope/", {"pk": "m9", "managed": "goauthentik.io/x"})
|
||||
self.ak.add("core/groups/", {"name": "YouTrack Users"})
|
||||
self.s = c.oidc_spec(YOUTRACK)
|
||||
|
||||
def run_once(self):
|
||||
c.ensure_oidc(self.ak, self.s, FLOWS, c.oidc_refs(self.ak))
|
||||
|
||||
def test_creates_owned_client(self):
|
||||
self.run_once()
|
||||
app = self.ak.get("core/applications/youtrack/")
|
||||
self.assertEqual(app["meta_publisher"], c.MANAGER)
|
||||
p = self.ak.db["providers/oauth2/"][app["provider"]]
|
||||
self.assertEqual((p["name"], p["client_id"], p["client_type"], p["signing_key"]),
|
||||
("oidc-youtrack", "youtrack", "confidential", "key-1"))
|
||||
self.assertNotIn("client_secret", p)
|
||||
self.assertEqual(p["grant_types"], ["authorization_code", "refresh_token"])
|
||||
self.assertEqual(p["property_mappings"], ["m0", "m1", "m2"])
|
||||
policy = self.ak.one("policies/expression/", name="oidc-youtrack")
|
||||
self.assertIn("'YouTrack Users'", policy["expression"])
|
||||
self.assertEqual([b["target"] for b in self.ak.list("policies/bindings/")], [app["pbm_uuid"]])
|
||||
|
||||
def test_second_pass_is_a_noop(self):
|
||||
self.run_once()
|
||||
before = len(self.ak.calls)
|
||||
self.run_once()
|
||||
self.assertEqual(self.ak.calls[before:], [])
|
||||
|
||||
def test_missing_group_is_a_gate_error(self):
|
||||
self.ak.db["core/groups/"].clear()
|
||||
with self.assertRaises(c.GateError):
|
||||
self.run_once()
|
||||
self.assertEqual(self.ak.list("providers/oauth2/"), [])
|
||||
|
||||
def test_refuses_unowned_application(self):
|
||||
self.ak.add("core/applications/", {"slug": "youtrack", "meta_publisher": "", "provider": None})
|
||||
with self.assertRaises(c.GateError):
|
||||
self.run_once()
|
||||
|
||||
def test_redirect_change_patches_provider(self):
|
||||
self.run_once()
|
||||
self.s = dict(self.s, redirect_uris=[{"matching_mode": "strict", "url": "https://youtrack.renner.uno/cb",
|
||||
"redirect_uri_type": "authorization"}])
|
||||
self.run_once()
|
||||
p = next(iter(self.ak.db["providers/oauth2/"].values()))
|
||||
self.assertEqual(p["redirect_uris"], self.s["redirect_uris"])
|
||||
|
||||
def test_missing_signing_key_is_transient(self):
|
||||
self.ak.db["crypto/certificatekeypairs/"].clear()
|
||||
with self.assertRaises(c.ApiError):
|
||||
c.oidc_refs(self.ak)
|
||||
|
||||
def test_ownership_scan_and_removal(self):
|
||||
foreign = self.ak.add("providers/oauth2/", {"name": "gitea", "assigned_application_slug": "gitea"})
|
||||
self.ak.add("core/applications/", {"slug": "gitea", "meta_publisher": "", "provider": foreign["pk"]})
|
||||
self.run_once()
|
||||
owned = list(c.owned_applications(self.ak))
|
||||
self.assertEqual([a["slug"] for a in owned], ["youtrack"])
|
||||
c.remove_gate(self.ak, owned[0])
|
||||
self.assertIsNone(self.ak.get("core/applications/youtrack/"))
|
||||
self.assertEqual([p["name"] for p in self.ak.list("providers/oauth2/")], ["gitea"])
|
||||
self.assertIsNone(self.ak.one("policies/expression/", name="oidc-youtrack"))
|
||||
|
||||
|
||||
def test_refuses_application_without_its_provider(self):
|
||||
gate_provider = self.ak.add("providers/proxy/", {"name": "g", "assigned_application_slug": "youtrack"})
|
||||
self.ak.add("core/applications/", {"slug": "youtrack", "meta_publisher": c.MANAGER,
|
||||
"provider": gate_provider["pk"], "pbm_uuid": "pbm-x"})
|
||||
with self.assertRaises(c.GateError):
|
||||
self.run_once()
|
||||
self.assertEqual(self.ak.list("providers/oauth2/"), [])
|
||||
self.assertEqual(self.ak.get("core/applications/youtrack/")["provider"], gate_provider["pk"])
|
||||
|
||||
|
||||
class FakeK8s:
|
||||
def __init__(self, ingresses=(), clients=(), services=None):
|
||||
self.ingresses, self.clients = list(ingresses), list(clients)
|
||||
self.services = services or {}
|
||||
self.status, self.applied = {}, []
|
||||
|
||||
def get(self, path):
|
||||
if path == "/apis/networking.k8s.io/v1/ingresses":
|
||||
return {"items": copy.deepcopy(self.ingresses)}
|
||||
if "labelSelector=" in path:
|
||||
return {"items": []}
|
||||
if path.startswith("/api/v1/namespaces/") and "/services/" in path:
|
||||
return copy.deepcopy(self.services[path.rsplit("/", 1)[1]])
|
||||
raise AssertionError(path)
|
||||
|
||||
def oidc_clients(self):
|
||||
return copy.deepcopy(self.clients)
|
||||
|
||||
def apply(self, obj):
|
||||
self.applied.append(obj)
|
||||
|
||||
def patch_status(self, path, status):
|
||||
self.status[path.rsplit("/", 1)[1]] = status
|
||||
for cr in self.clients:
|
||||
if cr["metadata"]["name"] == path.rsplit("/", 1)[1]:
|
||||
cr["status"] = status
|
||||
|
||||
|
||||
class ReconcileLoop(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.ak = FakeAuthentik()
|
||||
self.ak.db["flows/instances/"] = {}
|
||||
for slug, pk in FLOWS.items():
|
||||
self.ak.add("flows/instances/", {"slug": slug, "pk": pk})
|
||||
self.ak.add("outposts/instances/", {"name": "authentik Embedded Outpost", "managed": c.EMBEDDED_OUTPOST,
|
||||
"providers": [], "config": {}})
|
||||
self.ak.add("crypto/certificatekeypairs/", {"name": c.SIGNING_KEY, "pk": "key-1"})
|
||||
for i, scope in enumerate(c.OIDC_SCOPES):
|
||||
self.ak.add("propertymappings/provider/scope/", {"pk": f"m{i}", "managed": scope})
|
||||
self.ak.add("core/groups/", {"name": "YouTrack Users"})
|
||||
|
||||
def client(self, name="youtrack", **spec):
|
||||
cr = copy.deepcopy(YOUTRACK)
|
||||
cr["metadata"]["name"] = name
|
||||
cr["spec"].update(spec)
|
||||
return cr
|
||||
|
||||
def test_creates_then_quiet(self):
|
||||
k8s = FakeK8s(clients=[self.client()])
|
||||
c.reconcile(k8s, self.ak)
|
||||
self.assertEqual(k8s.status["youtrack"]["conditions"][0]["status"], "True")
|
||||
self.assertIsNotNone(self.ak.get("core/applications/youtrack/"))
|
||||
before, k8s.status = len(self.ak.calls), {}
|
||||
c.reconcile(k8s, self.ak)
|
||||
self.assertEqual(self.ak.calls[before:], [])
|
||||
self.assertEqual(k8s.status, {}) # unchanged status is not re-written
|
||||
|
||||
def test_duplicate_slug_withholds_both(self):
|
||||
k8s = FakeK8s(clients=[self.client(), self.client("other", slug="youtrack")])
|
||||
c.reconcile(k8s, self.ak)
|
||||
self.assertIsNone(self.ak.get("core/applications/youtrack/"))
|
||||
self.assertEqual({k: v["conditions"][0]["status"] for k, v in k8s.status.items()},
|
||||
{"youtrack": "False", "other": "False"})
|
||||
|
||||
def test_transient_error_keeps_client(self):
|
||||
k8s = FakeK8s(clients=[self.client()])
|
||||
c.reconcile(k8s, self.ak)
|
||||
self.ak.db["crypto/certificatekeypairs/"].clear()
|
||||
c.reconcile(k8s, self.ak)
|
||||
self.assertIsNotNone(self.ak.get("core/applications/youtrack/"))
|
||||
self.assertIn("transient", k8s.status["youtrack"]["conditions"][0]["message"])
|
||||
|
||||
def test_sweeps_client_whose_resource_is_gone(self):
|
||||
k8s = FakeK8s(clients=[self.client()])
|
||||
c.reconcile(k8s, self.ak)
|
||||
k8s.clients = []
|
||||
c.reconcile(k8s, self.ak)
|
||||
self.assertIsNone(self.ak.get("core/applications/youtrack/"))
|
||||
self.assertEqual(self.ak.list("providers/oauth2/"), [])
|
||||
|
||||
def test_slug_equal_to_gate_name_leaves_gate_alone(self):
|
||||
k8s = FakeK8s(ingresses=[LLAMA], services={"llama-server": SERVICE})
|
||||
c.reconcile(k8s, self.ak)
|
||||
gate = c.gate_name(LLAMA)
|
||||
app = self.ak.get(f"core/applications/{gate}/")
|
||||
k8s.clients = [self.client("evil", slug=gate)]
|
||||
c.reconcile(k8s, self.ak)
|
||||
self.assertEqual(self.ak.get(f"core/applications/{gate}/")["provider"], app["provider"])
|
||||
self.assertEqual(self.ak.list("providers/oauth2/"), [])
|
||||
self.assertEqual(k8s.status["evil"]["conditions"][0]["status"], "False")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:24-alpine@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53
|
||||
FROM node:24-alpine@sha256:8a32c8cf649a49915a66e81e7a609223c708d0adabf288ee3f4077d765542f85
|
||||
|
||||
# Default to the latest Claude Code. Pinning a fixed version is the wrong
|
||||
# default here: old releases eventually stop working against the API, so a
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# Cloudflare. The unit tests (no tags, so no envtest) gate the image: the
|
||||
# final stage copies the binary out of this stage, so a failing test fails the
|
||||
# build.
|
||||
FROM golang:1.27@sha256:f20d83a83e18dbd2a4714b8ffd4b30b287b6243962dd75608e86abcc63ba4efc AS build
|
||||
FROM golang:1.27@sha256:2920bb124f1a2e12a1d39f0cab756ae0068441ee1da7655dec7f5e374944095b AS build
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
@@ -10,7 +10,7 @@ COPY . .
|
||||
RUN go test ./...
|
||||
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/cloudflare-lb-sync ./cmd/cloudflare-lb-sync
|
||||
|
||||
FROM gcr.io/distroless/static:nonroot@sha256:18f4cc26c9ffa7f20a359298be7deadac5c607c8e03a02b8119f1a5fc2f82c99
|
||||
FROM gcr.io/distroless/static:nonroot@sha256:25ac8f0c3b2334a9eb05452266bd4697864a8c8ef0ef71e5e6480d7f92868329
|
||||
COPY --from=build /out/cloudflare-lb-sync /cloudflare-lb-sync
|
||||
USER 65532
|
||||
ENTRYPOINT ["/cloudflare-lb-sync"]
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
# namespace, so can0 is only reachable with network_mode: host. That is also
|
||||
# how the local MQTT broker on 127.0.0.1:1883 is reached. CAN_RAW sockets need
|
||||
# no capabilities, so both run unprivileged.
|
||||
FROM python:3.15-slim@sha256:533b0b2f9b5d46fd097091cf826f27902f1d6c3bca714b6dc74551356aa97a51 AS build
|
||||
FROM python:3.15-slim@sha256:94d3426f1def7db4a106a232ac121bfd975393e5dfd40a619b955fae45fe7913 AS build
|
||||
|
||||
# Pins live here, in the artifact, rather than in Ansible variables.
|
||||
#
|
||||
@@ -56,7 +56,7 @@ RUN git clone --quiet https://github.com/MyHomeMyData/E3onCAN.git /opt/e3oncan \
|
||||
&& rm -rf /opt/e3oncan/.git
|
||||
|
||||
|
||||
FROM python:3.15-slim@sha256:533b0b2f9b5d46fd097091cf826f27902f1d6c3bca714b6dc74551356aa97a51
|
||||
FROM python:3.15-slim@sha256:94d3426f1def7db4a106a232ac121bfd975393e5dfd40a619b955fae45fe7913
|
||||
|
||||
ARG OPEN3E_REF=6553c10453a6bae1210908f38a5af2481a46a3e5
|
||||
ARG E3ONCAN_REF=461dcbdf97e8d72d2edb9929c63b39382af6c3a1
|
||||
|
||||
@@ -6,10 +6,10 @@ RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends jq \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=ghcr.io/siderolabs/talosctl:v1.14.4@sha256:b3233234327350f20ed194f3fad83d90a64344c1940f839c7b0c0abef50c8c33 /talosctl /usr/local/bin/talosctl
|
||||
COPY --from=alpine/kubectl:1.37.1@sha256:a5d0fbecab3f7b6c2ab7398f1daa73a3e0ed6ece18556d21698233e9e3759cc7 /usr/local/bin/kubectl /usr/local/bin/kubectl
|
||||
COPY --from=ghcr.io/fluxcd/flux-cli:v2.9.6@sha256:b1ac18156f227af9a524b842a96f2c20986c7a779ef721df3ba4e4540f449d76 /usr/local/bin/flux /usr/local/bin/flux
|
||||
COPY --from=quay.io/openbao/openbao:2.7.1@sha256:6d2b93856e3fcf7b18ad855a0b51eaba474dc8b79cf554379ea32034797d2acf /usr/bin/bao /usr/local/bin/bao
|
||||
COPY --from=ghcr.io/siderolabs/talosctl:v1.14.5@sha256:0b852e35c1f513ee5eacc22183d6193486012420569fde39fcd5260c51938443 /talosctl /usr/local/bin/talosctl
|
||||
COPY --from=alpine/kubectl:1.37.2@sha256:5e2de75d37e811ba6eb92e06890b1b3263eae4f0a1c17b49269edec589b1352a /usr/local/bin/kubectl /usr/local/bin/kubectl
|
||||
COPY --from=ghcr.io/fluxcd/flux-cli:v2.10.0@sha256:1a5178b1770052d6e88ab1c7c8434c5ac9bca8a8ab984dcaf6b9df31814ce9f2 /usr/local/bin/flux /usr/local/bin/flux
|
||||
COPY --from=quay.io/openbao/openbao:2.7.2@sha256:5046911075e137e154ce49ab051502f4ec7cf367d484ae4a4dce665d9b90561f /usr/bin/bao /usr/local/bin/bao
|
||||
COPY --from=gitea/tea:0.16.0@sha256:af50a2060ce4d8fd76b7ebe75a3d6db4bb0c615c5bc8f2bdfd8ea83207c4752d /usr/bin/tea /usr/local/bin/tea
|
||||
|
||||
RUN talosctl version --client \
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# kuma-sync: reconciles KumaMonitor resources into Uptime Kuma. The unit tests
|
||||
# (no tags, so no Docker and no envtest) gate the image: the final stage copies
|
||||
# the binary out of this stage, so a failing test fails the build.
|
||||
FROM golang:1.27@sha256:f20d83a83e18dbd2a4714b8ffd4b30b287b6243962dd75608e86abcc63ba4efc AS build
|
||||
FROM golang:1.27@sha256:2920bb124f1a2e12a1d39f0cab756ae0068441ee1da7655dec7f5e374944095b AS build
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
@@ -9,7 +9,7 @@ COPY . .
|
||||
RUN go test ./...
|
||||
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/kuma-sync ./cmd/kuma-sync
|
||||
|
||||
FROM gcr.io/distroless/static:nonroot@sha256:18f4cc26c9ffa7f20a359298be7deadac5c607c8e03a02b8119f1a5fc2f82c99
|
||||
FROM gcr.io/distroless/static:nonroot@sha256:25ac8f0c3b2334a9eb05452266bd4697864a8c8ef0ef71e5e6480d7f92868329
|
||||
COPY --from=build /out/kuma-sync /kuma-sync
|
||||
USER 65532
|
||||
ENTRYPOINT ["/kuma-sync"]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# domains -> TLS passthrough -> Traefik ClusterIP) from Ingresses carrying the
|
||||
# public label. Standard library only, so the image is the interpreter plus one
|
||||
# file; runs unprivileged as its own user.
|
||||
FROM python:3.15-slim@sha256:533b0b2f9b5d46fd097091cf826f27902f1d6c3bca714b6dc74551356aa97a51
|
||||
FROM python:3.15-slim@sha256:94d3426f1def7db4a106a232ac121bfd975393e5dfd40a619b955fae45fe7913
|
||||
|
||||
RUN useradd --system --no-create-home --shell /usr/sbin/nologin controller
|
||||
COPY controller.py /opt/controller.py
|
||||
|
||||
@@ -22,6 +22,13 @@ Two rules, one per family, send every packet with a pod source to table 47
|
||||
(`talos/patches/site-homelab.yaml`, priorities 1000/1001, below main). The
|
||||
table decides by longest prefix; no rule carves out exceptions.
|
||||
|
||||
A reply from a LoadBalancer VIP is pod traffic as well, but it reaches the
|
||||
routing decision with the VIP as its source. Two more rules (1002/1003) send
|
||||
the LoadBalancer pool to table 47, so the reply leaves over `ingress0`, where
|
||||
the request arrived as the BGP next hop. Without them main sent it out
|
||||
`mgmt0`. The pool is a site fact: the site patch's `lb-pool-site` ConfigMap,
|
||||
which `bgp-pool` takes by Flux substitution, and the rules beside it.
|
||||
|
||||
bird (`kubernetes/infrastructure/bird/`) owns table 47 and fills it with:
|
||||
|
||||
- the node's attached networks — `mgmt0`, `ingress0`, `pod0`, `node0` —
|
||||
|
||||
@@ -48,7 +48,7 @@ spec:
|
||||
# cert when doing server-side OIDC token exchange. Bundle both
|
||||
# roots into one PEM since NODE_EXTRA_CA_CERTS accepts one file.
|
||||
- name: fetch-le-staging-roots
|
||||
image: curlimages/curl:8.23.0@sha256:11e64e28c5f58c8d93ee95fcf22ac055f48b0bc440e0ce7d8013bbb5ad6aa8ac
|
||||
image: curlimages/curl:8.23.0@sha256:3fb7e13e5d179a845c97d78204f3f1064d1907037f26a3ebf91710571151fd93
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
|
||||
@@ -1262,12 +1262,6 @@ data:
|
||||
- authentik_policies_expression.add_expressionpolicy
|
||||
- authentik_policies_expression.change_expressionpolicy
|
||||
- authentik_policies_expression.delete_expressionpolicy
|
||||
- authentik_providers_oauth2.view_oauth2provider
|
||||
- authentik_providers_oauth2.add_oauth2provider
|
||||
- authentik_providers_oauth2.change_oauth2provider
|
||||
- authentik_providers_oauth2.delete_oauth2provider
|
||||
- authentik_providers_oauth2.view_scopemapping
|
||||
- authentik_crypto.view_certificatekeypair
|
||||
- authentik_core.view_group
|
||||
- authentik_flows.view_flow
|
||||
- authentik_outposts.view_outpostserviceconnection
|
||||
|
||||
@@ -62,12 +62,6 @@ rules:
|
||||
- apiGroups: ["cilium.io"]
|
||||
resources: ["ciliumnetworkpolicies"]
|
||||
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||
- apiGroups: ["auth.renner.uno"]
|
||||
resources: ["oidcclients"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["auth.renner.uno"]
|
||||
resources: ["oidcclients/status"]
|
||||
verbs: ["get", "patch", "update"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
@@ -111,7 +105,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: controller
|
||||
image: gitea.renner.uno/gitea_admin/authentik-gate-sync:2026-10-29-1349 # {"$imagepolicy": "flux-system:authentik-gate-sync"}
|
||||
image: gitea.renner.uno/gitea_admin/authentik-gate-sync:2026-11-01-1059 # {"$imagepolicy": "flux-system:authentik-gate-sync"}
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
|
||||
@@ -20,7 +20,7 @@ spec:
|
||||
# Pinned to match the samba-bridge fork's app image (server.Dockerfile
|
||||
# AUTHENTIK_VERSION). Bump in lockstep with the bridge rebuild — a
|
||||
# floating range drifts the chart ahead of the pinned app image.
|
||||
version: "2026.8.4"
|
||||
version: "2026.8.5"
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: authentik
|
||||
|
||||
@@ -28,7 +28,7 @@ spec:
|
||||
enableServiceLinks: false
|
||||
containers:
|
||||
- name: evcc
|
||||
image: evcc/evcc:0.318.1@sha256:2f8163ab22f04863afd86cd511c09607ba71debd9eebd65511f3d654922a80d1
|
||||
image: evcc/evcc:0.318.2@sha256:7ef9bcb15a4fdbfe3bf75d7a0857c40b6272b8783ea833fcbb22f8f134b336c7
|
||||
# evcc auto-discovers /etc/evcc.yaml on its default search path;
|
||||
# the file is delivered (with the MQTT password) via the
|
||||
# sops-encrypted evcc-config Secret mounted below.
|
||||
|
||||
@@ -88,7 +88,7 @@ spec:
|
||||
# deliver, and the ClusterIP has no backend while HA restarts. The
|
||||
# trap forwards SIGTERM so the pod still terminates promptly.
|
||||
- name: socat-ipv4
|
||||
image: alpine/socat:1.8.1.3@sha256:603174b77eebaba25ba8f8ebb15e1234d6600409554e95d6fb203fd106150287
|
||||
image: alpine/socat:1.8.1.3@sha256:466c8a0383e16463fabdb7df01008292fb78d07da60224a46cb5d3142ce122ed
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- /bin/sh
|
||||
@@ -130,7 +130,7 @@ spec:
|
||||
# depends on the kernel's bindv6only default rather than on anything
|
||||
# stated here, so the roles are made explicit instead of inherited.
|
||||
- name: socat-ipv6
|
||||
image: alpine/socat:1.8.1.3@sha256:603174b77eebaba25ba8f8ebb15e1234d6600409554e95d6fb203fd106150287
|
||||
image: alpine/socat:1.8.1.3@sha256:466c8a0383e16463fabdb7df01008292fb78d07da60224a46cb5d3142ce122ed
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- /bin/sh
|
||||
|
||||
@@ -46,7 +46,7 @@ spec:
|
||||
k8s.v1.cni.cncf.io/networks: shelly-manager-iot
|
||||
# Changing this rolls the pod, which is the only way a change to the
|
||||
# shelly-manager-iot attachment takes effect.
|
||||
net.renner.uno/iot-attachment-config: "tuning-no-ra"
|
||||
net.renner.uno/iot-attachment-config: "tuning-no-ra-no-mtu-probing"
|
||||
spec:
|
||||
# eth0 (Cilium-managed pod network) is still the primary — it
|
||||
# carries the ingress, mosquitto LB return path, etc. The macvlan
|
||||
@@ -64,7 +64,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: shelly-manager
|
||||
image: gitea.renner.uno/konairius/shelly-manager:2026-10-30-1343 # {"$imagepolicy": "flux-system:shelly-manager"}
|
||||
image: gitea.renner.uno/konairius/shelly-manager:2026-11-02-1412 # {"$imagepolicy": "flux-system:shelly-manager"}
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
|
||||
@@ -50,6 +50,9 @@ spec:
|
||||
# to IPv6 clients on v30iot leave via net1, bypassing Cilium's LoadBalancer
|
||||
# reverse NAT. tuning turns RA processing off on net1; the link-local
|
||||
# address mDNS uses stays.
|
||||
#
|
||||
# tcp_mtu_probing (pod-wide; Cilium sets 1): Shelly flash-write stalls
|
||||
# trip black-hole detection, which cuts firmware downloads to 48-byte MSS.
|
||||
config: |
|
||||
{
|
||||
"cniVersion": "0.3.1",
|
||||
@@ -67,7 +70,8 @@ spec:
|
||||
"type": "tuning",
|
||||
"sysctl": {
|
||||
"net.ipv6.conf.IFNAME.accept_ra": "0",
|
||||
"net.ipv6.conf.IFNAME.autoconf": "0"
|
||||
"net.ipv6.conf.IFNAME.autoconf": "0",
|
||||
"net.ipv4.tcp_mtu_probing": "0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -60,7 +60,7 @@ spec:
|
||||
# repo's configuration.yaml reads this key, so HA does not start
|
||||
# without it. Removing this init container takes the recorder down.
|
||||
- name: render-db-url
|
||||
image: busybox:1.38@sha256:fd7dc98638c8e305f4dc34e979f1c0fdfdcaeb0fbf8fcff77ae834b6da3d7e6e
|
||||
image: busybox:1.38@sha256:a46257e06c82ad85242d0d38077d0a52d9896389ca60a1a59d3463db586e5fa4
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
@@ -97,7 +97,7 @@ spec:
|
||||
# (homeassistant repo packages/matrix_assist.yaml), same filter-then-append
|
||||
# render as recorder_db_url above. Value is hex, nothing to escape.
|
||||
- name: render-matrix-password
|
||||
image: busybox:1.38@sha256:fd7dc98638c8e305f4dc34e979f1c0fdfdcaeb0fbf8fcff77ae834b6da3d7e6e
|
||||
image: busybox:1.38@sha256:a46257e06c82ad85242d0d38077d0a52d9896389ca60a1a59d3463db586e5fa4
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
@@ -275,7 +275,7 @@ spec:
|
||||
# any LAN client can edit, matching the user's trust-the-network
|
||||
# model. Runs as root so its writes match HA's root-owned files.
|
||||
- name: code-server
|
||||
image: codercom/code-server:4.143.1@sha256:377b66bec5645510ed52549bb00e648c273edc604d230a3f90fd2b2433afcb97
|
||||
image: codercom/code-server:4.144.0@sha256:b9e8056bb74bd0c4f90284c7559eddeccd38400cfedc28e64c9d3ca50fef1f83
|
||||
args:
|
||||
- "--auth=none"
|
||||
- "--bind-addr=0.0.0.0:8080"
|
||||
@@ -337,7 +337,7 @@ spec:
|
||||
# dedicated long-lived token.
|
||||
# Runs as root to match HA's root-owned /config files. See config-sync.sh.
|
||||
- name: config-sync
|
||||
image: alpine/git:2.54.0@sha256:e8ceedc73bd80992ae8d58a7d24c58b8322146c354f1d2e81a97db4452068ec0
|
||||
image: alpine/git:2.54.0@sha256:ac6c828f15d82775d204d3d75112e1d34af8945da80ccf922713c9891ba68e7a
|
||||
command: ["/bin/sh", "/scripts/sync.sh"]
|
||||
env:
|
||||
- name: SYNC_BRANCH
|
||||
|
||||
@@ -36,7 +36,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: esphome
|
||||
image: ghcr.io/esphome/esphome:2026.10.0@sha256:1dff19a59070cdb523b76576cdc74d200f862d43c1407809088855920cf57145
|
||||
image: ghcr.io/esphome/esphome:2026.10.1@sha256:eab8cd6b4f51077c3883af4cedc058280ddc34a94f3b2ffee23e9d2cd5f1db64
|
||||
imagePullPolicy: IfNotPresent
|
||||
# Default entrypoint runs the dashboard (Device Builder) against the
|
||||
# mounted /config — no command override needed.
|
||||
|
||||
@@ -223,7 +223,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: controller
|
||||
image: gitea.renner.uno/konairius/mqtt-account-controller:2026-10-29-1634 # {"$imagepolicy": "flux-system:mqtt-account-controller"}
|
||||
image: gitea.renner.uno/konairius/mqtt-account-controller:2026-11-02-1058 # {"$imagepolicy": "flux-system:mqtt-account-controller"}
|
||||
env:
|
||||
- name: BROKER_URL
|
||||
value: ssl://mosquitto.home-assistant.svc.cluster.local:8884
|
||||
|
||||
@@ -163,7 +163,7 @@ spec:
|
||||
# Pinned tag. No Flux image-automation marker yet (unlike mosquitto) —
|
||||
# add an infrastructure/image-automation/zigbee2mqtt.yaml policy as a
|
||||
# follow-up if desired; pinned-and-manual is fine for the initial cutover.
|
||||
image: ghcr.io/koenkk/zigbee2mqtt:2.14.2@sha256:addbfb08fcef2e4477de1535636118241e10d13e898fbe4d9faba8a4d7396311
|
||||
image: ghcr.io/koenkk/zigbee2mqtt:2.14.3@sha256:fb4a6b0f6868ad1cd6a9996479802b2420e599c3463b62bbff8c8cd9a908f43a
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: TZ
|
||||
|
||||
@@ -17,7 +17,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: ical-proxy
|
||||
image: gitea.renner.uno/konairius/ical-proxy:2026-10-28-0651 # {"$imagepolicy": "flux-system:ical-proxy"}
|
||||
image: gitea.renner.uno/konairius/ical-proxy:2026-11-02-1055 # {"$imagepolicy": "flux-system:ical-proxy"}
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
|
||||
@@ -66,7 +66,7 @@ spec:
|
||||
# The library PVC is populated by the seed phase before this
|
||||
# Deployment reconciles, so no rsync init is needed here.
|
||||
- name: fetch-le-staging-roots
|
||||
image: curlimages/curl:8.23.0@sha256:11e64e28c5f58c8d93ee95fcf22ac055f48b0bc440e0ce7d8013bbb5ad6aa8ac
|
||||
image: curlimages/curl:8.23.0@sha256:3fb7e13e5d179a845c97d78204f3f1064d1907037f26a3ebf91710571151fd93
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
|
||||
@@ -57,7 +57,7 @@ spec:
|
||||
runAsNonRoot: false
|
||||
initContainers:
|
||||
- name: wait-for-influxd
|
||||
image: influxdb:2.9.1@sha256:8f106f7ff12659c5e367b99f8a0e7d14cbd4e6ca1f2914094062db6a9813d9c2
|
||||
image: influxdb:2.9.1@sha256:a450f6faf9573e6c2e96a0f575885ae8b7387cc87e4de94599e247ebfa3ac417
|
||||
env:
|
||||
- { name: INFLUX_HOST, value: http://influxdb-influxdb2.influxdb.svc.cluster.local:8086 }
|
||||
- { name: INFLUX_ORG, value: renner }
|
||||
@@ -80,7 +80,7 @@ spec:
|
||||
echo "[$(date -u +%H:%M:%S)] $INFLUX_HOST ready and authenticated"
|
||||
containers:
|
||||
- name: create
|
||||
image: influxdb:2.9.1@sha256:8f106f7ff12659c5e367b99f8a0e7d14cbd4e6ca1f2914094062db6a9813d9c2
|
||||
image: influxdb:2.9.1@sha256:a450f6faf9573e6c2e96a0f575885ae8b7387cc87e4de94599e247ebfa3ac417
|
||||
env:
|
||||
- { name: INFLUX_HOST, value: http://influxdb-influxdb2.influxdb.svc.cluster.local:8086 }
|
||||
- { name: INFLUX_ORG, value: renner }
|
||||
|
||||
@@ -88,7 +88,7 @@ spec:
|
||||
# state). Two-stage so we don't try to create an auth before
|
||||
# the chart's `influx setup` has finished.
|
||||
- name: wait-for-influxd
|
||||
image: influxdb:2.9.1@sha256:8f106f7ff12659c5e367b99f8a0e7d14cbd4e6ca1f2914094062db6a9813d9c2
|
||||
image: influxdb:2.9.1@sha256:a450f6faf9573e6c2e96a0f575885ae8b7387cc87e4de94599e247ebfa3ac417
|
||||
env:
|
||||
- { name: INFLUX_HOST, value: http://influxdb-influxdb2.influxdb.svc.cluster.local:8086 }
|
||||
- { name: INFLUX_ORG, value: renner }
|
||||
@@ -111,7 +111,7 @@ spec:
|
||||
echo "[$(date -u +%H:%M:%S)] $INFLUX_HOST ready and authenticated"
|
||||
containers:
|
||||
- name: create
|
||||
image: influxdb:2.9.1@sha256:8f106f7ff12659c5e367b99f8a0e7d14cbd4e6ca1f2914094062db6a9813d9c2
|
||||
image: influxdb:2.9.1@sha256:a450f6faf9573e6c2e96a0f575885ae8b7387cc87e4de94599e247ebfa3ac417
|
||||
env:
|
||||
- { name: INFLUX_HOST, value: http://influxdb-influxdb2.influxdb.svc.cluster.local:8086 }
|
||||
- { name: INFLUX_ORG, value: renner }
|
||||
|
||||
@@ -55,7 +55,7 @@ spec:
|
||||
repository: influxdb
|
||||
# Was pinned to VM 108's 2.7.4 for the one-time `influx restore`; that
|
||||
# migration is done. No downgrade below 2.9 without `influxd downgrade`.
|
||||
tag: "2.9.1@sha256:8f106f7ff12659c5e367b99f8a0e7d14cbd4e6ca1f2914094062db6a9813d9c2"
|
||||
tag: "2.9.1@sha256:a450f6faf9573e6c2e96a0f575885ae8b7387cc87e4de94599e247ebfa3ac417"
|
||||
# Single replica — InfluxDB OSS doesn't cluster. RWO Ceph-block + Velero
|
||||
# backups (pending) is the durability story.
|
||||
persistence:
|
||||
|
||||
@@ -48,7 +48,7 @@ spec:
|
||||
# this sentinel: the entrypoint copies AccountsSettings.json
|
||||
# into ephemeral /config-internal rather than symlinking it.
|
||||
- name: seed-config
|
||||
image: busybox:1.38.0@sha256:fd7dc98638c8e305f4dc34e979f1c0fdfdcaeb0fbf8fcff77ae834b6da3d7e6e
|
||||
image: busybox:1.38.0@sha256:a46257e06c82ad85242d0d38077d0a52d9896389ca60a1a59d3463db586e5fa4
|
||||
command: ["/bin/sh", "-eu", "-c"]
|
||||
args:
|
||||
- |
|
||||
@@ -94,7 +94,7 @@ spec:
|
||||
- name: libation
|
||||
# Pinned by digest: Spegel caches mutable tag resolutions, so
|
||||
# :latest stayed on 13.7.11 despite imagePullPolicy Always.
|
||||
image: docker.io/rmcrackan/libation:14.2.2@sha256:fe188ac3c4ce63cc305971c76e648748341427e16bcf884011da8a384fd3908c
|
||||
image: docker.io/rmcrackan/libation:14.2.5@sha256:7bb47a417311d154c6691747320d0b62ce15527280ad99f0e2a85ac9a6b07e40
|
||||
# The entrypoint stages config into ephemeral /config-internal
|
||||
# by copy (only the DB is symlinked to the PVC), so Audible
|
||||
# tokens refreshed during a run are lost at container exit.
|
||||
|
||||
@@ -72,7 +72,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: element-web
|
||||
image: ghcr.io/element-hq/element-web:v1.12.32@sha256:f8700633d4336258af7a05b53865a4f479e0d628cf2d9ca83d28b5939314d4ce
|
||||
image: ghcr.io/element-hq/element-web:v1.12.33@sha256:b0aa4b5a4fc1afaaff62e2dbd0c92d57e2b20724f3ccb2fef53aae949088c3d2
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
|
||||
@@ -137,7 +137,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: mas
|
||||
image: ghcr.io/element-hq/matrix-authentication-service:1.27.1@sha256:02712e68c90f3743fedbd5e3343517481b2b4f246a9fd9e2f050dc2553033847
|
||||
image: ghcr.io/element-hq/matrix-authentication-service:1.28.0@sha256:494c67f08ee8344368f4ed336611dea02b26c4e058bf37a212d090d50b612659
|
||||
# The server applies database migrations and syncs the upstream
|
||||
# providers from the config on start.
|
||||
args: ["server", "--config", "/config/config.yaml"]
|
||||
|
||||
@@ -179,7 +179,7 @@ spec:
|
||||
# first. The marker on the data PVC makes later starts skip it.
|
||||
# Debug image: the plain one has no shell for the marker check.
|
||||
- name: migrate-to-mas
|
||||
image: ghcr.io/element-hq/matrix-authentication-service:1.27.1-debug@sha256:75f369b206785f1480f4a9f8dd81d7f9f3c6793c40fd184358ecd5d7acae17d4
|
||||
image: ghcr.io/element-hq/matrix-authentication-service:1.28.0-debug@sha256:362b8cc6b1176aec059f3539da2d47c8b47587d1584431bf1aca285146d42b08
|
||||
command:
|
||||
- /busybox/sh
|
||||
- -ec
|
||||
@@ -234,7 +234,7 @@ spec:
|
||||
mountPath: /data
|
||||
containers:
|
||||
- name: synapse
|
||||
image: ghcr.io/element-hq/synapse:v1.163.2@sha256:c3c21919a322f74d177a43bce8031adc81ef839ddd117a8dd03cfec350e76e02
|
||||
image: ghcr.io/element-hq/synapse:v1.164.1@sha256:fa4aa980f279bcf26d4e7ecc2a2c9a701c73f2ebe8841da9bdcf3b2fdf693d75
|
||||
# Bypass /start.py: it expects to run as root and template a config
|
||||
# from env. Default mode is generate-missing-and-run, which creates
|
||||
# the signing key on first start.
|
||||
|
||||
@@ -17,7 +17,7 @@ spec:
|
||||
chart:
|
||||
spec:
|
||||
chart: kube-prometheus-stack
|
||||
version: "92.13.1"
|
||||
version: "92.17.0"
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: prometheus-community
|
||||
|
||||
@@ -74,7 +74,7 @@ spec:
|
||||
# alpine/kubectl is the maintained replacement. Pin the tag
|
||||
# to the cluster's server version so kubectl is in lock-step
|
||||
# (mismatched skew can break exec streaming).
|
||||
image: docker.io/alpine/kubectl:1.37.1@sha256:a5d0fbecab3f7b6c2ab7398f1daa73a3e0ed6ece18556d21698233e9e3759cc7
|
||||
image: docker.io/alpine/kubectl:1.37.2@sha256:5e2de75d37e811ba6eb92e06890b1b3263eae4f0a1c17b49269edec589b1352a
|
||||
env:
|
||||
- name: DATASET_TLV
|
||||
valueFrom:
|
||||
|
||||
@@ -47,7 +47,7 @@ spec:
|
||||
#
|
||||
# Hence socat is installed at startup and run inside this container.
|
||||
- name: otbr
|
||||
image: openthread/otbr:latest@sha256:f3e43ccd86d6da87323873efa0c9b6f92a2a8cc75a804ea50e34c96680363e2e
|
||||
image: openthread/otbr:latest@sha256:51f9af250252f4faae85d0197574a7c17ada873c78799854461a93e3e42cf324
|
||||
command:
|
||||
- bash
|
||||
- -c
|
||||
|
||||
@@ -287,7 +287,7 @@ spec:
|
||||
add: ["CHOWN", "DAC_OVERRIDE", "FOWNER", "SETGID", "SETUID"]
|
||||
# anchor: bridge-tls-proxy
|
||||
- name: bridge-tls-proxy
|
||||
image: alpine/socat:1.8.1.3@sha256:603174b77eebaba25ba8f8ebb15e1234d6600409554e95d6fb203fd106150287
|
||||
image: alpine/socat:1.8.1.3@sha256:466c8a0383e16463fabdb7df01008292fb78d07da60224a46cb5d3142ce122ed
|
||||
args:
|
||||
- "-d"
|
||||
- "TCP-LISTEN:11143,fork,reuseaddr,bind=127.0.0.1"
|
||||
|
||||
@@ -59,7 +59,7 @@ spec:
|
||||
- name: renovate
|
||||
# Pinned by tag and digest. Renovate updates itself through
|
||||
# homelab's renovate.json, like any other image here.
|
||||
image: docker.io/renovate/renovate:44.181.8@sha256:b28b2de1dfd35de7e8198756d7f7cdd84ebc28c3f25e80f5b09e598d3faefb2a
|
||||
image: docker.io/renovate/renovate:44.199.2@sha256:0538f498be1b074e2434f8449cab7b2be30f8c0d0baf3b72814010f19db5bcfb
|
||||
env:
|
||||
- name: RENOVATE_CONFIG_FILE
|
||||
value: /config/config.json
|
||||
|
||||
@@ -32,7 +32,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: review-bot
|
||||
image: gitea.renner.uno/gitea_admin/review-bot:2026-10-29-1346 # {"$imagepolicy": "flux-system:review-bot"}
|
||||
image: gitea.renner.uno/gitea_admin/review-bot:2026-11-02-1054 # {"$imagepolicy": "flux-system:review-bot"}
|
||||
env:
|
||||
- name: GITEA_URL
|
||||
value: https://gitea.renner.uno
|
||||
|
||||
@@ -57,7 +57,7 @@ spec:
|
||||
# Pinned, and deliberately NOT under Flux image automation. rbac.yaml
|
||||
# is derived from what this exact version calls; an automatic bump
|
||||
# could add a call the Role does not cover, silently.
|
||||
image: ghcr.io/stakater/ingressmonitorcontroller:v2.2.13@sha256:3301afb61c10abc4e334deab9cd8601cef069e352b4d7f4dd0897787fca32eac
|
||||
image: ghcr.io/stakater/ingressmonitorcontroller:v2.2.14@sha256:6d7b7fcb39d72296b6b6c0301b443fb31e12d86c55e706adcc7e4f92a887a3ba
|
||||
command: ["/manager"]
|
||||
args:
|
||||
- --health-probe-bind-address=:8081
|
||||
|
||||
@@ -68,7 +68,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: sync
|
||||
image: curlimages/curl:8.22.0@sha256:58adaa4e8dca9c988bae2aba4ab3434a0bb2da16bbe3f92dec39ec7785166777
|
||||
image: curlimages/curl:8.23.0@sha256:3fb7e13e5d179a845c97d78204f3f1064d1907037f26a3ebf91710571151fd93
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
@@ -128,7 +128,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: run
|
||||
image: curlimages/curl:8.22.0@sha256:58adaa4e8dca9c988bae2aba4ab3434a0bb2da16bbe3f92dec39ec7785166777
|
||||
image: curlimages/curl:8.23.0@sha256:3fb7e13e5d179a845c97d78204f3f1064d1907037f26a3ebf91710571151fd93
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
|
||||
@@ -25,7 +25,7 @@ spec:
|
||||
fsGroup: 33
|
||||
containers:
|
||||
- name: wordpress
|
||||
image: wordpress:7.1.3-php8.5-apache@sha256:ce31904fa1e51b73543fa2e35cc8f1747589b2abf48dc747b422749b88b77e9e
|
||||
image: wordpress:7.1.4-php8.5-apache@sha256:131532d893b5dc4fdf4312a878b7e5e4e4d1d40eb54c854c78eddf6282457dfb
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 80
|
||||
|
||||
@@ -89,6 +89,9 @@ spec:
|
||||
substituteFrom:
|
||||
- kind: ConfigMap
|
||||
name: cluster-settings
|
||||
# The LoadBalancer pool, from the site's Talos patch (site-*.yaml).
|
||||
- kind: ConfigMap
|
||||
name: lb-pool-site
|
||||
dependsOn:
|
||||
- name: infrastructure
|
||||
---
|
||||
|
||||
@@ -69,7 +69,7 @@ spec:
|
||||
# first build of that day — imagePullPolicy: Always does not help,
|
||||
# because the tag itself resolves stale. Update this when the image
|
||||
# is rebuilt.
|
||||
image: gitea.renner.uno/gitea_admin/bird@sha256:65fc93efead1e9b065d2c78e748f6c3c1ad7216e28b757d9fa4e204fa2ffe9a9
|
||||
image: gitea.renner.uno/gitea_admin/bird@sha256:fee5645ad0d718661fb90da6ea9090d30fe47597bebeecb246c01c2adc6df585
|
||||
command: ["/bin/sh", "-c"]
|
||||
args:
|
||||
- |
|
||||
@@ -95,7 +95,7 @@ spec:
|
||||
mountPath: /etc/bird
|
||||
containers:
|
||||
- name: bird
|
||||
image: gitea.renner.uno/gitea_admin/bird@sha256:65fc93efead1e9b065d2c78e748f6c3c1ad7216e28b757d9fa4e204fa2ffe9a9
|
||||
image: gitea.renner.uno/gitea_admin/bird@sha256:fee5645ad0d718661fb90da6ea9090d30fe47597bebeecb246c01c2adc6df585
|
||||
resources:
|
||||
requests:
|
||||
cpu: "50m"
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
# OIDCClient: an Authentik OAuth2/OpenID client, reconciled by authentik-gate-sync (ci/images/authentik-gate-sync).
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: oidcclients.auth.renner.uno
|
||||
annotations:
|
||||
kustomize.toolkit.fluxcd.io/prune: disabled
|
||||
spec:
|
||||
group: auth.renner.uno
|
||||
names:
|
||||
kind: OIDCClient
|
||||
listKind: OIDCClientList
|
||||
plural: oidcclients
|
||||
singular: oidcclient
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
additionalPrinterColumns:
|
||||
- jsonPath: .status.issuer
|
||||
name: Issuer
|
||||
type: string
|
||||
- jsonPath: .status.conditions[?(@.type=="Ready")].status
|
||||
name: Ready
|
||||
type: string
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
required: [spec]
|
||||
properties:
|
||||
apiVersion:
|
||||
type: string
|
||||
kind:
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
type: object
|
||||
required: [launchURL, redirectURIs]
|
||||
properties:
|
||||
slug:
|
||||
description: Application slug and client ID; issuer is /application/o/<slug>/. Default metadata.name (then checked by the controller only).
|
||||
type: string
|
||||
maxLength: 50
|
||||
pattern: ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$
|
||||
launchURL:
|
||||
description: The app's URL, shown in the Authentik library.
|
||||
type: string
|
||||
pattern: ^https?://
|
||||
redirectURIs:
|
||||
type: array
|
||||
minItems: 1
|
||||
items:
|
||||
type: object
|
||||
required: [url]
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
matchingMode:
|
||||
type: string
|
||||
enum: [strict, regex]
|
||||
default: strict
|
||||
group:
|
||||
description: Authentik group whose members may sign in (managed in Authentik); none = any user.
|
||||
type: string
|
||||
authorizationFlow:
|
||||
description: Authorization flow slug; default default-provider-authorization-implicit-consent.
|
||||
type: string
|
||||
status:
|
||||
type: object
|
||||
properties:
|
||||
observedGeneration:
|
||||
type: integer
|
||||
format: int64
|
||||
issuer:
|
||||
type: string
|
||||
conditions:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required: [type, status]
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
reason:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
observedGeneration:
|
||||
type: integer
|
||||
format: int64
|
||||
lastTransitionTime:
|
||||
type: string
|
||||
format: date-time
|
||||
@@ -1,4 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- crd-oidcclients.yaml
|
||||
@@ -27,7 +27,6 @@ resources:
|
||||
- admission-policies/
|
||||
- kuma-sync-crds/
|
||||
- cloudflare-lb-sync-crds/
|
||||
- authentik-gate-sync-crds/
|
||||
# The OpenBao ClusterSecretStore lives in post-crds/ (needs ESO's CRDs and
|
||||
# an initialized OpenBao; see kubernetes/MIGRATION-sops-openbao.md).
|
||||
# cnpg-barman-plugin/ lives in its own Flux Kustomization
|
||||
|
||||
@@ -24,7 +24,7 @@ spec:
|
||||
# window. The StatefulSet rolls one pod at a time (updateStrategyType
|
||||
# below). Undo after a pod has run the new server = restore the
|
||||
# snapshot, not a git revert.
|
||||
version: ">=0.30.4 <0.31.0"
|
||||
version: ">=0.30.6 <0.31.0"
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: openbao
|
||||
|
||||
@@ -71,7 +71,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: rotate
|
||||
image: gitea.renner.uno/gitea_admin/secret-rotator:2026-10-29 # {"$imagepolicy": "flux-system:secret-rotator"}
|
||||
image: gitea.renner.uno/gitea_admin/secret-rotator:2026-11-02 # {"$imagepolicy": "flux-system:secret-rotator"}
|
||||
args: ["ha-token"]
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -133,7 +133,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: rotate
|
||||
image: gitea.renner.uno/gitea_admin/secret-rotator:2026-10-29 # {"$imagepolicy": "flux-system:secret-rotator"}
|
||||
image: gitea.renner.uno/gitea_admin/secret-rotator:2026-11-02 # {"$imagepolicy": "flux-system:secret-rotator"}
|
||||
args: ["ha-token"]
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
@@ -195,7 +195,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: rotate
|
||||
image: gitea.renner.uno/gitea_admin/secret-rotator:2026-10-29 # {"$imagepolicy": "flux-system:secret-rotator"}
|
||||
image: gitea.renner.uno/gitea_admin/secret-rotator:2026-11-02 # {"$imagepolicy": "flux-system:secret-rotator"}
|
||||
args: ["ha-token"]
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
|
||||
@@ -45,7 +45,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: snapshot
|
||||
image: quay.io/openbao/openbao:2.7.1@sha256:6d2b93856e3fcf7b18ad855a0b51eaba474dc8b79cf554379ea32034797d2acf # same image as the server — bao CLI included
|
||||
image: quay.io/openbao/openbao:2.7.2@sha256:5046911075e137e154ce49ab051502f4ec7cf367d484ae4a4dce665d9b90561f # same image as the server — bao CLI included
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
# Selector is opt-in via `lb-pool: bgp`. There is no default pool —
|
||||
# Services without this label do not get a LoadBalancer IP.
|
||||
#
|
||||
# The ranges are a site fact: lb-pool-site from the site's Talos patch
|
||||
# (talos/patches/site-*.yaml), by Flux substitution. A CIDR block uses all its
|
||||
# addresses (allowFirstLastIPs defaults to Yes).
|
||||
#
|
||||
# IPv4 + IPv6 ranges keep matching last bytes (10.2.99.x ↔ fd99::x)
|
||||
# so each LB has the same "tail" in both families.
|
||||
apiVersion: cilium.io/v2alpha1
|
||||
@@ -19,10 +23,8 @@ spec:
|
||||
matchLabels:
|
||||
lb-pool: bgp
|
||||
blocks:
|
||||
- start: "10.2.99.0"
|
||||
stop: "10.2.99.255"
|
||||
- start: "fd99::"
|
||||
stop: "fd99::ff"
|
||||
- cidr: "${LB_POOL_V4}"
|
||||
- cidr: "${LB_POOL_V6}"
|
||||
---
|
||||
# Cilium BGPv2 control-plane config peering each node with the UDM over the
|
||||
# ingress VLAN 47 — never over v30iot, which is the untrusted IoT segment.
|
||||
|
||||
@@ -5,7 +5,7 @@ metadata:
|
||||
namespace: rook-ceph
|
||||
spec:
|
||||
cephVersion:
|
||||
image: quay.io/ceph/ceph:v20.2.4 # {"$imagepolicy": "flux-system:ceph"}
|
||||
image: quay.io/ceph/ceph:v20.2.5 # {"$imagepolicy": "flux-system:ceph"}
|
||||
allowUnsupported: false
|
||||
dataDirHostPath: /var/lib/rook
|
||||
# Defaults to false. With three OSDs and size 3 there is no spare copy, so
|
||||
|
||||
@@ -38,7 +38,7 @@ spec:
|
||||
serviceAccountName: rook-ceph-default
|
||||
containers:
|
||||
- name: rook-ceph-tools
|
||||
image: quay.io/ceph/ceph:v20.2.4 # {"$imagepolicy": "flux-system:ceph"}
|
||||
image: quay.io/ceph/ceph:v20.2.5 # {"$imagepolicy": "flux-system:ceph"}
|
||||
command:
|
||||
- /bin/bash
|
||||
- -c
|
||||
|
||||
@@ -67,6 +67,18 @@ cluster:
|
||||
nameservers:
|
||||
- 185.12.64.1
|
||||
- 185.12.64.2
|
||||
# The LoadBalancer pool (see site-homelab.yaml). Services pin their
|
||||
# addresses from the production pool, so a test run carries the same one.
|
||||
- name: lb-pool-site
|
||||
contents: |
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: lb-pool-site
|
||||
namespace: flux-system
|
||||
data:
|
||||
LB_POOL_V4: 10.2.99.0/24
|
||||
LB_POOL_V6: fd99::/120
|
||||
---
|
||||
# Host DNS asks k8s-gateway first, so containerd resolves in-cluster names
|
||||
# (gitea.renner.uno) in the cluster; bootstrap.sh installs k8s-gateway right
|
||||
|
||||
@@ -85,6 +85,18 @@ cluster:
|
||||
nameservers:
|
||||
- fd02:47::1
|
||||
- 10.2.47.1
|
||||
# The LoadBalancer pool (bgp-pool, infrastructure/post-crds/cilium-bgp.yaml,
|
||||
# by Flux substitution). The routing rules 1002/1003 below name it too.
|
||||
- name: lb-pool-site
|
||||
contents: |
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: lb-pool-site
|
||||
namespace: flux-system
|
||||
data:
|
||||
LB_POOL_V4: 10.2.99.0/24
|
||||
LB_POOL_V6: fd99::/120
|
||||
---
|
||||
# All pod traffic goes to table 47, and the table decides — longest prefix
|
||||
# match, no exceptions to carve out. bird fills it with the node's attached
|
||||
@@ -115,6 +127,21 @@ name: "1001"
|
||||
src: 10.244.0.0/16
|
||||
table: "47"
|
||||
---
|
||||
# A LoadBalancer reply is pod traffic too, but it reaches the routing decision
|
||||
# with the VIP as its source, so the rules above miss it and main sends it out
|
||||
# mgmt0 while the request came in over ingress0. The pool is lb-pool-site above.
|
||||
apiVersion: v1alpha1
|
||||
kind: RoutingRuleConfig
|
||||
name: "1002"
|
||||
src: fd99::/120
|
||||
table: "47"
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
kind: RoutingRuleConfig
|
||||
name: "1003"
|
||||
src: 10.2.99.0/24
|
||||
table: "47"
|
||||
---
|
||||
# Host DNS asks k8s-gateway first, so containerd resolves in-cluster names
|
||||
# (gitea.renner.uno) in the cluster; bootstrap.sh installs k8s-gateway right
|
||||
# after the CNI, before anything else needs DNS. The UDM is the site's
|
||||
|
||||
@@ -47,3 +47,15 @@ cluster:
|
||||
values.yaml: |
|
||||
deployment:
|
||||
dnsPolicy: Default
|
||||
# The LoadBalancer pool (see site-homelab.yaml). Services pin their
|
||||
# addresses from the production pool, so a test run carries the same one.
|
||||
- name: lb-pool-site
|
||||
contents: |
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: lb-pool-site
|
||||
namespace: flux-system
|
||||
data:
|
||||
LB_POOL_V4: 10.2.99.0/24
|
||||
LB_POOL_V6: fd99::/120
|
||||
|
||||
+3
-2
@@ -10,7 +10,8 @@
|
||||
"dockerfile"
|
||||
],
|
||||
"ignorePaths": [
|
||||
"kubernetes/clusters/homelab/flux-system/**"
|
||||
"kubernetes/clusters/homelab/flux-system/**",
|
||||
"kubernetes/infrastructure/image-automation/**"
|
||||
],
|
||||
"flux": {
|
||||
"managerFilePatterns": [
|
||||
@@ -40,6 +41,7 @@
|
||||
"ghcr.io/home-assistant/home-assistant",
|
||||
"ghcr.io/immich-app/immich-machine-learning",
|
||||
"ghcr.io/immich-app/immich-server",
|
||||
"ghcr.io/linuxserver/lidarr",
|
||||
"ghcr.io/matter-js/python-matter-server",
|
||||
"ghcr.io/music-assistant/server",
|
||||
"ghcr.io/paperless-ngx/paperless-ngx",
|
||||
@@ -57,7 +59,6 @@
|
||||
"gitea.renner.uno/konairius/vsftpd",
|
||||
"gitea.renner.uno/konairius/wiki",
|
||||
"gotenberg/gotenberg",
|
||||
"lscr.io/linuxserver/lidarr",
|
||||
"netbirdio/netbird",
|
||||
"quay.io/ceph/ceph",
|
||||
"redis",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# Nothing in the final image but the binary and the CA bundle it needs to reach the Mistral API
|
||||
# (the binary has no shell and no package manager to patch or to exec).
|
||||
FROM golang:1.27.3-bookworm@sha256:972b4110ba0f47539a975c31e52c64765e1ff19f2d74469ae4a595c6676e80a5 AS build
|
||||
FROM golang:1.27.3-bookworm@sha256:d9b6a1beb9474adc2a1ce842573639b44b2a0dd49b6669f51d3821baf9ec37c9 AS build
|
||||
|
||||
WORKDIR /build
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# lives inside the binary rather than in /etc/ssl. (In-cluster traffic to
|
||||
# OpenBao and Home Assistant is plain HTTP anyway; TLS is here so the same
|
||||
# binary can take over the external-API rotators — Cloudflare, Proxmox.)
|
||||
FROM rust:1.99-alpine@sha256:b1d5dd5a22a09404d3e798c0cf051b689077360374eb505fa5bb93c479693d87 AS build
|
||||
FROM rust:1.99-alpine@sha256:7e9e2d068a107a2adafd19930011178d14a78326053cd4c3a9f7ad3fd049557d AS build
|
||||
|
||||
# musl-dev provides the C runtime bits the linker needs for a fully static
|
||||
# binary; nothing from it ends up in the final image.
|
||||
|
||||
Reference in New Issue
Block a user