From e044b328c5555114f929e80065aa4d60aee86234 Mon Sep 17 00:00:00 2001 From: "@tanya_r" Date: Fri, 19 Jun 2026 14:23:02 -0300 Subject: [PATCH] feat(web): add a helm chart for the sandbox dashboard Package the static nginx dashboard as a Helm chart, mirroring the ledger chart but trimmed for a static SPA: no config, secret or service monitor. The pod runs non-root as the nginx-unprivileged uid with a read-only root filesystem, dropped capabilities and the default seccomp profile, with emptyDir mounts for the paths nginx needs to write under that lockdown. Liveness and readiness probe the served index, the service is a ClusterIP on 8080, and the image defaults to the chart version. The web chart joins the ledger and payments charts in the helm lint plus kubeconform CI matrix, with the service-monitor render step skipped since a static SPA exposes no metrics. Closes #296 --- .github/workflows/helm-test.yml | 3 +- deploy/helm/web/.helmignore | 6 ++ deploy/helm/web/Chart.yaml | 11 ++++ deploy/helm/web/templates/NOTES.txt | 11 ++++ deploy/helm/web/templates/_helpers.tpl | 35 ++++++++++ deploy/helm/web/templates/deployment.yaml | 65 +++++++++++++++++++ deploy/helm/web/templates/hpa.yaml | 22 +++++++ deploy/helm/web/templates/service.yaml | 15 +++++ deploy/helm/web/templates/serviceaccount.yaml | 8 +++ deploy/helm/web/values.yaml | 61 +++++++++++++++++ 10 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 deploy/helm/web/.helmignore create mode 100644 deploy/helm/web/Chart.yaml create mode 100644 deploy/helm/web/templates/NOTES.txt create mode 100644 deploy/helm/web/templates/_helpers.tpl create mode 100644 deploy/helm/web/templates/deployment.yaml create mode 100644 deploy/helm/web/templates/hpa.yaml create mode 100644 deploy/helm/web/templates/service.yaml create mode 100644 deploy/helm/web/templates/serviceaccount.yaml create mode 100644 deploy/helm/web/values.yaml diff --git a/.github/workflows/helm-test.yml b/.github/workflows/helm-test.yml index 16eaa73..5339081 100644 --- a/.github/workflows/helm-test.yml +++ b/.github/workflows/helm-test.yml @@ -27,7 +27,7 @@ jobs: strategy: matrix: - chart: [ledger, payments] + chart: [ledger, payments, web] env: KUBECONFORM_VERSION: v0.6.7 @@ -57,6 +57,7 @@ jobs: | kubeconform -strict -summary -kubernetes-version "$KUBERNETES_VERSION" - name: Validate the ServiceMonitor renders (operator CRD skipped) + if: matrix.chart != 'web' run: | rendered=$(helm template ${{ matrix.chart }} deploy/helm/${{ matrix.chart }} \ --set image.tag=test --set serviceMonitor.enabled=true) diff --git a/deploy/helm/web/.helmignore b/deploy/helm/web/.helmignore new file mode 100644 index 0000000..9494725 --- /dev/null +++ b/deploy/helm/web/.helmignore @@ -0,0 +1,6 @@ +.DS_Store +.git/ +.gitignore +*.tmp +*.bak +*.swp diff --git a/deploy/helm/web/Chart.yaml b/deploy/helm/web/Chart.yaml new file mode 100644 index 0000000..6612404 --- /dev/null +++ b/deploy/helm/web/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v2 +name: web +description: FinCore sandbox dashboard, a static React SPA served by nginx +type: application +version: 0.1.0 +appVersion: "0.1.0" +home: https://github.com/tiana-code/fincore-engine +sources: + - https://github.com/tiana-code/fincore-engine +maintainers: + - name: FinCore Engine Authors diff --git a/deploy/helm/web/templates/NOTES.txt b/deploy/helm/web/templates/NOTES.txt new file mode 100644 index 0000000..c1cf4dd --- /dev/null +++ b/deploy/helm/web/templates/NOTES.txt @@ -0,0 +1,11 @@ +The sandbox dashboard has been deployed as release {{ .Release.Name }}. + +Service (ClusterIP) reachable in-cluster at: + {{ include "web.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.port }} + +Open it locally with port-forward: + kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ include "web.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }} + open http://localhost:{{ .Values.service.port }}/ + +The SPA reaches the backend via the API base URL baked at image build time; point it at the +ledger/payments/compliance/decision services for your environment. diff --git a/deploy/helm/web/templates/_helpers.tpl b/deploy/helm/web/templates/_helpers.tpl new file mode 100644 index 0000000..7a75c25 --- /dev/null +++ b/deploy/helm/web/templates/_helpers.tpl @@ -0,0 +1,35 @@ +{{- define "web.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "web.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name (include "web.name" .) | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} + +{{- define "web.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "web.selectorLabels" -}} +app.kubernetes.io/name: {{ include "web.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{- define "web.labels" -}} +helm.sh/chart: {{ include "web.chart" . }} +{{ include "web.selectorLabels" . }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{- define "web.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} +{{- default (include "web.fullname" .) .Values.serviceAccount.name -}} +{{- else -}} +{{- default "default" .Values.serviceAccount.name -}} +{{- end -}} +{{- end -}} diff --git a/deploy/helm/web/templates/deployment.yaml b/deploy/helm/web/templates/deployment.yaml new file mode 100644 index 0000000..f288acd --- /dev/null +++ b/deploy/helm/web/templates/deployment.yaml @@ -0,0 +1,65 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "web.fullname" . }} + labels: + {{- include "web.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "web.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "web.labels" . | nindent 8 }} + spec: + serviceAccountName: {{ include "web.serviceAccountName" . }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: web + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }} + periodSeconds: {{ .Values.probes.liveness.periodSeconds }} + timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }} + failureThreshold: {{ .Values.probes.liveness.failureThreshold }} + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }} + periodSeconds: {{ .Values.probes.readiness.periodSeconds }} + timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }} + failureThreshold: {{ .Values.probes.readiness.failureThreshold }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + # nginx-unprivileged writes its pid and temp files under these paths; with a + # read-only root filesystem they must be writable emptyDir mounts or the pod CrashLoops. + volumeMounts: + - name: tmp + mountPath: /tmp + - name: nginx-cache + mountPath: /var/cache/nginx + volumes: + - name: tmp + emptyDir: {} + - name: nginx-cache + emptyDir: {} diff --git a/deploy/helm/web/templates/hpa.yaml b/deploy/helm/web/templates/hpa.yaml new file mode 100644 index 0000000..04f9468 --- /dev/null +++ b/deploy/helm/web/templates/hpa.yaml @@ -0,0 +1,22 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "web.fullname" . }} + labels: + {{- include "web.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "web.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} +{{- end }} diff --git a/deploy/helm/web/templates/service.yaml b/deploy/helm/web/templates/service.yaml new file mode 100644 index 0000000..77a5b4b --- /dev/null +++ b/deploy/helm/web/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "web.fullname" . }} + labels: + {{- include "web.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + selector: + {{- include "web.selectorLabels" . | nindent 4 }} diff --git a/deploy/helm/web/templates/serviceaccount.yaml b/deploy/helm/web/templates/serviceaccount.yaml new file mode 100644 index 0000000..cc3869d --- /dev/null +++ b/deploy/helm/web/templates/serviceaccount.yaml @@ -0,0 +1,8 @@ +{{- if .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "web.serviceAccountName" . }} + labels: + {{- include "web.labels" . | nindent 4 }} +{{- end }} diff --git a/deploy/helm/web/values.yaml b/deploy/helm/web/values.yaml new file mode 100644 index 0000000..1aedbdd --- /dev/null +++ b/deploy/helm/web/values.yaml @@ -0,0 +1,61 @@ +replicaCount: 1 + +nameOverride: "" +fullnameOverride: "" + +image: + repository: ghcr.io/tiana-code/fincore-web + # Defaults to the chart appVersion when empty. + tag: "" + pullPolicy: IfNotPresent + +imagePullSecrets: [] + +serviceAccount: + create: true + name: "" + +service: + type: ClusterIP + port: 8080 + +resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 250m + memory: 128Mi + +autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 5 + targetCPUUtilizationPercentage: 70 + +podSecurityContext: + runAsNonRoot: true + runAsUser: 101 + runAsGroup: 101 + fsGroup: 101 + seccompProfile: + type: RuntimeDefault + +securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + +probes: + liveness: + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + readiness: + initialDelaySeconds: 3 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3