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