From 981b4385b134f2b81b21173f2908448945284403 Mon Sep 17 00:00:00 2001 From: "@tanya_r" Date: Thu, 18 Jun 2026 15:03:05 -0300 Subject: [PATCH] feat(payments): add helm chart Ship a deployment chart for the payments service, mirroring the ledger chart: a read-only-root-filesystem deployment with a /tmp emptyDir, non-root, dropped capabilities, a config map for non-secret env, secretKeyRef injection of the datasource password, Keycloak client secret and webhook HMAC secret from a pre-existing secret, actuator liveness and readiness probes, a v2 autoscaler and a service account. The sandbox bank defaults on (the only provider shipped here) while the dispatcher and retry schedulers default off. Adds the actuator health group config the probes need, and validates the chart in the helm CI matrix. Closes #218 --- .github/workflows/helm-test.yml | 8 +- deploy/helm/payments/.helmignore | 6 ++ deploy/helm/payments/Chart.yaml | 11 +++ deploy/helm/payments/templates/NOTES.txt | 22 +++++ deploy/helm/payments/templates/_helpers.tpl | 35 ++++++++ deploy/helm/payments/templates/configmap.yaml | 16 ++++ .../helm/payments/templates/deployment.yaml | 80 +++++++++++++++++ deploy/helm/payments/templates/hpa.yaml | 22 +++++ deploy/helm/payments/templates/service.yaml | 15 ++++ .../payments/templates/serviceaccount.yaml | 8 ++ deploy/helm/payments/values.yaml | 90 +++++++++++++++++++ .../src/main/resources/application.yml | 35 ++++++++ 12 files changed, 346 insertions(+), 2 deletions(-) create mode 100644 deploy/helm/payments/.helmignore create mode 100644 deploy/helm/payments/Chart.yaml create mode 100644 deploy/helm/payments/templates/NOTES.txt create mode 100644 deploy/helm/payments/templates/_helpers.tpl create mode 100644 deploy/helm/payments/templates/configmap.yaml create mode 100644 deploy/helm/payments/templates/deployment.yaml create mode 100644 deploy/helm/payments/templates/hpa.yaml create mode 100644 deploy/helm/payments/templates/service.yaml create mode 100644 deploy/helm/payments/templates/serviceaccount.yaml create mode 100644 deploy/helm/payments/values.yaml diff --git a/.github/workflows/helm-test.yml b/.github/workflows/helm-test.yml index f73cd52..18a52da 100644 --- a/.github/workflows/helm-test.yml +++ b/.github/workflows/helm-test.yml @@ -25,6 +25,10 @@ jobs: permissions: contents: read + strategy: + matrix: + chart: [ledger, payments] + env: KUBECONFORM_VERSION: v0.6.7 KUBERNETES_VERSION: "1.31.0" @@ -39,7 +43,7 @@ jobs: version: "v3.16.4" - name: Helm lint - run: helm lint deploy/helm/ledger + run: helm lint deploy/helm/${{ matrix.chart }} - name: Install kubeconform run: | @@ -49,5 +53,5 @@ jobs: - name: Helm template and validate manifests run: | - helm template ledger deploy/helm/ledger --set image.tag=test \ + helm template ${{ matrix.chart }} deploy/helm/${{ matrix.chart }} --set image.tag=test \ | kubeconform -strict -summary -kubernetes-version "$KUBERNETES_VERSION" diff --git a/deploy/helm/payments/.helmignore b/deploy/helm/payments/.helmignore new file mode 100644 index 0000000..9494725 --- /dev/null +++ b/deploy/helm/payments/.helmignore @@ -0,0 +1,6 @@ +.DS_Store +.git/ +.gitignore +*.tmp +*.bak +*.swp diff --git a/deploy/helm/payments/Chart.yaml b/deploy/helm/payments/Chart.yaml new file mode 100644 index 0000000..484133a --- /dev/null +++ b/deploy/helm/payments/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v2 +name: payments +description: FinCore payments service, payment orchestration with idempotency and an outbox +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/payments/templates/NOTES.txt b/deploy/helm/payments/templates/NOTES.txt new file mode 100644 index 0000000..1d4d615 --- /dev/null +++ b/deploy/helm/payments/templates/NOTES.txt @@ -0,0 +1,22 @@ +The payments service has been deployed as release {{ .Release.Name }}. + +Service (ClusterIP) reachable in-cluster at: + {{ include "payments.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.port }} + +Probe a pod locally with port-forward: + kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ include "payments.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }} + curl http://localhost:{{ .Values.service.port }}/actuator/health/readiness + +{{- if not .Values.existingSecret }} + +WARNING: existingSecret is empty. Set existingSecret to a Secret holding keys +"{{ .Values.secretKeys.datasourcePassword }}", "{{ .Values.secretKeys.keycloakClientSecret }}" and "{{ .Values.secretKeys.webhookHmacSecret }}" before running against a real database. +{{- end }} + +{{- if eq .Values.config.sandboxBankEnabled "false" }} + +WARNING: config.sandboxBankEnabled is false and no real bank provider ships with +the open-source service. PaymentOrchestrator requires a bank provider bean, so +the pod will fail to start unless you supply one. Keep the sandbox on or provide +a real provider. +{{- end }} diff --git a/deploy/helm/payments/templates/_helpers.tpl b/deploy/helm/payments/templates/_helpers.tpl new file mode 100644 index 0000000..ee2406b --- /dev/null +++ b/deploy/helm/payments/templates/_helpers.tpl @@ -0,0 +1,35 @@ +{{- define "payments.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "payments.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name (include "payments.name" .) | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} + +{{- define "payments.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "payments.selectorLabels" -}} +app.kubernetes.io/name: {{ include "payments.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{- define "payments.labels" -}} +helm.sh/chart: {{ include "payments.chart" . }} +{{ include "payments.selectorLabels" . }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{- define "payments.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} +{{- default (include "payments.fullname" .) .Values.serviceAccount.name -}} +{{- else -}} +{{- default "default" .Values.serviceAccount.name -}} +{{- end -}} +{{- end -}} diff --git a/deploy/helm/payments/templates/configmap.yaml b/deploy/helm/payments/templates/configmap.yaml new file mode 100644 index 0000000..e244025 --- /dev/null +++ b/deploy/helm/payments/templates/configmap.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "payments.fullname" . }} + labels: + {{- include "payments.labels" . | nindent 4 }} +data: + SPRING_PROFILES_ACTIVE: {{ .Values.config.springProfilesActive | quote }} + KEYCLOAK_ISSUER_URI: {{ .Values.config.keycloakIssuerUri | quote }} + OTLP_TRACING_ENDPOINT: {{ .Values.config.otlpTracingEndpoint | quote }} + SPRING_DATASOURCE_URL: {{ .Values.config.datasourceUrl | quote }} + SPRING_DATASOURCE_USERNAME: {{ .Values.config.datasourceUsername | quote }} + FINCORE_PAYMENTS_BANK_SANDBOX_ENABLED: {{ .Values.config.sandboxBankEnabled | quote }} + FINCORE_PAYMENTS_DISPATCHER_ENABLED: {{ .Values.config.dispatcherEnabled | quote }} + FINCORE_PAYMENTS_RETRY_ENABLED: {{ .Values.config.retryEnabled | quote }} + FINCORE_EVENTBUS_BOOTSTRAP_SERVERS: {{ .Values.config.eventbusBootstrapServers | quote }} diff --git a/deploy/helm/payments/templates/deployment.yaml b/deploy/helm/payments/templates/deployment.yaml new file mode 100644 index 0000000..9eae92e --- /dev/null +++ b/deploy/helm/payments/templates/deployment.yaml @@ -0,0 +1,80 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "payments.fullname" . }} + labels: + {{- include "payments.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "payments.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "payments.labels" . | nindent 8 }} + spec: + serviceAccountName: {{ include "payments.serviceAccountName" . }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: payments + 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 + envFrom: + - configMapRef: + name: {{ include "payments.fullname" . }} + {{- if .Values.existingSecret }} + env: + - name: SPRING_DATASOURCE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.existingSecret }} + key: {{ .Values.secretKeys.datasourcePassword }} + - name: KEYCLOAK_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: {{ .Values.existingSecret }} + key: {{ .Values.secretKeys.keycloakClientSecret }} + - name: FINCORE_PAYMENTS_WEBHOOK_HMAC_SECRET + valueFrom: + secretKeyRef: + name: {{ .Values.existingSecret }} + key: {{ .Values.secretKeys.webhookHmacSecret }} + {{- end }} + livenessProbe: + httpGet: + path: /actuator/health/liveness + 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: /actuator/health/readiness + 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 }} + volumeMounts: + - name: tmp + mountPath: /tmp + volumes: + - name: tmp + emptyDir: {} diff --git a/deploy/helm/payments/templates/hpa.yaml b/deploy/helm/payments/templates/hpa.yaml new file mode 100644 index 0000000..2fb58a2 --- /dev/null +++ b/deploy/helm/payments/templates/hpa.yaml @@ -0,0 +1,22 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "payments.fullname" . }} + labels: + {{- include "payments.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "payments.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/payments/templates/service.yaml b/deploy/helm/payments/templates/service.yaml new file mode 100644 index 0000000..daa5bfd --- /dev/null +++ b/deploy/helm/payments/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "payments.fullname" . }} + labels: + {{- include "payments.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + selector: + {{- include "payments.selectorLabels" . | nindent 4 }} diff --git a/deploy/helm/payments/templates/serviceaccount.yaml b/deploy/helm/payments/templates/serviceaccount.yaml new file mode 100644 index 0000000..182e906 --- /dev/null +++ b/deploy/helm/payments/templates/serviceaccount.yaml @@ -0,0 +1,8 @@ +{{- if .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "payments.serviceAccountName" . }} + labels: + {{- include "payments.labels" . | nindent 4 }} +{{- end }} diff --git a/deploy/helm/payments/values.yaml b/deploy/helm/payments/values.yaml new file mode 100644 index 0000000..a3c416d --- /dev/null +++ b/deploy/helm/payments/values.yaml @@ -0,0 +1,90 @@ +replicaCount: 1 + +nameOverride: "" +fullnameOverride: "" + +image: + repository: ghcr.io/tiana-code/fincore-payments + # Defaults to the chart appVersion when empty. + tag: "" + pullPolicy: IfNotPresent + +imagePullSecrets: [] + +serviceAccount: + create: true + name: "" + +service: + type: ClusterIP + port: 8080 + +resources: + requests: + cpu: 250m + memory: 512Mi + limits: + cpu: 1000m + memory: 1Gi + +autoscaling: + enabled: true + minReplicas: 1 + maxReplicas: 10 + targetCPUUtilizationPercentage: 70 + +# Non-secret environment surfaced through the ConfigMap. Probes assume the +# actuator stays on the main 8080 port, so leave springProfilesActive empty +# (a prod profile that moves actuator to 9090 would break the 8080 probes). +# +# sandboxBankEnabled defaults true: the sandbox is the only bank provider +# shipped in the open-source service, and PaymentOrchestrator requires a +# provider to start. Supply a real provider and flip this off in private +# deployments. The dispatcher and retry schedulers default off; enable the +# dispatcher only alongside eventbusBootstrapServers. +config: + springProfilesActive: "" + keycloakIssuerUri: "" + otlpTracingEndpoint: "" + datasourceUrl: "" + datasourceUsername: payments + sandboxBankEnabled: "true" + dispatcherEnabled: "false" + retryEnabled: "false" + eventbusBootstrapServers: "" + +# Name of a pre-existing Kubernetes Secret holding the credentials. The chart +# never contains secret values; it only references them. Leave empty to render +# without secret-backed env (for example during template validation). +existingSecret: "" +secretKeys: + datasourcePassword: datasource-password + keycloakClientSecret: keycloak-client-secret + webhookHmacSecret: webhook-hmac-secret + +podSecurityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + seccompProfile: + type: RuntimeDefault + +securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + +probes: + liveness: + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + readiness: + initialDelaySeconds: 10 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 6 diff --git a/services/payments/src/main/resources/application.yml b/services/payments/src/main/resources/application.yml index 4329d30..5646400 100644 --- a/services/payments/src/main/resources/application.yml +++ b/services/payments/src/main/resources/application.yml @@ -1,6 +1,11 @@ +server: + port: 8080 + shutdown: graceful spring: application: name: payments + lifecycle: + timeout-per-shutdown-phase: 30s jpa: hibernate: ddl-auto: none @@ -11,3 +16,33 @@ spring: resourceserver: jwt: issuer-uri: ${KEYCLOAK_ISSUER_URI} +management: + endpoint: + health: + probes: + enabled: true + show-components: always + show-details: never + group: + liveness: + include: livenessState + readiness: + include: readinessState,db + endpoints: + web: + exposure: + include: health,info,prometheus + metrics: + distribution: + percentiles-histogram: + http.server.requests: true + tracing: + sampling: + probability: 0.1 + otlp: + tracing: + endpoint: ${OTLP_TRACING_ENDPOINT:http://localhost:4318/v1/traces} +logging: + structured: + format: + console: logstash