diff --git a/.github/workflows/helm-test.yml b/.github/workflows/helm-test.yml index fa3a14c..f73cd52 100644 --- a/.github/workflows/helm-test.yml +++ b/.github/workflows/helm-test.yml @@ -19,40 +19,35 @@ permissions: jobs: chart-test: - name: Lint & Install (kind) + name: Lint & Validate runs-on: ubuntu-latest permissions: contents: read + env: + KUBECONFORM_VERSION: v0.6.7 + KUBERNETES_VERSION: "1.31.0" + steps: - name: Checkout uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Set up Helm uses: azure/setup-helm@v4 with: version: "v3.16.4" - - name: Set up chart-testing (ct) - uses: helm/chart-testing-action@v2 - - - name: Run chart linting - run: ct lint --chart-dirs deploy/helm --all + - name: Helm lint + run: helm lint deploy/helm/ledger - - name: Create kind cluster - uses: helm/kind-action@v1 - with: - cluster_name: fincore-test - - - name: Run chart-testing (install) - run: ct install --chart-dirs deploy/helm --all --debug + - name: Install kubeconform + run: | + curl -sSL "https://github.com/yannh/kubeconform/releases/download/${KUBECONFORM_VERSION}/kubeconform-linux-amd64.tar.gz" \ + | tar -xz kubeconform + sudo mv kubeconform /usr/local/bin/ - - name: Helm template render check + - name: Helm template and validate manifests run: | - helm template fincore-test deploy/helm/fincore-engine \ - --set global.image.tag=ci \ - --debug \ - > /dev/null + helm template ledger deploy/helm/ledger --set image.tag=test \ + | kubeconform -strict -summary -kubernetes-version "$KUBERNETES_VERSION" diff --git a/deploy/helm/ledger/.helmignore b/deploy/helm/ledger/.helmignore new file mode 100644 index 0000000..9494725 --- /dev/null +++ b/deploy/helm/ledger/.helmignore @@ -0,0 +1,6 @@ +.DS_Store +.git/ +.gitignore +*.tmp +*.bak +*.swp diff --git a/deploy/helm/ledger/Chart.yaml b/deploy/helm/ledger/Chart.yaml new file mode 100644 index 0000000..12da04f --- /dev/null +++ b/deploy/helm/ledger/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v2 +name: ledger +description: FinCore ledger service, a double-entry accounting core +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/ledger/templates/NOTES.txt b/deploy/helm/ledger/templates/NOTES.txt new file mode 100644 index 0000000..f2797ec --- /dev/null +++ b/deploy/helm/ledger/templates/NOTES.txt @@ -0,0 +1,14 @@ +The ledger service has been deployed as release {{ .Release.Name }}. + +Service (ClusterIP) reachable in-cluster at: + {{ include "ledger.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.port }} + +Probe a pod locally with port-forward: + kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ include "ledger.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 }}" and "{{ .Values.secretKeys.keycloakClientSecret }}" before running against a real database. +{{- end }} diff --git a/deploy/helm/ledger/templates/_helpers.tpl b/deploy/helm/ledger/templates/_helpers.tpl new file mode 100644 index 0000000..c0d1de5 --- /dev/null +++ b/deploy/helm/ledger/templates/_helpers.tpl @@ -0,0 +1,35 @@ +{{- define "ledger.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "ledger.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name (include "ledger.name" .) | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} + +{{- define "ledger.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "ledger.selectorLabels" -}} +app.kubernetes.io/name: {{ include "ledger.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{- define "ledger.labels" -}} +helm.sh/chart: {{ include "ledger.chart" . }} +{{ include "ledger.selectorLabels" . }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{- define "ledger.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} +{{- default (include "ledger.fullname" .) .Values.serviceAccount.name -}} +{{- else -}} +{{- default "default" .Values.serviceAccount.name -}} +{{- end -}} +{{- end -}} diff --git a/deploy/helm/ledger/templates/configmap.yaml b/deploy/helm/ledger/templates/configmap.yaml new file mode 100644 index 0000000..7c11bb4 --- /dev/null +++ b/deploy/helm/ledger/templates/configmap.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "ledger.fullname" . }} + labels: + {{- include "ledger.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 }} + FINCORE_LEDGER_CLEANUP_ENABLED: {{ .Values.config.cleanupEnabled | quote }} + SPRING_DATASOURCE_URL: {{ .Values.config.datasourceUrl | quote }} + SPRING_DATASOURCE_USERNAME: {{ .Values.config.datasourceUsername | quote }} diff --git a/deploy/helm/ledger/templates/deployment.yaml b/deploy/helm/ledger/templates/deployment.yaml new file mode 100644 index 0000000..2ea7869 --- /dev/null +++ b/deploy/helm/ledger/templates/deployment.yaml @@ -0,0 +1,75 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ledger.fullname" . }} + labels: + {{- include "ledger.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "ledger.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "ledger.labels" . | nindent 8 }} + spec: + serviceAccountName: {{ include "ledger.serviceAccountName" . }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: ledger + 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 "ledger.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 }} + {{- 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/ledger/templates/hpa.yaml b/deploy/helm/ledger/templates/hpa.yaml new file mode 100644 index 0000000..52997f4 --- /dev/null +++ b/deploy/helm/ledger/templates/hpa.yaml @@ -0,0 +1,22 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "ledger.fullname" . }} + labels: + {{- include "ledger.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "ledger.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/ledger/templates/service.yaml b/deploy/helm/ledger/templates/service.yaml new file mode 100644 index 0000000..2431e84 --- /dev/null +++ b/deploy/helm/ledger/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "ledger.fullname" . }} + labels: + {{- include "ledger.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + selector: + {{- include "ledger.selectorLabels" . | nindent 4 }} diff --git a/deploy/helm/ledger/templates/serviceaccount.yaml b/deploy/helm/ledger/templates/serviceaccount.yaml new file mode 100644 index 0000000..577abe0 --- /dev/null +++ b/deploy/helm/ledger/templates/serviceaccount.yaml @@ -0,0 +1,8 @@ +{{- if .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "ledger.serviceAccountName" . }} + labels: + {{- include "ledger.labels" . | nindent 4 }} +{{- end }} diff --git a/deploy/helm/ledger/values.yaml b/deploy/helm/ledger/values.yaml new file mode 100644 index 0000000..69b67df --- /dev/null +++ b/deploy/helm/ledger/values.yaml @@ -0,0 +1,80 @@ +replicaCount: 1 + +nameOverride: "" +fullnameOverride: "" + +image: + repository: ghcr.io/tiana-code/fincore-ledger + # 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 +# (the prod profile moves actuator to 9090 and would break the 8080 probes). +config: + springProfilesActive: "" + keycloakIssuerUri: "" + otlpTracingEndpoint: "" + cleanupEnabled: "false" + datasourceUrl: "" + datasourceUsername: ledger + +# 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 + +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