Compare commits

..
Author SHA1 Message Date
claude-botandClaude Opus 5.5 790e258894 feat(kuma-sync): deploy controller
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
review-bot reviewed: comment
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Namespace, CRD (Flux prune disabled so a prune never cascades into
deleting every Kuma monitor via finalizers), cluster RBAC matching the
controller-gen role, Recreate Deployment of the first master build
(2026-10-19-0545) under image automation, and a default-deny
CiliumNetworkPolicy allowing only DNS, the kube-apiserver and
kuma.renner.uno:443 out, kubelet probes in.

The Kuma login comes from its own OpenBao path infrastructure/kuma-sync
(a copy of the single Kuma admin account) rather than reading
infrastructure/uptime-kuma.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 07:51:41 +02:00
k.renner 5e99ca4370 Merge pull request 'feat(kuma-sync): in-house Uptime Kuma controller (Tasks 1-6)' (#1342) from feat/kuma-sync into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
ci/woodpecker/push/build-ci-images Pipeline was successful
Reviewed-on: k.renner/homelab#1342
2026-10-19 05:09:52 +00:00
13 changed files with 537 additions and 0 deletions
@@ -0,0 +1,296 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.23.0
kustomize.toolkit.fluxcd.io/prune: disabled
name: kumamonitors.uptime.renner.uno
spec:
group: uptime.renner.uno
names:
kind: KumaMonitor
listKind: KumaMonitorList
plural: kumamonitors
singular: kumamonitor
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .spec.type
name: Type
type: string
- jsonPath: .status.conditions[?(@.type=="Ready")].status
name: Ready
type: string
name: v1alpha1
schema:
openAPIV3Schema:
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
properties:
description:
type: string
displayName:
minLength: 1
type: string
dns:
properties:
hostname:
minLength: 1
type: string
port:
default: 53
format: int32
maximum: 65535
minimum: 1
type: integer
resolveServer:
default: 1.1.1.1
type: string
resolveType:
enum:
- A
- AAAA
- CNAME
- MX
- NS
- PTR
- SOA
- SRV
- TXT
- CAA
type: string
required:
- hostname
- resolveType
type: object
http:
description: HTTPSpec configures http, keyword and json-query monitors.
properties:
acceptedStatusCodes:
default:
- 200-299
items:
type: string
type: array
body:
type: string
expiryNotification:
type: boolean
headers:
additionalProperties:
type: string
type: object
ignoreTLS:
type: boolean
maxRedirects:
format: int32
type: integer
method:
default: GET
type: string
url:
minLength: 1
type: string
required:
- url
type: object
interval:
default: 60
description: Seconds.
format: int32
minimum: 20
type: integer
jsonQuery:
properties:
expected:
type: string
expression:
description: JSONata expression.
minLength: 1
type: string
operator:
enum:
- ==
- '!='
- <
- <=
- '>'
- '>='
- contains
type: string
required:
- expected
- expression
- operator
type: object
keyword:
properties:
invert:
type: boolean
keyword:
minLength: 1
type: string
required:
- keyword
type: object
maxRetries:
default: 0
format: int32
minimum: 0
type: integer
parentRef:
description: LocalRef references a KumaMonitor in the same namespace.
properties:
name:
type: string
required:
- name
type: object
paused:
type: boolean
port:
properties:
hostname:
minLength: 1
type: string
port:
format: int32
maximum: 65535
minimum: 1
type: integer
required:
- hostname
- port
type: object
retryInterval:
default: 60
description: Seconds.
format: int32
minimum: 20
type: integer
timeout:
description: Seconds.
format: int32
minimum: 1
type: integer
type:
enum:
- group
- http
- keyword
- json-query
- dns
- port
type: string
required:
- displayName
- type
type: object
x-kubernetes-validations:
- message: http is required for types http, keyword and json-query, and
forbidden otherwise
rule: (self.type in ['http','keyword','json-query']) == has(self.http)
- message: keyword is required for type keyword and forbidden otherwise
rule: (self.type == 'keyword') == has(self.keyword)
- message: jsonQuery is required for type json-query and forbidden otherwise
rule: (self.type == 'json-query') == has(self.jsonQuery)
- message: dns is required for type dns and forbidden otherwise
rule: (self.type == 'dns') == has(self.dns)
- message: port is required for type port and forbidden otherwise
rule: (self.type == 'port') == has(self.port)
- message: type group takes no type blocks
rule: self.type != 'group' || !(has(self.http) || has(self.keyword)
|| has(self.jsonQuery) || has(self.dns) || has(self.port))
status:
properties:
conditions:
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
monitorID:
format: int64
type: integer
observedGeneration:
format: int64
type: integer
type: object
required:
- spec
type: object
served: true
storage: true
subresources:
status: {}
+72
View File
@@ -0,0 +1,72 @@
---
# kuma-sync: KumaMonitor -> Uptime Kuma monitors on kuma.renner.uno (ci/images/kuma-sync).
apiVersion: apps/v1
kind: Deployment
metadata:
name: kuma-sync
namespace: kuma-sync
labels:
app.kubernetes.io/name: kuma-sync
annotations:
secret.reloader.stakater.com/reload: kuma-sync-kuma
spec:
replicas: 1
# One writer against Kuma at a time; the controller runs without leader election.
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: kuma-sync
template:
metadata:
labels:
app.kubernetes.io/name: kuma-sync
spec:
serviceAccountName: kuma-sync
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: controller
image: gitea.renner.uno/gitea_admin/kuma-sync:2026-10-19-0545 # {"$imagepolicy": "flux-system:kuma-sync"}
env:
- name: KUMA_URL
value: https://kuma.renner.uno
- name: KUMA_USERNAME
valueFrom:
secretKeyRef:
name: kuma-sync-kuma
key: username
- name: KUMA_PASSWORD
valueFrom:
secretKeyRef:
name: kuma-sync-kuma
key: password
ports:
- name: metrics
containerPort: 8080
- name: probes
containerPort: 8081
livenessProbe:
httpGet:
path: /healthz
port: probes
readinessProbe:
httpGet:
path: /readyz
port: probes
resources:
requests:
cpu: 10m
memory: 64Mi
limits:
memory: 256Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
terminationGracePeriodSeconds: 30
@@ -0,0 +1,23 @@
---
# Uptime Kuma login for kuma-sync; OpenBao homelab/infrastructure/kuma-sync.
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: kuma-sync-kuma
namespace: kuma-sync
spec:
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: openbao
target:
name: kuma-sync-kuma
data:
- secretKey: username
remoteRef:
key: infrastructure/kuma-sync
property: username
- secretKey: password
remoteRef:
key: infrastructure/kuma-sync
property: password
@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- crd-kumamonitors.yaml
- externalsecret.yaml
- rbac.yaml
- deployment.yaml
+43
View File
@@ -0,0 +1,43 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: kuma-sync
namespace: kuma-sync
labels:
app.kubernetes.io/name: kuma-sync
---
# Cluster-wide: KumaMonitor objects live in every app namespace.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kuma-sync
rules:
- apiGroups: ["uptime.renner.uno"]
resources: ["kumamonitors"]
verbs: ["get", "list", "watch", "update", "patch"]
- apiGroups: ["uptime.renner.uno"]
resources: ["kumamonitors/status"]
verbs: ["get", "update", "patch"]
- apiGroups: ["uptime.renner.uno"]
resources: ["kumamonitors/finalizers"]
verbs: ["update"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "patch"]
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: kuma-sync
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: kuma-sync
subjects:
- kind: ServiceAccount
name: kuma-sync
namespace: kuma-sync
@@ -0,0 +1,25 @@
# kuma-sync controller: KumaMonitor -> Uptime Kuma monitors. Ships no KumaMonitor objects.
# healthCheckExprs: Flux has no status reader for ExternalSecret; fail only on the non-retrying reasons.
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: kuma-sync
namespace: flux-system
spec:
interval: 10m
retryInterval: 2m
timeout: 5m
sourceRef:
kind: GitRepository
name: flux-system
path: ./kubernetes/apps/kuma-sync
prune: true
wait: true
healthCheckExprs:
- apiVersion: external-secrets.io/v1
kind: ExternalSecret
failed: "has(status.conditions) && status.conditions.exists(c, c.type == 'Ready' && c.status == 'False' && (c.reason == 'SecretOwnedByOther' || c.reason == 'SecretImmutable'))"
current: "has(status.conditions) && status.conditions.exists(c, c.type == 'Ready' && c.status == 'True')"
dependsOn:
- name: infrastructure
- name: infrastructure-crds
@@ -44,6 +44,7 @@ existing `flux-system` GitRepository whose Secret was created by
| netbird-ingress | `0.x` | `-rootless` suffix filter; pre-1.0, bounded to the `0.x` major |
| wyoming-{whisper,openwakeword,speech-to-phrase} | `3.x` / `2.x` / `1.x` | |
| authentik-gate-sync | `YYYY-MM-DD-HHMM` | In-house (build-ci-images.yml); newest tag wins |
| kuma-sync | `YYYY-MM-DD-HHMM` | In-house (build-ci-images.yml); newest tag wins |
| gatus | `5.x` | v-prefix filter |
| velero-plugin-for-aws | `1.13.x` | Tight: must match velero server major.minor |
| gotenberg | `8.x` | Plain-variant filter (rejects `-libreoffice`/`-chromium`/`-cloudrun`/`-aws-lambda`) |
@@ -0,0 +1,23 @@
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageRepository
metadata:
name: kuma-sync
namespace: flux-system
spec:
image: gitea.renner.uno/gitea_admin/kuma-sync
interval: 30m
---
# build-ci-images.yml tags each build YYYY-MM-DD-HHMM; newest wins.
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImagePolicy
metadata:
name: kuma-sync
namespace: flux-system
spec:
imageRepositoryRef:
name: kuma-sync
filterTags:
pattern: '^\d{4}-\d{2}-\d{2}-\d{4}$'
policy:
alphabetical:
order: asc
@@ -12,6 +12,7 @@ resources:
- gotenberg.yaml
- home-assistant.yaml
- immich.yaml
- kuma-sync.yaml
- lidarr.yaml
- matter-server.yaml
- mmwave-configurator.yaml
@@ -288,3 +288,9 @@ apiVersion: v1
kind: Namespace
metadata:
name: renovate
---
# kuma-sync controller; declared here because it has a CiliumNetworkPolicy applied by infrastructure-crds.
apiVersion: v1
kind: Namespace
metadata:
name: kuma-sync
@@ -0,0 +1,38 @@
# Baseline policy for the `kuma-sync` namespace: egress to kube-dns, kube-apiserver and kuma.renner.uno:443; ingress only kubelet probes.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: default-deny
namespace: kuma-sync
spec:
endpointSelector: {}
ingress:
- fromEntities:
- host
toPorts:
- ports:
- port: "8081"
protocol: TCP
egress:
# rules.dns lets Cilium learn the toFQDNs mapping
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
rules:
dns:
- matchPattern: "*"
- toEntities:
- kube-apiserver
- toFQDNs:
- matchName: "kuma.renner.uno"
toPorts:
- ports:
- port: "443"
protocol: TCP
@@ -32,6 +32,7 @@ resources:
- ai-ns.yaml
- navidrome-ns.yaml
- uptime-monitor-ns.yaml
- kuma-sync-ns.yaml
- renovate-ns.yaml
- allow-cilium-health.yaml
- kube-system-hubble-relay.yaml
@@ -27,6 +27,7 @@ spec:
- immich
- influxdb
- ingress
- kuma-sync # kuma-sync-kuma (apps/kuma-sync/externalsecret.yaml)
- matrix # synapse-secrets, mx-pg barman creds (apps/matrix)
- monitoring
- paperless