From 102aaf6dd26541f9854cb3eeb8e0510647c7d03c Mon Sep 17 00:00:00 2001 From: Ciaran Roche Date: Mon, 8 Dec 2025 13:46:09 +0000 Subject: [PATCH 1/5] Add image Makefile targets and Helm chart (HYPERFLEET-312) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add image, image-push, image-dev Makefile targets for container builds - Update Dockerfile to multi-stage build (consistent with sentinel/adapter) - Add Helm chart with configurable PostgreSQL support: - Built-in PostgreSQL for development (default) - External database support for production (GCP Cloud SQL, etc.) - Update README with container image and Helm deployment docs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- Dockerfile | 36 +++++--- Makefile | 45 ++++++++++ README.md | 84 +++++++++++++++++++ charts/Chart.yaml | 15 ++++ charts/templates/_helpers.tpl | 62 ++++++++++++++ charts/templates/deployment.yaml | 109 ++++++++++++++++++++++++ charts/templates/hpa.yaml | 32 +++++++ charts/templates/postgresql.yaml | 120 +++++++++++++++++++++++++++ charts/templates/service.yaml | 15 ++++ charts/templates/serviceaccount.yaml | 12 +++ charts/values.yaml | 118 ++++++++++++++++++++++++++ 11 files changed, 636 insertions(+), 12 deletions(-) create mode 100644 charts/Chart.yaml create mode 100644 charts/templates/_helpers.tpl create mode 100644 charts/templates/deployment.yaml create mode 100644 charts/templates/hpa.yaml create mode 100644 charts/templates/postgresql.yaml create mode 100644 charts/templates/service.yaml create mode 100644 charts/templates/serviceaccount.yaml create mode 100644 charts/values.yaml diff --git a/Dockerfile b/Dockerfile index cf119fa..a9bcb9a 100755 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,33 @@ -FROM registry.access.redhat.com/ubi9/ubi-minimal:9.2-750.1697534106 +# Build stage +FROM golang:1.24-alpine AS builder -RUN \ - microdnf install -y \ - util-linux \ - && \ - microdnf clean all +WORKDIR /build -COPY \ - hyperfleet-api \ - /usr/local/bin/ +# Copy go mod files +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source code +COPY . . + +# Build binary +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o hyperfleet-api ./cmd/hyperfleet-api + +# Runtime stage +FROM gcr.io/distroless/static-debian12:nonroot + +WORKDIR /app + +# Copy binary from builder +COPY --from=builder /build/hyperfleet-api /app/hyperfleet-api EXPOSE 8000 -ENTRYPOINT ["/usr/local/bin/hyperfleet-api", "serve"] +ENTRYPOINT ["/app/hyperfleet-api"] +CMD ["serve"] LABEL name="hyperfleet-api" \ vendor="Red Hat" \ version="0.0.1" \ - summary="HyperFleet API" \ - description="HyperFleet API" + summary="HyperFleet API - Cluster Lifecycle Management Service" \ + description="HyperFleet API for cluster lifecycle management" diff --git a/Makefile b/Makefile index a0695be..bc281ee 100755 --- a/Makefile +++ b/Makefile @@ -15,6 +15,16 @@ version:=$(shell date +%s) # a tool for managing containers and images, etc. You can set it as docker container_tool ?= podman +# Image configuration +IMAGE_REGISTRY ?= quay.io/openshift-hyperfleet +IMAGE_NAME ?= hyperfleet-api +IMAGE_TAG ?= latest + +# Dev image configuration - set QUAY_USER to push to personal registry +# Usage: QUAY_USER=myuser make image-dev +QUAY_USER ?= +DEV_TAG ?= dev-$(git_sha) + # Database connection details db_name:=hyperfleet db_host=hyperfleet-db.$(namespace) @@ -53,6 +63,9 @@ help: @echo "make generate-mocks generate mock implementations for services" @echo "make generate-all generate all code (openapi + mocks)" @echo "make clean delete temporary generated files" + @echo "make image build container image" + @echo "make image-push build and push container image" + @echo "make image-dev build and push to personal Quay registry" @echo "$(fake)" .PHONY: help @@ -283,3 +296,35 @@ db/login: db/teardown: $(container_tool) stop psql-hyperfleet $(container_tool) rm psql-hyperfleet + +# Build container image (multi-stage build, no local binary needed) +.PHONY: image +image: + @echo "Building container image $(IMAGE_REGISTRY)/$(IMAGE_NAME):$(IMAGE_TAG)..." + $(container_tool) build -t $(IMAGE_REGISTRY)/$(IMAGE_NAME):$(IMAGE_TAG) . + @echo "✅ Image built: $(IMAGE_REGISTRY)/$(IMAGE_NAME):$(IMAGE_TAG)" + +# Build and push container image to registry +.PHONY: image-push +image-push: image + @echo "Pushing image $(IMAGE_REGISTRY)/$(IMAGE_NAME):$(IMAGE_TAG)..." + $(container_tool) push $(IMAGE_REGISTRY)/$(IMAGE_NAME):$(IMAGE_TAG) + @echo "✅ Image pushed: $(IMAGE_REGISTRY)/$(IMAGE_NAME):$(IMAGE_TAG)" + +# Build and push to personal Quay registry (requires QUAY_USER) +.PHONY: image-dev +image-dev: +ifndef QUAY_USER + @echo "❌ ERROR: QUAY_USER is not set" + @echo "" + @echo "Usage: QUAY_USER=myuser make image-dev" + @echo "" + @echo "This will build and push to: quay.io/$$QUAY_USER/$(IMAGE_NAME):$(DEV_TAG)" + @exit 1 +endif + @echo "Building dev image quay.io/$(QUAY_USER)/$(IMAGE_NAME):$(DEV_TAG)..." + $(container_tool) build -t quay.io/$(QUAY_USER)/$(IMAGE_NAME):$(DEV_TAG) . + @echo "Pushing dev image..." + $(container_tool) push quay.io/$(QUAY_USER)/$(IMAGE_NAME):$(DEV_TAG) + @echo "" + @echo "✅ Dev image pushed: quay.io/$(QUAY_USER)/$(IMAGE_NAME):$(DEV_TAG)" diff --git a/README.md b/README.md index 3ad1d9c..267b073 100755 --- a/README.md +++ b/README.md @@ -464,6 +464,90 @@ make db/teardown # Remove PostgreSQL container make db/login # Connect to database shell ``` +## Container Image + +Build and push container images using the multi-stage Dockerfile: + +```bash +# Build container image +make image + +# Build with custom tag +make image IMAGE_TAG=v1.0.0 + +# Build and push to default registry +make image-push + +# Build and push to personal Quay registry (for development) +QUAY_USER=myuser make image-dev +``` + +Default image: `quay.io/openshift-hyperfleet/hyperfleet-api:latest` + +## Kubernetes Deployment + +### Using Helm Chart + +The project includes a Helm chart for Kubernetes deployment with configurable PostgreSQL support. + +**Development deployment (with built-in PostgreSQL):** +```bash +helm install hyperfleet-api ./charts/ \ + --namespace hyperfleet-system \ + --create-namespace +``` + +**Production deployment (with external database like GCP Cloud SQL):** +```bash +# First, create a secret with database credentials +kubectl create secret generic hyperfleet-db-external \ + --namespace hyperfleet-system \ + --from-literal=db.host= \ + --from-literal=db.port=5432 \ + --from-literal=db.name=hyperfleet \ + --from-literal=db.user=hyperfleet \ + --from-literal=db.password= + +# Deploy with external database +helm install hyperfleet-api ./charts/ \ + --namespace hyperfleet-system \ + --set database.postgresql.enabled=false \ + --set database.external.enabled=true \ + --set database.external.secretName=hyperfleet-db-external +``` + +**Custom image deployment:** +```bash +helm install hyperfleet-api ./charts/ \ + --namespace hyperfleet-system \ + --set image.registry=quay.io/myuser \ + --set image.repository=hyperfleet-api \ + --set image.tag=v1.0.0 +``` + +**Upgrade deployment:** +```bash +helm upgrade hyperfleet-api ./charts/ --namespace hyperfleet-system +``` + +**Uninstall:** +```bash +helm uninstall hyperfleet-api --namespace hyperfleet-system +``` + +### Helm Values + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `image.registry` | Container registry | `quay.io/openshift-hyperfleet` | +| `image.repository` | Image repository | `hyperfleet-api` | +| `image.tag` | Image tag | `latest` | +| `database.postgresql.enabled` | Deploy built-in PostgreSQL | `true` | +| `database.external.enabled` | Use external database | `false` | +| `database.external.secretName` | Secret with db credentials | `""` | +| `auth.enableJwt` | Enable JWT authentication | `true` | +| `auth.enableAuthz` | Enable authorization | `true` | + ## API Authentication **Development mode (no auth):** diff --git a/charts/Chart.yaml b/charts/Chart.yaml new file mode 100644 index 0000000..fd9cfe3 --- /dev/null +++ b/charts/Chart.yaml @@ -0,0 +1,15 @@ +apiVersion: v2 +name: hyperfleet-api +description: HyperFleet API - Cluster Lifecycle Management Service +type: application +version: 1.0.0 +appVersion: "1.0.0" +maintainers: + - name: HyperFleet Team + email: hyperfleet-team@redhat.com +keywords: + - hyperfleet + - api + - kubernetes + - cluster-management +home: https://github.com/openshift-hyperfleet/hyperfleet-api diff --git a/charts/templates/_helpers.tpl b/charts/templates/_helpers.tpl new file mode 100644 index 0000000..b1d4c00 --- /dev/null +++ b/charts/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "hyperfleet-api.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 "hyperfleet-api.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "hyperfleet-api.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "hyperfleet-api.labels" -}} +helm.sh/chart: {{ include "hyperfleet-api.chart" . }} +{{ include "hyperfleet-api.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "hyperfleet-api.selectorLabels" -}} +app.kubernetes.io/name: {{ include "hyperfleet-api.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "hyperfleet-api.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "hyperfleet-api.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/charts/templates/deployment.yaml b/charts/templates/deployment.yaml new file mode 100644 index 0000000..194d0df --- /dev/null +++ b/charts/templates/deployment.yaml @@ -0,0 +1,109 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "hyperfleet-api.fullname" . }} + labels: + {{- include "hyperfleet-api.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "hyperfleet-api.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "hyperfleet-api.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "hyperfleet-api.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + {{- if .Values.database.postgresql.enabled }} + initContainers: + - name: wait-for-db + image: busybox:1.36 + command: ['sh', '-c', 'until nc -z {{ include "hyperfleet-api.fullname" . }}-postgresql {{ .Values.database.postgresql.port }}; do echo waiting for postgresql; sleep 2; done'] + {{- end }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 10 }} + image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + workingDir: /app + ports: + - name: http + containerPort: 8000 + protocol: TCP + env: + {{- if .Values.auth.jwksUrl }} + - name: JWKS_URL + value: {{ .Values.auth.jwksUrl | quote }} + {{- end }} + - name: ENABLE_JWT + value: {{ .Values.auth.enableJwt | quote }} + - name: ENABLE_AUTHZ + value: {{ .Values.auth.enableAuthz | quote }} + {{- if .Values.env }} + {{- range .Values.env }} + - name: {{ .name }} + value: {{ .value | quote }} + {{- end }} + {{- end }} + livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + resources: + {{- toYaml .Values.resources | nindent 10 }} + volumeMounts: + - name: secrets + mountPath: /build/secrets + readOnly: true + {{- if .Values.extraVolumeMounts }} + {{- toYaml .Values.extraVolumeMounts | nindent 8 }} + {{- end }} + volumes: + - name: secrets + secret: + {{- if .Values.database.external.enabled }} + secretName: {{ .Values.database.external.secretName }} + {{- else if .Values.database.postgresql.enabled }} + secretName: {{ include "hyperfleet-api.fullname" . }}-db-secrets + {{- end }} + {{- if .Values.extraVolumes }} + {{- toYaml .Values.extraVolumes | nindent 6 }} + {{- 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 }} diff --git a/charts/templates/hpa.yaml b/charts/templates/hpa.yaml new file mode 100644 index 0000000..cb738bb --- /dev/null +++ b/charts/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "hyperfleet-api.fullname" . }} + labels: + {{- include "hyperfleet-api.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "hyperfleet-api.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/templates/postgresql.yaml b/charts/templates/postgresql.yaml new file mode 100644 index 0000000..145a8a6 --- /dev/null +++ b/charts/templates/postgresql.yaml @@ -0,0 +1,120 @@ +{{- if .Values.database.postgresql.enabled }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "hyperfleet-api.fullname" . }}-db-secrets + labels: + {{- include "hyperfleet-api.labels" . | nindent 4 }} +type: Opaque +stringData: + db.host: {{ include "hyperfleet-api.fullname" . }}-postgresql + db.port: {{ .Values.database.postgresql.port | quote }} + db.name: {{ .Values.database.postgresql.database }} + db.user: {{ .Values.database.postgresql.user }} + db.password: {{ .Values.database.postgresql.password }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "hyperfleet-api.fullname" . }}-postgresql + labels: + {{- include "hyperfleet-api.labels" . | nindent 4 }} + app.kubernetes.io/component: postgresql +spec: + type: ClusterIP + ports: + - port: {{ .Values.database.postgresql.port }} + targetPort: postgresql + protocol: TCP + name: postgresql + selector: + {{- include "hyperfleet-api.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: postgresql +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "hyperfleet-api.fullname" . }}-postgresql + labels: + {{- include "hyperfleet-api.labels" . | nindent 4 }} + app.kubernetes.io/component: postgresql +spec: + replicas: 1 + selector: + matchLabels: + {{- include "hyperfleet-api.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: postgresql + template: + metadata: + labels: + {{- include "hyperfleet-api.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: postgresql + spec: + containers: + - name: postgresql + image: {{ .Values.database.postgresql.image }} + ports: + - name: postgresql + containerPort: {{ .Values.database.postgresql.port }} + protocol: TCP + env: + - name: POSTGRES_DB + value: {{ .Values.database.postgresql.database }} + - name: POSTGRES_USER + value: {{ .Values.database.postgresql.user }} + - name: POSTGRES_PASSWORD + value: {{ .Values.database.postgresql.password }} + resources: + {{- toYaml .Values.database.postgresql.resources | nindent 10 }} + {{- if .Values.database.postgresql.persistence.enabled }} + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + {{- end }} + readinessProbe: + exec: + command: + - pg_isready + - -U + - {{ .Values.database.postgresql.user }} + - -d + - {{ .Values.database.postgresql.database }} + initialDelaySeconds: 5 + periodSeconds: 5 + livenessProbe: + exec: + command: + - pg_isready + - -U + - {{ .Values.database.postgresql.user }} + - -d + - {{ .Values.database.postgresql.database }} + initialDelaySeconds: 30 + periodSeconds: 10 + {{- if .Values.database.postgresql.persistence.enabled }} + volumes: + - name: data + persistentVolumeClaim: + claimName: {{ include "hyperfleet-api.fullname" . }}-postgresql + {{- end }} +--- +{{- if .Values.database.postgresql.persistence.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "hyperfleet-api.fullname" . }}-postgresql + labels: + {{- include "hyperfleet-api.labels" . | nindent 4 }} + app.kubernetes.io/component: postgresql +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.database.postgresql.persistence.size }} + {{- if .Values.database.postgresql.persistence.storageClass }} + storageClassName: {{ .Values.database.postgresql.persistence.storageClass }} + {{- end }} +{{- end }} +{{- end }} diff --git a/charts/templates/service.yaml b/charts/templates/service.yaml new file mode 100644 index 0000000..ca6b65c --- /dev/null +++ b/charts/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "hyperfleet-api.fullname" . }} + labels: + {{- include "hyperfleet-api.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "hyperfleet-api.selectorLabels" . | nindent 4 }} diff --git a/charts/templates/serviceaccount.yaml b/charts/templates/serviceaccount.yaml new file mode 100644 index 0000000..bead449 --- /dev/null +++ b/charts/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "hyperfleet-api.serviceAccountName" . }} + labels: + {{- include "hyperfleet-api.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/values.yaml b/charts/values.yaml new file mode 100644 index 0000000..a227c3d --- /dev/null +++ b/charts/values.yaml @@ -0,0 +1,118 @@ +# Default values for hyperfleet-api +# This is a YAML-formatted file. + +replicaCount: 1 + +image: + registry: quay.io/openshift-hyperfleet + repository: hyperfleet-api + pullPolicy: Always + tag: "latest" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: 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: "" + +podAnnotations: {} + +podSecurityContext: + fsGroup: 65532 + runAsNonRoot: true + runAsUser: 65532 + +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: false + seccompProfile: + type: RuntimeDefault + +service: + type: ClusterIP + port: 8000 + +resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 10 + targetCPUUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: 80 + +# Database configuration +database: + # For PRODUCTION: Use external database (GCP Cloud SQL, etc.) + # Set external.enabled=true and provide connection details + # + # For DEVELOPMENT: Use built-in PostgreSQL pod + # Set postgresql.enabled=true (default) + + # External database configuration (production) + external: + enabled: false + # Name of existing secret with db.host, db.port, db.name, db.user, db.password keys + secretName: "" + + # Built-in PostgreSQL for development/testing + postgresql: + enabled: true + image: docker.io/library/postgres:14.2 + database: hyperfleet + user: hyperfleet + password: hyperfleet-dev-password + port: 5432 + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi + persistence: + enabled: false + size: 1Gi + storageClass: "" + +# Authentication configuration +auth: + # Enable JWT authentication + enableJwt: true + # Enable authorization + enableAuthz: true + # JWKS URL for token verification + jwksUrl: "https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/certs" + +# Additional environment variables +env: [] + # - name: GLOG_V + # value: "10" + +# Volume mounts for additional configs +extraVolumeMounts: [] + +# Additional volumes +extraVolumes: [] From ba0c890b05e8a0ce7f9af086c01f2b9925b05f95 Mon Sep 17 00:00:00 2001 From: Ciaran Roche Date: Tue, 9 Dec 2025 11:55:45 +0000 Subject: [PATCH 2/5] Address PR review feedback for Helm chart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Guard secrets volume against both DB modes being disabled - Use simple name for Service (hyperfleet-api) instead of fullname for predictable service discovery across components - Add JWKS URL override comment for custom identity providers - Disable enableAuthz by default (not required for MVP) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- charts/templates/deployment.yaml | 8 +++++++- charts/templates/service.yaml | 2 +- charts/values.yaml | 5 +++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/charts/templates/deployment.yaml b/charts/templates/deployment.yaml index 194d0df..f1cdf14 100644 --- a/charts/templates/deployment.yaml +++ b/charts/templates/deployment.yaml @@ -77,24 +77,30 @@ spec: failureThreshold: 3 resources: {{- toYaml .Values.resources | nindent 10 }} + {{- if or .Values.database.external.enabled .Values.database.postgresql.enabled }} volumeMounts: - name: secrets mountPath: /build/secrets readOnly: true + {{- end }} {{- if .Values.extraVolumeMounts }} {{- toYaml .Values.extraVolumeMounts | nindent 8 }} {{- end }} + {{- if or .Values.database.external.enabled .Values.database.postgresql.enabled .Values.extraVolumes }} volumes: + {{- if or .Values.database.external.enabled .Values.database.postgresql.enabled }} - name: secrets secret: {{- if .Values.database.external.enabled }} secretName: {{ .Values.database.external.secretName }} - {{- else if .Values.database.postgresql.enabled }} + {{- else }} secretName: {{ include "hyperfleet-api.fullname" . }}-db-secrets {{- end }} + {{- end }} {{- if .Values.extraVolumes }} {{- toYaml .Values.extraVolumes | nindent 6 }} {{- end }} + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/charts/templates/service.yaml b/charts/templates/service.yaml index ca6b65c..1de0969 100644 --- a/charts/templates/service.yaml +++ b/charts/templates/service.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Service metadata: - name: {{ include "hyperfleet-api.fullname" . }} + name: {{ include "hyperfleet-api.name" . }} labels: {{- include "hyperfleet-api.labels" . | nindent 4 }} spec: diff --git a/charts/values.yaml b/charts/values.yaml index a227c3d..25b5931 100644 --- a/charts/values.yaml +++ b/charts/values.yaml @@ -101,9 +101,10 @@ database: auth: # Enable JWT authentication enableJwt: true - # Enable authorization - enableAuthz: true + # Enable authorization (not required for MVP) + enableAuthz: false # JWKS URL for token verification + # Override with: --set auth.jwksUrl="https://your-idp/.../.well-known/jwks.json" jwksUrl: "https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/certs" # Additional environment variables From 3e39da1d615dfbaf7fa52cf625924135c79db56d Mon Sep 17 00:00:00 2001 From: Ciaran Roche Date: Tue, 9 Dec 2025 16:23:47 +0000 Subject: [PATCH 3/5] Fix Helm chart for Kubernetes deployment (HYPERFLEET-312) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix bind addresses to use :PORT format for all interfaces - Add health and metrics ports to deployment - Fix health probe path from /healthz to /healthcheck - Add app.kubernetes.io/component label to differentiate from PostgreSQL - Add .helmignore to exclude .git from chart packaging - Add configurable server bind addresses in values.yaml 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- charts/.helmignore | 26 ++++++++++++++++++++++++++ charts/templates/_helpers.tpl | 1 + charts/templates/deployment.yaml | 19 +++++++++++++++---- charts/values.yaml | 7 +++++++ 4 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 charts/.helmignore diff --git a/charts/.helmignore b/charts/.helmignore new file mode 100644 index 0000000..f3586bf --- /dev/null +++ b/charts/.helmignore @@ -0,0 +1,26 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ +# Parent directory patterns +../.git/ +../../.git/ diff --git a/charts/templates/_helpers.tpl b/charts/templates/_helpers.tpl index b1d4c00..af44a49 100644 --- a/charts/templates/_helpers.tpl +++ b/charts/templates/_helpers.tpl @@ -48,6 +48,7 @@ Selector labels {{- define "hyperfleet-api.selectorLabels" -}} app.kubernetes.io/name: {{ include "hyperfleet-api.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/component: api {{- end }} {{/* diff --git a/charts/templates/deployment.yaml b/charts/templates/deployment.yaml index f1cdf14..67c10bf 100644 --- a/charts/templates/deployment.yaml +++ b/charts/templates/deployment.yaml @@ -40,10 +40,21 @@ spec: image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} workingDir: /app + args: + - serve + - --api-server-bindaddress={{ .Values.server.bindAddress | default ":8000" }} + - --health-check-server-bindaddress={{ .Values.server.healthBindAddress | default ":8083" }} + - --metrics-server-bindaddress={{ .Values.server.metricsBindAddress | default ":8080" }} ports: - name: http containerPort: 8000 protocol: TCP + - name: health + containerPort: 8083 + protocol: TCP + - name: metrics + containerPort: 8080 + protocol: TCP env: {{- if .Values.auth.jwksUrl }} - name: JWKS_URL @@ -61,16 +72,16 @@ spec: {{- end }} livenessProbe: httpGet: - path: /healthz - port: http + path: /healthcheck + port: health initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 readinessProbe: httpGet: - path: /healthz - port: http + path: /healthcheck + port: health initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 3 diff --git a/charts/values.yaml b/charts/values.yaml index 25b5931..9e179cc 100644 --- a/charts/values.yaml +++ b/charts/values.yaml @@ -13,6 +13,13 @@ imagePullSecrets: [] nameOverride: "" fullnameOverride: "" +# Server bind addresses +# Use ":PORT" format to bind to all interfaces (required for Kubernetes) +server: + bindAddress: ":8000" + healthBindAddress: ":8083" + metricsBindAddress: ":8080" + serviceAccount: # Specifies whether a service account should be created create: true From 11714b8d351b44b62c03afbd1db91eb83e8b8433 Mon Sep 17 00:00:00 2001 From: Ciaran Roche Date: Wed, 10 Dec 2025 11:23:57 +0000 Subject: [PATCH 4/5] Address PR review comments for Helm chart and Dockerfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add OpenAPI generation stage to Dockerfile (multi-stage build) - Add health (8083) and metrics (8080) ports to service.yaml - Add db-migrate init container to run migrations before serve - Add /tmp emptyDir volume for readOnlyRootFilesystem support - Enable readOnlyRootFilesystem: true in security context 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- Dockerfile | 19 +++++++++++++++++++ charts/templates/deployment.yaml | 20 ++++++++++++++++---- charts/templates/service.yaml | 8 ++++++++ charts/values.yaml | 2 +- 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index a9bcb9a..6bb7fa0 100755 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,19 @@ +# OpenAPI generation stage +FROM openapitools/openapi-generator-cli:v7.16.0 AS openapi-gen + +WORKDIR /local + +# Copy OpenAPI spec +COPY openapi/openapi.yaml /local/openapi/openapi.yaml + +# Generate Go client/models from OpenAPI spec +RUN bash /usr/local/bin/docker-entrypoint.sh generate \ + -i /local/openapi/openapi.yaml \ + -g go \ + -o /local/pkg/api/openapi && \ + rm -f /local/pkg/api/openapi/go.mod /local/pkg/api/openapi/go.sum && \ + rm -rf /local/pkg/api/openapi/test + # Build stage FROM golang:1.24-alpine AS builder @@ -10,6 +26,9 @@ RUN go mod download # Copy source code COPY . . +# Copy generated OpenAPI code from openapi-gen stage +COPY --from=openapi-gen /local/pkg/api/openapi ./pkg/api/openapi + # Build binary RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o hyperfleet-api ./cmd/hyperfleet-api diff --git a/charts/templates/deployment.yaml b/charts/templates/deployment.yaml index 67c10bf..7ea0b7c 100644 --- a/charts/templates/deployment.yaml +++ b/charts/templates/deployment.yaml @@ -27,12 +27,22 @@ spec: serviceAccountName: {{ include "hyperfleet-api.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} - {{- if .Values.database.postgresql.enabled }} + {{- if or .Values.database.postgresql.enabled .Values.database.external.enabled }} initContainers: + {{- if .Values.database.postgresql.enabled }} - name: wait-for-db image: busybox:1.36 command: ['sh', '-c', 'until nc -z {{ include "hyperfleet-api.fullname" . }}-postgresql {{ .Values.database.postgresql.port }}; do echo waiting for postgresql; sleep 2; done'] {{- end }} + - name: db-migrate + image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: ["/app/hyperfleet-api", "migrate"] + volumeMounts: + - name: secrets + mountPath: /build/secrets + readOnly: true + {{- end }} containers: - name: {{ .Chart.Name }} securityContext: @@ -88,8 +98,10 @@ spec: failureThreshold: 3 resources: {{- toYaml .Values.resources | nindent 10 }} - {{- if or .Values.database.external.enabled .Values.database.postgresql.enabled }} volumeMounts: + - name: tmp + mountPath: /tmp + {{- if or .Values.database.external.enabled .Values.database.postgresql.enabled }} - name: secrets mountPath: /build/secrets readOnly: true @@ -97,8 +109,9 @@ spec: {{- if .Values.extraVolumeMounts }} {{- toYaml .Values.extraVolumeMounts | nindent 8 }} {{- end }} - {{- if or .Values.database.external.enabled .Values.database.postgresql.enabled .Values.extraVolumes }} volumes: + - name: tmp + emptyDir: {} {{- if or .Values.database.external.enabled .Values.database.postgresql.enabled }} - name: secrets secret: @@ -111,7 +124,6 @@ spec: {{- if .Values.extraVolumes }} {{- toYaml .Values.extraVolumes | nindent 6 }} {{- end }} - {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/charts/templates/service.yaml b/charts/templates/service.yaml index 1de0969..6eb3628 100644 --- a/charts/templates/service.yaml +++ b/charts/templates/service.yaml @@ -11,5 +11,13 @@ spec: targetPort: http protocol: TCP name: http + - port: 8083 + targetPort: health + protocol: TCP + name: health + - port: 8080 + targetPort: metrics + protocol: TCP + name: metrics selector: {{- include "hyperfleet-api.selectorLabels" . | nindent 4 }} diff --git a/charts/values.yaml b/charts/values.yaml index 9e179cc..d5535f8 100644 --- a/charts/values.yaml +++ b/charts/values.yaml @@ -41,7 +41,7 @@ securityContext: capabilities: drop: - ALL - readOnlyRootFilesystem: false + readOnlyRootFilesystem: true seccompProfile: type: RuntimeDefault From ca7bfac4109ddd9eebd430095490c4ee03d378f3 Mon Sep 17 00:00:00 2001 From: Ciaran Roche Date: Wed, 10 Dec 2025 12:41:06 +0000 Subject: [PATCH 5/5] Add OpenAPI schema to Docker image for validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copy openapi/openapi.yaml into the runtime container and set OPENAPI_SCHEMA_PATH environment variable to enable schema validation in production deployments. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- Dockerfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Dockerfile b/Dockerfile index 6bb7fa0..92eae0e 100755 --- a/Dockerfile +++ b/Dockerfile @@ -40,6 +40,12 @@ WORKDIR /app # Copy binary from builder COPY --from=builder /build/hyperfleet-api /app/hyperfleet-api +# Copy OpenAPI schema for validation (uses the source spec, not the generated one) +COPY --from=builder /build/openapi/openapi.yaml /app/openapi/openapi.yaml + +# Set default schema path (can be overridden by Helm for provider-specific schemas) +ENV OPENAPI_SCHEMA_PATH=/app/openapi/openapi.yaml + EXPOSE 8000 ENTRYPOINT ["/app/hyperfleet-api"]