Compare commits

..

29 Commits

Author SHA1 Message Date
unkin-agent f56cb72b8c kea: bump to v0.1.7 from docker-internal
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/kubeconform Pipeline was successful
2026-10-07 00:38:17 +11:00
unkin-agent 9b857bef64 logging: expose vlselect on internal traefik (#525)
Querying the VLCluster from outside the cluster (CLI tools, puppet VMs) needs a resolvable TLS endpoint for vlselect, mirroring the existing logs-ingest endpoint for vlinsert.

- add `vlselect` Gateway on `traefik-internal` with vault-issuer TLS and external-dns for `vlselect.k8s.syd1.au.unkin.net`
- add HTTP->HTTPS redirect route and HTTPS route to `vlselect-logs:9471`, unauthenticated

Reviewed-on: #525
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-05 14:08:33 +11:00
unkin-agent d1825bb655 authentik: raise server cpu request to 500m (#526)
authentik-server requests only 50m CPU, so on busy nodes it is starved, `/-/health/live/` exceeds the 3s liveness timeout and kubelet restarts it (~100 restarts per pod).

- raise `server.resources.requests.cpu` from 50m to 500m

Reviewed-on: #526
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-05 14:07:45 +11:00
unkin-agent 206fe90f80 Raise puppetserver compiler requests to match real usage (#524)
Compiler pods average ~2Gi memory but request 1Gi, so the scheduler overpacks nodes and the HPA's utilisation target is skewed.

- raise compiler requests to 1 cpu / 2Gi and limits to 4 cpu / 4Gi
- scale up faster (30s window, 75%) and down slower (600s window)

Reviewed-on: #524
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-05 12:34:19 +11:00
unkin-agent 6d842c7d66 feat(bind-internal): publish haproxy edge sites from k8s DNS (#523)
The VM haproxies are being decommissioned; the sites they published via puppet CNAMEs are already served by the k8s haproxy edge, so k8s DNS publishes them instead.

- add main.unkin.net A records for sonarr, radarr, lidarr, readarr, prowlarr, nzbget, jellyfin -> 198.18.199.0
- add unkin.net A records for fafflix and git (git-edge.yaml) -> 198.18.199.0, auth -> 198.18.200.4
- point the commented git.yaml cutover at replacing git-edge.yaml

Merge alongside puppet-prod halb DNS removal; A records only take effect once the puppet CNAMEs are gone.

Reviewed-on: #523
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-05 01:31:06 +11:00
unkin-agent d9cc24dbdb Serve grafana.unkin.net from traefik-external (#522)
grafana.unkin.net still routes through the puppet haproxy edge to the old grafana VMs; the k8s grafana should serve it directly like identity and vlogs.

- add grafana-external Gateway (traefik-external, *.unkin.net wildcard) with redirect + main HTTPRoutes
- reflect wildcard-unkin-net-tls into grafana
- set grafana root_url to https://grafana.unkin.net
- add grafana A record -> 198.18.199.0 in the bind-operator unkin.net zone
- drop grafana.unkin.net from the k8s haproxy routes and config

Requires terraform-authentik grafana redirect URI PR applied first, and the puppet halb vrrp_cnames grafana.unkin.net CNAME removed.

Reviewed-on: #522
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-05 00:38:21 +11:00
unkin-agent 115cdd492f Bump artifactapi to v3.12.0 (#521)
artifactapi v3.12.0 makes DELETE eviction clear every cache layer, so stale mutable indexes (e.g. epel repomd) no longer survive eviction. It also adds `?prefix=` filtering to object listings.

- bump artifactapi and artifactapi-ui images to v3.12.0

Reviewed-on: #521
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-04 23:41:15 +11:00
unkin-agent 9375052d6c Set traefik resource requests so HPAs can scale (#520)
Both traefik HPAs report `ScalingActive False: missing request for cpu` because the chart values set no container resources, so the CPU-utilisation metric can't be computed and neither Deployment ever scales.

- traefik-external requests 200m CPU / 128Mi, memory limit 512Mi
- traefik-internal requests 500m CPU / 256Mi, memory limit 1Gi
- No CPU limits, to avoid throttling the proxy
- The chart derives GOMEMLIMIT (90% of the memory limit) from the new limits

Reviewed-on: #520
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-04 15:27:49 +11:00
unkin-agent af61ac5e92 Set traefik-external externalTrafficPolicy to Local (#519)
With the default Cluster external policy, kube-proxy SNATs inbound traffic to the traefik-external LoadBalancer, hiding real client IPs from traefik and adding a cross-node hop. Local preserves source IPs. Internal policy stays Cluster so in-cluster callers on nodes without a traefik-external pod still reach it.

- Set `externalTrafficPolicy: Local` on the traefik-external Service
- Set `internalTrafficPolicy: Cluster` explicitly

Reviewed-on: #519
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-04 15:26:23 +11:00
unkin-agent 2bcce4894f Drop git.unkin.net from k8s gitea gateway and routes (#518)
git.unkin.net is served by the VM haproxy during the forge migration. Claiming it on the k8s gitea Gateway/HTTPRoutes competes with that path, so the k8s gitea serves only its admin name until cutover.

- remove git.unkin.net from the gitea HTTPRoute and redirect route hostnames
- remove the http-primary/https-primary listeners and their parentRefs
- set the gateway cert common-name to git.k8s.syd1.au.unkin.net

Reviewed-on: #518
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-04 15:07:08 +11:00
unkin-agent b0bfa4605f vmagent: raise kubernetes-apiservers max_scrape_size to 64MiB (#517)
The kubernetes-apiservers job reports up=0 on every target because the apiserver /metrics response exceeds vmagent's default 16MiB scrape size limit.

- set max_scrape_size: 64MiB on the kubernetes-apiservers job only

Reviewed-on: #517
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-04 14:01:14 +11:00
unkin-agent cc0dd24991 vmagent: fix consul target address relabel (#516)
The consul job joins node and port with ":" but then sets `replacement: "${1}:${2}"` with the default `(.*)` regex, so `__address__` becomes `node:port:`. vmagent rejects every consul target as an invalid port, including node_exporter.

- drop the replacement so the default `$1` yields `node:port`

Reviewed-on: #516
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-03 22:51:46 +10:00
unkin-agent 27dd7e7f3d vmagent: cap consul SD wait time under nginx timeout (#515)
nginx in front of Consul closes requests at 90s. vmagent's consul SD sends blocking queries with a 525s wait, so every watch fails with a 504 and the consul job finds no targets. That leaves no puppet-host metrics (node_exporter etc.) in VictoriaMetrics.

- set `promscrape.consul.waitTime: 50s` on the main VMAgent

Reviewed-on: #515
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-03 22:15:16 +10:00
unkin-agent 96624a69c4 Rename the terraform-infra CI ServiceAccount to terraform-netbox (#514)
terraform-infra is replaced by terraform-netbox, whose Vault kubernetes auth role binds the terraform-netbox service account in the woodpecker namespace.

- rename serviceaccount_terraform_infra to serviceaccount_terraform_netbox

Pairs with terraform-vault #161 (the auth role) and terraform-git #105 (the repo).

Reviewed-on: #514
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-03 09:34:19 +10:00
unkin-agent 73cb3903c2 Add k8up Grafana dashboard (#513)
With k8up operator metrics now scraped into VictoriaMetrics, backup job and schedule state still has no dashboard to view it.

- add the k8up dashboard from M0NsTeRRR/grafana-dashboards as a gzipped GrafanaDashboard CR
- resolve its datasource to the in-cluster VictoriaMetrics uid
- register it in the grafana kustomization

Reviewed-on: #513
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-02 23:29:40 +10:00
unkin-agent 575bd5d361 Scrape k8up operator metrics (#512)
The k8up operator already serves Prometheus metrics (backup/check/prune job counts, controller-runtime) on its k8up-metrics Service, but nothing scrapes them, so backup health is invisible in VictoriaMetrics.

- add a VMServiceScrape for the k8up-metrics Service (port http, /metrics)

Reviewed-on: #512
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-02 23:28:59 +10:00
unkin-agent 79e8ccb3d8 feat(grafana): sync kubernetes dashboards from victoria-metrics-k8s-stack (#511)
Grafana lacks Kubernetes cluster/workload dashboards; the victoria-metrics-k8s-stack sync-job ships a maintained set as GrafanaDashboard CRs.

- render victoria-metrics-k8s-stack 0.95.0 in the grafana overlay with only the dashboard sync-job enabled
- sync dotdc kubernetes-views/api-server and kube-prometheus dashboards via the artifactapi github_user remote
- skip dashboards already in apps/base/grafana and those without scraped metrics
- trust vault-ca-cert for artifactapi TLS

Requires terraform-artifactapi #49 applied first.

Reviewed-on: #511
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-02 23:04:57 +10:00
unkin-agent 5a8559e97c grafana: drop alerting Service and POD_IP env owned by grafana-operator (#510)
grafana-operator already creates the headless `grafana-alerting` Service (port 9094) and injects `POD_IP` into the grafana container, so the copies added in #509 make Argo CD and the operator fight over the same objects.

- remove `service-alerting.yaml` from the grafana base
- remove the `POD_IP` env entry from the Grafana CR
- keep the `unified_alerting` `ha_*` config, which targets the operator's Service

Reviewed-on: #510
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-02 22:44:54 +10:00
unkin-agent ec60a8c9d7 grafana: enable unified alerting HA gossip (#509)
Grafana runs 3 replicas with no alerting HA config, so every replica sends every notification.

- add headless `grafana-alerting` Service exposing gossip on 9094 TCP/UDP
- inject `POD_IP` from `status.podIP` into the grafana container
- set `unified_alerting` `ha_peers`, `ha_listen_address` and `ha_advertise_address` so replicas form one Alertmanager cluster

Reviewed-on: #509
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-02 22:37:19 +10:00
unkin-agent ee2476fe5d grafana: scale to 3 replicas, raise limits (#508)
A single Grafana pod with a 1 cpu/1Gi limit is a single point of failure and gets throttled under dashboard load. State lives in Postgres, so extra replicas are safe; spreading them keeps a node loss from taking out Grafana.

- set `replicas: 3` on the grafana deployment
- raise grafana container limits to 2 cpu / 4Gi
- spread grafana pods across nodes with a soft hostname topology constraint

Reviewed-on: #508
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-10-02 22:15:25 +10:00
unkin-agent e6e882abfc Give vlogs its own namespace (#507)
The Vault KV layout is kubernetes/namespace/<ns>/<sa>/<secret>, so vlogs and logviewer both running as SA default in the shared logging namespace would collide on one oauth-credentials entry. Splitting vlogs out resolves it without widening any Vault policy.

- Move apps/base/logging/vlogs to apps/base/vlogs, namespace vlogs
- Add namespace.yaml and a vlogs-scoped VaultAuth (role default)
- Point the VaultStaticSecret at kubernetes/namespace/vlogs/default/oauth-credentials
- Add the au-syd1 overlay, platform ApplicationSet path and project destination
- Move the wildcard-unkin-net-tls reflection from logging to vlogs; vlogs was its only consumer there

Secret is already seeded at the new Vault path.

Reviewed-on: #507
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-28 22:55:28 +10:00
unkin-agent c979579c50 Collect pod logs with VLAgent, retire the NATS/ClickHouse chain (#506)
The vector -> JetStream -> ClickHouse chain duplicated storage the in-cluster VictoriaLogs already provides, and its NATS StatefulSet keeps the logging Application from syncing.

- collect pod logs with a VLAgent DaemonSet remote-writing to vlinsert-logs
- delete vector, NATS, ClickHouse, logarchiver and logviewer
- drop clickhouse-system from the logging ApplicationSet

---------

Co-authored-by: BenVincent <benvin@main.unkin.net>
Reviewed-on: #506
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-27 20:23:58 +10:00
unkin-agent 9f6019b0e4 Drop obsolete vector-test CI pipeline (#505)
Vector is being removed from the logging stack in favour of a VictoriaLogs VLAgent, so every path this pipeline tests is going away and the pipeline becomes dead weight.

- Delete `.woodpecker/vector-test.yaml`.

Companion PR removes the vector manifests under `apps/base/logging/vector/`.

Reviewed-on: #505
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-27 20:12:45 +10:00
unkinben a5a5cc44e9 Promote bind-operator to v0.3.1 (#503)
bind-operator v0.3.1 fixes the apex NS bug two ways: the operator no longer glues apex NS records to pod IPs, and a new CEL rule on `DNSRecordSpec` rejects apex NS DNSRecords at admission. The CRDs are pinned by tag URL, so bumping only the image would leave the admission half inert.

- Bump the bind-operator image to `v0.3.1`
- Bump the CRD install URL to the matching `v0.3.1` tag

---------

Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Reviewed-on: #503
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
2026-09-27 18:11:38 +10:00
unkin-agent be2c751175 Redirect vlogs root to the VictoriaLogs UI (#504)
Visiting https://vlogs.unkin.net/ currently lands on the bare VictoriaLogs index page with relative links, not the query UI.

- Add an Exact `/` rule to the `vlogs` HTTPRoute that 302s to `/select/vmui/`
- Keep the PathPrefix `/` catch-all pointed at `vlogs-oauth2:80`, so auth is unchanged

Reviewed-on: #504
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-27 18:10:35 +10:00
unkin-agent 216b1d72ac Split bind-internal DNSRecords into one file per record (#501)
records.yaml had grown to ten DNSRecord documents across two zones, so finding or reviewing a single record meant scanning the whole file and every change touched it.

- move each record to authoritative/<zone>/<type>/<record>.yaml
- add a kustomization.yaml per zone and per type directory
- keep the git.unkin.net cutover record commented out, alongside a commented kustomization entry
- move the DNSRecord-namespace rationale onto the authoritative kustomization
- delete records.yaml

Rendered output is unchanged.

Reviewed-on: #501
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-27 17:43:16 +10:00
unkin-agent 7581578df1 Split bind-external DNSRecords into one file per record (#500)
A single records.yaml holding every DNSRecord means any record change touches a shared file, and deleting one record is a hunk edit rather than a file removal. One file per record under <zone>/<type>/<record>.yaml makes each record independently editable and removable.

- move acme-apex-ns to acme-unkin-net/ns/apex.yaml and acme-ns1-a to acme-unkin-net/a/ns1.yaml
- add kustomization.yaml per zone and type directory, referencing directories from the parent
- carry the delegation rationale onto the zone kustomization
- drop records.yaml and reference the zone directory from the app kustomization

Rendered output is unchanged.

Reviewed-on: #500
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-27 17:28:37 +10:00
unkin-agent 6515e7f637 Expose VictoriaLogs UI at vlogs.unkin.net behind oauth2-proxy (#498)
The vlselect query UI is only reachable in-cluster, so every log search needs a port-forward. Publish it at vlogs.unkin.net behind oauth2-proxy on the external (DMZ) Traefik.

- Add `apps/base/logging/vlogs`: external Gateway, http->https redirect and main HTTPRoute
- Terminate TLS with the reflected Let's Encrypt `*.unkin.net` wildcard; reflect it into `logging`
- Publish the `vlogs` A record at the DMZ gateway VIP from the bind-operator `unkin.net` zone
- Route all traffic through the `vlogs-oauth2` Service, upstreaming to `vlselect-logs:9471`
- Gate on the Authentik `vlogs` application, group `akP-vlogs-admin`
- Read OIDC credentials from `kv/kubernetes/namespace/logging/default/vlogs-oauth-credentials`

Depends on unkin/terraform-authentik#40.

Reviewed-on: #498
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-27 17:21:40 +10:00
unkin-agent 90d512fadf Drop the gocache serve Deployment and nginx sidecar (#499)
`connect` mode gives the client no local cache, so every cache operation is a network round trip -- about 2.7x slower than pointing the plugin straight at S3. Developers and CI run the plugin in direct mode instead.

- Remove the `go-cache-plugin serve` Deployment and its nginx stream-proxy ConfigMap
- Remove the PureLB Service, freeing `198.18.200.11`
- Keep the `gocache` bucket and `gocache-s3` Secret for direct-mode clients

Reviewed-on: #499
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-09-27 11:20:59 +10:00
118 changed files with 1015 additions and 3618 deletions
-29
View File
@@ -1,29 +0,0 @@
when:
- event: pull_request
steps:
- name: vector-test
image: artifactapi.k8s.syd1.au.unkin.net/dockerhub/timberio/vector:0.57.0-debian
commands:
# Dummy creds + writable dirs so the full topologies build; the unit tests
# only exercise the transforms (sources are not started).
- export CLICKHOUSE_USER=ci CLICKHOUSE_PASSWORD=ci
- export NATS_PRODUCER_PASSWORD=ci NATS_CONSUMER_PASSWORD=ci
- mkdir -p /vector-data-dir /etc/vault-ca
- cp /etc/ssl/certs/ca-certificates.crt /etc/vault-ca/ca.crt
# Transform tier + VM ingest: unit-tested transforms.
- vector test apps/base/logging/vector/aggregator.yaml apps/base/logging/vector/aggregator-tests.yaml
- vector test apps/base/logging/vector/vm-ingest.yaml apps/base/logging/vector/vm-ingest-tests.yaml
# Agent has no transforms to unit-test; validate it builds. (The archiver
# leg is now the logarchiver service, not a Vector pipeline.)
- vector validate --no-environment apps/base/logging/vector/agent.yaml
backend_options:
kubernetes:
serviceAccountName: default
resources:
requests:
memory: 256Mi
cpu: 250m
limits:
memory: 1Gi
cpu: 1
+1 -1
View File
@@ -36,7 +36,7 @@ spec:
mountPath: /combined-certs
containers:
- name: api
image: git.unkin.net/unkin/artifactapi:v3.11.2
image: git.unkin.net/unkin/artifactapi:v3.12.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8000
+1 -1
View File
@@ -22,7 +22,7 @@ spec:
automountServiceAccountToken: true
containers:
- name: ui
image: git.unkin.net/unkin/artifactapi-ui:v3.11.2
image: git.unkin.net/unkin/artifactapi-ui:v3.12.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
@@ -3,4 +3,4 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- ns1.yaml
@@ -0,0 +1,16 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: acme-ns1-a
namespace: bind-external
spec:
zoneRef: acme-unkin-net
name: ns1
type: A
ttl: 3600
values:
# Public address of this cluster's external BIND, same target as
# acme-ns1.unkin.net. Resolvers that cached the seeded ns1.acme.unkin.net
# NS name must still reach the zone.
- 103.216.191.185
@@ -0,0 +1,11 @@
---
# Authoritative delegation records for acme.unkin.net. Without these the zone
# only holds the operator's seed apex (NS ns1.acme.unkin.net glued to the
# primary pod IP), which is unroutable off-cluster and goes stale on
# reschedule. DNSRecords must live in the same namespace as their BindZone.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ns
- a
@@ -0,0 +1,16 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: acme-apex-ns
namespace: bind-external
spec:
zoneRef: acme-unkin-net
# "@" is the zone apex.
name: "@"
type: NS
ttl: 3600
values:
# Matches the parent delegation in Google Cloud DNS. Out of zone, so the
# child needs no glue of its own.
- acme-ns1.unkin.net.
@@ -0,0 +1,6 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- apex.yaml
+1 -1
View File
@@ -7,5 +7,5 @@ resources:
- cluster.yaml
- tsigkey.yaml
- zones.yaml
- records.yaml
- acme-unkin-net
- agent-dns-rolebinding.yaml
-36
View File
@@ -1,36 +0,0 @@
# Authoritative delegation records for acme.unkin.net. Without these the zone
# only holds the operator's seed apex (NS ns1.acme.unkin.net glued to the
# primary pod IP), which is unroutable off-cluster and goes stale on
# reschedule. DNSRecords must live in the same namespace as their BindZone.
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: acme-apex-ns
namespace: bind-external
spec:
zoneRef: acme-unkin-net
# "@" is the zone apex.
name: "@"
type: NS
ttl: 3600
values:
# Matches the parent delegation in Google Cloud DNS. Out of zone, so the
# child needs no glue of its own.
- acme-ns1.unkin.net.
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: acme-ns1-a
namespace: bind-external
spec:
zoneRef: acme-unkin-net
name: ns1
type: A
ttl: 3600
values:
# Public address of this cluster's external BIND, same target as
# acme-ns1.unkin.net. Resolvers that cached the seeded ns1.acme.unkin.net
# NS name must still reach the zone.
- 103.216.191.185
@@ -0,0 +1,15 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: dashboard-ceph-cname
namespace: bind-internal
spec:
zoneRef: ceph-unkin-net
name: dashboard
type: CNAME
ttl: 600
values:
# Ceph mgr dashboard, reached via lb1. Lets in-cluster clients (the
# cephrgw-operator) resolve dashboard.ceph.unkin.net.
- lb1.unkin.net.
@@ -0,0 +1,7 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- dashboard.yaml
- s3.yaml
@@ -0,0 +1,15 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: s3-ceph-cname
namespace: bind-internal
spec:
zoneRef: ceph-unkin-net
name: s3
type: CNAME
ttl: 600
values:
# radosgw S3 endpoint. Points at the Consul service for now; the real
# target will be changed later.
- radosgw.service.consul.
@@ -0,0 +1,6 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- cname
@@ -2,9 +2,15 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
# Individually-managed authoritative records live under <zone>/<type>/<record>.yaml.
# DNSRecords must live in the same namespace as their BindZone (the operator
# resolves zoneRef/clusterRef/updateKeyRef within the record's namespace), so
# these sit alongside the zone in bind-internal, not in the app namespace.
resources:
- cluster.yaml
- tsigkey.yaml
- zones.yaml
- records.yaml
- unkin-net
- main-unkin-net
- ceph-unkin-net
- acls.yaml
@@ -0,0 +1,14 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: jellyfin-dns-internal
namespace: bind-internal
spec:
zoneRef: main-unkin-net
name: jellyfin
type: A
ttl: 600
values:
# traefik-external haproxy edge Gateway VIP (TLS passthrough).
- 198.18.199.0
@@ -0,0 +1,12 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- jellyfin.yaml
- lidarr.yaml
- nzbget.yaml
- prowlarr.yaml
- radarr.yaml
- readarr.yaml
- sonarr.yaml
@@ -0,0 +1,14 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: lidarr-dns-internal
namespace: bind-internal
spec:
zoneRef: main-unkin-net
name: lidarr
type: A
ttl: 600
values:
# traefik-external haproxy edge Gateway VIP (TLS passthrough).
- 198.18.199.0
@@ -0,0 +1,14 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: nzbget-dns-internal
namespace: bind-internal
spec:
zoneRef: main-unkin-net
name: nzbget
type: A
ttl: 600
values:
# traefik-external haproxy edge Gateway VIP (TLS passthrough).
- 198.18.199.0
@@ -0,0 +1,14 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: prowlarr-dns-internal
namespace: bind-internal
spec:
zoneRef: main-unkin-net
name: prowlarr
type: A
ttl: 600
values:
# traefik-external haproxy edge Gateway VIP (TLS passthrough).
- 198.18.199.0
@@ -0,0 +1,14 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: radarr-dns-internal
namespace: bind-internal
spec:
zoneRef: main-unkin-net
name: radarr
type: A
ttl: 600
values:
# traefik-external haproxy edge Gateway VIP (TLS passthrough).
- 198.18.199.0
@@ -0,0 +1,14 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: readarr-dns-internal
namespace: bind-internal
spec:
zoneRef: main-unkin-net
name: readarr
type: A
ttl: 600
values:
# traefik-external haproxy edge Gateway VIP (TLS passthrough).
- 198.18.199.0
@@ -0,0 +1,14 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: sonarr-dns-internal
namespace: bind-internal
spec:
zoneRef: main-unkin-net
name: sonarr
type: A
ttl: 600
values:
# traefik-external haproxy edge Gateway VIP (TLS passthrough).
- 198.18.199.0
@@ -0,0 +1,6 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- a
@@ -1,164 +0,0 @@
# Individually-managed authoritative records for the unkin.net zone.
# DNSRecords must live in the same namespace as their BindZone (the operator
# resolves zoneRef/clusterRef/updateKeyRef within the record's namespace), so
# these sit alongside the zone in bind-internal, not in the app namespace.
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
# "internal" in the name distinguishes this from the external DNS that
# Authentik will manage its own records from later.
name: identity-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: identity
type: A
ttl: 600
values:
# traefik-EXTERNAL (DMZ) gateway VIP; the authentik Gateway serves the
# identity.unkin.net hostname there.
- 198.18.199.0
---
# PRODUCTION CUTOVER RECORD — intentionally commented out.
# git.unkin.net currently resolves to the LIVE VM forge (HAProxy VRRP VIP
# 198.18.19.17), which holds every repo the estate depends on. Uncommenting this
# repoints the whole org's git.unkin.net at the new k8s Gitea gateway VIP, so it
# is the FINAL step of the forge migration — gated on the data migration (gitea
# dump/restore + SECRET_KEY copy) in argocd-apps docs/gitea-migration.md.
# NOTE: the live git.unkin.net answer is served by the puppet DNS master today
# (profiles::dns::master, records from PuppetDB); this k8s apex zone holds only
# SOA+NS + a few DNSRecords so far. Confirm the k8s bind cluster is the live
# authority for unkin.net (or update the puppet record instead) before relying
# on this CR at cutover.
# ---
# apiVersion: bind.unkin.net/v1alpha1
# kind: DNSRecord
# metadata:
# name: git-dns-internal
# namespace: bind-internal
# spec:
# zoneRef: unkin-net
# name: git
# type: A
# ttl: 600
# values:
# # traefik-internal gateway VIP; the gitea Gateway serves git.unkin.net there.
# - 198.18.200.4
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: s3-ceph-cname
namespace: bind-internal
spec:
zoneRef: ceph-unkin-net
name: s3
type: CNAME
ttl: 600
values:
# radosgw S3 endpoint. Points at the Consul service for now; the real
# target will be changed later.
- radosgw.service.consul.
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: dashboard-ceph-cname
namespace: bind-internal
spec:
zoneRef: ceph-unkin-net
name: dashboard
type: CNAME
ttl: 600
values:
# Ceph mgr dashboard, reached via lb1. Lets in-cluster clients (the
# cephrgw-operator) resolve dashboard.ceph.unkin.net.
- lb1.unkin.net.
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: lb1-unkin-net
namespace: bind-internal
spec:
zoneRef: unkin-net
name: lb1
type: A
ttl: 600
values:
- 103.216.191.185
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: ghp-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: ghp
type: A
ttl: 600
values:
# traefik-internal gateway VIP; the ghp Gateway serves ghp.unkin.net there.
- 198.18.200.4
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: arrstack-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: arrstack
type: A
ttl: 600
values:
# traefik-EXTERNAL (DMZ) gateway VIP; the arrproxy Gateway serves the
# arrstack.unkin.net front door (oauth2-proxy) there.
- 198.18.199.0
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: logviewer-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: logviewer
type: A
ttl: 600
values:
# traefik-internal gateway VIP; the logviewer Gateway serves
# logviewer.unkin.net there.
- 198.18.200.4
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: cheeztv-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: cheeztv
type: A
ttl: 600
values:
# traefik-internal gateway VIP; the cheeztv Gateway serves cheeztv.unkin.net
# there.
- 198.18.200.4
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: watchstate-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: watchstate
type: A
ttl: 600
values:
# traefik-EXTERNAL (DMZ) gateway VIP; the watchstate-external Gateway serves
# the watchstate.unkin.net front door (oauth2-proxy) there.
- 198.18.199.0
@@ -0,0 +1,15 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: arrstack-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: arrstack
type: A
ttl: 600
values:
# traefik-EXTERNAL (DMZ) gateway VIP; the arrproxy Gateway serves the
# arrstack.unkin.net front door (oauth2-proxy) there.
- 198.18.199.0
@@ -0,0 +1,14 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: auth-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: auth
type: A
ttl: 600
values:
# traefik-internal gateway VIP; the kanidm Gateway serves auth.unkin.net there.
- 198.18.200.4
@@ -0,0 +1,15 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: cheeztv-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: cheeztv
type: A
ttl: 600
values:
# traefik-internal gateway VIP; the cheeztv Gateway serves cheeztv.unkin.net
# there.
- 198.18.200.4
@@ -0,0 +1,14 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: fafflix-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: fafflix
type: A
ttl: 600
values:
# traefik-external haproxy edge Gateway VIP (TLS passthrough).
- 198.18.199.0
@@ -0,0 +1,14 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: ghp-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: ghp
type: A
ttl: 600
values:
# traefik-internal gateway VIP; the ghp Gateway serves ghp.unkin.net there.
- 198.18.200.4
@@ -0,0 +1,14 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: git-edge-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: git
type: A
ttl: 600
values:
# traefik-external haproxy edge Gateway VIP (TLS passthrough).
- 198.18.199.0
@@ -0,0 +1,23 @@
---
# PRODUCTION CUTOVER RECORD — intentionally commented out.
# git.unkin.net currently resolves to the haproxy edge (git-edge.yaml), which
# forwards to the LIVE VM forge holding every repo. Uncommenting this
# repoints the whole org's git.unkin.net at the new k8s Gitea gateway VIP, so it
# is the FINAL step of the forge migration — gated on the data migration (gitea
# dump/restore + SECRET_KEY copy) in argocd-apps docs/gitea-migration.md.
# Uncomment this record AND its entry in kustomization.yaml (removing
# git-edge.yaml there) to activate it.
# ---
# apiVersion: bind.unkin.net/v1alpha1
# kind: DNSRecord
# metadata:
# name: git-dns-internal
# namespace: bind-internal
# spec:
# zoneRef: unkin-net
# name: git
# type: A
# ttl: 600
# values:
# # traefik-internal gateway VIP; the gitea Gateway serves git.unkin.net there.
# - 198.18.200.4
@@ -0,0 +1,15 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: grafana-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: grafana
type: A
ttl: 600
values:
# traefik-EXTERNAL (DMZ) gateway VIP; the grafana-external Gateway serves
# grafana.unkin.net there.
- 198.18.199.0
@@ -0,0 +1,17 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
# "internal" in the name distinguishes this from the external DNS that
# Authentik will manage its own records from later.
name: identity-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: identity
type: A
ttl: 600
values:
# traefik-EXTERNAL (DMZ) gateway VIP; the authentik Gateway serves the
# identity.unkin.net hostname there.
- 198.18.199.0
@@ -0,0 +1,20 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- arrstack.yaml
- auth.yaml
- cheeztv.yaml
- fafflix.yaml
# PRODUCTION CUTOVER RECORD — see git.yaml. Uncomment together with the
# record itself, and remove git-edge.yaml.
# - git.yaml
- ghp.yaml
- git-edge.yaml
- grafana.yaml
- identity.yaml
- lb1.yaml
- logviewer.yaml
- vlogs.yaml
- watchstate.yaml
@@ -0,0 +1,13 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: lb1-unkin-net
namespace: bind-internal
spec:
zoneRef: unkin-net
name: lb1
type: A
ttl: 600
values:
- 103.216.191.185
@@ -0,0 +1,15 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: logviewer-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: logviewer
type: A
ttl: 600
values:
# traefik-internal gateway VIP; the logviewer Gateway serves
# logviewer.unkin.net there.
- 198.18.200.4
@@ -0,0 +1,15 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: vlogs-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: vlogs
type: A
ttl: 600
values:
# traefik-EXTERNAL (DMZ) gateway VIP; the vlogs-external Gateway serves the
# vlogs.unkin.net front door (oauth2-proxy) there.
- 198.18.199.0
@@ -0,0 +1,15 @@
---
apiVersion: bind.unkin.net/v1alpha1
kind: DNSRecord
metadata:
name: watchstate-dns-internal
namespace: bind-internal
spec:
zoneRef: unkin-net
name: watchstate
type: A
ttl: 600
values:
# traefik-EXTERNAL (DMZ) gateway VIP; the watchstate-external Gateway serves
# the watchstate.unkin.net front door (oauth2-proxy) there.
- 198.18.199.0
@@ -0,0 +1,6 @@
---
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- a
+1 -1
View File
@@ -21,7 +21,7 @@ spec:
runAsNonRoot: true
containers:
- name: operator
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/bind-operator:v0.3.0
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/bind-operator:v0.3.1
args:
- --metrics-bind-address=:8080
- --health-probe-bind-address=:8081
+1 -1
View File
@@ -6,7 +6,7 @@ resources:
- namespace.yaml
# CRDs are pulled from the bind-operator repo at the matching tag rather than
# vendored here, so they never drift from the operator.
- https://git.unkin.net/unkin/bind-operator/raw/tag/v0.3.0/config/crd/install.yaml
- https://git.unkin.net/unkin/bind-operator/raw/tag/v0.3.1/config/crd/install.yaml
- rbac.yaml
- agent-dns-rbac.yaml
- deployment.yaml
@@ -14,9 +14,9 @@ spec:
secretTemplate:
annotations:
reflector.v1.k8s.emberstack.com/reflection-allowed: "true"
reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: "cheeztv,arrstack,authentik,gitea,watchstate,mediamark,repospawner,haproxy"
reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: "cheeztv,arrstack,authentik,gitea,watchstate,mediamark,repospawner,haproxy,vlogs,grafana"
reflector.v1.k8s.emberstack.com/reflection-auto-enabled: "true"
reflector.v1.k8s.emberstack.com/reflection-auto-namespaces: "cheeztv,arrstack,authentik,gitea,watchstate,mediamark,repospawner,haproxy"
reflector.v1.k8s.emberstack.com/reflection-auto-namespaces: "cheeztv,arrstack,authentik,gitea,watchstate,mediamark,repospawner,haproxy,vlogs,grafana"
privateKey:
size: 4096
dnsNames:
@@ -1,7 +0,0 @@
---
apiVersion: v1
kind: Namespace
metadata:
labels:
app.kubernetes.io/name: clickhouse-system
name: clickhouse-system
+1 -1
View File
@@ -11,7 +11,7 @@ metadata:
argocd.argoproj.io/sync-wave: "1"
spec:
replicas: 1
image: git.unkin.net/unkin/kea-api:v0.1.3
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/kea-api:v0.1.7
tokenSecretName: kea-api-token
service:
type: ClusterIP
+1 -1
View File
@@ -11,7 +11,7 @@ metadata:
argocd.argoproj.io/sync-wave: "1"
spec:
replicas: 2
image: git.unkin.net/unkin/kea:v0.1.3
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/kea:v0.1.7
domainName: main.unkin.net
defaultLeaseTime: 1200
maxLeaseTime: 86400
+1 -1
View File
@@ -21,7 +21,7 @@ spec:
runAsNonRoot: true
containers:
- name: operator
image: git.unkin.net/unkin/kea-operator:v0.1.5
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/kea-operator:v0.1.7
args:
- --metrics-bind-address=:8080
- --health-probe-bind-address=:8081
+3 -31
View File
@@ -1,11 +1,6 @@
---
# HTTPS front for the k8s Gitea, served on two names:
# git.unkin.net — canonical/production (apex, bind-operator zone;
# DNS flip is the gated cutover step, see the doc)
# git.k8s.syd1.au.unkin.net — admin/backup route (external-dns k8s.syd1 zone),
# same dual-name pattern as identity.unkin.net.
# The cert-manager Certificate (vault-issuer) takes CN git.unkin.net and gets a
# DNS SAN for each TLS listener hostname automatically.
# HTTPS front for the k8s Gitea on git.k8s.syd1.au.unkin.net (external-dns
# k8s.syd1 zone). git.unkin.net stays on the VM haproxy until cutover.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
@@ -17,36 +12,13 @@ metadata:
traefik.io/instance: internal
annotations:
cert-manager.io/cluster-issuer: vault-issuer
cert-manager.io/common-name: git.unkin.net
cert-manager.io/common-name: git.k8s.syd1.au.unkin.net
cert-manager.io/private-key-size: "4096"
# Only the k8s admin route is published by external-dns (it owns just the
# k8s.syd1.au.unkin.net zone). git.unkin.net lives in the apex zone and is
# flipped at cutover — NOT managed here.
external-dns.alpha.kubernetes.io/hostname: git.k8s.syd1.au.unkin.net
external-dns.alpha.kubernetes.io/target: 198.18.200.4
spec:
gatewayClassName: traefik-internal
listeners:
- name: http-primary
port: 80
protocol: HTTP
hostname: git.unkin.net
allowedRoutes:
namespaces:
from: Same
- name: https-primary
port: 443
protocol: HTTPS
hostname: git.unkin.net
allowedRoutes:
namespaces:
from: Same
tls:
mode: Terminate
certificateRefs:
- group: ""
kind: Secret
name: gitea-tls
- name: http-admin
port: 80
protocol: HTTP
-10
View File
@@ -9,13 +9,8 @@ metadata:
app.kubernetes.io/instance: gitea
spec:
hostnames:
- git.unkin.net
- git.k8s.syd1.au.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: gitea
sectionName: http-primary
- group: gateway.networking.k8s.io
kind: Gateway
name: gitea
@@ -41,13 +36,8 @@ metadata:
app.kubernetes.io/instance: gitea
spec:
hostnames:
- git.unkin.net
- git.k8s.syd1.au.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: gitea
sectionName: https-primary
- group: gateway.networking.k8s.io
kind: Gateway
name: gitea
+15
View File
@@ -0,0 +1,15 @@
---
# k8up dashboard (M0NsTeRRR/grafana-dashboards), datasource resolved to the
# in-cluster VictoriaMetrics. Backed by the k8up VMServiceScrape.
apiVersion: grafana.integreatly.org/v1beta1
kind: GrafanaDashboard
metadata:
name: k8up
namespace: grafana
spec:
instanceSelector:
matchLabels:
dashboards: "grafana"
resyncPeriod: 5m
allowCrossNamespaceImport: false
gzipJson: H4sIAAAAAAACA+2ce3PbNhLAvwqHc3eTzlixqIdfSTvjptc2M7Hjq9Pc3NUeDUhCEiIIYAHQsqLRffZbAHzAop0qaSRHKf4SCZLAcrH7wwJYcREixrhCinAmw5NgEVIiFRz8tgjjnFD1ksFJtBeEKVJI8lwk2Nym5pk+CEcCDRFDIdyRk1SXtFrBT7YwaLXCJVzADMVU361EjuF8TFLnjCScveCUC/2wGMXoSXsv6ETQZqff3wuib3TdDE1Nc6e1tME/glOKhZL6eilOiuQ45kik4fJaN51imQiS6Qf05clRngXVPcFYqUye7O/r4qeE64pwStRdaYdEJoj+ByNxqZBQZ5ypMVwFIcMRw+qlfulOOzo42DPayMZvOKeKZMU9RikspxSOKWETreXfrs3JDT7nMzgdIip1SxlimEqr/IRTijKJU+f6SJD0gttuGhfdop/v9ODgtmhvrn+XZcPdO9XqZhVR5u3C1xkWSIHaHfUJkAeeXTzU25ngU6zGOJdOh6dYdeb9foqS3rtJYnr8QUlXBI0qQSOtPp7Vdpjw1LZNERvlaGTbp4gwhW+Vbl6O+ewVYfg8n8ZYSEdR+soZYeQMZWWpbggMTWGmrTvUz09tC+EUiUnKZ8xIntF8RNhbqK8wmaj9tPO0G7qaOwdjlBkC3TiqM1JtQ3edhu46R7uju8tkjNOcYhm847F8FAX2GgrstndHgW+AfxSqJRSn29dht9DhQUOHx7ujwu9RMoFxQMJAkj+O+o6b/NshE3wxxsnkMdUXNRmo44Vd0d+pSMYw+D+qBvtNDXZ2R4O/YAmRy+Nq8Kipwe7uaPBC5OxR9deJmvrr7V4cs00V9h6Ioju1Bg+3qsHnZqoEKphT/O1VOCOpGp9E7fbfnwVjTEZjZU9iLlIsTtrZbSA5JWkQUxiBn12F312JKxYEz1Va1aHFaiFKRuwkSKAZLJ7dwCSPwBSsLJ6SNKX4A7U+z6rqhiBsa2ZlCWJOU339b6wM4J/vZ6UM+yo1h3CgX+o71zrGakr/yDIEzjAyWsG3GRcKpwPmzhPs9R+IwEk5G73ZiNUMCabpC86GZGSeT/EQ5VSV9mAn2ovq3dRYYOhpmkrz9BRlGWGjerpYX3afQjHoPFfmzaTCWT1rtfP4mObm2g3SB3YCbGfkOSNGS4wzbFrk0L9g6Ng2uY7ddxp232lGnyDIWSGsFaKcnpelRgAofJdLRYbzshjlyiwEcEHA/FDZV2WxAK9P8Gu3KUQTIzv4mFTnXJ3Du4bXZU/IEjVGCul6ku7yhozgRPgVmvNcFUsQa9ndPXaFxAirol/+pEnpRZFanXpdCDzPrJbc4inQRDjUAOs3FiDz6RO9rjKQ5YxvMEL5CC+a7vHt/67C2ievwqVZ8iEM0Go4U1ZN8Qiz9EcuptbRBoOqV4BuzpKNwEOzLBOehstrx8c0q79AH2P8beEmYTv8dJ8bCYxZw+n2HIm09bo3RJ/NJXsNl+xG3iW35pJmmP+wPxpf1OsFA2VWEFrBfd7513K8Bxwtw0KHHmj0oKulEC62Vpyp6W0Nh4Ro6K7HFS2Z049zvNWFGGcsXPG7KVgpef+ziYGg4FDP/GzZv3W8VhZtwbl0QPmmVPkrFNtVaTfcrK6egYJtNFpYliTvoecqydYOxtaLwbbgmNWoGMTz4ElzEPwmeFK7qMyTBEs5zOkg4bmOgtcYNvf0cuAbEBfi3tgscemhNNgP1mzUcOGT29kQOSyVPDo2gI7jBjoOPTo8OhK9urt5clTNfOXgME+fEXaGbp3X+wpxEjWn5UeeJ54nyO52bJ4oTkM+GNk5evQb9Oh5enh6CLvTt3l6OA15euwcPY4a9Djw9PD0yPQu9+bZUTXjybFr5KjSEGpy9D05PDnKfZHNw8NtafP8+FN55Qd1okn/4cTyi1yOg5+QwjM030Ry+UcyLEMUK4VbCXibJLaKJIdgb2puQ7dEfm9SSS7Hd7Lx9YUXJgMFp//Fgq9ecrczy2QafcH4ZenDuuCCQp9Pi5SZsudiJE51OktRrjWcCjS71Hkr+j5K7FbokFD6GoyGqHn5pwOBUk2a1U1T/a+KH0XxVtZkHJFV9aeEsuSGvHfZAmaHhUGRyxQtxkutg4zTCm26EJn9f31UQlBbTcYJU5fkPS4wqv85gX8gUgkS58XjdYcX9ZRku9APS1dJ4C1sVSJwjmRiSbYAXfA8swbvpOtU+9j1+FTqtTYLPhyGy8+Y8XL/7vv1cr1h6ahwuWg1t+uw3tdbzRCu+tgZUvQLpERmFFW75+YPPYb2jhnGXGkHKPO/yprKze3aWmoFSFASta8PdKv1vNwq/w1r7a7TQE/ZSDJAN4hQnbM1kAzQNuZKLixE70ktWY+wi6qC5XIdzlaZvaUoQSVKADFKUNZ2J+GLgFQYggbpqeip+Fen4gKiGjmHKqa/4KHJuYQrl8Y9TPcZkRSEStaMrRvEc/NXrHA1YC+Ew7cJzS0+DAJMzD3B84RyNAlawX553Mq4VCCqbKX5NDOheAY+Tm6NEikNoCacqRMbAqP0NaPzkpYmkBY800mq5atY6aw7Pa0s0NHCR5iibeR6ud6wUW3COONG5MeNe8eNZKypng5SM53R0zc5SHMB4hY3fs5RBMxtschuko8bT15YEQNHxMCKGFgR/ejiRxc/unzmmDvq1/Dsenh+EJ7gKx+HzWGFR42oQcGoLfPUSO1J6knqSboRktZ/0HVQ2vMo/bwo9cT0xPTE/Lpiz063BmbfA/NeYDI8+5In7ed45ifsHpoemtsLM11qHnhqemp6anpqemp+ONTsOl8QPPTQvBeaOQMRCWDhi94m+rWS0iPUI9QjdHuBp8vQjmfoHzH0i1zidOj5qauc17Z+bZolW3Ra7xTVydXdY9MPjiWbT7Wo0lnq724nuRDWDBbLhz+8vW53AX8JIxUcNP4GNrN8NW95757k5rD+XLf5mDUzGTWnlNZ6pSVR73wWeQrMJ/VN5fe772ST185iFPJ7jsXcvOPvYv7GvmbklH+S8FWvX1T6eosE0Tl6/9L1/tNYc+tOmdFb3Z2RORvh26prJyT7VdDLOUtc9FmvazsmYiW3qTvaXMzLDe0oAN45ax2af0AoXpyH5Y0ZSSY206ksea99uWi/slytA6f34yg6Oj6O41a/E0et3rCNWqjX7bXifq/TTnv9XoRiw8DKKPVceYbxxHzZ3FS//D9scYEQEl4AAA==
+33
View File
@@ -37,3 +37,36 @@ spec:
- group: ""
kind: Secret
name: grafana-tls
---
# Public grafana.unkin.net via the external Traefik; TLS uses the reflected
# Let's Encrypt *.unkin.net wildcard, DNS lives in the bind-operator zone.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
labels:
traefik.io/instance: external
name: grafana-external
namespace: grafana
spec:
gatewayClassName: traefik-external
listeners:
- allowedRoutes:
namespaces:
from: Same
hostname: grafana.unkin.net
name: http
port: 80
protocol: HTTP
- allowedRoutes:
namespaces:
from: Same
hostname: grafana.unkin.net
name: https
port: 443
protocol: HTTPS
tls:
certificateRefs:
- group: ""
kind: Secret
name: wildcard-unkin-net-tls
mode: Terminate
+16 -3
View File
@@ -9,8 +9,16 @@ metadata:
spec:
deployment:
spec:
replicas: 3
template:
spec:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: grafana
containers:
- name: grafana
env:
@@ -31,11 +39,11 @@ spec:
cpu: 100m
memory: 256Mi
limits:
cpu: "1"
memory: 1Gi
cpu: "2"
memory: 4Gi
config:
server:
root_url: "https://grafana.k8s.syd1.au.unkin.net"
root_url: "https://grafana.unkin.net"
database:
type: "postgres"
host: "postgres-pooler-rw.grafana.svc.cluster.local:5432"
@@ -61,3 +69,8 @@ spec:
# to akR-global-admin members (and direct members) via terraform-authentik.
role_attribute_path: "contains(ak_groups[*], 'akP-grafana-admin') && 'Admin' || 'Viewer'"
role_attribute_strict: "false"
# Gossip between replicas so each alert notification is sent once.
unified_alerting:
ha_peers: "grafana-alerting.grafana.svc.cluster.local:9094"
ha_listen_address: "${POD_IP}:9094"
ha_advertise_address: "${POD_IP}:9094"
+55
View File
@@ -53,3 +53,58 @@ spec:
- path:
type: PathPrefix
value: /
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: grafana-external-http-redirect
namespace: grafana
labels:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
spec:
hostnames:
- grafana.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: grafana-external
sectionName: http
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301
matches:
- path:
type: PathPrefix
value: /
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: grafana-external
namespace: grafana
labels:
app.kubernetes.io/name: grafana
app.kubernetes.io/instance: grafana
spec:
hostnames:
- grafana.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: grafana-external
sectionName: https
rules:
- backendRefs:
- group: ""
kind: Service
name: grafana-service
port: 3000
weight: 1
matches:
- path:
type: PathPrefix
value: /
+1
View File
@@ -18,6 +18,7 @@ resources:
- dashboards/frr-ospf-route-metrics.yaml
- dashboards/gitea.yaml
- dashboards/haproxy.yaml
- dashboards/k8up.yaml
- dashboards/media-dashboard.yaml
- dashboards/node-exporter-full.yaml
- dashboards/nzbget.yaml
-18
View File
@@ -21,7 +21,6 @@ data:
jellyfin.main.unkin.net be_jellyfin
fafflix.unkin.net be_jellyfin
git.unkin.net be_gitea
grafana.unkin.net be_grafana
dashboard.ceph.unkin.net be_ceph_dashboard
auth.unkin.net be_k8s_kanidm
@@ -124,23 +123,6 @@ data:
server ausyd1nxvm2081 198.18.27.117:443 check cookie ausyd1nxvm2081 fall 2 inter 2s rise 3 ssl verify none
server ausyd1nxvm2082 198.18.28.71:443 check cookie ausyd1nxvm2082 fall 2 inter 2s rise 3 ssl verify none
backend be_grafana
description Backend for grafana nodes
balance roundrobin
cookie SRVNAME insert indirect nocache
http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { dst_port 443 }
http-reuse always
option httpchk GET /
option forwardfor
option http-keep-alive
option prefer-last-server
redirect scheme https if !{ ssl_fc }
stick on src
stick-table type ip size 200k expire 30m
server ausyd1nxvm2015 198.18.27.2:443 check cookie ausyd1nxvm2015 fall 2 inter 2s rise 3 ssl verify none
server ausyd1nxvm2016 198.18.28.189:443 check cookie ausyd1nxvm2016 fall 2 inter 2s rise 3 ssl verify none
backend be_jellyfin
description Backend for au-syd1 jellyfin
balance roundrobin
-1
View File
@@ -17,7 +17,6 @@ spec:
- jellyfin.main.unkin.net
- fafflix.unkin.net
- git.unkin.net
- grafana.unkin.net
- dashboard.ceph.unkin.net
- auth.unkin.net
parentRefs:
-1
View File
@@ -17,7 +17,6 @@ spec:
- jellyfin.main.unkin.net
- fafflix.unkin.net
- git.unkin.net
- grafana.unkin.net
- dashboard.ceph.unkin.net
- auth.unkin.net
parentRefs:
+1
View File
@@ -4,3 +4,4 @@ kind: Kustomization
resources:
- namespace.yaml
- vmservicescrape.yaml
@@ -0,0 +1,15 @@
---
# Scrape the k8up operator metrics (k8up-metrics Service, port http :8080).
apiVersion: operator.victoriametrics.com/v1beta1
kind: VMServiceScrape
metadata:
name: k8up
namespace: k8up-system
spec:
selector:
matchLabels:
app.kubernetes.io/name: k8up
app.kubernetes.io/instance: k8up
endpoints:
- port: http
path: /metrics
@@ -1,16 +0,0 @@
---
# Confines the agent-observability service account (in vm-system) to the
# agent-observability ClusterRole within this namespace.
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: agent-observability
namespace: logging
subjects:
- kind: ServiceAccount
name: agent-observability
namespace: vm-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: agent-observability
-43
View File
@@ -1,43 +0,0 @@
---
# S3 bucket (Ceph RGW) for the long-term raw-log archive, provisioned by the
# in-estate cephrgw-operator. The archiver Vector deployment writes here.
apiVersion: ceph.unkin.net/v1alpha1
kind: ObjectStoreUser
metadata:
name: logs-archive-owner
namespace: logging
spec:
displayName: "Logging raw-archive bucket owner"
maxBuckets: 5
quota:
enabled: true
# 5 TiB soft cap; real retention is enforced RGW-side by a bucket lifecycle
# policy (see PR notes) — the operator does not manage lifecycle.
maxSizeBytes: 5497558138880
---
apiVersion: ceph.unkin.net/v1alpha1
kind: Bucket
metadata:
name: logs-archive
namespace: logging
spec:
bucketName: logs-archive
ownerRef: logs-archive-owner
versioning: false
tags:
app: logging
purpose: raw-log-archive
# Keep the bucket (and its objects) if this CR is ever deleted.
retainOnDelete: true
---
apiVersion: ceph.unkin.net/v1alpha1
kind: BucketAccess
metadata:
name: logs-archive-writer
namespace: logging
spec:
bucketRef: logs-archive
level: read-write
# Operator writes AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY (+ RGW_UID,
# S3_ENDPOINT, BUCKET_NAME) into this Secret; the archiver consumes it.
secretName: logs-archive-s3
@@ -1,97 +0,0 @@
---
apiVersion: clickhouse.altinity.com/v1
kind: ClickHouseInstallation
metadata:
name: logs
namespace: logging
spec:
defaults:
templates:
dataVolumeClaimTemplate: data-volume
serviceTemplate: chi-service
podTemplate: clickhouse
configuration:
users:
# Password hash is sourced from the Vault-synced clickhouse-credentials
# Secret; the plaintext never lands in git or the ClickHouse config.
vector/password_sha256_hex:
valueFrom:
secretKeyRef:
name: clickhouse-credentials
key: password_sha256_hex
vector/networks/ip:
- "::/0"
vector/profile: default
vector/quota: default
# Allow the vector user to create the logs database/table (bootstrap Job)
# and to INSERT. Restrict the built-in default user to loopback only.
vector/access_management: "1"
default/networks/ip:
- "127.0.0.1"
- "::1"
# Read-only user for CLI tools + the logviewer UI. Hash sourced from the
# Vault-synced clickhouse-logreader Secret, same mechanism as vector.
# Scoped to the logs database only (unlike vector, which bootstraps it).
logreader/password_sha256_hex:
valueFrom:
secretKeyRef:
name: clickhouse-logreader
key: password_sha256_hex
logreader/networks/ip:
- "::/0"
logreader/profile: readonly
logreader/quota: default
logreader/allow_databases/database:
- "logs"
profiles:
default/max_memory_usage: "10000000000"
default/max_execution_time: "120"
readonly/readonly: "2"
readonly/max_memory_usage: "10000000000"
readonly/max_execution_time: "120"
clusters:
- name: logs
layout:
shardsCount: 1
replicasCount: 1
templates:
volumeClaimTemplates:
- name: data-volume
spec:
storageClassName: cephrbd-fast-delete
accessModes:
- ReadWriteOnce
resources:
requests:
# 3d TTL on logs.raw. At ~130 GiB/day raw, ClickHouse LZ4/ZSTD
# (~6x on log text) stores ~20-25 GiB/day => ~60-75 GiB/3d, plus
# merge headroom (~2x peak). logs.raw is the only table. 150Gi
# gives comfortable headroom; long-term data lives in S3, not here.
storage: 150Gi
serviceTemplates:
- name: chi-service
generateName: "clickhouse-{chi}"
spec:
type: ClusterIP
ports:
- name: http
port: 8123
- name: tcp
port: 9000
podTemplates:
- name: clickhouse
spec:
securityContext:
fsGroup: 101
runAsUser: 101
runAsGroup: 101
containers:
- name: clickhouse
image: docker.io/clickhouse/clickhouse-server:24.8
resources:
requests:
cpu: 500m
memory: 2Gi
limits:
cpu: "2"
memory: 8Gi
@@ -1,40 +0,0 @@
---
# logarchiver non-secret config. Secrets (NATS/S3/ClickHouse creds) and the
# subject filter come from env; everything else uses the binary's built-in
# defaults, which already target this stack. ack_wait MUST exceed batch.max_age
# so unacked messages in an open batch are not redelivered mid-batch.
apiVersion: v1
kind: ConfigMap
metadata:
name: logarchiver-config
namespace: logging
data:
config.yaml: |
nats:
ack_wait: 5m
fetch_batch: 512
batch:
max_bytes: 67108864
max_events: 200000
max_age: 2m
# Pin the proven RGW endpoint/bucket; ignore the secret's S3_ENDPOINT/BUCKET_NAME
# (AWS creds still come from the secret env). endpoint_env/bucket_env off.
s3:
endpoint: "https://s3.ceph.unkin.net"
bucket: "logs-archive"
region: "us-east-1"
path_style: true
ca_file: /etc/vault-ca/ca.crt
endpoint_env: ""
bucket_env: ""
crypto:
key_name: logarchive
pubkey_source: vault
vault:
address: "https://vault.service.consul:8200"
mount: gpg
auth_method: kubernetes
k8s_mount: k8s/au/syd1
k8s_role: logging_logarchiver
k8s_jwt_path: /var/run/secrets/vault/token
ca_file: /etc/vault-ca/ca.crt
@@ -1,134 +0,0 @@
---
# logarchiver — replaces the vector-archiver leg. Independent JetStream durable
# consumer (archiver) that seals raw logs to S3 as zstd + OpenPGP objects and
# indexes each object in ClickHouse (logs.archive_index). Acks only after the
# object is in S3 AND indexed. Reuses the same NATS/S3/CA wiring the Vector
# archiver used; the OpenPGP public key is delivered as a mounted file.
apiVersion: apps/v1
kind: Deployment
metadata:
name: logarchiver
namespace: logging
annotations:
configmap.reloader.stakater.com/auto: "true"
secret.reloader.stakater.com/reload: "vault-ca-cert"
labels:
app.kubernetes.io/name: logarchiver
app.kubernetes.io/component: archiver
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: logarchiver
template:
metadata:
labels:
app.kubernetes.io/name: logarchiver
vector.dev/exclude: "true"
spec:
# Dedicated SA whose projected vault-audience token authenticates the
# k8s-auth login used to fetch the logarchive public key from the gpg engine.
serviceAccountName: logarchiver
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: logarchiver
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/logarchiver:v0.1.0
imagePullPolicy: IfNotPresent
args: ["run"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
ports:
- containerPort: 9090
name: metrics
protocol: TCP
env:
- name: LOGARCHIVER_CONFIG
value: /etc/logarchiver/config.yaml
# Server-side subject filter; must match the archiver consumer's filter.
- name: ARCHIVE_SUBJECTS
value: "logs.k8s.vault.>"
- name: NATS_CONSUMER_PASSWORD
valueFrom:
secretKeyRef:
name: nats-auth
key: consumer_password
- name: CLICKHOUSE_USER
valueFrom:
secretKeyRef:
name: clickhouse-credentials
key: username
- name: CLICKHOUSE_PASSWORD
valueFrom:
secretKeyRef:
name: clickhouse-credentials
key: password
# S3 creds + S3_ENDPOINT + BUCKET_NAME from the cephrgw BucketAccess Secret.
envFrom:
- secretRef:
name: logs-archive-s3
livenessProbe:
httpGet:
path: /healthz
port: metrics
initialDelaySeconds: 15
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /healthz
port: metrics
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: "1"
memory: 1Gi
volumeMounts:
- name: config
mountPath: /etc/logarchiver/config.yaml
subPath: config.yaml
readOnly: true
- name: vault-token
mountPath: /var/run/secrets/vault
readOnly: true
- name: vault-ca-cert
mountPath: /etc/vault-ca/ca.crt
subPath: ca.crt
readOnly: true
- name: tmp
mountPath: /tmp
volumes:
- name: config
configMap:
name: logarchiver-config
# Projected SA token with audience "vault" for the gpg-engine k8s login.
- name: vault-token
projected:
sources:
- serviceAccountToken:
path: token
audience: vault
expirationSeconds: 600
- name: vault-ca-cert
secret:
secretName: vault-ca-cert
- name: tmp
emptyDir: {}
+40
View File
@@ -39,3 +39,43 @@ spec:
- group: ""
kind: Secret
name: logs-ingest-tls
---
# Unauthenticated LogsQL query endpoint fronting the VLCluster vlselect service.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: vlselect
namespace: logging
labels:
app.kubernetes.io/name: victorialogs
app.kubernetes.io/component: query
traefik.io/instance: internal
annotations:
cert-manager.io/cluster-issuer: vault-issuer
cert-manager.io/common-name: vlselect.k8s.syd1.au.unkin.net
cert-manager.io/private-key-size: "4096"
external-dns.alpha.kubernetes.io/hostname: vlselect.k8s.syd1.au.unkin.net
external-dns.alpha.kubernetes.io/target: 198.18.200.4
spec:
gatewayClassName: traefik-internal
listeners:
- name: http
port: 80
protocol: HTTP
hostname: vlselect.k8s.syd1.au.unkin.net
allowedRoutes:
namespaces:
from: Same
- name: https
port: 443
protocol: HTTPS
hostname: vlselect.k8s.syd1.au.unkin.net
allowedRoutes:
namespaces:
from: Same
tls:
mode: Terminate
certificateRefs:
- group: ""
kind: Secret
name: vlselect-tls
+55
View File
@@ -53,3 +53,58 @@ spec:
- path:
type: PathPrefix
value: /
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: vlselect-http-redirect
namespace: logging
labels:
app.kubernetes.io/name: victorialogs
app.kubernetes.io/component: query
spec:
hostnames:
- vlselect.k8s.syd1.au.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: vlselect
sectionName: http
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
statusCode: 301
matches:
- path:
type: PathPrefix
value: /
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: vlselect
namespace: logging
labels:
app.kubernetes.io/name: victorialogs
app.kubernetes.io/component: query
spec:
hostnames:
- vlselect.k8s.syd1.au.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: vlselect
sectionName: https
rules:
- backendRefs:
- group: ""
kind: Service
name: vlselect-logs
port: 9471
weight: 1
matches:
- path:
type: PathPrefix
value: /
@@ -1,131 +0,0 @@
---
# Declarative ClickHouse schema bootstrap. Runs as an ArgoCD PostSync hook so it
# executes after the ClickHouseInstallation is reconciled, and re-runs on every
# sync (idempotent CREATE ... IF NOT EXISTS). Edit the DDL here to evolve the
# schema; the Vector aggregator writes to logs.raw with skip_unknown_fields, so
# adding columns is backward-compatible.
apiVersion: batch/v1
kind: Job
metadata:
name: clickhouse-schema
namespace: logging
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
labels:
app.kubernetes.io/name: clickhouse-schema
app.kubernetes.io/component: bootstrap
spec:
backoffLimit: 20
activeDeadlineSeconds: 1800
ttlSecondsAfterFinished: 3600
template:
metadata:
labels:
app.kubernetes.io/name: clickhouse-schema
vector.dev/exclude: "true"
spec:
restartPolicy: OnFailure
securityContext:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
containers:
- name: clickhouse-schema
image: docker.io/clickhouse/clickhouse-server:24.8
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
env:
- name: HOME
value: /tmp
- name: CLICKHOUSE_USER
valueFrom:
secretKeyRef:
name: clickhouse-credentials
key: username
- name: CLICKHOUSE_PASSWORD
valueFrom:
secretKeyRef:
name: clickhouse-credentials
key: password
command:
- /bin/bash
- -ec
- |
host=clickhouse-logs.logging.svc.cluster.local
echo "Waiting for ClickHouse at ${host}:9000 ..."
until clickhouse-client --host "$host" --port 9000 \
--user "$CLICKHOUSE_USER" --password "$CLICKHOUSE_PASSWORD" \
--query "SELECT 1" >/dev/null 2>&1; do
echo " not ready, retrying in 5s"; sleep 5
done
echo "Applying schema ..."
clickhouse-client --host "$host" --port 9000 \
--user "$CLICKHOUSE_USER" --password "$CLICKHOUSE_PASSWORD" \
--multiquery <<'EOSQL'
CREATE DATABASE IF NOT EXISTS logs;
CREATE TABLE IF NOT EXISTS logs.raw
(
timestamp DateTime64(3) DEFAULT now64(3),
host LowCardinality(String) DEFAULT '',
source LowCardinality(String) DEFAULT '',
namespace LowCardinality(String) DEFAULT '',
pod String DEFAULT '',
container LowCardinality(String) DEFAULT '',
stream LowCardinality(String) DEFAULT '',
severity LowCardinality(String) DEFAULT '',
message String DEFAULT '',
labels Map(LowCardinality(String), String),
fields Map(LowCardinality(String), String)
)
ENGINE = MergeTree
PARTITION BY toDate(timestamp)
ORDER BY (source, namespace, host, timestamp)
TTL toDateTime(timestamp) + INTERVAL 3 DAY
SETTINGS index_granularity = 8192;
-- One row per archived S3 object (written by logarchiver). No TTL:
-- the index must outlive logs.raw so the long-term S3 archive stays
-- searchable. Keep in sync with logarchiver internal/index/ddl.go.
CREATE TABLE IF NOT EXISTS logs.archive_index
(
object_key String,
bucket LowCardinality(String),
subject LowCardinality(String),
hosts Array(LowCardinality(String)),
min_ts DateTime64(3),
max_ts DateTime64(3),
event_count UInt64,
raw_bytes UInt64,
stored_bytes UInt64,
compression LowCardinality(String),
cipher LowCardinality(String),
container_format LowCardinality(String),
key_name LowCardinality(String),
key_fingerprint String,
created_at DateTime64(3) DEFAULT now64(3),
INDEX idx_hosts hosts TYPE bloom_filter GRANULARITY 1
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(min_ts)
ORDER BY (subject, min_ts, object_key);
EOSQL
echo "Schema applied."
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
+1 -42
View File
@@ -5,48 +5,7 @@ kind: Kustomization
resources:
- namespace.yaml
- vaultauth.yaml
- vaultstaticsecret.yaml
- clickhouseinstallation.yaml
- job_clickhouse-schema.yaml
- nats-bootstrap-job.yaml
- cephrgw.yaml
- vlcluster.yaml
- vlagent.yaml
- gateway.yaml
- httproute.yaml
- serviceaccount_logarchiver.yaml
- configmap_logarchiver.yaml
- deployment_logarchiver.yaml
- logviewer
- agent-observability-rolebinding.yaml
# Vector pipelines are the single source of truth (also validated by
# `vector test` in CI). Mounted into each tier via `existingConfigMaps`.
configMapGenerator:
# Tunable JetStream stream limits (the nats-bootstrap Job reads these and does
# create-or-update). Hash suffix is INTENTIONALLY left on: editing a value
# renames the ConfigMap, which rewrites the Job's env reference, which changes
# the PostSync hook Job's spec and forces Argo to re-run it -> new limits apply.
# Sizing assumes ~1500 events/s avg @ ~1 KiB/event with S2 compression (~4x):
# ~33 GiB/day compressed -> ~100 GiB/3d per replica. max_bytes 130 GiB sits
# under the 180Gi/node PVC (see values-nats.yaml). Raising retention beyond the
# PVC requires bumping BOTH max_bytes here and fileStore PVC size in values.
- name: nats-stream-limits
literals:
- max_age=72h
- max_bytes=139586437120
- dupe_window=2m
- name: vector-agent-config
files:
- agent.yaml=vector/agent.yaml
options:
disableNameSuffixHash: true
- name: vector-aggregator-config
files:
- aggregator.yaml=vector/aggregator.yaml
options:
disableNameSuffixHash: true
- name: vector-vm-ingest-config
files:
- vm-ingest.yaml=vector/vm-ingest.yaml
options:
disableNameSuffixHash: true
@@ -1,80 +0,0 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: logviewer
namespace: logging
annotations:
secret.reloader.stakater.com/reload: "clickhouse-logreader"
spec:
replicas: 1
selector:
matchLabels:
app: logviewer
strategy:
type: Recreate
template:
metadata:
labels:
app: logviewer
spec:
serviceAccountName: default
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: logviewer
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/logviewer:v0.1.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
name: http
protocol: TCP
env:
- name: CH_URL
value: http://clickhouse-logs.logging.svc.cluster.local:8123
- name: CH_USER
valueFrom:
secretKeyRef:
name: clickhouse-logreader
key: username
- name: CH_PASSWORD
valueFrom:
secretKeyRef:
name: clickhouse-logreader
key: password
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
restartPolicy: Always
-38
View File
@@ -1,38 +0,0 @@
---
# Internal front for the logviewer UI (cf. mediamover/pdbmux).
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
labels:
traefik.io/instance: internal
annotations:
cert-manager.io/cluster-issuer: vault-issuer
cert-manager.io/common-name: logviewer.unkin.net
cert-manager.io/private-key-size: "4096"
external-dns.alpha.kubernetes.io/hostname: logviewer.unkin.net
external-dns.alpha.kubernetes.io/target: 198.18.200.4
name: logviewer
namespace: logging
spec:
gatewayClassName: traefik-internal
listeners:
- allowedRoutes:
namespaces:
from: Same
hostname: logviewer.unkin.net
name: http
port: 80
protocol: HTTP
- allowedRoutes:
namespaces:
from: Same
hostname: logviewer.unkin.net
name: https
port: 443
protocol: HTTPS
tls:
certificateRefs:
- group: ""
kind: Secret
name: logviewer-tls
mode: Terminate
-36
View File
@@ -1,36 +0,0 @@
---
apiVersion: v1
kind: Service
metadata:
name: logviewer
namespace: logging
spec:
internalTrafficPolicy: Cluster
ports:
- name: http
port: 8080
protocol: TCP
targetPort: http
selector:
app: logviewer
sessionAffinity: None
type: ClusterIP
---
# Front-door entry Service: the HTTPRoute for logviewer.unkin.net targets this;
# all traffic enters via oauth2-proxy.
apiVersion: v1
kind: Service
metadata:
name: logviewer-oauth2
namespace: logging
spec:
internalTrafficPolicy: Cluster
ports:
- name: http
port: 80
protocol: TCP
targetPort: http
selector:
app: logviewer-oauth2
sessionAffinity: None
type: ClusterIP
@@ -1,21 +0,0 @@
---
# Authentik OIDC client for logviewer (client_id, client_secret, cookie_secret)
# seeded at kv/kubernetes/namespace/logging/default/oauth-credentials; the
# logging/default templated policy already grants read, so no terraform-vault
# change is needed.
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: logviewer-oauth-credentials
namespace: logging
spec:
destination:
create: true
name: logviewer-oauth-credentials
overwrite: true
hmacSecretData: true
mount: kv
path: kubernetes/namespace/logging/default/oauth-credentials
refreshAfter: 5m
type: kv-v2
vaultAuthRef: default
-158
View File
@@ -1,158 +0,0 @@
---
# Declarative JetStream provisioning: the LOGS stream + durable consumers.
# ArgoCD PostSync hook, idempotent create-or-UPDATE, re-runs each sync.
#
# Stream LOGS: file storage, 3 replicas, retention=limits (NOT workqueue) so the
# transform tier AND the archiver each independently see every message — reading
# never deletes; only max-age/max-bytes do. S2 compression is on (logs compress
# well). Replay window = max-age (3d default). Beyond that, the S3 archive is the
# ONLY long-term source — everything else is gone after 3 days (accepted design).
#
# TUNABLE LIMITS LIVE IN A CONFIGMAP (nats-stream-limits): max_age, max_bytes,
# dupe_window. Change the ConfigMap and re-sync — this Job re-runs and applies
# the new limits via `nats stream edit` (no manual surgery). The ConfigMap is
# generated with a content-hash suffix (kustomize), so editing it changes both
# the ConfigMap name AND this Job's env reference → the PostSync hook Job's spec
# changes and Argo re-runs it (belt-and-suspenders on top of hooks running each
# sync; hook-delete-policy=BeforeHookCreation recreates it every time).
#
# Consumers (independent offsets = true fan-out):
# transform -> whole log stream, feeds the ClickHouse transform tier
# archiver -> configurable security-relevant subset, feeds the S3 archiver.
# Default filter is Vault audit (logs.k8s.vault.>); ADD subjects
# by editing ARCHIVE_SUBJECTS (space-separated -> repeated
# --filter). Exact default set is an open decision for Ben.
#
# Runbook (replay):
# (a) reprocess from JetStream (within max-age, 3d): scale the transform tier
# to 0, then `nats consumer rm LOGS transform` and re-run this Job
# (recreates at DeliverAll), or `nats consumer edit`/`--replay` from a
# start seq/time.
# (b) long-horizon (beyond JetStream): re-ingest S3 archive objects back
# through the transform tier (vector aws_s3 source or a one-shot Job).
apiVersion: batch/v1
kind: Job
metadata:
name: nats-bootstrap
namespace: logging
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
labels:
app.kubernetes.io/name: nats-bootstrap
app.kubernetes.io/component: bootstrap
spec:
backoffLimit: 20
activeDeadlineSeconds: 1800
ttlSecondsAfterFinished: 3600
template:
metadata:
labels:
app.kubernetes.io/name: nats-bootstrap
vector.dev/exclude: "true"
spec:
restartPolicy: OnFailure
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
containers:
- name: nats-bootstrap
image: docker.io/natsio/nats-box:0.18.0
# nats CLI stats the working directory when loading its response
# schemas; under readOnlyRootFilesystem + runAsUser 1000 the image's
# default WORKDIR is not accessible ("stat .: permission denied"), so
# run from the writable /tmp emptyDir.
workingDir: /tmp
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
env:
- name: HOME
value: /tmp
- name: NATS_URL
value: "nats://nats.logging.svc.cluster.local:4222"
- name: NATS_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: nats-auth
key: admin_password
# Tunable stream limits — sourced from the ConfigMap.
- name: MAX_AGE
valueFrom:
configMapKeyRef:
name: nats-stream-limits
key: max_age
- name: MAX_BYTES
valueFrom:
configMapKeyRef:
name: nats-stream-limits
key: max_bytes
- name: DUPE_WINDOW
valueFrom:
configMapKeyRef:
name: nats-stream-limits
key: dupe_window
# Space-separated subject filters for the archiver consumer.
- name: ARCHIVE_SUBJECTS
value: "logs.k8s.vault.>"
command:
- /bin/sh
- -ec
- |
export NATS_USER=log-admin NATS_PASSWORD="$NATS_ADMIN_PASSWORD"
echo "Waiting for NATS + JetStream ..."
until nats --server "$NATS_URL" account info >/dev/null 2>&1; do
echo " not ready, retry in 5s"; sleep 5
done
echo "Ensuring stream LOGS (max_age=$MAX_AGE max_bytes=$MAX_BYTES dupe=$DUPE_WINDOW) ..."
# Create if absent; otherwise converge the mutable limits from the
# ConfigMap. (storage/retention/replicas are immutable, set only on
# create.)
nats stream add LOGS \
--subjects='logs.>' --storage=file --replicas=3 \
--retention=limits --discard=old --compression=s2 \
--max-age="$MAX_AGE" --max-bytes="$MAX_BYTES" \
--max-msgs=-1 --max-msgs-per-subject=-1 --max-msg-size=-1 \
--max-consumers=-1 --dupe-window="$DUPE_WINDOW" --defaults 2>/dev/null \
&& echo " created" \
|| nats stream edit -f LOGS \
--subjects='logs.>' --discard=old --compression=s2 \
--max-age="$MAX_AGE" --max-bytes="$MAX_BYTES" \
--max-msgs=-1 --max-msgs-per-subject=-1 --max-msg-size=-1 \
--max-consumers=-1 --dupe-window="$DUPE_WINDOW"
echo "Ensuring consumer transform ..."
nats consumer add LOGS transform \
--pull --filter='logs.>' --deliver=all --ack=explicit \
--max-deliver=-1 --replay=instant --defaults 2>/dev/null \
|| echo " transform already exists"
echo "Ensuring consumer archiver (filters: $ARCHIVE_SUBJECTS) ..."
filter_args=""
for s in $ARCHIVE_SUBJECTS; do filter_args="$filter_args --filter=$s"; done
# shellcheck disable=SC2086
nats consumer add LOGS archiver \
--pull $filter_args --deliver=all --ack=explicit \
--max-deliver=-1 --replay=instant --defaults 2>/dev/null \
|| echo " archiver already exists"
echo "Done."
nats stream info LOGS
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 500m
memory: 256Mi
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
@@ -1,9 +0,0 @@
---
# Dedicated SA for logarchiver's Vault k8s-auth login (role logging_logarchiver,
# terraform-vault). Only used to fetch the logarchive public key.
apiVersion: v1
kind: ServiceAccount
metadata:
name: logarchiver
namespace: logging
automountServiceAccountToken: false
-73
View File
@@ -1,73 +0,0 @@
---
# ClickHouse credentials for the `vector` user.
#
# Seed the Vault KV entry once (values are NOT stored in git), e.g.:
# PW=$(openssl rand -base64 24)
# HASH=$(printf '%s' "$PW" | sha256sum | cut -d' ' -f1)
# vault kv put kv/kubernetes/namespace/logging/default/clickhouse-credentials \
# username=vector password="$PW" password_sha256_hex="$HASH"
#
# The `logging/default` ServiceAccount reads this path via the templated
# `policies/kv/kubernetes/default.yaml` policy (k8s auth role `default`), so no
# terraform-vault change is required — only the value above must be written.
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: clickhouse-credentials
namespace: logging
spec:
destination:
create: true
name: clickhouse-credentials
overwrite: true
hmacSecretData: true
mount: kv
path: kubernetes/namespace/logging/default/clickhouse-credentials
refreshAfter: 5m
type: kv-v2
vaultAuthRef: default
---
# ClickHouse credentials for the read-only `logreader` user (CLI tools +
# logviewer UI). Seeded the same way as clickhouse-credentials above:
# PW=$(openssl rand -hex 24)
# HASH=$(printf '%s' "$PW" | sha256sum | cut -d' ' -f1)
# vault kv put kv/kubernetes/namespace/logging/default/clickhouse-logreader \
# username=logreader password="$PW" password_sha256_hex="$HASH"
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: clickhouse-logreader
namespace: logging
spec:
destination:
create: true
name: clickhouse-logreader
overwrite: true
hmacSecretData: true
mount: kv
path: kubernetes/namespace/logging/default/clickhouse-logreader
refreshAfter: 5m
type: kv-v2
vaultAuthRef: default
---
# NATS JetStream auth. Distinct passwords for the producer (edge), consumer
# (transform tier + archiver) and admin (bootstrap Job) users. Seed once:
# for k in admin producer consumer; do declare P_$k=$(openssl rand -base64 24); done
# vault kv put kv/kubernetes/namespace/logging/default/nats-auth \
# admin_password="$P_admin" producer_password="$P_producer" consumer_password="$P_consumer"
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: nats-auth
namespace: logging
spec:
destination:
create: true
name: nats-auth
overwrite: true
hmacSecretData: true
mount: kv
path: kubernetes/namespace/logging/default/nats-auth
refreshAfter: 5m
type: kv-v2
vaultAuthRef: default
-43
View File
@@ -1,43 +0,0 @@
---
# Vector EDGE agent pipeline (DaemonSet) — thin publisher, single source of
# truth. Mounted via existingConfigMaps (NOT the chart's customConfig, whose
# Helm `tpl` pass collides with Vector's own {{ }} / ${ } syntax). Tails all pod
# logs, attaches only routing tokens, publishes to JetStream. No parsing.
data_dir: /vector-data-dir
api:
enabled: false
sources:
kubernetes_logs:
type: kubernetes_logs
transforms:
# Routing metadata only: NATS-subject-safe namespace + container tokens.
keymeta:
type: remap
inputs:
- kubernetes_logs
source: |
ns = to_string(.kubernetes.pod_namespace || "unknown") ?? "unknown"
.ns_token = replace(ns, r'[^a-zA-Z0-9_-]', "_")
cont = to_string(.kubernetes.container_name || "unknown") ?? "unknown"
.cont_token = replace(cont, r'[^a-zA-Z0-9_-]', "_")
sinks:
to_jetstream:
type: nats
inputs:
- keymeta
url: nats://nats.logging.svc.cluster.local:4222
connection_name: vector-agent
subject: "logs.k8s.{{ ns_token }}.{{ cont_token }}"
jetstream:
enabled: true
auth:
strategy: user_password
user_password:
user: log-producer
password: ${NATS_PRODUCER_PASSWORD}
encoding:
codec: json
@@ -1,668 +0,0 @@
---
# `vector test` unit tests for the aggregator transforms. Merged with
# aggregator.yaml in CI (.woodpecker/vector-test.yaml). This is the pattern the
# per-app parsing follow-ups extend: add a test per new transform here.
tests:
- name: subject_routes_k8s_vs_vm
inputs:
- insert_at: route
type: log
log_fields:
subject: "logs.k8s.shop.web"
message: "routed"
outputs:
- extract_from: route.k8s
conditions:
- type: vrl
source: |
assert_eq!(.message, "routed")
- name: k8s_log_is_normalised
inputs:
- insert_at: k8s_shape
type: log
log_fields:
message: "hello from pod"
stream: "stdout"
timestamp: "2026-07-27T00:00:00Z"
kubernetes.pod_name: "web-abc"
kubernetes.pod_namespace: "shop"
kubernetes.container_name: "web"
kubernetes.pod_node_name: "node-1"
outputs:
- extract_from: k8s_shape
conditions:
- type: vrl
source: |
assert_eq!(.source, "k8s")
assert_eq!(.namespace, "shop")
assert_eq!(.pod, "web-abc")
assert_eq!(.container, "web")
assert_eq!(.host, "node-1")
assert_eq!(.stream, "stdout")
assert_eq!(.message, "hello from pod")
- name: vm_log_is_normalised
inputs:
- insert_at: vm_shape
type: log
log_fields:
message: "sshd started"
host: "vm-db-1"
severity: "info"
role: "database"
outputs:
- extract_from: vm_shape
conditions:
- type: vrl
source: |
assert_eq!(.source, "vm")
assert_eq!(.host, "vm-db-1")
assert_eq!(.severity, "info")
assert_eq!(.message, "sshd started")
assert_eq!(.labels.role, "database")
# --- catch-all preservation: an unclaimed k8s event still flows app_route ->
# generic route -> k8s_shape (proves the two-stage chain keeps the fallback) ---
- name: unclaimed_k8s_falls_through_to_generic
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.k8s.shop.web"
message: "plain app log"
outputs:
- extract_from: route.k8s
conditions:
- type: vrl
source: |
assert_eq!(.message, "plain app log")
# --- Tier-1: Authentik SSO (k8s, LIVE NOW) ---
- name: authentik_routes_by_subject
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.k8s.authentik.server"
message: "routed"
outputs:
- extract_from: app_route.authentik
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: authentik_parse_extracts_event
inputs:
- insert_at: authentik_parse
type: log
log_fields:
subject: "logs.k8s.authentik.server"
stream: "stdout"
kubernetes.pod_namespace: "authentik"
kubernetes.container_name: "server"
kubernetes.pod_node_name: "node-2"
message: '{"event":"login","action":"login","user":"alice","client_ip":"203.0.113.9","result":"success","level":"info","logger":"authentik.events","timestamp":"2026-07-27T00:00:00Z"}'
outputs:
- extract_from: authentik_parse
conditions:
- type: vrl
source: |
assert_eq!(.source, "k8s")
assert_eq!(.namespace, "authentik")
assert_eq!(.container, "server")
assert_eq!(.severity, "info")
assert_eq!(.message, "login")
assert_eq!(.labels.app, "authentik")
assert_eq!(.fields.event, "login")
assert_eq!(.fields.action, "login")
assert_eq!(.fields.user, "alice")
assert_eq!(.fields.client_ip, "203.0.113.9")
assert_eq!(.fields.result, "success")
# --- Tier-1: Traefik ingress (k8s, JSON access logs) ---
- name: traefik_routes_by_subject
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.k8s.traefik-system.traefik"
message: "routed"
outputs:
- extract_from: app_route.traefik
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: traefik_parse_extracts_access_fields
inputs:
- insert_at: traefik_parse
type: log
log_fields:
subject: "logs.k8s.traefik-system.traefik"
kubernetes.pod_namespace: "traefik-system"
kubernetes.container_name: "traefik"
kubernetes.pod_node_name: "node-3"
message: '{"RouterName":"web@kubernetes","ServiceName":"shop-svc@kubernetes","RequestMethod":"GET","RequestPath":"/api","RequestHost":"shop.example.net","RequestProtocol":"HTTP/1.1","DownstreamStatus":200,"Duration":5000000,"ClientHost":"203.0.113.5","StartUTC":"2026-07-27T00:00:00Z"}'
outputs:
- extract_from: traefik_parse
conditions:
- type: vrl
source: |
assert_eq!(.source, "k8s")
assert_eq!(.namespace, "traefik-system")
assert_eq!(.labels.app, "traefik")
assert_eq!(.message, "GET /api 200")
assert_eq!(.fields.route, "web@kubernetes")
assert_eq!(.fields.service, "shop-svc@kubernetes")
assert_eq!(.fields.method, "GET")
assert_eq!(.fields.path, "/api")
assert_eq!(.fields.host, "shop.example.net")
assert_eq!(.fields.status, "200")
assert_eq!(.fields.duration_ms, "5")
assert_eq!(.fields.client_ip, "203.0.113.5")
# --- Tier-1: Vault/OpenBao file audit (VM, awaiting VM vector) ---
- name: vault_routes_by_file
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.vm.vault1_syd1"
file: "/var/log/vault_audit.log"
message: "routed"
outputs:
- extract_from: app_route.vault
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: vault_parse_extracts_request
inputs:
- insert_at: vault_parse
type: log
log_fields:
subject: "logs.vm.vault1_syd1"
host: "vault1"
file: "/var/log/vault_audit.log"
message: '{"time":"2026-07-27T00:00:00Z","type":"response","auth":{"display_name":"token"},"request":{"operation":"read","path":"secret/data/app","remote_address":"10.0.0.9"},"error":""}'
outputs:
- extract_from: vault_parse
conditions:
- type: vrl
source: |
assert_eq!(.source, "vm")
assert_eq!(.host, "vault1")
assert_eq!(.labels.app, "vault")
assert_eq!(.message, "read secret/data/app")
assert_eq!(.fields.type, "response")
assert_eq!(.fields.display_name, "token")
assert_eq!(.fields.operation, "read")
assert_eq!(.fields.path, "secret/data/app")
assert_eq!(.fields.remote_address, "10.0.0.9")
# --- Tier-1: nginx access (VM, awaiting VM vector) ---
- name: nginx_access_routes_by_file
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.vm.web1_syd1"
file: "/var/log/nginx/shop_access.log"
message: "routed"
outputs:
- extract_from: app_route.nginx_access
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: nginx_access_parse_extracts_combined
inputs:
- insert_at: nginx_access_parse
type: log
log_fields:
subject: "logs.vm.web1_syd1"
host: "web1"
file: "/var/log/nginx/shop_access.log"
message: '192.0.2.10 - - [27/Jul/2026:00:00:00 +0000] "GET /index.html HTTP/1.1" 200 1024 "https://ref.example/" "Mozilla/5.0" 0.012'
outputs:
- extract_from: nginx_access_parse
conditions:
- type: vrl
source: |
assert_eq!(.source, "vm")
assert_eq!(.stream, "access")
assert_eq!(.labels.log_type, "access")
assert_eq!(.fields.client_ip, "192.0.2.10")
assert_eq!(.fields.method, "GET")
assert_eq!(.fields.path, "/index.html")
assert_eq!(.fields.status, "200")
assert_eq!(.fields.bytes, "1024")
assert_eq!(.fields.referer, "https://ref.example/")
assert_eq!(.fields.user_agent, "Mozilla/5.0")
assert_eq!(.fields.request_time, "0.012")
# --- Tier-1: nginx error (VM, awaiting VM vector) ---
- name: nginx_error_routes_by_file
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.vm.web1_syd1"
file: "/var/log/nginx/shop_error.log"
message: "routed"
outputs:
- extract_from: app_route.nginx_error
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: nginx_error_parse_extracts_fields
inputs:
- insert_at: nginx_error_parse
type: log
log_fields:
subject: "logs.vm.web1_syd1"
host: "web1"
file: "/var/log/nginx/shop_error.log"
message: '2026/07/27 00:00:00 [error] 1234#0: *5 open() "/var/www/x" failed (2: No such file or directory), client: 192.0.2.20, server: shop, request: "GET / HTTP/1.1", host: "shop"'
outputs:
- extract_from: nginx_error_parse
conditions:
- type: vrl
source: |
assert_eq!(.stream, "error")
assert_eq!(.severity, "error")
assert_eq!(.labels.log_type, "error")
assert_eq!(.fields.level, "error")
assert_eq!(.fields.pid, "1234")
assert_eq!(.fields.cid, "5")
assert_eq!(.fields.client_ip, "192.0.2.20")
# --- Tier-1: HAProxy httplog (VM journald, awaiting VM vector) ---
- name: haproxy_routes_by_identifier
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.vm.halb1_syd1"
SYSLOG_IDENTIFIER: "haproxy"
message: "routed"
outputs:
- extract_from: app_route.haproxy
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: haproxy_parse_extracts_timers
inputs:
- insert_at: haproxy_parse
type: log
log_fields:
subject: "logs.vm.halb1_syd1"
host: "halb1"
SYSLOG_IDENTIFIER: "haproxy"
message: '192.0.2.30:54321 [27/Jul/2026:00:00:00.123] fe_http be_app/app1 10/0/1/2/13 200 512 - - ---- 5/4/3/2/0 0/0 "GET /health HTTP/1.1"'
outputs:
- extract_from: haproxy_parse
conditions:
- type: vrl
source: |
assert_eq!(.source, "vm")
assert_eq!(.labels.app, "haproxy")
assert_eq!(.fields.client_ip, "192.0.2.30")
assert_eq!(.fields.frontend, "fe_http")
assert_eq!(.fields.backend, "be_app")
assert_eq!(.fields.server, "app1")
assert_eq!(.fields.tq, "10")
assert_eq!(.fields.tw, "0")
assert_eq!(.fields.tc, "1")
assert_eq!(.fields.tr, "2")
assert_eq!(.fields.tt, "13")
assert_eq!(.fields.termination_state, "----")
assert_eq!(.fields.retries, "0")
assert_eq!(.fields.status, "200")
assert_eq!(.fields.bytes, "512")
# --- Tier-1: glauth LDAP (VM, awaiting VM vector) ---
- name: glauth_routes_by_identifier
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.vm.ldap1_syd1"
SYSLOG_IDENTIFIER: "glauth"
message: "routed"
outputs:
- extract_from: app_route.glauth
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: glauth_parse_extracts_bind
inputs:
- insert_at: glauth_parse
type: log
log_fields:
subject: "logs.vm.ldap1_syd1"
host: "ldap1"
SYSLOG_IDENTIFIER: "glauth"
message: '{"level":"info","msg":"Bind success as user","bindDN":"cn=admin,dc=example,dc=com","src":"192.0.2.40:1234","time":"2026-07-27T00:00:00Z"}'
outputs:
- extract_from: glauth_parse
conditions:
- type: vrl
source: |
assert_eq!(.source, "vm")
assert_eq!(.host, "ldap1")
assert_eq!(.severity, "info")
assert_eq!(.labels.app, "glauth")
assert_eq!(.fields.bindDN, "cn=admin,dc=example,dc=com")
assert_eq!(.fields.remote, "192.0.2.40:1234")
assert_eq!(.fields.success, "true")
# ================= Tier-2 (stacks on #318) =================
# --- BIND query logs (k8s bind-* + VM named) ---
- name: bind_routes_k8s_by_subject
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.k8s.bind-internal.named"
message: "routed"
outputs:
- extract_from: app_route.bind_query
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: bind_routes_vm_by_identifier
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.vm.dns1_syd1"
SYSLOG_IDENTIFIER: "named"
message: "routed"
outputs:
- extract_from: app_route.bind_query
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: bind_parse_extracts_query
inputs:
- insert_at: bind_query_parse
type: log
log_fields:
subject: "logs.k8s.bind-internal.named"
kubernetes.pod_namespace: "bind-internal"
kubernetes.container_name: "named"
kubernetes.pod_node_name: "node-4"
message: '02-Aug-2026 00:00:00.123 client @0x7f 192.0.2.1#40426 (www.example.com): view internal: query: www.example.com IN A +E(0)K (198.18.200.7)'
outputs:
- extract_from: bind_query_parse
conditions:
- type: vrl
source: |
assert_eq!(.source, "k8s")
assert_eq!(.namespace, "bind-internal")
assert_eq!(.labels.app, "bind")
assert_eq!(.message, "query www.example.com A")
assert_eq!(.fields.client_ip, "192.0.2.1")
assert_eq!(.fields.qname, "www.example.com")
assert_eq!(.fields.qclass, "IN")
assert_eq!(.fields.qtype, "A")
assert_eq!(.fields.view, "internal")
# --- Rancher audit (k8s, cattle-system sidecar) ---
- name: rancher_routes_by_subject
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.k8s.cattle-system.rancher-audit-log"
message: "routed"
outputs:
- extract_from: app_route.rancher_audit
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: rancher_parse_extracts_audit
inputs:
- insert_at: rancher_audit_parse
type: log
log_fields:
subject: "logs.k8s.cattle-system.rancher-audit-log"
kubernetes.pod_namespace: "cattle-system"
kubernetes.container_name: "rancher-audit-log"
kubernetes.pod_node_name: "node-5"
message: '{"auditID":"abc-123","requestURI":"/v3/tokens","user":{"name":"u-alice","group":["admins"]},"method":"GET","remoteAddr":"10.42.0.9:1234","responseCode":200,"requestTimestamp":"2026-08-01T00:00:00Z"}'
outputs:
- extract_from: rancher_audit_parse
conditions:
- type: vrl
source: |
assert_eq!(.source, "k8s")
assert_eq!(.namespace, "cattle-system")
assert_eq!(.labels.app, "rancher")
assert_eq!(.labels.log_type, "audit")
assert_eq!(.message, "GET /v3/tokens 200")
assert_eq!(.fields.user, "u-alice")
assert_eq!(.fields.verb, "GET")
assert_eq!(.fields.uri, "/v3/tokens")
assert_eq!(.fields.status, "200")
# --- CNPG Postgres (ONE transform, all clusters) ---
- name: cnpg_routes_by_postgres_container
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.k8s.litellm.postgres"
message: "routed"
outputs:
- extract_from: app_route.cnpg_pg
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
# mutual exclusivity: an app-namespace CNPG pod (authentik) is claimed by
# cnpg_pg, NOT the authentik app route (which now carves out .postgres).
- name: cnpg_authentik_postgres_routes_to_cnpg
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.k8s.authentik.postgres"
message: "routed"
outputs:
- extract_from: app_route.cnpg_pg
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: cnpg_parse_extracts_record
inputs:
- insert_at: cnpg_pg_parse
type: log
log_fields:
subject: "logs.k8s.litellm.postgres"
kubernetes.pod_namespace: "litellm"
kubernetes.container_name: "postgres"
kubernetes.pod_node_name: "node-6"
kubernetes.pod_labels."cnpg.io/cluster": "litellm-postgres"
message: '{"level":"info","ts":"2026-08-01T00:00:00Z","logger":"postgres","msg":"record","record":{"user_name":"litellm","database_name":"litellm","error_severity":"LOG","message":"duration: 12.345 ms statement: SELECT 1","query":""}}'
outputs:
- extract_from: cnpg_pg_parse
conditions:
- type: vrl
source: |
assert_eq!(.source, "k8s")
assert_eq!(.namespace, "litellm")
assert_eq!(.severity, "LOG")
assert_eq!(.labels.app, "cnpg")
assert_eq!(.labels.cluster, "litellm-postgres")
assert_eq!(.fields.error_severity, "LOG")
assert_eq!(.fields.duration_ms, "12.345")
assert_eq!(.fields.user, "litellm")
assert_eq!(.fields.database, "litellm")
# --- Gitea router/access (k8s + VM) ---
- name: gitea_routes_k8s_by_subject
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.k8s.gitea.gitea"
message: "routed"
outputs:
- extract_from: app_route.gitea
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: gitea_parse_router_line
inputs:
- insert_at: gitea_parse
type: log
log_fields:
subject: "logs.k8s.gitea.gitea"
kubernetes.pod_namespace: "gitea"
kubernetes.container_name: "gitea"
kubernetes.pod_node_name: "node-7"
message: '2026/08/01 00:00:00 .../router.go:100:func() [I] router: completed GET /user/login for 10.0.0.1:0, 200 OK in 12.3ms @ web/base.go:1'
outputs:
- extract_from: gitea_parse
conditions:
- type: vrl
source: |
assert_eq!(.source, "k8s")
assert_eq!(.namespace, "gitea")
assert_eq!(.labels.app, "gitea")
assert_eq!(.message, "GET /user/login 200")
assert_eq!(.fields.method, "GET")
assert_eq!(.fields.path, "/user/login")
assert_eq!(.fields.status, "200")
assert_eq!(.fields.latency, "12.3ms")
- name: gitea_parse_access_line
inputs:
- insert_at: gitea_parse
type: log
log_fields:
subject: "logs.k8s.gitea.gitea"
kubernetes.pod_namespace: "gitea"
kubernetes.container_name: "gitea"
message: '10.0.0.5 - alice [01/Aug/2026:00:00:00 +0000] "POST /repo/foo HTTP/1.1" 201 512 "-" "git/2.0"'
outputs:
- extract_from: gitea_parse
conditions:
- type: vrl
source: |
assert_eq!(.fields.method, "POST")
assert_eq!(.fields.path, "/repo/foo")
assert_eq!(.fields.status, "201")
assert_eq!(.fields.user, "alice")
assert_eq!(.fields.client_ip, "10.0.0.5")
# --- PuppetServer / PuppetDB (k8s stdout) ---
- name: puppet_routes_by_subject
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.k8s.puppet.puppetserver"
message: "routed"
outputs:
- extract_from: app_route.puppet
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: puppet_parse_logback_line
inputs:
- insert_at: puppet_parse
type: log
log_fields:
subject: "logs.k8s.puppet.puppetserver"
kubernetes.pod_namespace: "puppet"
kubernetes.container_name: "puppetserver"
kubernetes.pod_node_name: "node-8"
message: '2026-08-01 00:00:00,123 INFO [qtp123-45] [puppetserver] Compiled catalog for web01.unkin.net in environment production in 1.23 seconds'
outputs:
- extract_from: puppet_parse
conditions:
- type: vrl
source: |
assert_eq!(.source, "k8s")
assert_eq!(.namespace, "puppet")
assert_eq!(.severity, "INFO")
assert_eq!(.labels.app, "puppet")
assert_eq!(.fields.level, "INFO")
assert_eq!(.fields.logger, "puppetserver")
assert_eq!(.fields.node, "web01.unkin.net")
# --- LiteLLM request logs (k8s JSON) ---
- name: litellm_routes_by_subject
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.k8s.litellm.litellm"
message: "routed"
outputs:
- extract_from: app_route.litellm
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: litellm_parse_extracts_request
inputs:
- insert_at: litellm_parse
type: log
log_fields:
subject: "logs.k8s.litellm.litellm"
kubernetes.pod_namespace: "litellm"
kubernetes.container_name: "litellm"
kubernetes.pod_node_name: "node-9"
message: '{"message":"Request completed","level":"info","model":"gpt-4o","total_tokens":1234,"response_time":0.532,"api_key":"sk-abc","status":"success","timestamp":"2026-08-01T00:00:00Z"}'
outputs:
- extract_from: litellm_parse
conditions:
- type: vrl
source: |
assert_eq!(.source, "k8s")
assert_eq!(.namespace, "litellm")
assert_eq!(.severity, "info")
assert_eq!(.message, "Request completed")
assert_eq!(.labels.app, "litellm")
assert_eq!(.fields.model, "gpt-4o")
assert_eq!(.fields.tokens, "1234")
assert_eq!(.fields.latency, "0.532")
assert_eq!(.fields.key, "sk-abc")
assert_eq!(.fields.status, "success")
# --- Postfix maillog (VM, per-line best-effort) ---
- name: postfix_routes_by_identifier
inputs:
- insert_at: app_route
type: log
log_fields:
subject: "logs.vm.mail1_syd1"
SYSLOG_IDENTIFIER: "postfix/qmgr"
message: "routed"
outputs:
- extract_from: app_route.postfix
conditions:
- type: vrl
source: 'assert_eq!(.message, "routed")'
- name: postfix_parse_extracts_line
inputs:
- insert_at: postfix_parse
type: log
log_fields:
subject: "logs.vm.mail1_syd1"
host: "mail1"
SYSLOG_IDENTIFIER: "postfix/smtp"
message: 'ABC123DEF: to=<rcpt@example.com>, relay=mx.example.com[1.2.3.4]:25, delay=1.2, delays=0.1/0/0.5/0.6, dsn=2.0.0, status=sent (250 OK)'
outputs:
- extract_from: postfix_parse
conditions:
- type: vrl
source: |
assert_eq!(.source, "vm")
assert_eq!(.host, "mail1")
assert_eq!(.labels.app, "postfix")
assert_eq!(.fields.qid, "ABC123DEF")
assert_eq!(.fields.to, "rcpt@example.com")
assert_eq!(.fields.relay, "mx.example.com[1.2.3.4]:25")
assert_eq!(.fields.delay, "1.2")
assert_eq!(.fields.status, "sent")
assert_eq!(.fields.program, "postfix/smtp")
-900
View File
@@ -1,900 +0,0 @@
---
# Vector TRANSFORM tier (the "brain") — single source of truth, also validated
# by `vector test` in CI. Consumes the whole log stream from JetStream via the
# durable `transform` consumer (at-least-once; durable offsets tracked by
# JetStream), routes by subject, normalises into the logs.raw columns, and is
# the ONLY ClickHouse writer.
#
# Routing model (two stages):
# 1. app_route — peels off Tier-1 per-app streams by subject / source tag and
# hands each to a dedicated parse transform that emits the full logs.raw
# shape plus structured .fields. Conditions are MUTUALLY EXCLUSIVE, so an
# event is claimed by at most one app (no double-insert).
# 2. route (generic catch-all) — everything app_route did NOT claim
# (app_route._unmatched) is split k8s/vm and shaped generically. This is the
# fallback for all un-parsed traffic and MUST stay intact.
# Add a new per-app pipeline by appending a mutually-exclusive route to
# app_route, a parse transform, and its id to the clickhouse sink `inputs`.
#
# Structured fields go into the logs.raw `fields Map(String,String)` column — no
# DDL change is needed (values are stringified; empties are compacted away).
#
# VM source-tag convention (the puppet-side vector rollout MUST follow it so
# these transforms light up): file sources set `.file` (absolute log path);
# journald sources set `.SYSLOG_IDENTIFIER` (falls back to `.program`/`.appname`).
#
# Durability model: JetStream (3d / 130 GiB, S2-compressed) is the SOLE
# durability layer and the replay window. This
# tier is stateless (no PVC, memory buffer). If ClickHouse is down the sink
# blocks (when_full=block); back-pressure stops the source pulling, so unpulled
# messages stay in JetStream and are redelivered. NB: Vector's NATS source has
# no end-to-end acks (acks on receipt), so a pod killed mid-outage can lose the
# in-memory buffer's worth of already-pulled events — accepted for a stateless,
# autoscalable tier.
data_dir: /vector-data-dir
api:
enabled: true
address: 0.0.0.0:8686
sources:
js_in:
type: nats
url: nats://nats.logging.svc.cluster.local:4222
connection_name: vector-transform
subject: "logs.>"
jetstream:
stream: LOGS
consumer: transform
auth:
strategy: user_password
user_password:
user: log-consumer
password: ${NATS_CONSUMER_PASSWORD}
decoding:
codec: json
transforms:
# Stage 1: peel off Tier-1 per-app streams. Mutually exclusive conditions;
# anything unmatched falls through to the generic `route` below.
app_route:
type: route
inputs:
- js_in
route:
# k8s: authentik SSO — structlog JSON on stdout. The `.postgres` container
# is the authentik-namespace CNPG cluster; carve it out so it is claimed by
# the single `cnpg_pg` route below (keeps app_route mutually exclusive).
authentik: 'starts_with(to_string(.subject) ?? "", "logs.k8s.authentik.") && !ends_with(to_string(.subject) ?? "", ".postgres")'
# k8s: Traefik ingress — JSON access logs (requires logs.access.format=json,
# flipped in the traefik-system overlay values in this same change).
traefik: 'starts_with(to_string(.subject) ?? "", "logs.k8s.traefik-system.")'
# VM: Vault/OpenBao file audit device (/var/log/vault_audit.log), JSON.
vault: 'starts_with(to_string(.subject) ?? "", "logs.vm.") && contains(to_string(.file) ?? "", "vault_audit")'
# VM: nginx combined access log (/var/log/nginx/<vhost>_access.log).
nginx_access: 'starts_with(to_string(.subject) ?? "", "logs.vm.") && contains(to_string(.file) ?? "", "nginx") && ends_with(to_string(.file) ?? "", "access.log")'
# VM: nginx error log (/var/log/nginx/<vhost>_error.log).
nginx_error: 'starts_with(to_string(.subject) ?? "", "logs.vm.") && contains(to_string(.file) ?? "", "nginx") && ends_with(to_string(.file) ?? "", "error.log")'
# VM: HAProxy httplog via journald.
haproxy: 'starts_with(to_string(.subject) ?? "", "logs.vm.") && ((to_string(.SYSLOG_IDENTIFIER) ?? "") == "haproxy" || (to_string(.program) ?? "") == "haproxy" || (to_string(.appname) ?? "") == "haproxy")'
# VM: glauth LDAP — structuredlog (logrus) JSON.
glauth: 'starts_with(to_string(.subject) ?? "", "logs.vm.") && (contains(to_string(.file) ?? "", "glauth") || (to_string(.SYSLOG_IDENTIFIER) ?? "") == "glauth" || (to_string(.program) ?? "") == "glauth" || (to_string(.appname) ?? "") == "glauth")'
# --- Tier-2 (stacks on #318) ---
# BIND query logs, k8s + VM. k8s: any bind-* namespace (bind-internal DNS
# servers, bind-system operator) — query logging enabled via `querylog yes`
# in the BindCluster extraOptions in this change. VM: puppet-managed named
# (file /var/log/named/*.log or journald `named`) — puppet-side enable is a
# required follow-up (profiles/dns/server.pp).
bind_query: 'starts_with(to_string(.subject) ?? "", "logs.k8s.bind") || (starts_with(to_string(.subject) ?? "", "logs.vm.") && (contains(to_string(.file) ?? "", "named") || (to_string(.SYSLOG_IDENTIFIER) ?? "") == "named" || (to_string(.program) ?? "") == "named" || (to_string(.appname) ?? "") == "named"))'
# k8s: Rancher audit log — JSON, emitted by the `rancher-audit-log` sidecar
# (auditLog.enabled level 1, already on in the cattle-system overlay).
rancher_audit: 'starts_with(to_string(.subject) ?? "", "logs.k8s.cattle-system.rancher-audit-log")'
# k8s: CNPG Postgres — ONE route for ALL clusters. The CNPG main container is
# always named `postgres`, so logs.k8s.<ns>.postgres uniquely identifies every
# cluster across all namespaces (authentik/litellm/artifactapi/woodpecker/
# puppet/paperclip/grafana/netbox/gitea/encapi). Mutually exclusive because the
# app routes above/below carve out `.postgres`.
cnpg_pg: 'starts_with(to_string(.subject) ?? "", "logs.k8s.") && ends_with(to_string(.subject) ?? "", ".postgres")'
# Gitea router/access logs. k8s: the new k8s gitea (ns gitea) with router +
# access logging enabled in the overlay values in this change — carve out
# `.postgres` (gitea-namespace CNPG). VM: puppet-managed gitea (file or
# journald `gitea`) — puppet-side log-format enable is a follow-up.
gitea: '(starts_with(to_string(.subject) ?? "", "logs.k8s.gitea.") && !ends_with(to_string(.subject) ?? "", ".postgres")) || (starts_with(to_string(.subject) ?? "", "logs.vm.") && (contains(to_string(.file) ?? "", "gitea") || (to_string(.SYSLOG_IDENTIFIER) ?? "") == "gitea" || (to_string(.program) ?? "") == "gitea" || (to_string(.appname) ?? "") == "gitea"))'
# PuppetServer / PuppetDB. k8s: openvoxserver/openvoxdb stdout (ns puppet) —
# carve out `.postgres` (puppet-namespace CNPG). VM file logs (multiline
# logback + puppetserver-access.log) are a puppet-side vector concern (the
# multiline join must happen at the edge) — follow-up.
puppet: 'starts_with(to_string(.subject) ?? "", "logs.k8s.puppet.") && !ends_with(to_string(.subject) ?? "", ".postgres")'
# k8s: LiteLLM request logs — JSON once JSON_LOGS=True (flipped in the litellm
# env in this change). Carve out `.postgres` (litellm-namespace CNPG).
litellm: 'starts_with(to_string(.subject) ?? "", "logs.k8s.litellm.") && !ends_with(to_string(.subject) ?? "", ".postgres")'
# VM: Postfix maillog — journald (SYSLOG_IDENTIFIER postfix/*) or file maillog.
postfix: 'starts_with(to_string(.subject) ?? "", "logs.vm.") && (starts_with(to_string(.SYSLOG_IDENTIFIER) ?? "", "postfix") || starts_with(to_string(.program) ?? "", "postfix") || starts_with(to_string(.appname) ?? "", "postfix") || contains(to_string(.file) ?? "", "maillog"))'
# Stage 2: generic catch-all for everything app_route did not claim.
route:
type: route
inputs:
- app_route._unmatched
route:
k8s: 'starts_with(to_string(.subject) ?? "", "logs.k8s.")'
vm: 'starts_with(to_string(.subject) ?? "", "logs.vm.")'
k8s_shape:
type: remap
inputs:
- route.k8s
source: |
ts = .timestamp || now()
node = to_string(.kubernetes.pod_node_name || "") ?? ""
ns = to_string(.kubernetes.pod_namespace || "") ?? ""
pod = to_string(.kubernetes.pod_name || "") ?? ""
container = to_string(.kubernetes.container_name || "") ?? ""
strm = to_string(.stream || "") ?? ""
msg = to_string(.message || "") ?? ""
lbls = object(.kubernetes.pod_labels) ?? {}
. = {
"timestamp": ts,
"host": node,
"source": "k8s",
"namespace": ns,
"pod": pod,
"container": container,
"stream": strm,
"severity": "",
"message": msg,
"labels": lbls,
"fields": {}
}
vm_shape:
type: remap
inputs:
- route.vm
source: |
ts = .timestamp || .ts || now()
host = to_string(.host || .hostname || "") ?? ""
msg = to_string(.message || .msg || "") ?? ""
sev = to_string(.severity || .level || "") ?? ""
role = to_string(.role || "") ?? ""
lbls = {}
if role != "" {
lbls = {"role": role}
}
. = {
"timestamp": ts,
"host": host,
"source": "vm",
"namespace": "",
"pod": "",
"container": "",
"stream": "",
"severity": sev,
"message": msg,
"labels": lbls,
"fields": {}
}
# --- Tier-1 per-app parse transforms (each emits the full logs.raw shape) ---
# Authentik SSO (k8s, ns authentik) — structlog JSON on stdout.
# LIVE NOW: authentik pods already stream to logs.k8s.authentik.*.
authentik_parse:
type: remap
inputs:
- app_route.authentik
source: |
node = to_string(.kubernetes.pod_node_name || "") ?? ""
pod = to_string(.kubernetes.pod_name || "") ?? ""
container = to_string(.kubernetes.container_name || "") ?? ""
strm = to_string(.stream || "") ?? ""
raw = to_string(.message || "") ?? ""
ev = object(parse_json(raw) ?? {}) ?? {}
ts = ev.timestamp || .timestamp || now()
user = ""
if is_string(ev.user) {
user = to_string(ev.user) ?? ""
} else if is_object(ev.user) {
user = to_string(ev.user.username) ?? ""
}
fields = compact({
"event": to_string(ev.event) ?? "",
"action": to_string(ev.action) ?? "",
"user": user,
"client_ip": to_string(ev.client_ip) ?? "",
"result": to_string(ev.result) ?? "",
"logger": to_string(ev.logger) ?? ""
}, string: true)
sev = to_string(ev.level) ?? ""
msg = raw
if ev.event != null {
msg = to_string(ev.event) ?? raw
}
. = {
"timestamp": ts,
"host": node,
"source": "k8s",
"namespace": "authentik",
"pod": pod,
"container": container,
"stream": strm,
"severity": sev,
"message": msg,
"labels": {"app": "authentik"},
"fields": fields
}
# Traefik ingress (k8s, ns traefik-system) — JSON access logs. Non-access
# traefik lines (app logs) simply parse to no access fields and keep .message.
# geoip on client_ip is a PREREQUISITE (no enrichment table yet — see PR note).
traefik_parse:
type: remap
inputs:
- app_route.traefik
source: |
node = to_string(.kubernetes.pod_node_name || "") ?? ""
pod = to_string(.kubernetes.pod_name || "") ?? ""
container = to_string(.kubernetes.container_name || "") ?? ""
strm = to_string(.stream || "") ?? ""
raw = to_string(.message || "") ?? ""
ev = object(parse_json(raw) ?? {}) ?? {}
ts = ev.StartUTC || ev.time || .timestamp || now()
status = ""
if ev.DownstreamStatus != null {
status = to_string(ev.DownstreamStatus) ?? ""
}
dur_ns = to_int(ev.Duration) ?? 0
dur_ms = ""
if dur_ns > 0 {
dur_ms = to_string(dur_ns / 1000000)
}
method = to_string(ev.RequestMethod) ?? ""
path = to_string(ev.RequestPath) ?? ""
fields = compact({
"route": to_string(ev.RouterName) ?? "",
"service": to_string(ev.ServiceName) ?? "",
"method": method,
"path": path,
"host": to_string(ev.RequestHost) ?? "",
"status": status,
"duration_ms": dur_ms,
"client_ip": to_string(ev.ClientHost) ?? "",
"protocol": to_string(ev.RequestProtocol) ?? ""
}, string: true)
msg = raw
if method != "" {
msg = method + " " + path + " " + status
}
. = {
"timestamp": ts,
"host": node,
"source": "k8s",
"namespace": "traefik-system",
"pod": pod,
"container": container,
"stream": strm,
"severity": "",
"message": msg,
"labels": {"app": "traefik"},
"fields": fields
}
# Vault/OpenBao file audit device (VM, /var/log/vault_audit.log) — JSON, one
# object per request/response. AWAITING VM VECTOR (in-cluster vault is quiet;
# lights up when the puppet vector rollout ships logs.vm.* with .file set).
vault_parse:
type: remap
inputs:
- app_route.vault
source: |
host = to_string(.host || .hostname || "") ?? ""
raw = to_string(.message || .msg || "") ?? ""
ev = object(parse_json(raw) ?? {}) ?? {}
ts = ev.time || .timestamp || .ts || now()
auth = object(ev.auth) ?? {}
req = object(ev.request) ?? {}
fields = compact({
"type": to_string(ev.type) ?? "",
"display_name": to_string(auth.display_name) ?? "",
"operation": to_string(req.operation) ?? "",
"path": to_string(req.path) ?? "",
"remote_address": to_string(req.remote_address) ?? "",
"error": to_string(ev.error) ?? ""
}, string: true)
op = to_string(req.operation) ?? ""
pth = to_string(req.path) ?? ""
msg = raw
if op != "" || pth != "" {
msg = op + " " + pth
}
. = {
"timestamp": ts,
"host": host,
"source": "vm",
"namespace": "",
"pod": "",
"container": "",
"stream": "",
"severity": "",
"message": msg,
"labels": {"app": "vault"},
"fields": fields
}
# nginx access log (VM) — combined/CLF + optional trailing request_time.
# AWAITING VM VECTOR. geoip on client_ip is a PREREQUISITE (see PR note).
nginx_access_parse:
type: remap
inputs:
- app_route.nginx_access
source: |
host = to_string(.host || .hostname || "") ?? ""
raw = to_string(.message || .msg || "") ?? ""
ts = .timestamp || .ts || now()
m = parse_regex(raw, r'^(?P<client_ip>\S+) \S+ (?P<user>\S+) \[(?P<time_local>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) (?P<protocol>[^"]*)" (?P<status>\d{3}) (?P<bytes>\d+|-) "(?P<referer>[^"]*)" "(?P<user_agent>[^"]*)"(?: (?P<request_time>[\d.]+))?') ?? {}
fields = compact({
"client_ip": to_string(m.client_ip),
"method": to_string(m.method),
"path": to_string(m.path),
"status": to_string(m.status),
"bytes": to_string(m.bytes),
"referer": to_string(m.referer),
"user_agent": to_string(m.user_agent),
"request_time": to_string(m.request_time)
}, string: true)
. = {
"timestamp": ts,
"host": host,
"source": "vm",
"namespace": "",
"pod": "",
"container": "",
"stream": "access",
"severity": "",
"message": raw,
"labels": {"app": "nginx", "log_type": "access"},
"fields": fields
}
# nginx error log (VM). AWAITING VM VECTOR.
nginx_error_parse:
type: remap
inputs:
- app_route.nginx_error
source: |
host = to_string(.host || .hostname || "") ?? ""
raw = to_string(.message || .msg || "") ?? ""
ts = .timestamp || .ts || now()
m = parse_regex(raw, r'^(?P<time_local>\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}) \[(?P<level>\w+)\] (?P<pid>\d+)#(?P<tid>\d+): (?:\*(?P<cid>\d+) )?(?P<err>.*)$') ?? {}
c = parse_regex(raw, r'client: (?P<client_ip>[0-9a-fA-F:.]+)') ?? {}
lvl = to_string(m.level)
err = to_string(m.err)
fields = compact({
"level": lvl,
"pid": to_string(m.pid),
"cid": to_string(m.cid),
"client_ip": to_string(c.client_ip),
"error": err
}, string: true)
msg = raw
if err != "" {
msg = err
}
. = {
"timestamp": ts,
"host": host,
"source": "vm",
"namespace": "",
"pod": "",
"container": "",
"stream": "error",
"severity": lvl,
"message": msg,
"labels": {"app": "nginx", "log_type": "error"},
"fields": fields
}
# HAProxy httplog (VM, journald). AWAITING VM VECTOR.
# httplog: %ci:%cp [%tr] %ft %b/%s %Tq/%Tw/%Tc/%Tr/%Tt %ST %B %CC %CS %tsc
# %ac/%fc/%bc/%sc/%rc %sq/%bq {hdrs} "%r"
haproxy_parse:
type: remap
inputs:
- app_route.haproxy
source: |
host = to_string(.host || .hostname || "") ?? ""
raw = to_string(.message || .msg || "") ?? ""
ts = .timestamp || .ts || now()
m = parse_regex(raw, r'(?P<client_ip>\d{1,3}(?:\.\d{1,3}){3}):(?P<client_port>\d+) \[(?P<accept_date>[^\]]+)\] (?P<frontend>\S+) (?P<backend>[^/ ]+)/(?P<server>\S+) (?P<tq>-?\d+)/(?P<tw>-?\d+)/(?P<tc>-?\d+)/(?P<tr>-?\d+)/(?P<tt>[+-]?\d+) (?P<status>\d{3}) (?P<bytes>\d+) \S+ \S+ (?P<termination_state>\S{4}) (?P<actconn>\d+)/(?P<feconn>\d+)/(?P<beconn>\d+)/(?P<srvconn>\d+)/(?P<retries>\d+) (?P<srv_queue>\d+)/(?P<backend_queue>\d+)') ?? {}
fields = compact({
"client_ip": to_string(m.client_ip),
"frontend": to_string(m.frontend),
"backend": to_string(m.backend),
"server": to_string(m.server),
"tq": to_string(m.tq),
"tw": to_string(m.tw),
"tc": to_string(m.tc),
"tr": to_string(m.tr),
"tt": to_string(m.tt),
"termination_state": to_string(m.termination_state),
"retries": to_string(m.retries),
"status": to_string(m.status),
"bytes": to_string(m.bytes)
}, string: true)
. = {
"timestamp": ts,
"host": host,
"source": "vm",
"namespace": "",
"pod": "",
"container": "",
"stream": "",
"severity": "",
"message": raw,
"labels": {"app": "haproxy"},
"fields": fields
}
# glauth LDAP (VM) — structuredlog (logrus) JSON. AWAITING VM VECTOR.
glauth_parse:
type: remap
inputs:
- app_route.glauth
source: |
host = to_string(.host || .hostname || "") ?? ""
raw = to_string(.message || .msg || "") ?? ""
ev = object(parse_json(raw) ?? {}) ?? {}
ts = ev.time || .timestamp || .ts || now()
binddn = to_string(ev.bindDN) ?? ""
if binddn == "" {
binddn = to_string(ev.binddn) ?? ""
}
remote = to_string(ev.src) ?? ""
if remote == "" {
remote = to_string(ev.remoteAddr) ?? ""
}
lvl = to_string(ev.level) ?? ""
gmsg = to_string(ev.msg) ?? ""
success = "false"
if contains(downcase(gmsg), "success") || (lvl == "info" && contains(downcase(gmsg), "bind")) {
success = "true"
}
fields = compact({
"bindDN": binddn,
"remote": remote,
"success": success,
"level": lvl,
"msg": gmsg
}, string: true)
msg = gmsg
if msg == "" {
msg = raw
}
. = {
"timestamp": ts,
"host": host,
"source": "vm",
"namespace": "",
"pod": "",
"container": "",
"stream": "",
"severity": lvl,
"message": msg,
"labels": {"app": "glauth"},
"fields": fields
}
# --- Tier-2 per-app parse transforms (stacks on #318) ---
# BIND query logs (k8s bind-* namespaces + VM named). LIVE on k8s once the
# `querylog yes` extraOptions (this change) roll out; VM AWAITS the puppet-side
# enable (profiles/dns/server.pp). rcode is NOT present in standard query-log
# lines (that needs response logging / dnstap) — extracted only if a
# response-style `status:` line is seen. Non-query lines keep .message.
bind_query_parse:
type: remap
inputs:
- app_route.bind_query
source: |
subj = to_string(.subject) ?? ""
is_k8s = starts_with(subj, "logs.k8s.")
raw = to_string(.message || .msg || "") ?? ""
ts = .timestamp || .ts || now()
node = ""
ns = ""
pod = ""
container = ""
strm = ""
hostv = ""
src = "vm"
if is_k8s {
src = "k8s"
node = to_string(.kubernetes.pod_node_name || "") ?? ""
ns = to_string(.kubernetes.pod_namespace || "") ?? ""
pod = to_string(.kubernetes.pod_name || "") ?? ""
container = to_string(.kubernetes.container_name || "") ?? ""
strm = to_string(.stream || "") ?? ""
hostv = node
} else {
hostv = to_string(.host || .hostname || "") ?? ""
}
m = parse_regex(raw, r'client\s+(?:@\S+\s+)?(?P<client_ip>[0-9a-fA-F:.]+)#(?P<port>\d+)(?:\s+\([^)]*\))?:\s+(?:view\s+(?P<view>\S+):\s+)?query:\s+(?P<qname>\S+)\s+(?P<qclass>\S+)\s+(?P<qtype>\S+)(?:\s+(?P<flags>\S+))?') ?? {}
rc = parse_regex(raw, r'status:\s+(?P<rcode>\w+)') ?? {}
fields = compact({
"client_ip": to_string(m.client_ip),
"qname": to_string(m.qname),
"qtype": to_string(m.qtype),
"qclass": to_string(m.qclass),
"view": to_string(m.view),
"flags": to_string(m.flags),
"rcode": to_string(rc.rcode)
}, string: true)
qn = to_string(m.qname)
msg = raw
if qn != "" {
msg = "query " + qn + " " + to_string(m.qtype)
}
. = {
"timestamp": ts,
"host": hostv,
"source": src,
"namespace": ns,
"pod": pod,
"container": container,
"stream": strm,
"severity": "",
"message": msg,
"labels": {"app": "bind"},
"fields": fields
}
# Rancher audit log (k8s, cattle-system rancher-audit-log sidecar) — JSON,
# auditLog level 1 (already enabled in the overlay). LIVE NOW.
rancher_audit_parse:
type: remap
inputs:
- app_route.rancher_audit
source: |
node = to_string(.kubernetes.pod_node_name || "") ?? ""
pod = to_string(.kubernetes.pod_name || "") ?? ""
container = to_string(.kubernetes.container_name || "") ?? ""
strm = to_string(.stream || "") ?? ""
raw = to_string(.message || "") ?? ""
ev = object(parse_json(raw) ?? {}) ?? {}
ts = ev.requestTimestamp || ev.time || .timestamp || now()
user = ""
if is_object(ev.user) {
user = to_string(ev.user.name) ?? ""
} else if is_string(ev.user) {
user = to_string(ev.user) ?? ""
}
verb = to_string(ev.method) ?? ""
if verb == "" { verb = to_string(ev.verb) ?? "" }
uri = to_string(ev.requestURI) ?? ""
if uri == "" { uri = to_string(ev.uri) ?? "" }
status = ""
if ev.responseCode != null { status = to_string(ev.responseCode) ?? "" }
if status == "" && is_object(ev.responseStatus) { status = to_string(ev.responseStatus.code) ?? "" }
fields = compact({
"user": user,
"verb": verb,
"uri": uri,
"status": status,
"auditID": to_string(ev.auditID) ?? "",
"remote_addr": to_string(ev.remoteAddr) ?? ""
}, string: true)
msg = raw
if verb != "" || uri != "" {
msg = verb + " " + uri + " " + status
}
. = {
"timestamp": ts,
"host": node,
"source": "k8s",
"namespace": "cattle-system",
"pod": pod,
"container": container,
"stream": strm,
"severity": "",
"message": msg,
"labels": {"app": "rancher", "log_type": "audit"},
"fields": fields
}
# CNPG Postgres — ONE transform for ALL clusters (10 namespaces). The instance
# manager wraps postgres logs as JSON on stdout; the postgres CSV columns nest
# under `.record` (logger == "postgres"). Non-postgres lines (instance-manager
# operator logs) keep .message and set no PG fields. LIVE NOW.
cnpg_pg_parse:
type: remap
inputs:
- app_route.cnpg_pg
source: |
node = to_string(.kubernetes.pod_node_name || "") ?? ""
ns = to_string(.kubernetes.pod_namespace || "") ?? ""
pod = to_string(.kubernetes.pod_name || "") ?? ""
container = to_string(.kubernetes.container_name || "") ?? ""
strm = to_string(.stream || "") ?? ""
raw = to_string(.message || "") ?? ""
cluster = to_string(.kubernetes.pod_labels."cnpg.io/cluster" || "") ?? ""
ev = object(parse_json(raw) ?? {}) ?? {}
ts = .timestamp || now()
rec = object(ev.record) ?? {}
logger = to_string(ev.logger) ?? ""
sev = ""
pgmsg = ""
fields = {}
if logger == "postgres" {
sev = to_string(rec.error_severity) ?? ""
pgmsg = to_string(rec.message) ?? ""
dm = parse_regex(pgmsg, r'duration:\s+(?P<ms>[0-9.]+)\s+ms') ?? {}
fields = compact({
"error_severity": sev,
"message": pgmsg,
"query": to_string(rec.query) ?? "",
"duration_ms": to_string(dm.ms),
"user": to_string(rec.user_name) ?? "",
"database": to_string(rec.database_name) ?? ""
}, string: true)
}
lbls = {"app": "cnpg"}
if cluster != "" {
lbls = {"app": "cnpg", "cluster": cluster}
}
msg = raw
if pgmsg != "" { msg = pgmsg }
. = {
"timestamp": ts,
"host": node,
"source": "k8s",
"namespace": ns,
"pod": pod,
"container": container,
"stream": strm,
"severity": sev,
"message": msg,
"labels": lbls,
"fields": fields
}
# Gitea router/access logs (k8s gitea + VM gitea). Router "completed" lines give
# method/path/status/latency; NCSA access lines give method/path/status/user.
# k8s LIVE once the overlay log config (this change) rolls out; VM AWAITS the
# puppet-side log-format enable.
gitea_parse:
type: remap
inputs:
- app_route.gitea
source: |
subj = to_string(.subject) ?? ""
is_k8s = starts_with(subj, "logs.k8s.")
raw = to_string(.message || .msg || "") ?? ""
ts = .timestamp || .ts || now()
node = ""
ns = ""
pod = ""
container = ""
strm = ""
hostv = ""
src = "vm"
if is_k8s {
src = "k8s"
node = to_string(.kubernetes.pod_node_name || "") ?? ""
ns = to_string(.kubernetes.pod_namespace || "") ?? ""
pod = to_string(.kubernetes.pod_name || "") ?? ""
container = to_string(.kubernetes.container_name || "") ?? ""
strm = to_string(.stream || "") ?? ""
hostv = node
} else {
hostv = to_string(.host || .hostname || "") ?? ""
}
r = parse_regex(raw, r'completed (?P<method>\S+) (?P<path>\S+) for (?P<client>\S+), (?P<status>\d{3}) [^ ]+ in (?P<latency>[0-9.]+\w+)') ?? {}
a = parse_regex(raw, r'^(?P<client_ip>\S+) \S+ (?P<user>\S+) \[[^\]]+\] "(?P<method>\S+) (?P<path>\S+) [^"]*" (?P<status>\d{3})') ?? {}
method = to_string(r.method)
if method == "" { method = to_string(a.method) }
path = to_string(r.path)
if path == "" { path = to_string(a.path) }
status = to_string(r.status)
if status == "" { status = to_string(a.status) }
user = to_string(a.user)
if user == "-" { user = "" }
fields = compact({
"method": method,
"path": path,
"status": status,
"latency": to_string(r.latency),
"user": user,
"client_ip": to_string(a.client_ip)
}, string: true)
msg = raw
if method != "" {
msg = method + " " + path + " " + status
}
. = {
"timestamp": ts,
"host": hostv,
"source": src,
"namespace": ns,
"pod": pod,
"container": container,
"stream": strm,
"severity": "",
"message": msg,
"labels": {"app": "gitea"},
"fields": fields
}
# PuppetServer / PuppetDB (k8s openvoxserver/openvoxdb stdout, ns puppet). Per
# line logback parse (level/logger/message + node) and an access-log line
# (method/status/node) where present. VM multiline stacktrace join +
# puppetserver-access.log are a puppet-side edge concern (follow-up). LIVE NOW.
puppet_parse:
type: remap
inputs:
- app_route.puppet
source: |
node = to_string(.kubernetes.pod_node_name || "") ?? ""
pod = to_string(.kubernetes.pod_name || "") ?? ""
container = to_string(.kubernetes.container_name || "") ?? ""
strm = to_string(.stream || "") ?? ""
raw = to_string(.message || "") ?? ""
ts = .timestamp || now()
lb = parse_regex(raw, r'^(?P<ts>\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}[,.]\d+)\s+(?P<level>[A-Z]+)\s+\[(?P<thread>[^\]]*)\]\s+\[(?P<logger>[^\]]*)\]\s+(?P<msg>.*)$') ?? {}
ac = parse_regex(raw, r'^(?P<client_ip>\S+) \S+ \S+ \[[^\]]+\] "(?P<method>\S+) (?P<path>\S+) [^"]*" (?P<status>\d{3})') ?? {}
nd = parse_regex(raw, r'(?:catalog for|for node)\s+(?P<node>[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+)') ?? {}
lvl = to_string(lb.level)
pmsg = to_string(lb.msg)
err = ""
if lvl == "ERROR" { err = pmsg }
fields = compact({
"level": lvl,
"logger": to_string(lb.logger),
"node": to_string(nd.node),
"method": to_string(ac.method),
"status": to_string(ac.status),
"path": to_string(ac.path),
"client_ip": to_string(ac.client_ip),
"error": err
}, string: true)
msg = raw
if pmsg != "" { msg = pmsg }
. = {
"timestamp": ts,
"host": node,
"source": "k8s",
"namespace": "puppet",
"pod": pod,
"container": container,
"stream": strm,
"severity": lvl,
"message": msg,
"labels": {"app": "puppet"},
"fields": fields
}
# LiteLLM request logs (k8s) — JSON once JSON_LOGS=True (flipped in the litellm
# env this change). parse_json -> model/tokens/latency/key/status (best-effort
# against litellm's JSON schema); non-JSON lines keep .message. Field keys light
# up once the env flip rolls out.
litellm_parse:
type: remap
inputs:
- app_route.litellm
source: |
node = to_string(.kubernetes.pod_node_name || "") ?? ""
pod = to_string(.kubernetes.pod_name || "") ?? ""
container = to_string(.kubernetes.container_name || "") ?? ""
strm = to_string(.stream || "") ?? ""
raw = to_string(.message || "") ?? ""
ev = object(parse_json(raw) ?? {}) ?? {}
ts = ev.timestamp || .timestamp || now()
sev = to_string(ev.level) ?? ""
lmsg = to_string(ev.message) ?? ""
fields = compact({
"model": to_string(ev.model) ?? "",
"tokens": to_string(ev.total_tokens) ?? "",
"latency": to_string(ev.response_time) ?? "",
"key": to_string(ev.api_key) ?? "",
"status": to_string(ev.status) ?? "",
"user": to_string(ev.user) ?? ""
}, string: true)
msg = raw
if lmsg != "" { msg = lmsg }
. = {
"timestamp": ts,
"host": node,
"source": "k8s",
"namespace": "litellm",
"pod": pod,
"container": container,
"stream": strm,
"severity": sev,
"message": msg,
"labels": {"app": "litellm"},
"fields": fields
}
# Postfix maillog (VM) — best-effort PER-LINE parse (qid + from/to/status/relay/
# delay). Full qid-lifecycle correlation is a query-time GROUP BY qid in
# ClickHouse, NOT a stateless-aggregator job (stitching the multi-line lifecycle
# needs a stateful reduce). AWAITS VM VECTOR.
postfix_parse:
type: remap
inputs:
- app_route.postfix
source: |
host = to_string(.host || .hostname || "") ?? ""
raw = to_string(.message || .msg || "") ?? ""
ts = .timestamp || .ts || now()
prog = to_string(.SYSLOG_IDENTIFIER || .program || .appname || "") ?? ""
q = parse_regex(raw, r'^(?P<qid>[0-9A-F]{6,}):') ?? {}
frm = parse_regex(raw, r'from=<(?P<from>[^>]*)>') ?? {}
rcpt = parse_regex(raw, r'to=<(?P<to>[^>]*)>') ?? {}
st = parse_regex(raw, r'status=(?P<status>\w+)') ?? {}
rel = parse_regex(raw, r'relay=(?P<relay>[^,]+)') ?? {}
dly = parse_regex(raw, r'delay=(?P<delay>[0-9.]+)') ?? {}
fields = compact({
"qid": to_string(q.qid),
"from": to_string(frm.from),
"to": to_string(rcpt.to),
"status": to_string(st.status),
"relay": to_string(rel.relay),
"delay": to_string(dly.delay),
"program": prog
}, string: true)
. = {
"timestamp": ts,
"host": host,
"source": "vm",
"namespace": "",
"pod": "",
"container": "",
"stream": "",
"severity": "",
"message": raw,
"labels": {"app": "postfix"},
"fields": fields
}
sinks:
clickhouse:
type: clickhouse
inputs:
- k8s_shape
- vm_shape
- authentik_parse
- traefik_parse
- vault_parse
- nginx_access_parse
- nginx_error_parse
- haproxy_parse
- glauth_parse
- bind_query_parse
- rancher_audit_parse
- cnpg_pg_parse
- gitea_parse
- puppet_parse
- litellm_parse
- postfix_parse
endpoint: http://clickhouse-logs.logging.svc.cluster.local:8123
database: logs
table: raw
skip_unknown_fields: true
date_time_best_effort: true
auth:
strategy: basic
user: "${CLICKHOUSE_USER}"
password: "${CLICKHOUSE_PASSWORD}"
batch:
max_events: 500000
max_bytes: 134217728
timeout_secs: 10
# Stateless: in-memory buffer, block on full so back-pressure reaches the
# JetStream pull source (which then stops acking). JetStream is durability.
buffer:
type: memory
max_events: 2000
when_full: block
healthcheck:
enabled: true
@@ -1,29 +0,0 @@
---
# `vector test` unit tests for the VM-ingest routing transform.
tests:
- name: host_token_is_subject_safe
inputs:
- insert_at: tag
type: log
log_fields:
host: "db1.syd1.example.net"
message: "sshd accepted"
outputs:
- extract_from: tag
conditions:
- type: vrl
source: |
assert_eq!(.host_token, "db1_syd1_example_net")
- name: missing_host_defaults_to_unknown
inputs:
- insert_at: tag
type: log
log_fields:
message: "no host field"
outputs:
- extract_from: tag
conditions:
- type: vrl
source: |
assert_eq!(.host_token, "unknown")
-50
View File
@@ -1,50 +0,0 @@
---
# Vector VM-INGEST tier — the VM front door. Thin: accepts NDJSON over HTTPS
# (behind the logs-ingest Gateway) from puppet-managed VMs, attaches only a
# routing token, and publishes into JetStream (subject logs.vm.<host>). No
# parsing here — shaping happens in the transform tier after JetStream, so VM
# logs get the same durability/replay/fan-out as k8s logs.
data_dir: /vector-data-dir
api:
enabled: true
address: 0.0.0.0:8686
sources:
vm_http:
type: http_server
address: 0.0.0.0:8080
path: /
method: POST
decoding:
codec: json
framing:
method: newline_delimited
transforms:
# Routing metadata only: derive a NATS-subject-safe host token.
tag:
type: remap
inputs:
- vm_http
source: |
host = to_string(.host || .hostname || "unknown") ?? "unknown"
.host_token = replace(host, r'[^a-zA-Z0-9_-]', "_")
sinks:
to_jetstream:
type: nats
inputs:
- tag
url: nats://nats.logging.svc.cluster.local:4222
connection_name: vector-vm-ingest
subject: "logs.vm.{{ host_token }}"
jetstream:
enabled: true
auth:
strategy: user_password
user_password:
user: log-producer
password: ${NATS_PRODUCER_PASSWORD}
encoding:
codec: json
+26
View File
@@ -0,0 +1,26 @@
---
apiVersion: operator.victoriametrics.com/v1
kind: VLAgent
metadata:
name: logs
namespace: logging
spec:
componentVersion: v1.52.0
k8sCollector:
enabled: true
remoteWrite:
# vlagent appends ?version=v1 itself; the path must be the native endpoint
- url: http://vlinsert-logs.logging.svc.cluster.local:9481/insert/native
remoteWriteSettings:
# collector mode buffers to the node's /var/lib/vlagent-data, so cap it:
# a vlinsert outage must not fill the host disk (500MB chunks)
maxDiskUsagePerURL: 2GiB
tolerations:
- operator: Exists
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: "1"
memory: 1Gi
@@ -1,16 +0,0 @@
---
# Confines the agent-observability service account (in vm-system) to the
# agent-observability ClusterRole within this namespace.
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: agent-observability
namespace: observability
subjects:
- kind: ServiceAccount
name: agent-observability
namespace: vm-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: agent-observability
@@ -13,4 +13,3 @@ resources:
- httproute.yaml
- vmpodscrape-cnpg.yaml
- vmpodscrape-traefik.yaml
- agent-observability-rolebinding.yaml
+3 -1
View File
@@ -12,6 +12,8 @@ spec:
selectAllByDefault: true
extraArgs:
loggerFormat: json
# nginx in front of Consul cuts blocking queries at 90s; default is 525s.
promscrape.consul.waitTime: 50s
remoteWrite:
- url: http://vminsert-main.observability.svc.cluster.local:8480/insert/0/prometheus/
resources:
@@ -35,6 +37,7 @@ spec:
static_configs:
- targets: ["localhost:8429"]
- job_name: "kubernetes-apiservers"
max_scrape_size: 64MiB
kubernetes_sd_configs:
- role: endpoints
scheme: https
@@ -118,7 +121,6 @@ spec:
- source_labels: [__meta_consul_node, __meta_consul_service_port]
separator: ":"
target_label: __address__
replacement: "${1}:${2}"
action: replace
- source_labels: [__meta_consul_tag_metrics_scheme]
target_label: __scheme__
@@ -32,11 +32,11 @@ spec:
imagePullPolicy: IfNotPresent
resources:
limits:
cpu: 2
memory: 3072Mi
cpu: 4
memory: 4096Mi
requests:
cpu: 200m
memory: 1024Mi
cpu: 1
memory: 2048Mi
ports:
- containerPort: 8140
name: puppetserver
@@ -24,13 +24,13 @@ spec:
type: Resource
behavior:
scaleUp:
stabilizationWindowSeconds: 60
stabilizationWindowSeconds: 30
policies:
- type: Percent
value: 50
value: 75
periodSeconds: 15
scaleDown:
stabilizationWindowSeconds: 300
stabilizationWindowSeconds: 600
policies:
- type: Percent
value: 25
+38
View File
@@ -0,0 +1,38 @@
---
# External (DMZ) front for the VictoriaLogs UI on vlogs.unkin.net via the
# external Traefik (LB VIP 198.18.199.0). TLS terminates with the real Let's
# Encrypt *.unkin.net wildcard (Certificate wildcard-unkin-net in cert-manager,
# reflected into this namespace as wildcard-unkin-net-tls by the emberstack
# reflector), so there is no cert-manager annotation here. The apex
# vlogs.unkin.net A record lives in the bind-operator unkin.net zone, NOT
# external-dns, so no external-dns annotation either. oauth2-proxy fronts it.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
labels:
traefik.io/instance: external
name: vlogs-external
namespace: vlogs
spec:
gatewayClassName: traefik-external
listeners:
- allowedRoutes:
namespaces:
from: Same
hostname: vlogs.unkin.net
name: http
port: 80
protocol: HTTP
- allowedRoutes:
namespaces:
from: Same
hostname: vlogs.unkin.net
name: https
port: 443
protocol: HTTPS
tls:
certificateRefs:
- group: ""
kind: Secret
name: wildcard-unkin-net-tls
mode: Terminate
@@ -2,15 +2,15 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: logviewer-http-redirect
namespace: logging
name: vlogs-http-redirect
namespace: vlogs
spec:
hostnames:
- logviewer.unkin.net
- vlogs.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: logviewer
name: vlogs-external
sectionName: http
rules:
- filters:
@@ -26,21 +26,33 @@ spec:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: logviewer
namespace: logging
name: vlogs
namespace: vlogs
spec:
hostnames:
- logviewer.unkin.net
- vlogs.unkin.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: logviewer
name: vlogs-external
sectionName: https
rules:
# Exact / outranks the PathPrefix catch-all; target needs the trailing slash
- filters:
- type: RequestRedirect
requestRedirect:
path:
type: ReplaceFullPath
replaceFullPath: /select/vmui/
statusCode: 302
matches:
- path:
type: Exact
value: /
- backendRefs:
- group: ""
kind: Service
name: logviewer-oauth2
name: vlogs-oauth2
port: 80
weight: 1
matches:
@@ -3,8 +3,9 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- vaultauth.yaml
- vaultstaticsecret.yaml
- deployment.yaml
- oauth2-proxy-configmap.yaml
- oauth2-proxy-deployment.yaml
- service.yaml
+5
View File
@@ -0,0 +1,5 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: vlogs
@@ -1,31 +1,27 @@
---
# Non-secret oauth2-proxy configuration (client_id/secret/cookie_secret come
# from the logviewer-oauth-credentials Secret). Single auth front for the
# logviewer UI: everything requires an Authentik session in akP-logviewer-admin.
apiVersion: v1
kind: ConfigMap
metadata:
name: logviewer-oauth2-env
namespace: logging
name: vlogs-oauth2-env
namespace: vlogs
data:
OAUTH2_PROXY_HTTP_ADDRESS: "0.0.0.0:4180"
OAUTH2_PROXY_PROVIDER: "oidc"
# Authentik logviewer app discovery issuer (terraform-authentik PR #21).
OAUTH2_PROXY_OIDC_ISSUER_URL: "https://identity.unkin.net/application/o/logviewer/"
OAUTH2_PROXY_REDIRECT_URL: "https://logviewer.unkin.net/oauth2/callback"
OAUTH2_PROXY_UPSTREAMS: "http://logviewer.logging.svc.cluster.local:8080/"
OAUTH2_PROXY_OIDC_ISSUER_URL: "https://identity.unkin.net/application/o/vlogs/"
OAUTH2_PROXY_REDIRECT_URL: "https://vlogs.unkin.net/oauth2/callback"
OAUTH2_PROXY_UPSTREAMS: "http://vlselect-logs.logging.svc.cluster.local:9471/"
OAUTH2_PROXY_SCOPE: "openid email profile ak_groups"
# Populate session.Groups from the Authentik ak_groups claim.
OAUTH2_PROXY_OIDC_GROUPS_CLAIM: "ak_groups"
OAUTH2_PROXY_ALLOWED_GROUPS: "akP-logviewer-admin"
OAUTH2_PROXY_ALLOWED_GROUPS: "akP-vlogs-admin"
OAUTH2_PROXY_PASS_USER_HEADERS: "true"
OAUTH2_PROXY_EMAIL_DOMAINS: "*"
# Authentik hardcodes email_verified=false in the id_token; authorization is
# enforced via ak_groups, so accepting the unverified email is safe.
OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL: "true"
OAUTH2_PROXY_COOKIE_SECURE: "true"
OAUTH2_PROXY_COOKIE_DOMAINS: "logviewer.unkin.net"
OAUTH2_PROXY_WHITELIST_DOMAINS: "logviewer.unkin.net"
OAUTH2_PROXY_COOKIE_DOMAINS: "vlogs.unkin.net"
OAUTH2_PROXY_WHITELIST_DOMAINS: "vlogs.unkin.net"
OAUTH2_PROXY_REVERSE_PROXY: "true"
OAUTH2_PROXY_PROVIDER_CA_FILES: "/etc/ssl/combined/ca-certificates.crt"
OAUTH2_PROXY_CODE_CHALLENGE_METHOD: "S256"
@@ -2,16 +2,16 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: logviewer-oauth2
namespace: logging
name: vlogs-oauth2
namespace: vlogs
annotations:
configmap.reloader.stakater.com/auto: "true"
secret.reloader.stakater.com/reload: "logviewer-oauth-credentials,vault-ca-cert"
secret.reloader.stakater.com/reload: "vlogs-oauth-credentials,vault-ca-cert"
spec:
replicas: 1
selector:
matchLabels:
app: logviewer-oauth2
app: vlogs-oauth2
strategy:
rollingUpdate:
maxUnavailable: 1
@@ -19,7 +19,7 @@ spec:
template:
metadata:
labels:
app: logviewer-oauth2
app: vlogs-oauth2
spec:
serviceAccountName: default
automountServiceAccountToken: false
@@ -69,23 +69,23 @@ spec:
protocol: TCP
envFrom:
- configMapRef:
name: logviewer-oauth2-env
name: vlogs-oauth2-env
optional: false
env:
- name: OAUTH2_PROXY_CLIENT_ID
valueFrom:
secretKeyRef:
name: logviewer-oauth-credentials
name: vlogs-oauth-credentials
key: client_id
- name: OAUTH2_PROXY_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: logviewer-oauth-credentials
name: vlogs-oauth-credentials
key: client_secret
- name: OAUTH2_PROXY_COOKIE_SECRET
valueFrom:
secretKeyRef:
name: logviewer-oauth-credentials
name: vlogs-oauth-credentials
key: cookie_secret
volumeMounts:
- name: combined-certs
+19
View File
@@ -0,0 +1,19 @@
---
# Front-door entry Service: the HTTPRoute for vlogs.unkin.net targets this, so
# all traffic enters via oauth2-proxy.
apiVersion: v1
kind: Service
metadata:
name: vlogs-oauth2
namespace: vlogs
spec:
internalTrafficPolicy: Cluster
ports:
- name: http
port: 80
protocol: TCP
targetPort: http
selector:
app: vlogs-oauth2
sessionAffinity: None
type: ClusterIP
+18
View File
@@ -0,0 +1,18 @@
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: default
namespace: vlogs
spec:
allowedNamespaces:
- vlogs
kubernetes:
audiences:
- vault
role: default
serviceAccount: default
tokenExpirationSeconds: 600
method: kubernetes
mount: k8s/au/syd1
vaultConnectionRef: vso-system/default
+17
View File
@@ -0,0 +1,17 @@
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: vlogs-oauth-credentials
namespace: vlogs
spec:
destination:
create: true
name: vlogs-oauth-credentials
overwrite: true
hmacSecretData: true
mount: kv
path: kubernetes/namespace/vlogs/default/oauth-credentials
refreshAfter: 5m
type: kv-v2
vaultAuthRef: default
@@ -1,50 +0,0 @@
---
# Static service account that Vault's kubernetes secret engine mints scoped
# tokens for (agent-observability role). RBAC is confined to the metrics and
# logging namespaces via the per-namespace RoleBindings, not a
# ClusterRoleBinding. Workloads and VictoriaMetrics CRs are deliberately
# patch/update only: deleting a VMCluster or VLCluster destroys data.
apiVersion: v1
kind: ServiceAccount
metadata:
name: agent-observability
namespace: vm-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: agent-observability
rules:
- apiGroups: ["operator.victoriametrics.com"]
resources: ["*"]
verbs: ["get", "list", "watch", "create", "patch", "update"]
- apiGroups: ["apps"]
resources: ["deployments", "statefulsets", "daemonsets"]
verbs: ["get", "list", "watch", "patch", "update"]
# delete permits a rolling restart without granting workload deletion.
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch", "delete"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get"]
- apiGroups: [""]
resources: ["services", "configmaps", "endpoints", "events"]
verbs: ["get", "list", "watch"]
- apiGroups: ["gateway.networking.k8s.io"]
resources: ["gateways", "httproutes"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: agent-observability
namespace: vm-system
subjects:
- kind: ServiceAccount
name: agent-observability
namespace: vm-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: agent-observability
-1
View File
@@ -4,4 +4,3 @@ kind: Kustomization
resources:
- namespace.yaml
- agent-observability-rbac.yaml
@@ -1,39 +0,0 @@
---
apiVersion: v1
kind: ConfigMap
metadata:
name: gocache-nginx
namespace: woodpecker
labels:
app.kubernetes.io/name: gocache
app.kubernetes.io/component: proxy
data:
nginx.conf: |
worker_processes auto;
error_log /dev/stderr warn;
pid /tmp/nginx.pid;
events {
worker_connections 512;
}
# GOCACHEPROG is a raw byte stream, not HTTP, so this must be stream{} not http{}.
stream {
server {
listen 9090;
# The protocol has no authentication: anyone who can reach this port can
# write cache entries, which become code in every build that reads them.
# Loopback is the kubectl port-forward fallback; in a pod netns it is
# only these two containers.
allow 127.0.0.1/32;
allow 10.10.12.200/32;
allow 10.42.0.0/16;
deny all;
# A connect session lasts the whole build; the 10m default cuts long builds off.
proxy_timeout 2h;
proxy_connect_timeout 5s;
proxy_pass 127.0.0.1:9080;
}
}
@@ -1,131 +0,0 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: gocache
namespace: woodpecker
annotations:
configmap.reloader.stakater.com/reload: "gocache-nginx"
secret.reloader.stakater.com/reload: "gocache-s3,vault-ca-cert"
labels:
app.kubernetes.io/name: gocache
app.kubernetes.io/component: cache
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: gocache
template:
metadata:
labels:
app.kubernetes.io/name: gocache
app.kubernetes.io/component: cache
spec:
serviceAccountName: default
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: go-cache-plugin
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/go-cache-plugin:v0.1.0
imagePullPolicy: IfNotPresent
# Root flags must precede the subcommand; only --plugin belongs to serve.
args:
- --cache-dir=/var/cache/gocache
- --bucket=gocache
# Explicit region skips the GetBucketLocation probe, which RGW handles poorly.
- --region=us-east-1
- --s3-endpoint-url=https://s3.ceph.unkin.net
- --s3-path-style
- serve
- --plugin=9080
env:
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: gocache-s3
key: AWS_ACCESS_KEY_ID
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: gocache-s3
key: AWS_SECRET_ACCESS_KEY
# s3.ceph.unkin.net is served by the estate CA, not a public root.
- name: AWS_CA_BUNDLE
value: /etc/ssl/vault-ca/ca.crt
volumeMounts:
- name: cache
mountPath: /var/cache/gocache
- name: vault-ca
mountPath: /etc/ssl/vault-ca
readOnly: true
securityContext:
runAsUser: 65532
runAsGroup: 65532
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: "2"
memory: 2Gi
- name: nginx
image: docker.io/nginx:1.29.8-alpine
imagePullPolicy: IfNotPresent
# Bypass the image entrypoint: its config scripts write to a read-only rootfs.
command:
- nginx
- -g
- daemon off;
ports:
- containerPort: 9090
name: gocache
protocol: TCP
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
readOnly: true
- name: tmp
mountPath: /tmp
securityContext:
runAsUser: 101
runAsGroup: 101
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 25m
memory: 32Mi
limits:
cpu: 500m
memory: 128Mi
volumes:
# Staging cache in front of S3: losing it costs a repopulate, not data.
- name: cache
emptyDir:
sizeLimit: 20Gi
- name: nginx-config
configMap:
name: gocache-nginx
- name: tmp
emptyDir: {}
- name: vault-ca
secret:
secretName: vault-ca-cert
items:
- key: ca.crt
path: ca.crt
+1 -4
View File
@@ -8,9 +8,6 @@ resources:
- cnpg_backup.yaml
- cnpg_pooler.yaml
- gocache_bucket.yaml
- configmap_gocache-nginx.yaml
- deployment_gocache.yaml
- service_gocache.yaml
- serviceaccount_arrproxy_ci.yaml
- serviceaccount_autobackup_operator_ci.yaml
- serviceaccount_ghp.yaml
@@ -25,7 +22,7 @@ resources:
- serviceaccount_terraform_authentik.yaml
- serviceaccount_terraform_enc.yaml
- serviceaccount_terraform_git.yaml
- serviceaccount_terraform_infra.yaml
- serviceaccount_terraform_netbox.yaml
- serviceaccount_terraform_prowlarr.yaml
- serviceaccount_terraform_rancher.yaml
- serviceaccount_terraform_radarr.yaml

Some files were not shown because too many files have changed in this diff Show More