From eeffabf0e3eec8ea30fd4d1064df18be9e9b60bf Mon Sep 17 00:00:00 2001 From: Ralf Grubenmann Date: Thu, 6 Aug 2026 08:23:07 +0200 Subject: [PATCH 1/2] add helm chart --- helm-chart/.helmignore | 23 +++++ helm-chart/Chart.yaml | 24 +++++ helm-chart/templates/NOTES.txt | 0 helm-chart/templates/_helpers.tpl | 62 ++++++++++++ helm-chart/values.yaml | 161 ++++++++++++++++++++++++++++++ 5 files changed, 270 insertions(+) create mode 100644 helm-chart/.helmignore create mode 100644 helm-chart/Chart.yaml create mode 100644 helm-chart/templates/NOTES.txt create mode 100644 helm-chart/templates/_helpers.tpl create mode 100644 helm-chart/values.yaml diff --git a/helm-chart/.helmignore b/helm-chart/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/helm-chart/.helmignore @@ -0,0 +1,23 @@ +# 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/ diff --git a/helm-chart/Chart.yaml b/helm-chart/Chart.yaml new file mode 100644 index 00000000..c1f27adc --- /dev/null +++ b/helm-chart/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: Hackagon +description: A Helm chart for Hackagon on Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/helm-chart/templates/NOTES.txt b/helm-chart/templates/NOTES.txt new file mode 100644 index 00000000..e69de29b diff --git a/helm-chart/templates/_helpers.tpl b/helm-chart/templates/_helpers.tpl new file mode 100644 index 00000000..a68e2b52 --- /dev/null +++ b/helm-chart/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "helm-chart.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 "helm-chart.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 "helm-chart.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "helm-chart.labels" -}} +helm.sh/chart: {{ include "helm-chart.chart" . }} +{{ include "helm-chart.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "helm-chart.selectorLabels" -}} +app.kubernetes.io/name: {{ include "helm-chart.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "helm-chart.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "helm-chart.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm-chart/values.yaml b/helm-chart/values.yaml new file mode 100644 index 00000000..88026efd --- /dev/null +++ b/helm-chart/values.yaml @@ -0,0 +1,161 @@ +# Default values for helm-chart. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ +replicaCount: 1 + +# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ +image: + repository: nginx + # This sets the pull policy for images. + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + 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: "" + +# 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: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# 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 + port: 80 + +# 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 + +# -- Expose the service via gateway-api HTTPRoute +# Requires Gateway API resources and suitable controller installed within the cluster +# (see: https://gateway-api.sigs.k8s.io/guides/) +httpRoute: + # HTTPRoute enabled. + enabled: false + # HTTPRoute annotations. + annotations: {} + # Which Gateways this Route is attached to. + parentRefs: + - name: gateway + sectionName: http + # namespace: default + # Hostnames matching HTTP header. + hostnames: + - chart-example.local + # List of rules and filters applied. + rules: + - matches: + - path: + type: PathPrefix + value: /headers + # filters: + # - type: RequestHeaderModifier + # requestHeaderModifier: + # set: + # - name: My-Overwrite-Header + # value: this-is-the-only-value + # remove: + # - User-Agent + # - matches: + # - path: + # type: PathPrefix + # value: /echo + # headers: + # - name: version + # value: v2 + +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: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# 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: / + port: http +readinessProbe: + httpGet: + path: / + port: http + +# 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: {} From 7c07c8933bd934ba67d94e387c9dc2e6a5a73e8c Mon Sep 17 00:00:00 2001 From: Ralf Grubenmann Date: Thu, 6 Aug 2026 17:49:51 +0200 Subject: [PATCH 2/2] fix deploy issues --- .gitignore | 3 + helm-chart/Chart.lock | 9 + helm-chart/Chart.yaml | 28 +- helm-chart/templates/NOTES.txt | 19 + helm-chart/templates/_helpers.tpl | 88 ++++- helm-chart/templates/backend-configmap.yaml | 26 ++ helm-chart/templates/backend-deployment.yaml | 55 +++ helm-chart/templates/backend-service.yaml | 18 + helm-chart/templates/frontend-configmap.yaml | 20 + helm-chart/templates/frontend-deployment.yaml | 67 ++++ helm-chart/templates/frontend-ingress.yaml | 41 ++ helm-chart/templates/frontend-secrets.yaml | 13 + helm-chart/templates/frontend-service.yaml | 18 + helm-chart/templates/keycloak-ingress.yaml | 28 ++ .../templates/keycloak-init-configmap.yaml | 15 + .../templates/keycloak-realm-configmap.yaml | 17 + helm-chart/values.yaml | 351 ++++++++++-------- tools/nix/hackagon/pkgs/treefmt.nix | 1 + 18 files changed, 630 insertions(+), 187 deletions(-) create mode 100644 helm-chart/Chart.lock create mode 100644 helm-chart/templates/backend-configmap.yaml create mode 100644 helm-chart/templates/backend-deployment.yaml create mode 100644 helm-chart/templates/backend-service.yaml create mode 100644 helm-chart/templates/frontend-configmap.yaml create mode 100644 helm-chart/templates/frontend-deployment.yaml create mode 100644 helm-chart/templates/frontend-ingress.yaml create mode 100644 helm-chart/templates/frontend-secrets.yaml create mode 100644 helm-chart/templates/frontend-service.yaml create mode 100644 helm-chart/templates/keycloak-ingress.yaml create mode 100644 helm-chart/templates/keycloak-init-configmap.yaml create mode 100644 helm-chart/templates/keycloak-realm-configmap.yaml diff --git a/.gitignore b/.gitignore index 4ce23d32..04438165 100644 --- a/.gitignore +++ b/.gitignore @@ -38,5 +38,8 @@ result # All .env files .env .env.yaml +# Helm dev values with passwords +helm-chart/values.dev.yaml +helm-chart/charts/ ## ============================================================================ diff --git a/helm-chart/Chart.lock b/helm-chart/Chart.lock new file mode 100644 index 00000000..f04a776c --- /dev/null +++ b/helm-chart/Chart.lock @@ -0,0 +1,9 @@ +dependencies: +- name: postgresql + repository: https://charts.bitnami.com/bitnami + version: 18.8.6 +- name: keycloak + repository: https://repo.helmforge.dev + version: 3.0.7 +digest: sha256:15cdc5bf9f1d24c4f3beb12f85675fa57c050e4f1662d36d50e35e430eeb301a +generated: "2026-08-07T09:27:51.820259829+02:00" diff --git a/helm-chart/Chart.yaml b/helm-chart/Chart.yaml index c1f27adc..619bcb6e 100644 --- a/helm-chart/Chart.yaml +++ b/helm-chart/Chart.yaml @@ -1,24 +1,18 @@ apiVersion: v2 -name: Hackagon +name: hackagon description: A Helm chart for Hackagon on Kubernetes -# A chart can be either an 'application' or a 'library' chart. -# -# Application charts are a collection of templates that can be packaged into versioned archives -# to be deployed. -# -# Library charts provide useful utilities or functions for the chart developer. They're included as -# a dependency of application charts to inject those utilities and functions into the rendering -# pipeline. Library charts do not define any templates and therefore cannot be deployed. type: application -# This is the chart version. This version number should be incremented each time you make changes -# to the chart and its templates, including the app version. -# Versions are expected to follow Semantic Versioning (https://semver.org/) version: 0.1.0 +appVersion: "1.0.0" -# This is the version number of the application being deployed. This version number should be -# incremented each time you make changes to the application. Versions are not expected to -# follow Semantic Versioning. They should reflect the version the application is using. -# It is recommended to use it with quotes. -appVersion: "1.16.0" +dependencies: + - name: postgresql + version: 18.x + repository: https://charts.bitnami.com/bitnami + condition: postgresql.enabled + - name: keycloak + version: 3.x + repository: https://repo.helmforge.dev + condition: keycloak.enabled diff --git a/helm-chart/templates/NOTES.txt b/helm-chart/templates/NOTES.txt index e69de29b..fa91b3a5 100644 --- a/helm-chart/templates/NOTES.txt +++ b/helm-chart/templates/NOTES.txt @@ -0,0 +1,19 @@ +Hackagon has been deployed! + +Frontend: https://{{ .Values.baseDomain }} +Keycloak: https://auth.{{ .Values.baseDomain }} + +To get the generated frontend OIDC secrets, run: + + kubectl get secret {{ include "hackagon.fullname" . }}-frontend-secrets \ + -n {{ .Release.Namespace }} -o jsonpath='{.data.secrets\.yaml}' | base64 -d + +Passwords are set in your values files. Retrieve them with: + + # Platform (hackagon) user password + kubectl get secret {{ include "hackagon.fullname" . }}-postgresql \ + -n {{ .Release.Namespace }} -o jsonpath='{.data.postgres-password}' | base64 -d + +Check the status of your release: + + helm status {{ .Release.Name }} -n {{ .Release.Namespace }} \ No newline at end of file diff --git a/helm-chart/templates/_helpers.tpl b/helm-chart/templates/_helpers.tpl index a68e2b52..9ae963e1 100644 --- a/helm-chart/templates/_helpers.tpl +++ b/helm-chart/templates/_helpers.tpl @@ -1,7 +1,7 @@ {{/* Expand the name of the chart. */}} -{{- define "helm-chart.name" -}} +{{- define "hackagon.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} @@ -10,7 +10,7 @@ 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 "helm-chart.fullname" -}} +{{- define "hackagon.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} @@ -26,16 +26,16 @@ If release name contains chart name it will be used as a full name. {{/* Create chart name and version as used by the chart label. */}} -{{- define "helm-chart.chart" -}} +{{- define "hackagon.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Common labels */}} -{{- define "helm-chart.labels" -}} -helm.sh/chart: {{ include "helm-chart.chart" . }} -{{ include "helm-chart.selectorLabels" . }} +{{- define "hackagon.labels" -}} +helm.sh/chart: {{ include "hackagon.chart" . }} +{{ include "hackagon.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} @@ -45,18 +45,86 @@ app.kubernetes.io/managed-by: {{ .Release.Service }} {{/* Selector labels */}} -{{- define "helm-chart.selectorLabels" -}} -app.kubernetes.io/name: {{ include "helm-chart.name" . }} +{{- define "hackagon.selectorLabels" -}} +app.kubernetes.io/name: {{ include "hackagon.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{/* Create the name of the service account to use */}} -{{- define "helm-chart.serviceAccountName" -}} +{{- define "hackagon.serviceAccountName" -}} {{- if .Values.serviceAccount.create }} -{{- default (include "helm-chart.fullname" .) .Values.serviceAccount.name }} +{{- default (include "hackagon.fullname" .) .Values.serviceAccount.name }} {{- else }} {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} + +{{/* +Base domain with substitution +*/}} +{{- define "hackagon.baseDomain" -}} +{{- .Values.baseDomain | replace "{baseDomain}" .Values.baseDomain }} +{{- end }} + +{{/* +Frontend host with substitution +*/}} +{{- define "hackagon.frontendHost" -}} +{{- printf "app.%s" (include "hackagon.baseDomain" .) | replace "{baseDomain}" .Values.baseDomain }} +{{- end }} + +{{/* +Keycloak host with substitution +*/}} +{{- define "hackagon.keycloakHost" -}} +{{- printf "auth.%s" (include "hackagon.baseDomain" .) | replace "{baseDomain}" .Values.baseDomain }} +{{- end }} + +{{/* +Frontend service name +*/}} +{{- define "hackagon.frontendServiceName" -}} +{{- printf "%s-frontend" (include "hackagon.fullname" .) }} +{{- end }} + +{{/* +Backend service name +*/}} +{{- define "hackagon.backendServiceName" -}} +{{- printf "%s-backend" (include "hackagon.fullname" .) }} +{{- end }} + +{{/* +Generate a random alphanumeric string of given length +Usage: include "hackagon.randAlphaNum" (dict "length" 32) +*/}} +{{- define "hackagon.randAlphaNum" -}} +{{- randAlphaNum .length | lower }} +{{- end }} + +{{/* +Keycloak service name (bitnami chart names it -keycloak) +*/}} +{{- define "hackagon.keycloakServiceName" -}} +{{- printf "%s-keycloak" .Release.Name }} +{{- end }} + +{{/* +PostgreSQL service name (bitnami chart names it -postgresql) +*/}} +{{- define "hackagon.postgresqlServiceName" -}} +{{- printf "%s-postgresql" .Release.Name }} +{{- end }} + +{{/* +Get password: use provided value or generate one +*/}} +{{- define "hackagon.getPassword" -}} +{{- if .value }} +{{- .value | b64enc }} +{{- else }} +{{- include "hackagon.randAlphaNum" .length | b64enc }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/helm-chart/templates/backend-configmap.yaml b/helm-chart/templates/backend-configmap.yaml new file mode 100644 index 00000000..4a0e6441 --- /dev/null +++ b/helm-chart/templates/backend-configmap.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "hackagon.fullname" . }}-backend-config + namespace: {{ .Release.Namespace }} + labels: + {{- include "hackagon.labels" . | nindent 4 }} +data: + config.yaml: | + server: + port: {{ .Values.backend.config.server.port | quote }} + adminemail: {{ .Values.backend.config.server.adminemail | replace "{baseDomain}" .Values.baseDomain | quote }} + adminkeycloakid: {{ .Values.backend.config.server.adminkeycloakid | quote }} + database: + driver: {{ .Values.backend.config.database.driver | quote }} + host: {{ include "hackagon.postgresqlServiceName" . | quote }} + port: {{ .Values.backend.config.database.port }} + dbname: {{ .Values.backend.config.database.dbname | quote }} + user: {{ .Values.backend.config.database.user | quote }} + password: {{ .Values.backend.config.database.postgresPassword | required "postgresql.auth.postgresPassword is required" | quote }} + oidc: + jwksurl: {{ .Values.backend.config.oidc.jwksurl | replace "{baseDomain}" .Values.baseDomain | quote }} + issuerurl: {{ .Values.backend.config.oidc.issuerurl | replace "{baseDomain}" .Values.baseDomain | quote }} + algorithm: {{ .Values.backend.config.oidc.algorithm | quote }} + logging: + level: {{ .Values.backend.config.logging.level | quote }} diff --git a/helm-chart/templates/backend-deployment.yaml b/helm-chart/templates/backend-deployment.yaml new file mode 100644 index 00000000..f543ab46 --- /dev/null +++ b/helm-chart/templates/backend-deployment.yaml @@ -0,0 +1,55 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "hackagon.fullname" . }}-backend + namespace: {{ .Release.Namespace }} + labels: + {{- include "hackagon.labels" . | nindent 4 }} + app.kubernetes.io/component: backend +spec: + replicas: {{ .Values.backend.replicaCount }} + selector: + matchLabels: + {{- include "hackagon.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: backend + template: + metadata: + {{- with .Values.backend.annotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "hackagon.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: backend + spec: + containers: + - name: backend + image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag }}" + imagePullPolicy: {{ .Values.backend.image.pullPolicy }} + args: + - "--config-dir=/etc/hackagon/" + ports: + - name: grpc + containerPort: 3000 + protocol: TCP + resources: + {{- toYaml .Values.backend.resources | nindent 12 }} + volumeMounts: + - name: config + mountPath: /etc/hackagon/config.yaml + subPath: config.yaml + readOnly: true + livenessProbe: + grpc: + port: 3000 + initialDelaySeconds: 10 + periodSeconds: 15 + readinessProbe: + grpc: + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 10 + volumes: + - name: config + configMap: + name: {{ include "hackagon.fullname" . }}-backend-config diff --git a/helm-chart/templates/backend-service.yaml b/helm-chart/templates/backend-service.yaml new file mode 100644 index 00000000..01fd7c7d --- /dev/null +++ b/helm-chart/templates/backend-service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "hackagon.fullname" . }}-backend + namespace: {{ .Release.Namespace }} + labels: + {{- include "hackagon.labels" . | nindent 4 }} + app.kubernetes.io/component: backend +spec: + type: {{ .Values.backend.service.type }} + ports: + - port: {{ .Values.backend.service.port }} + targetPort: grpc + protocol: TCP + name: grpc + selector: + {{- include "hackagon.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: backend \ No newline at end of file diff --git a/helm-chart/templates/frontend-configmap.yaml b/helm-chart/templates/frontend-configmap.yaml new file mode 100644 index 00000000..67a7d073 --- /dev/null +++ b/helm-chart/templates/frontend-configmap.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "hackagon.fullname" . }}-frontend-config + namespace: {{ .Release.Namespace }} + labels: + {{- include "hackagon.labels" . | nindent 4 }} +data: + config.yaml: | + log: + forceDevLog: {{ .Values.frontend.config.log.forceDevLog }} + backend: + hostname: {{ .Values.frontend.config.backend.hostname | quote }} + port: {{ .Values.frontend.config.backend.port }} + cookies: + useSecure: {{ .Values.frontend.config.cookies.useSecure }} + oidc: + clientId: {{ .Values.frontend.config.oidc.clientId | quote }} + issuer: {{ .Values.frontend.config.oidc.issuer | replace "{baseDomain}" .Values.baseDomain | quote }} + audience: {{ .Values.frontend.config.oidc.audience | quote }} \ No newline at end of file diff --git a/helm-chart/templates/frontend-deployment.yaml b/helm-chart/templates/frontend-deployment.yaml new file mode 100644 index 00000000..c31115ab --- /dev/null +++ b/helm-chart/templates/frontend-deployment.yaml @@ -0,0 +1,67 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "hackagon.fullname" . }}-frontend + namespace: {{ .Release.Namespace }} + labels: + {{- include "hackagon.labels" . | nindent 4 }} + app.kubernetes.io/component: frontend +spec: + replicas: {{ .Values.frontend.replicaCount }} + selector: + matchLabels: + {{- include "hackagon.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: frontend + template: + metadata: + {{- with .Values.frontend.annotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "hackagon.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: frontend + spec: + containers: + - name: frontend + image: "{{ .Values.frontend.image.repository }}:{{ .Values.frontend.image.tag }}" + imagePullPolicy: {{ .Values.frontend.image.pullPolicy }} + args: + - "--config-dir=/etc/hackagon" + env: + - name: PORT + value: "3000" + ports: + - name: http + containerPort: 3000 + protocol: TCP + resources: + {{- toYaml .Values.frontend.resources | nindent 12 }} + volumeMounts: + - name: config + mountPath: /etc/hackagon/config.yaml + subPath: config.yaml + readOnly: true + - name: secrets + mountPath: /etc/hackagon/secrets.yaml + subPath: secrets.yaml + readOnly: true + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 10 + periodSeconds: 15 + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + volumes: + - name: config + configMap: + name: {{ include "hackagon.fullname" . }}-frontend-config + - name: secrets + secret: + secretName: {{ include "hackagon.fullname" . }}-frontend-secrets \ No newline at end of file diff --git a/helm-chart/templates/frontend-ingress.yaml b/helm-chart/templates/frontend-ingress.yaml new file mode 100644 index 00000000..b72f6e40 --- /dev/null +++ b/helm-chart/templates/frontend-ingress.yaml @@ -0,0 +1,41 @@ +{{- if .Values.frontend.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "hackagon.fullname" . }}-frontend + namespace: {{ .Release.Namespace }} + labels: + {{- include "hackagon.labels" . | nindent 4 }} + app.kubernetes.io/component: frontend + annotations: + {{- with .Values.frontend.ingress.annotations }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + ingressClassName: {{ .Values.frontend.ingress.ingressClass }} + rules: + {{- range .Values.frontend.ingress.hosts }} + - host: {{ .host | replace "{baseDomain}" $.Values.baseDomain | replace "{releaseName}" $.Release.Name }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "hackagon.frontendServiceName" $ }} + port: + number: {{ $.Values.frontend.service.port }} + {{- end }} + {{- end }} + {{- with .Values.frontend.ingress.tls }} + tls: + {{- range . }} + - hosts: + {{- range .hosts }} + - {{ . | replace "{baseDomain}" $.Values.baseDomain | replace "{releaseName}" $.Release.Name }} + {{- end }} + secretName: {{ .secretName | replace "{releaseName}" $.Release.Name }} + {{- end }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/helm-chart/templates/frontend-secrets.yaml b/helm-chart/templates/frontend-secrets.yaml new file mode 100644 index 00000000..ea1636e0 --- /dev/null +++ b/helm-chart/templates/frontend-secrets.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "hackagon.fullname" . }}-frontend-secrets + namespace: {{ .Release.Namespace }} + labels: + {{- include "hackagon.labels" . | nindent 4 }} +type: Opaque +stringData: + secrets.yaml: | + oidc: + clientSecret: {{ .Values.frontendSecrets.clientSecret | required "frontendSecrets.clientSecret is required" | quote }} + authSecret: {{ .Values.frontendSecrets.authSecret | required "frontendSecrets.authSecret is required" | quote }} \ No newline at end of file diff --git a/helm-chart/templates/frontend-service.yaml b/helm-chart/templates/frontend-service.yaml new file mode 100644 index 00000000..cc385948 --- /dev/null +++ b/helm-chart/templates/frontend-service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "hackagon.fullname" . }}-frontend + namespace: {{ .Release.Namespace }} + labels: + {{- include "hackagon.labels" . | nindent 4 }} + app.kubernetes.io/component: frontend +spec: + type: {{ .Values.frontend.service.type }} + ports: + - port: {{ .Values.frontend.service.port }} + targetPort: 3000 + protocol: TCP + name: http + selector: + {{- include "hackagon.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: frontend \ No newline at end of file diff --git a/helm-chart/templates/keycloak-ingress.yaml b/helm-chart/templates/keycloak-ingress.yaml new file mode 100644 index 00000000..2b76cee4 --- /dev/null +++ b/helm-chart/templates/keycloak-ingress.yaml @@ -0,0 +1,28 @@ +{{- $keycloakHost := include "hackagon.keycloakHost" . }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "hackagon.fullname" . }}-keycloak + namespace: {{ .Release.Namespace }} + labels: + {{- include "hackagon.labels" . | nindent 4 }} + app.kubernetes.io/component: keycloak + annotations: + cert-manager.io/cluster-issuer: letsencrypt-production +spec: + ingressClassName: webapprouting.kubernetes.azure.com + rules: + - host: {{ $keycloakHost }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ include "hackagon.keycloakServiceName" . }} + port: + number: 8080 + tls: + - hosts: + - {{ $keycloakHost }} + secretName: {{ include "hackagon.fullname" . }}-keycloak-tls \ No newline at end of file diff --git a/helm-chart/templates/keycloak-init-configmap.yaml b/helm-chart/templates/keycloak-init-configmap.yaml new file mode 100644 index 00000000..78f2f04c --- /dev/null +++ b/helm-chart/templates/keycloak-init-configmap.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: hackagon-keycloak-init + namespace: {{ .Release.Namespace }} + labels: + {{- include "hackagon.labels" . | nindent 4 }} +data: + 01-create-keycloak-db.sql: | + CREATE USER keycloak WITH PASSWORD '{{ .Values.keycloak.database.external.password | required "keycloak.database.external.password is required" }}'; + CREATE DATABASE keycloak OWNER keycloak; + GRANT ALL PRIVILEGES ON DATABASE keycloak TO keycloak; + CREATE USER hackagon WITH PASSWORD '{{ .Values.backend.config.database.postgresPassword | required "backend.config.database.postgresPassword" }}'; + CREATE DATABASE hackagon OWNER hackagon; + GRANT ALL PRIVILEGES ON DATABASE hackagon TO hackagon; diff --git a/helm-chart/templates/keycloak-realm-configmap.yaml b/helm-chart/templates/keycloak-realm-configmap.yaml new file mode 100644 index 00000000..dbf29636 --- /dev/null +++ b/helm-chart/templates/keycloak-realm-configmap.yaml @@ -0,0 +1,17 @@ +{{- $host := .Values.keycloak.database.external.host | required "keycloak.database.external.host is required (e.g. \"hackagon-postgresql\")" -}} +{{- $keycloakPassword := .Values.keycloak.database.external.password | required "keycloak.database.external.password is required" -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "hackagon.fullname" . }}-realm + namespace: {{ .Release.Namespace }} + labels: + {{- include "hackagon.labels" . | nindent 4 }} +data: + hackagon.json: | + {{- $frontendHost := include "hackagon.frontendHost" . }} + {{- $raw := .Values.realmJson | default "" }} + {{- $processed := $raw | replace "http://localhost:8081/*##http://localhost:8081/auth/callback/keycloak" (printf "%s/*##%s/auth/callback/keycloak" $frontendHost $frontendHost) }} + {{- $processed = $processed | replace "http://localhost:8081/*" (printf "%s/*" $frontendHost) }} + {{- $processed = $processed | replace "http://localhost:8081" $frontendHost }} + {{- $processed | nindent 4 }} \ No newline at end of file diff --git a/helm-chart/values.yaml b/helm-chart/values.yaml index 88026efd..7a3a76bd 100644 --- a/helm-chart/values.yaml +++ b/helm-chart/values.yaml @@ -1,161 +1,192 @@ -# Default values for helm-chart. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. - -# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ -replicaCount: 1 - -# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ -image: - repository: nginx - # This sets the pull policy for images. - pullPolicy: IfNotPresent - # Overrides the image tag whose default is the chart appVersion. - 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 +# -- Base domain for all external services +baseDomain: "example.com" + +# -- Keycloak realm JSON (pass via --set-file: --set-file realmJson=@./tools/configs/keycloak/realm-hackagon.json) +# realmJson: "" + +# ============================================================ +# Frontend +# ============================================================ +frontend: + image: + repository: ghcr.io/swissdatasciencecenter/hackagon/temporary/frontend-service + tag: "latest" + pullPolicy: IfNotPresent + + replicaCount: 1 + annotations: + nginx.ingress.kubernetes.io/client-header-buffer-size: 16k + nginx.ingress.kubernetes.io/large-client-header-buffers: 4 16k + nginx.ingress.kubernetes.io/proxy-buffer-size: 16k + nginx.ingress.kubernetes.io/proxy-buffers: 8 16k + nginx.ingress.kubernetes.io/proxy_busy_buffers_size: 32k + nginx.ingress.kubernetes.io/ssl-redirect: "true" + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + + # -- Frontend config.yaml content (non-sensitive settings) + config: + log: + forceDevLog: false + backend: + hostname: hackagon-backend + port: 3000 + cookies: + useSecure: true + oidc: + clientId: hackagon-frontend + issuer: "https://auth.{baseDomain}/realms/hackagon" + audience: hackagon-backend + + service: + type: ClusterIP + port: 8080 + + ingress: + enabled: true + ingressClass: "webapprouting.kubernetes.azure.com" + annotations: + cert-manager.io/cluster-issuer: letsencrypt-production + hosts: + - host: "app.{baseDomain}" + paths: + - path: / + pathType: Prefix + tls: + - secretName: "{releaseName}-frontend-tls" + hosts: + - "app.{baseDomain}" + +# ============================================================ +# Backend +# ============================================================ +backend: + image: + repository: ghcr.io/swissdatasciencecenter/hackagon/temporary/backend-service + tag: "latest" + pullPolicy: IfNotPresent + + replicaCount: 1 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: "" - -# 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: {} - -podSecurityContext: {} - # fsGroup: 2000 - -securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 - -# 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 - port: 80 - -# 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 - -# -- Expose the service via gateway-api HTTPRoute -# Requires Gateway API resources and suitable controller installed within the cluster -# (see: https://gateway-api.sigs.k8s.io/guides/) -httpRoute: - # HTTPRoute enabled. - enabled: false - # HTTPRoute annotations. - annotations: {} - # Which Gateways this Route is attached to. - parentRefs: - - name: gateway - sectionName: http - # namespace: default - # Hostnames matching HTTP header. - hostnames: - - chart-example.local - # List of rules and filters applied. - rules: - - matches: - - path: - type: PathPrefix - value: /headers - # filters: - # - type: RequestHeaderModifier - # requestHeaderModifier: - # set: - # - name: My-Overwrite-Header - # value: this-is-the-only-value - # remove: - # - User-Agent - # - matches: - # - path: - # type: PathPrefix - # value: /echo - # headers: - # - name: version - # value: v2 - -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: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi - -# 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: / - port: http -readinessProbe: - httpGet: - path: / - port: http - -# 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: {} + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 1000m + memory: 512Mi + + # -- Backend config.yaml content + config: + server: + port: "3000" + adminemail: "admin@{baseDomain}" + adminkeycloakid: "" + database: + driver: postgres + host: "" # Auto-generated from release name in template + port: 5432 + dbname: hackagon + user: hackagon + postgresPassword: "" + oidc: + jwksurl: "https://auth.{baseDomain}/realms/hackagon/protocol/openid-connect/certs" + issuerurl: "https://auth.{baseDomain}/realms/hackagon" + algorithm: RS256 + logging: + level: info + + service: + type: ClusterIP + port: 3000 + +# ============================================================ +# Keycloak (helmforge) +# ============================================================ +keycloak: + enabled: true + replicaCount: 1 + + # -- Production mode requires hostname and database + mode: production + + # -- Hostname for Keycloak (public admin UI) + hostname: + hostname: "" # e.g. "https://auth.{baseDomain}" + + # -- Admin credentials (password from realm JSON, but username needed) + admin: + username: hackagon-admin + + # -- External database (reuse the same postgres instance) + database: + external: + vendor: postgres + host: "" # Required: set to -postgresql (e.g. "hackagon-postgresql") + port: 5432 + database: keycloak + user: keycloak + password: "" + + # -- Realm import from ConfigMap + realmImport: + enabled: true + existingConfigMap: hackagon-realm + + # -- Persistence (for Keycloak data, not DB) + persistence: + enabled: true + size: 10Gi + + # -- Resources + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: 1000m + memory: 1Gi + + # -- Proxy configuration (behind reverse proxy) + proxy: + headers: xforwarded + + # -- Ingress for Keycloak (managed by parent chart) + ingress: + enabled: false + +# ============================================================ +# PostgreSQL (bitnami) — two databases, two users +# ============================================================ +postgresql: + enabled: true + auth: + username: postgres + database: postgres + postgresPassword: "" + + primary: + persistence: + enabled: true + size: 10Gi + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + initdb: + scriptsConfigMap: hackagon-keycloak-init + +# ============================================================ +# Frontend OIDC secrets +# ============================================================ +frontendSecrets: + clientSecret: "" + authSecret: "" diff --git a/tools/nix/hackagon/pkgs/treefmt.nix b/tools/nix/hackagon/pkgs/treefmt.nix index 835d6c1c..07a0cb0a 100644 --- a/tools/nix/hackagon/pkgs/treefmt.nix +++ b/tools/nix/hackagon/pkgs/treefmt.nix @@ -32,6 +32,7 @@ _: { "*/api/openapi*" # this are symlinks, which prettier cannot deal with ".golangci.yaml" # this is a symlink, which prettier cannot deal with ".yamllint.yaml" # this is a symlink, which prettier cannot deal with + "helm-chart/*" # helm charts are not real yaml, they are go templates ]; programs.ruff-format.enable = true;