feat(observability-proxy): add the chart and image build

Tenants read their logs, and later their metrics, through an
authorizing proxy that sits in front of the VictoriaLogs and
VictoriaMetrics read paths, so the proxy needs a deployable package
before the console or tenant Grafana can reach it.

The proxy reviews every token and access decision against the API
server, so its own role is limited to creating TokenReviews and
SubjectAccessReviews and reading ApplicationDefinitions and namespaces.
The namespace logs root is authorized as list on cozystack.io logs,
which no API server serves; the rule is aggregated into the tenant view
role so that a namespace viewer passes that review, as upstream grants
pods/log at view.

The image ref lands without a digest until the first build stamps it,
and the package joins the root build list so every build path refreshes
it.

Assisted-by: LLM
Signed-off-by: ZverGuy <maximbel2003@gmail.com>
This commit is contained in:
ZverGuy committed 2026-11-09 15:48:57 +07:00
1 parent ca7f283b6a
commit 7ec648d863
13 files changed
+408

No files matched your search

+1
View File
@@ -17,6 +17,7 @@ build: build-deps
make -C packages/system/cozystack-api image
make -C packages/system/cozystack-controller image
make -C packages/system/backup-controller image
make -C packages/system/observability-proxy image
make -C packages/system/backupstrategy-controller image
make -C packages/system/lineage-controller-webhook image
make -C packages/system/migration-controller image
@@ -0,0 +1,3 @@
apiVersion: v2
name: cozy-observability-proxy
version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process
@@ -0,0 +1,20 @@
NAME=observability-proxy
NAMESPACE=cozy-observability-proxy
include ../../../hack/common-envs.mk
include ../../../hack/package.mk
test:
helm unittest .
image: image-observability-proxy
image-observability-proxy:
docker buildx build -f images/observability-proxy/Dockerfile ../../.. \
$(call image-tags,observability-proxy,v$(COZYSTACK_VERSION)) \
$(call cache-args,observability-proxy) \
--metadata-file images/observability-proxy.json \
$(BUILDX_ARGS)
IMAGE="$(REGISTRY)/observability-proxy:$(IMAGE_TAG)@$$(yq e '."containerimage.digest"' images/observability-proxy.json -o json -r)" \
yq -i '.observabilityProxy.image = strenv(IMAGE)' values.yaml
rm -f images/observability-proxy.json
@@ -0,0 +1,25 @@
FROM --platform=$BUILDPLATFORM golang:1.27-alpine@sha256:8a5910f31396cd4d89662f56c68b3ae31d374308270a1c3bd96672ee5ed43414 AS builder
ARG GOPROXY
ARG TARGETOS
ARG TARGETARCH
WORKDIR /workspace
COPY go.mod go.sum ./
RUN for attempt in 1 2 3; do GOOS=$TARGETOS GOARCH=$TARGETARCH go mod download && break; [ "$attempt" = 3 ] && exit 1; sleep $((attempt * 10)); done
COPY api api/
COPY pkg pkg/
COPY cmd cmd/
COPY internal internal/
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags="-extldflags=-static" -o /observability-proxy ./cmd/observability-proxy
FROM scratch
COPY --from=builder /observability-proxy /observability-proxy
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
USER 65532:65532
ENTRYPOINT ["/observability-proxy"]
@@ -0,0 +1,68 @@
{{- $clusterDomain := index (.Values._cluster | default dict) "cluster-domain" | default "cozy.local" }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: observability-proxy
labels:
app.kubernetes.io/name: observability-proxy
spec:
replicas: {{ .Values.observabilityProxy.replicas }}
selector:
matchLabels:
app.kubernetes.io/name: observability-proxy
template:
metadata:
labels:
app.kubernetes.io/name: observability-proxy
spec:
serviceAccountName: observability-proxy
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app.kubernetes.io/name: observability-proxy
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: observability-proxy
image: {{ .Values.observabilityProxy.image | quote }}
args:
- --listen-address=:8080
- --probe-address=:8081
- --cluster-domain={{ $clusterDomain }}
- --root-tenant={{ .Values.observabilityProxy.rootTenant }}
- --auth-cache-ttl={{ .Values.observabilityProxy.authCacheTTL }}
ports:
- name: http
containerPort: 8080
protocol: TCP
- name: probes
containerPort: 8081
protocol: TCP
readinessProbe:
httpGet:
path: /readyz
port: probes
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: probes
initialDelaySeconds: 15
periodSeconds: 20
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
{{- with .Values.observabilityProxy.resources }}
resources: {{- toYaml . | nindent 10 }}
{{- end }}
@@ -0,0 +1,33 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cozy:observability-proxy
rules:
# Every request is authenticated and authorized against the API server.
- apiGroups: ["authentication.k8s.io"]
resources: ["tokenreviews"]
verbs: ["create"]
- apiGroups: ["authorization.k8s.io"]
resources: ["subjectaccessreviews"]
verbs: ["create"]
# Maps an application plural in the path to its Kind.
- apiGroups: ["cozystack.io"]
resources: ["applicationdefinitions"]
verbs: ["get", "list", "watch"]
# The namespace.cozystack.io/monitoring label names the logs store.
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: cozy:observability-proxy
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cozy:observability-proxy
subjects:
- kind: ServiceAccount
name: observability-proxy
namespace: {{ .Release.Namespace }}
@@ -0,0 +1,4 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: observability-proxy
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: observability-proxy
labels:
app.kubernetes.io/name: observability-proxy
spec:
type: ClusterIP
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
selector:
app.kubernetes.io/name: observability-proxy
@@ -0,0 +1,14 @@
# Aggregated into cozy-tenant-view (and consequently use, admin, super-admin).
# No API server serves cozystack.io logs: the rule exists only for the
# SubjectAccessReview the observability proxy asks before it answers a read
# under /apis/cozystack.io/v1alpha1/namespaces/<ns>/logs/.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cozy:observability:view
labels:
rbac.cozystack.io/aggregate-to-tenant-view: "true"
rules:
- apiGroups: ["cozystack.io"]
resources: ["logs"]
verbs: ["list"]
@@ -0,0 +1,109 @@
suite: observability-proxy Deployment
# The flags, ports and pod label below are the contract the proxy binary, the
# Service, the console nginx and the tenant egress policy are written against.
# A rename on either side breaks routing without failing a render.
templates:
- templates/deployment.yaml
release:
name: observability-proxy
namespace: cozy-observability-proxy
tests:
- it: labels the pods with the name the tenant egress policy selects on
asserts:
- isKind:
of: Deployment
- equal:
path: spec.selector.matchLabels
value:
app.kubernetes.io/name: observability-proxy
- equal:
path: spec.template.metadata.labels["app.kubernetes.io/name"]
value: observability-proxy
- equal:
path: spec.template.spec.serviceAccountName
value: observability-proxy
- it: passes the listen, probe, root tenant and auth cache flags
asserts:
- contains:
path: spec.template.spec.containers[0].args
content: --listen-address=:8080
- contains:
path: spec.template.spec.containers[0].args
content: --probe-address=:8081
- contains:
path: spec.template.spec.containers[0].args
content: --root-tenant=tenant-root
- contains:
path: spec.template.spec.containers[0].args
content: --auth-cache-ttl=10s
- it: falls back to cozy.local when the platform passes no cluster domain
asserts:
- contains:
path: spec.template.spec.containers[0].args
content: --cluster-domain=cozy.local
- it: passes the platform cluster domain
set:
_cluster:
cluster-domain: cluster.local
asserts:
- contains:
path: spec.template.spec.containers[0].args
content: --cluster-domain=cluster.local
- notContains:
path: spec.template.spec.containers[0].args
content: --cluster-domain=cozy.local
- it: exposes the API and probe ports
asserts:
- equal:
path: spec.template.spec.containers[0].ports
value:
- name: http
containerPort: 8080
protocol: TCP
- name: probes
containerPort: 8081
protocol: TCP
- it: probes /healthz and /readyz on the probe port
asserts:
- equal:
path: spec.template.spec.containers[0].livenessProbe.httpGet
value:
path: /healthz
port: probes
- equal:
path: spec.template.spec.containers[0].readinessProbe.httpGet
value:
path: /readyz
port: probes
- it: runs from the image in values
set:
observabilityProxy.image: registry.example/observability-proxy:dev@sha256:0000
asserts:
- equal:
path: spec.template.spec.containers[0].image
value: registry.example/observability-proxy:dev@sha256:0000
- it: satisfies the restricted pod security profile
asserts:
- equal:
path: spec.template.spec.securityContext
value:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
- equal:
path: spec.template.spec.containers[0].securityContext
value:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
@@ -0,0 +1,70 @@
suite: observability-proxy RBAC
# The proxy authenticates and authorizes every request through the API server,
# so a dropped verb here turns every request into an error at runtime rather
# than a render failure. The tenant role is what lets a namespace viewer pass
# the SubjectAccessReview the proxy asks for the namespace logs root.
templates:
- templates/rbac.yaml
- templates/tenant-clusterroles.yaml
release:
name: observability-proxy
namespace: cozy-observability-proxy
tests:
- it: lets the proxy review tokens and access, and read ApplicationDefinitions and namespaces
template: templates/rbac.yaml
documentSelector:
path: kind
value: ClusterRole
asserts:
- equal:
path: rules
value:
- apiGroups: ["authentication.k8s.io"]
resources: ["tokenreviews"]
verbs: ["create"]
- apiGroups: ["authorization.k8s.io"]
resources: ["subjectaccessreviews"]
verbs: ["create"]
- apiGroups: ["cozystack.io"]
resources: ["applicationdefinitions"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get", "list", "watch"]
- it: binds the proxy role to the proxy ServiceAccount in the release namespace
template: templates/rbac.yaml
documentSelector:
path: kind
value: ClusterRoleBinding
asserts:
- equal:
path: roleRef
value:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cozy:observability-proxy
- equal:
path: subjects
value:
- kind: ServiceAccount
name: observability-proxy
namespace: cozy-observability-proxy
- it: aggregates list on cozystack.io logs into the tenant view role
template: templates/tenant-clusterroles.yaml
asserts:
- isKind:
of: ClusterRole
- equal:
path: metadata.labels
value:
rbac.cozystack.io/aggregate-to-tenant-view: "true"
- equal:
path: rules
value:
- apiGroups: ["cozystack.io"]
resources: ["logs"]
verbs: ["list"]
@@ -0,0 +1,30 @@
suite: observability-proxy Service
# The console nginx proxies to observability-proxy.<ns>.svc on port 80 and the
# tenant egress policy allows the pods' 8080, so the name, the port and the
# selector are pinned here.
templates:
- templates/service.yaml
release:
name: observability-proxy
namespace: cozy-observability-proxy
tests:
- it: serves port 80 named http on the pods' http port
asserts:
- isKind:
of: Service
- equal:
path: metadata.name
value: observability-proxy
- equal:
path: spec.ports
value:
- name: http
port: 80
targetPort: http
protocol: TCP
- equal:
path: spec.selector
value:
app.kubernetes.io/name: observability-proxy
@@ -0,0 +1,16 @@
# Injected by the platform from the cozystack-values Secret. The empty
# default keeps the chart renderable on its own, outside a cluster.
_cluster: {}
observabilityProxy:
image: "ghcr.io/cozystack/cozystack/observability-proxy:v0.0.0"
replicas: 2
rootTenant: tenant-root
authCacheTTL: 10s
resources:
requests:
cpu: 10m
memory: 64Mi
limits:
cpu: 500m
memory: 256Mi