From ee2adc20c46b496669b16c7d91fd02d87d6f748b Mon Sep 17 00:00:00 2001 From: TheMeinerLP Date: Mon, 24 Aug 2026 22:39:24 +0200 Subject: [PATCH] feat(helm): publish a chart alongside the image The backend is deployed from a generic `micronaut` chart that lives in the cluster repository and is shared with otis, so nothing here describes how this service runs. Every deployment therefore waits on someone editing an image tag by hand in that repository -- which is how dev ended up on 2.3.0 while prod ran 2.4.1. A chart of our own, versioned from the same release as the image, removes that step: image.tag is left empty and falls back to .Chart.AppVersion, so pinning the chart pins the image and an environment that tracks the newest chart release tracks the image with it. The chart is a fork of helm/micronaut rather than a rewrite -- the template helpers keep their `micronaut.*` names and the files are otherwise unchanged, so a later fix over there carries across with `diff -r`. Only what identifies this application is different: image repository, the 8080 the Micronaut server binds, and the Prometheus endpoint it always exposes. Packaged and pushed to the same Harbor project as the image, under charts/, chained into the release run for the same reason the docker job is: a GITHUB_TOKEN tag starts no `on: push: tags` workflow. --- .github/workflows/release-please.yml | 53 ++++ charts/vulpes-backend/Chart.yaml | 24 ++ charts/vulpes-backend/templates/NOTES.txt | 22 ++ charts/vulpes-backend/templates/_helpers.tpl | 73 +++++ .../vulpes-backend/templates/configmap.yaml | 13 + .../vulpes-backend/templates/deployment.yaml | 170 +++++++++++ charts/vulpes-backend/templates/hpa.yaml | 32 ++ .../vulpes-backend/templates/httproute.yaml | 38 +++ charts/vulpes-backend/templates/ingress.yaml | 43 +++ charts/vulpes-backend/templates/pdb.yaml | 17 ++ charts/vulpes-backend/templates/rbac.yaml | 33 +++ charts/vulpes-backend/templates/secret.yaml | 14 + charts/vulpes-backend/templates/service.yaml | 15 + .../templates/serviceaccount.yaml | 13 + .../templates/servicemonitor.yaml | 43 +++ .../templates/tests/test-connection.yaml | 15 + charts/vulpes-backend/values.yaml | 276 ++++++++++++++++++ release-please-config.json | 4 + 18 files changed, 898 insertions(+) create mode 100644 charts/vulpes-backend/Chart.yaml create mode 100644 charts/vulpes-backend/templates/NOTES.txt create mode 100644 charts/vulpes-backend/templates/_helpers.tpl create mode 100644 charts/vulpes-backend/templates/configmap.yaml create mode 100644 charts/vulpes-backend/templates/deployment.yaml create mode 100644 charts/vulpes-backend/templates/hpa.yaml create mode 100644 charts/vulpes-backend/templates/httproute.yaml create mode 100644 charts/vulpes-backend/templates/ingress.yaml create mode 100644 charts/vulpes-backend/templates/pdb.yaml create mode 100644 charts/vulpes-backend/templates/rbac.yaml create mode 100644 charts/vulpes-backend/templates/secret.yaml create mode 100644 charts/vulpes-backend/templates/service.yaml create mode 100644 charts/vulpes-backend/templates/serviceaccount.yaml create mode 100644 charts/vulpes-backend/templates/servicemonitor.yaml create mode 100644 charts/vulpes-backend/templates/tests/test-connection.yaml create mode 100644 charts/vulpes-backend/values.yaml diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index cb9a4ef..e692024 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -76,6 +76,59 @@ jobs: req-concurrent: "4" secrets: inherit + # Chained into this run rather than triggered by the tag: release-please tags + # with GITHUB_TOKEN, and a tag pushed that way starts no `on: push: tags` + # workflow. The release is also the only place that already knows the + # version, so nothing has to derive one. + # + # Charts are published on a release only. A branch build is deployed by + # pointing an existing release of the chart at the branch's image tag, which + # needs no chart of its own. + helm: + name: Publish the Helm chart + needs: release-please + if: needs.release-please.outputs.release_created == 'true' + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + ref: ${{ needs.release-please.outputs.tag_name }} + + - name: Set up Helm + uses: azure/setup-helm@v5.0.1 + + # Both versions come from the release rather than from Chart.yaml. + # Release Please keeps that file current anyway, but stating them here + # makes a mismatch impossible instead of something to check for: the + # chart cannot be packaged pointing at an image tag nobody built. + - name: Package + run: helm package charts/vulpes-backend --destination dist --version "${{ needs.release-please.outputs.version }}" --app-version "${{ needs.release-please.outputs.version }}" + + - name: Log in to OneLiteFeather Harbor + env: + REGISTRY: ${{ secrets.HARBOR_REGISTRY }} + USERNAME: ${{ secrets.HARBOR_USERNAME }} + PASSWORD: ${{ secrets.HARBOR_PASSWORD }} + run: printf '%s' "$PASSWORD" | helm registry login "$REGISTRY" --username "$USERNAME" --password-stdin + + # Into the same Harbor project as the image, under charts/ - the layout + # apus and stelaris-ui already use, so a Flux OCIRepository pointing here + # looks like every other one in the cluster repository. + - name: Push + env: + REGISTRY: ${{ secrets.HARBOR_REGISTRY }} + VERSION: ${{ needs.release-please.outputs.version }} + run: helm push "dist/vulpes-backend-${VERSION}.tgz" "oci://${REGISTRY}/onelitefeather/charts" + + - name: Log out + if: always() + env: + REGISTRY: ${{ secrets.HARBOR_REGISTRY }} + run: helm registry logout "$REGISTRY" || true + sbom: name: Attach SBOM to release needs: release-please diff --git a/charts/vulpes-backend/Chart.yaml b/charts/vulpes-backend/Chart.yaml new file mode 100644 index 0000000..0d8ad12 --- /dev/null +++ b/charts/vulpes-backend/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: vulpes-backend +description: The Vulpes backend, a Micronaut service deployed to Kubernetes +type: application + +# Both lines are kept current by Release Please, which is why the chart version +# and the app version move together: the chart only ever describes the image +# built from the same commit, so a separate chart version would be a second +# number to reason about with nothing extra to say. The cluster repository's +# OCIRepository pins the chart, and image.tag is left empty so it falls back to +# this appVersion -- pinning the chart therefore pins the image. +version: 2.5.0 # x-release-please-version +appVersion: "2.5.0" # x-release-please-version + +home: https://github.com/OneLiteFeatherNET/Vulpes-Backend +sources: + - https://github.com/OneLiteFeatherNET/Vulpes-Backend +maintainers: + - name: OneLiteFeatherNET + email: contact@onelitefeather.net + url: https://onelitefeather.net +keywords: + - vulpes + - micronaut diff --git a/charts/vulpes-backend/templates/NOTES.txt b/charts/vulpes-backend/templates/NOTES.txt new file mode 100644 index 0000000..24d9a9c --- /dev/null +++ b/charts/vulpes-backend/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "micronaut.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "micronaut.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "micronaut.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "micronaut.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/charts/vulpes-backend/templates/_helpers.tpl b/charts/vulpes-backend/templates/_helpers.tpl new file mode 100644 index 0000000..19cb6ae --- /dev/null +++ b/charts/vulpes-backend/templates/_helpers.tpl @@ -0,0 +1,73 @@ +{{/* +The helpers below are still named `micronaut.*`, not `vulpes-backend.*`, and +that is deliberate: this chart is a fork of helm/micronaut in the cluster +repository, and keeping the templates byte-identical is what lets a later fix +over there be carried across with `diff -r`. Renaming them would touch every +template file and turn that diff into noise. + +The rendered resource names are unaffected either way -- they come from +.Chart.Name/.Release.Name, and the cluster overlays pin them with +nameOverride/fullnameOverride so the Deployment's immutable selector survives +this chart move. +*/}} + +{{/* +Expand the name of the chart. +*/}} +{{- define "micronaut.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "micronaut.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- if contains .Chart.Name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Chart name and version +*/}} +{{- define "micronaut.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "micronaut.labels" -}} +app.kubernetes.io/name: {{ include "micronaut.name" . }} +helm.sh/chart: {{ include "micronaut.chart" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + + +{{/* +Create the name of the service account to use +*/}} +{{- define "micronaut.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "micronaut.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "micronaut.selectorLabels" -}} +app.kubernetes.io/name: {{ include "micronaut.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} \ No newline at end of file diff --git a/charts/vulpes-backend/templates/configmap.yaml b/charts/vulpes-backend/templates/configmap.yaml new file mode 100644 index 0000000..bb8dc36 --- /dev/null +++ b/charts/vulpes-backend/templates/configmap.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "micronaut.fullname" . }}-config + labels: + {{- include "micronaut.labels" . | nindent 4 }} +data: + application.yml: |- +{{- nindent 4 .Values.config.base }} +{{- range $p, $content := .Values.config.profiles }} + application-{{ $p }}.yml: |- +{{- nindent 4 $content }} +{{- end }} diff --git a/charts/vulpes-backend/templates/deployment.yaml b/charts/vulpes-backend/templates/deployment.yaml new file mode 100644 index 0000000..fe9d040 --- /dev/null +++ b/charts/vulpes-backend/templates/deployment.yaml @@ -0,0 +1,170 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "micronaut.fullname" . }} + labels: + {{- include "micronaut.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + {{- with .Values.strategy }} + strategy: + {{- toYaml . | nindent 4 }} + {{- end }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "micronaut.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "micronaut.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + {{- with .Values.podLabels }} +{{ toYaml . | indent 8 }} + {{- end }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- if and .Values.secrets.enabled .Values.secrets.files }} + checksum/secrets: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + {{- end }} + {{- with .Values.podAnnotations }} +{{ toYaml . | indent 8 }} + {{- end }} + spec: + {{- if .Values.imagePullSecrets }} + imagePullSecrets: +{{ toYaml .Values.imagePullSecrets | indent 8 }} + {{- end }} + serviceAccountName: {{- if .Values.serviceAccount.create }} {{ include "micronaut.fullname" . }} {{- else }} {{ default "default" .Values.serviceAccount.name }} {{- end }} + {{- if .Values.securityContext.enabled }} + securityContext: + {{- omit .Values.securityContext "enabled" | toYaml | nindent 8 }} + {{- end }} + {{- with .Values.priorityClassName }} + priorityClassName: {{ . }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- if .Values.tracing.enabled }} + initContainers: + - name: otel-agent + image: {{ .Values.tracing.image }} + command: ["cp", "/javaagent.jar", "/otel/javaagent.jar"] + {{- if .Values.containerSecurityContext.enabled }} + securityContext: + {{- omit .Values.containerSecurityContext "enabled" | toYaml | nindent 12 }} + {{- end }} + resources: + {{- toYaml .Values.tracing.resources | nindent 12 }} + volumeMounts: + - name: otel-agent + mountPath: /otel + {{- end }} + containers: + - name: app + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- if .Values.containerSecurityContext.enabled }} + securityContext: + {{- omit .Values.containerSecurityContext "enabled" | toYaml | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + env: + - name: MICRONAUT_ENVIRONMENTS + value: "{{ join "," .Values.profiles }}" + - name: MICRONAUT_CONFIG_FILES + value: "/config/application.yml{{- range $p := .Values.profiles }},/config/application-{{$p}}.yml{{- end }}{{- if and .Values.secrets.enabled .Values.secrets.files }}{{- range $p := .Values.profiles }},/secrets/application-{{$p}}.yml{{- end }}{{- end }}" + {{- range .Values.env }} + - name: {{ .name }} + value: {{ .value | quote }} + {{- end }} + {{- if .Values.tracing.enabled }} + # JDK_JAVA_OPTIONS is independent of JAVA_TOOL_OPTIONS (which + # apps commonly override wholesale via .Values.env above), so + # the agent attaches regardless of what that list contains. + - name: JDK_JAVA_OPTIONS + value: "-javaagent:/otel/javaagent.jar" + - name: OTEL_SERVICE_NAME + value: {{ .Release.Name | quote }} + - name: OTEL_TRACES_EXPORTER + value: "otlp" + - name: OTEL_METRICS_EXPORTER + value: "none" + - name: OTEL_LOGS_EXPORTER + value: "none" + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: {{ .Values.tracing.endpoint | quote }} + - name: OTEL_EXPORTER_OTLP_PROTOCOL + value: "grpc" + {{- end }} + {{- if .Values.envFrom }} + envFrom: +{{ toYaml .Values.envFrom | indent 12 }} + {{- end }} + {{- if .Values.startupProbe.enabled }} + startupProbe: + httpGet: + path: {{ .Values.readinessProbe.path }} + port: http + failureThreshold: {{ .Values.startupProbe.failureThreshold }} + periodSeconds: {{ .Values.startupProbe.periodSeconds }} + {{- end }} + readinessProbe: + httpGet: + path: {{ .Values.readinessProbe.path }} + port: http + initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.readinessProbe.periodSeconds }} + livenessProbe: + httpGet: + path: {{ .Values.livenessProbe.path }} + port: http + initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }} + periodSeconds: {{ .Values.livenessProbe.periodSeconds }} + resources: +{{ toYaml .Values.resources | indent 12 }} + volumeMounts: + # The root filesystem is read-only; the JVM still wants a scratch + # dir (java.io.tmpdir, Netty, JIT dumps). + - name: tmp + mountPath: /tmp + - name: config + mountPath: /config + {{- if and .Values.secrets.enabled .Values.secrets.files }} + - name: secrets + mountPath: /secrets + {{- end }} + {{- if .Values.tracing.enabled }} + - name: otel-agent + mountPath: /otel + readOnly: true + {{- end }} + volumes: + - name: tmp + emptyDir: {} + - name: config + configMap: + name: {{ include "micronaut.fullname" . }}-config + {{- if and .Values.secrets.enabled .Values.secrets.files }} + - name: secrets + secret: + secretName: {{ include "micronaut.fullname" . }}-secrets + {{- end }} + {{- if .Values.tracing.enabled }} + - name: otel-agent + emptyDir: {} + {{- end }} diff --git a/charts/vulpes-backend/templates/hpa.yaml b/charts/vulpes-backend/templates/hpa.yaml new file mode 100644 index 0000000..bf264fb --- /dev/null +++ b/charts/vulpes-backend/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "micronaut.fullname" . }} + labels: + {{- include "micronaut.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "micronaut.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/charts/vulpes-backend/templates/httproute.yaml b/charts/vulpes-backend/templates/httproute.yaml new file mode 100644 index 0000000..63f1a02 --- /dev/null +++ b/charts/vulpes-backend/templates/httproute.yaml @@ -0,0 +1,38 @@ +{{- if .Values.httpRoute.enabled -}} +{{- $fullName := include "micronaut.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ $fullName }} + labels: + {{- include "micronaut.labels" . | nindent 4 }} + {{- with .Values.httpRoute.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + parentRefs: + {{- with .Values.httpRoute.parentRefs }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.httpRoute.hostnames }} + hostnames: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- range .Values.httpRoute.rules }} + {{- with .matches }} + - matches: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .filters }} + filters: + {{- toYaml . | nindent 8 }} + {{- end }} + backendRefs: + - name: {{ $fullName }} + port: {{ $svcPort }} + weight: 1 + {{- end }} +{{- end }} diff --git a/charts/vulpes-backend/templates/ingress.yaml b/charts/vulpes-backend/templates/ingress.yaml new file mode 100644 index 0000000..7967720 --- /dev/null +++ b/charts/vulpes-backend/templates/ingress.yaml @@ -0,0 +1,43 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "micronaut.fullname" . }} + labels: + {{- include "micronaut.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include "micronaut.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/vulpes-backend/templates/pdb.yaml b/charts/vulpes-backend/templates/pdb.yaml new file mode 100644 index 0000000..18e3afe --- /dev/null +++ b/charts/vulpes-backend/templates/pdb.yaml @@ -0,0 +1,17 @@ +{{- if .Values.podDisruptionBudget.enabled }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "micronaut.fullname" . }} + labels: + {{- include "micronaut.labels" . | nindent 4 }} +spec: + {{- if .Values.podDisruptionBudget.minAvailable }} + minAvailable: {{ .Values.podDisruptionBudget.minAvailable }} + {{- else }} + maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable | default 1 }} + {{- end }} + selector: + matchLabels: + {{- include "micronaut.selectorLabels" . | nindent 6 }} +{{- end }} diff --git a/charts/vulpes-backend/templates/rbac.yaml b/charts/vulpes-backend/templates/rbac.yaml new file mode 100644 index 0000000..9bbd2d2 --- /dev/null +++ b/charts/vulpes-backend/templates/rbac.yaml @@ -0,0 +1,33 @@ +{{- if .Values.rbac.create -}} +{{- /* + Namespace-scoped read access for micronaut-kubernetes. services/endpoints are + used by the discovery client; pods is read by the KubernetesHealthIndicator + (it looks up its own pod for /health). Scoped to the release namespace only, + so it grants no cross-namespace access. +*/ -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "micronaut.fullname" . }}-discovery + labels: + {{- include "micronaut.labels" . | nindent 4 }} +rules: + - apiGroups: [""] + resources: ["services", "endpoints", "pods"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "micronaut.fullname" . }}-discovery + labels: + {{- include "micronaut.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "micronaut.fullname" . }}-discovery +subjects: + - kind: ServiceAccount + name: {{ include "micronaut.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/charts/vulpes-backend/templates/secret.yaml b/charts/vulpes-backend/templates/secret.yaml new file mode 100644 index 0000000..94421f1 --- /dev/null +++ b/charts/vulpes-backend/templates/secret.yaml @@ -0,0 +1,14 @@ +{{- if and .Values.secrets.enabled .Values.secrets.files }} +apiVersion: v1 +kind: Secret +type: Opaque +metadata: + name: {{ include "micronaut.fullname" . }}-secrets + labels: + {{- include "micronaut.labels" . | nindent 4 }} +stringData: +{{- range $p, $content := .Values.secrets.files }} + application-{{ $p }}.yml: |- +{{- nindent 4 $content }} +{{- end }} +{{- end }} diff --git a/charts/vulpes-backend/templates/service.yaml b/charts/vulpes-backend/templates/service.yaml new file mode 100644 index 0000000..bc6c31a --- /dev/null +++ b/charts/vulpes-backend/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "micronaut.fullname" . }} + labels: + {{- include "micronaut.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "micronaut.selectorLabels" . | nindent 4 }} diff --git a/charts/vulpes-backend/templates/serviceaccount.yaml b/charts/vulpes-backend/templates/serviceaccount.yaml new file mode 100644 index 0000000..748e7b2 --- /dev/null +++ b/charts/vulpes-backend/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "micronaut.serviceAccountName" . }} + labels: + {{- include "micronaut.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/charts/vulpes-backend/templates/servicemonitor.yaml b/charts/vulpes-backend/templates/servicemonitor.yaml new file mode 100644 index 0000000..3cfa22d --- /dev/null +++ b/charts/vulpes-backend/templates/servicemonitor.yaml @@ -0,0 +1,43 @@ +{{- if and .Values.metrics.enabled .Values.metrics.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "micronaut.fullname" . }} + {{- with .Values.metrics.serviceMonitor.namespace }} + namespace: {{ . }} + {{- end }} + labels: + {{- include "micronaut.labels" . | nindent 4 }} + {{- with .Values.metrics.serviceMonitor.additionalLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.metrics.serviceMonitor.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + namespaceSelector: + matchNames: + - {{ .Release.Namespace }} + selector: + matchLabels: + {{- include "micronaut.selectorLabels" . | nindent 6 }} + endpoints: + - port: {{ .Values.metrics.port }} + path: {{ .Values.metrics.path }} + scheme: {{ .Values.metrics.scheme }} + {{- with .Values.metrics.serviceMonitor.interval }} + interval: {{ . }} + {{- end }} + {{- with .Values.metrics.serviceMonitor.scrapeTimeout }} + scrapeTimeout: {{ . }} + {{- end }} + {{- with .Values.metrics.serviceMonitor.relabelings }} + relabelings: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.metrics.serviceMonitor.metricRelabelings }} + metricRelabelings: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/vulpes-backend/templates/tests/test-connection.yaml b/charts/vulpes-backend/templates/tests/test-connection.yaml new file mode 100644 index 0000000..77121e6 --- /dev/null +++ b/charts/vulpes-backend/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "micronaut.fullname" . }}-test-connection" + labels: + {{- include "micronaut.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "micronaut.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/charts/vulpes-backend/values.yaml b/charts/vulpes-backend/values.yaml new file mode 100644 index 0000000..7c1a30a --- /dev/null +++ b/charts/vulpes-backend/values.yaml @@ -0,0 +1,276 @@ +# Default values for vulpes-backend. +# +# This chart is a fork of the generic `micronaut` chart that lives in the +# cluster repository (Kubernetes-FLUX, helm/micronaut), taken so that Vulpes +# ships its own deployment alongside its image and the two are versioned from +# one release. `otis` still uses the original, so the two will drift; the +# template helpers here are deliberately still named `micronaut.*` and the +# files kept otherwise identical, which makes `diff -r` against helm/micronaut +# the way to pull a later fix across. +# +# Only what identifies *this* application is changed from the original below +# (image, port, metrics path). Everything environment-specific -- hostnames, +# replica counts, database secrets, log labels -- stays in the cluster +# repository's overlays. + +# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ +replicaCount: 1 + +# Deployment update strategy. Empty -> Kubernetes default (RollingUpdate 25%/25%). +# For seamless updates with >=2 replicas, set maxUnavailable: 0 / maxSurge: 1. +strategy: {} + # type: RollingUpdate + # rollingUpdate: + # maxUnavailable: 0 + # maxSurge: 1 + +# Scheduling priority. Empty -> cluster default PriorityClass. +priorityClassName: "" + +# PodDisruptionBudget for the workload. Disabled by default so single-replica +# apps are unaffected. Set minAvailable OR maxUnavailable (minAvailable wins). +podDisruptionBudget: + enabled: false + minAvailable: "" + maxUnavailable: "" + +# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ +image: + repository: harbor.onelitefeather.dev/onelitefeather/vulpes-backend + # This sets the pull policy for images. + pullPolicy: IfNotPresent + # Left empty on purpose: it falls back to .Chart.AppVersion, which Release + # Please moves together with the chart version. Setting it in an overlay + # pins the image away from the chart it was released with -- and Spegel + # caches mutable tags P2P, so "latest" is not an option here either. + tag: "" + +# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +imagePullSecrets: [] +# This is to override the chart name. +nameOverride: "" +fullnameOverride: "" + +# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +# RBAC for Micronaut Kubernetes service discovery. +# When enabled, creates a namespace-scoped Role + RoleBinding granting the +# ServiceAccount read access to services and endpoints in the release namespace, +# as required by micronaut-kubernetes-discovery-client. Off by default. +rbac: + create: false + +# This is for setting Kubernetes Annotations to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: {} +# This is for setting Kubernetes Labels to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} + +# Pod-level. Verified against the otis and vulpes-backend images with a probe +# pod: both reach Micronaut context startup as uid 1000 with a read-only root +# filesystem, so nothing here depends on running as root. +securityContext: + enabled: true + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + +# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ +service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports + # The port the Micronaut server binds (micronaut.server.port), not the + # chart's generic 80. + port: 8080 + +# Micrometer's Prometheus endpoint, which this application exposes in every +# environment -- so it is on here rather than repeated in each overlay. +metrics: + enabled: true + path: /prometheus + port: http + scheme: http + serviceMonitor: + enabled: false + namespace: "" + additionalLabels: {} + annotations: {} + interval: "" + scrapeTimeout: "" + relabelings: [] + metricRelabelings: [] + +# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +httpRoute: + enabled: false + annotations: {} + parentRefs: + - name: gateway + sectionName: http + hostnames: + - chart-example.local + rules: + - matches: + - path: + type: PathPrefix + value: / + +resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: + cpu: 1 + memory: 512Mi + requests: + cpu: 100m + memory: 256Mi + +# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +livenessProbe: + httpGet: + path: /health + port: http +readinessProbe: + httpGet: + path: /health + port: http + +# Off by default. Mainly needed when tracing.enabled: OTel bytecode +# instrumentation adds real seconds to JVM cold start (class transformation +# on Micronaut/Hibernate/Hikari), which can push first-successful-health +# past livenessProbe's steady-state deadline and SIGTERM-loop the pod +# before it ever finishes starting. Uses the readiness path/port since +# that's the only health endpoint the app exposes. +startupProbe: + enabled: false + failureThreshold: 30 + periodSeconds: 10 + +# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +# Specific +profiles: ["prod"] # This is for setting up the Micronaut application profiles. + +# This section is for setting up the Micronaut application configuration. +config: + base: |- + micronaut: + application: + name: Vulpes Backend + profiles: + prod: |- + # application-prod.yml + logger: + levels: + root: INFO + feature-x: |- + # application-feature-x.yml + feature: + x: true + +secrets: + enabled: false + files: {} + # Example: + # files: + # prod: |- + # datasource: + # username: ${DB_USER} + # password: ${DB_PASS} + +# Environment-Variablen +env: + - name: JAVA_TOOL_OPTIONS + value: "-XX:MaxRAMPercentage=75.0 -Duser.timezone=UTC" + +envFrom: [] + +# Traces-only OTel Java agent instrumentation. Metrics/logs already flow via +# the chart's own Prometheus scrape (metrics.serviceMonitor) and container +# stdout shipping, so only OTEL_TRACES_EXPORTER is enabled here to avoid +# duplicating either. An initContainer copies the javaagent jar out of the +# upstream image into an emptyDir the main container mounts read-only -- +# no OpenTelemetry Operator/CRDs involved, just the jar-holder image it publishes. +tracing: + enabled: false + # Matches the tempo-gateway target already used by EnvoyProxy's native + # tracing and the alloy-receiver OTLP gateway elsewhere in this cluster. + endpoint: http://tempo-gateway.grafana.svc.cluster.local:4317 + # otis/vulpes-backend run JDK 25; older javaagent releases fail to attach + # there (InaccessibleObjectException on ClassLoader.findLoadedClass -- + # module java.base doesn't opens java.lang to the unnamed module under + # newer JDKs). 2.29.0 is the latest available and includes the JDK23+ + # MethodHandles.Lookup fix for boot-loader class injection. + image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-java:2.30.0 + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + cpu: 100m + memory: 64Mi + +containerSecurityContext: + enabled: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] diff --git a/release-please-config.json b/release-please-config.json index 75d48ae..f8204f9 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -13,6 +13,10 @@ { "type": "generic", "path": "build.gradle.kts" + }, + { + "type": "generic", + "path": "charts/vulpes-backend/Chart.yaml" } ] }