From 1a0b04feecd9f56141e800959af2a3f6074d57f2 Mon Sep 17 00:00:00 2001 From: keyhan Date: Wed, 22 Apr 2026 16:43:29 +0330 Subject: [PATCH] feat(helm): add cloudhost-app Helm chart for all runtimes - deployment.yaml with imagePullSecrets, health probes, WordPress volumes - db-deployment.yaml for PostgreSQL/MySQL with readiness/liveness probes - ingress.yaml with cert-manager TLS - registry-pull-secret.yaml for insecure registries - PVCs and secrets with helm.sh/resource-policy: keep - _helpers.tpl with shared template functions - values.yaml with comprehensive defaults --- backend/helm/cloudhost-app/Chart.yaml | 6 + .../helm/cloudhost-app/templates/_helpers.tpl | 81 ++++++++++++ .../templates/db-deployment.yaml | 102 ++++++++++++++ .../helm/cloudhost-app/templates/db-pvc.yaml | 20 +++ .../cloudhost-app/templates/db-secret.yaml | 17 +++ .../cloudhost-app/templates/db-service.yaml | 21 +++ .../cloudhost-app/templates/deployment.yaml | 124 ++++++++++++++++++ .../helm/cloudhost-app/templates/ingress.yaml | 31 +++++ .../templates/registry-pull-secret.yaml | 13 ++ .../helm/cloudhost-app/templates/secret.yaml | 16 +++ .../helm/cloudhost-app/templates/service.yaml | 17 +++ .../helm/cloudhost-app/templates/wp-pvc.yaml | 19 +++ backend/helm/cloudhost-app/values.yaml | 59 +++++++++ 13 files changed, 526 insertions(+) create mode 100644 backend/helm/cloudhost-app/Chart.yaml create mode 100644 backend/helm/cloudhost-app/templates/_helpers.tpl create mode 100644 backend/helm/cloudhost-app/templates/db-deployment.yaml create mode 100644 backend/helm/cloudhost-app/templates/db-pvc.yaml create mode 100644 backend/helm/cloudhost-app/templates/db-secret.yaml create mode 100644 backend/helm/cloudhost-app/templates/db-service.yaml create mode 100644 backend/helm/cloudhost-app/templates/deployment.yaml create mode 100644 backend/helm/cloudhost-app/templates/ingress.yaml create mode 100644 backend/helm/cloudhost-app/templates/registry-pull-secret.yaml create mode 100644 backend/helm/cloudhost-app/templates/secret.yaml create mode 100644 backend/helm/cloudhost-app/templates/service.yaml create mode 100644 backend/helm/cloudhost-app/templates/wp-pvc.yaml create mode 100644 backend/helm/cloudhost-app/values.yaml diff --git a/backend/helm/cloudhost-app/Chart.yaml b/backend/helm/cloudhost-app/Chart.yaml new file mode 100644 index 0000000..378d64e --- /dev/null +++ b/backend/helm/cloudhost-app/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: cloudhost-app +description: CloudHost PaaS — generic application chart (Node.js, Laravel, WordPress) +type: application +version: 0.1.0 +appVersion: "1.0.0" diff --git a/backend/helm/cloudhost-app/templates/_helpers.tpl b/backend/helm/cloudhost-app/templates/_helpers.tpl new file mode 100644 index 0000000..b1f818a --- /dev/null +++ b/backend/helm/cloudhost-app/templates/_helpers.tpl @@ -0,0 +1,81 @@ +{{/* +CloudHost App — template helpers +*/}} + +{{/* +Expand the name of the chart. +*/}} +{{- define "cloudhost-app.name" -}} +{{- .Values.app.name | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Namespace for the release. +*/}} +{{- define "cloudhost-app.namespace" -}} +{{- .Values.app.namespace }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "cloudhost-app.labels" -}} +app: {{ include "cloudhost-app.name" . }} +app.kubernetes.io/managed-by: cloudhost-helm +runtime: {{ .Values.app.runtime }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "cloudhost-app.selectorLabels" -}} +app: {{ include "cloudhost-app.name" . }} +{{- end }} + +{{/* +Database name (app name with dashes replaced by underscores) +*/}} +{{- define "cloudhost-app.dbName" -}} +{{- .Values.app.name | replace "-" "_" }} +{{- end }} + +{{/* +Database deployment name +*/}} +{{- define "cloudhost-app.dbDeploymentName" -}} +{{- printf "%s-db" .Values.app.name }} +{{- end }} + +{{/* +Database image — auto-computed from type + version if not explicitly set +*/}} +{{- define "cloudhost-app.dbImage" -}} +{{- if .Values.database.image }} + {{- .Values.database.image }} +{{- else if eq .Values.database.type "postgresql" }} + {{- printf "postgres:%s-alpine" .Values.database.version }} +{{- else }} + {{- printf "mysql:%s" .Values.database.version }} +{{- end }} +{{- end }} + +{{/* +Database port +*/}} +{{- define "cloudhost-app.dbPort" -}} +{{- if eq .Values.database.type "postgresql" }}5432{{- else }}3306{{- end }} +{{- end }} + +{{/* +Database data mount path (volume mount target) +*/}} +{{- define "cloudhost-app.dbDataPath" -}} +{{- if eq .Values.database.type "postgresql" }}/var/lib/postgresql/data{{- else }}/var/lib/mysql{{- end }} +{{- end }} + +{{/* +PostgreSQL PGDATA path — must be a subdirectory of the mount to avoid "initdb: directory not empty" errors +*/}} +{{- define "cloudhost-app.pgDataDir" -}} +/var/lib/postgresql/data/pgdata +{{- end }} diff --git a/backend/helm/cloudhost-app/templates/db-deployment.yaml b/backend/helm/cloudhost-app/templates/db-deployment.yaml new file mode 100644 index 0000000..1752f8f --- /dev/null +++ b/backend/helm/cloudhost-app/templates/db-deployment.yaml @@ -0,0 +1,102 @@ +{{- if .Values.database.enabled }} +{{- $name := include "cloudhost-app.name" . -}} +{{- $ns := include "cloudhost-app.namespace" . -}} +{{- $dbName := include "cloudhost-app.dbDeploymentName" . -}} +{{- $dbPort := include "cloudhost-app.dbPort" . -}} +{{- $dbData := include "cloudhost-app.dbDataPath" . -}} +{{- $dbImg := include "cloudhost-app.dbImage" . -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ $dbName }} + namespace: {{ $ns }} + labels: + app: {{ $dbName }} + {{- include "cloudhost-app.labels" . | nindent 4 }} +spec: + replicas: 1 + selector: + matchLabels: + app: {{ $dbName }} + template: + metadata: + labels: + app: {{ $dbName }} + spec: + containers: + - name: {{ $dbName }} + image: {{ $dbImg }} + ports: + - containerPort: {{ include "cloudhost-app.dbPort" . | int }} + env: + {{- if eq .Values.database.type "postgresql" }} + - name: PGDATA + value: {{ include "cloudhost-app.pgDataDir" . }} + - name: POSTGRES_DB + value: {{ include "cloudhost-app.dbName" . }} + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: {{ $name }}-db-secret + key: username + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $name }}-db-secret + key: password + {{- else }} + - name: MYSQL_DATABASE + value: {{ include "cloudhost-app.dbName" . }} + - name: MYSQL_USER + valueFrom: + secretKeyRef: + name: {{ $name }}-db-secret + key: username + - name: MYSQL_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $name }}-db-secret + key: password + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $name }}-db-secret + key: password + {{- end }} + volumeMounts: + - name: db-storage + mountPath: {{ $dbData }} + resources: + requests: + cpu: {{ .Values.database.resources.cpuRequest | quote }} + memory: {{ .Values.database.resources.memoryRequest | quote }} + limits: + cpu: {{ .Values.database.resources.cpuLimit | quote }} + memory: {{ .Values.database.resources.memoryLimit | quote }} + readinessProbe: + {{- if eq .Values.database.type "postgresql" }} + exec: + command: ["pg_isready", "-U", {{ .Values.database.username | quote }}] + {{- else }} + exec: + command: ["mysqladmin", "ping", "-h", "127.0.0.1"] + {{- end }} + initialDelaySeconds: 10 + periodSeconds: 5 + failureThreshold: 6 + livenessProbe: + {{- if eq .Values.database.type "postgresql" }} + exec: + command: ["pg_isready", "-U", {{ .Values.database.username | quote }}] + {{- else }} + exec: + command: ["mysqladmin", "ping", "-h", "127.0.0.1"] + {{- end }} + initialDelaySeconds: 30 + periodSeconds: 10 + failureThreshold: 5 + volumes: + - name: db-storage + persistentVolumeClaim: + claimName: {{ $dbName }} +{{- end }} diff --git a/backend/helm/cloudhost-app/templates/db-pvc.yaml b/backend/helm/cloudhost-app/templates/db-pvc.yaml new file mode 100644 index 0000000..3d8a35f --- /dev/null +++ b/backend/helm/cloudhost-app/templates/db-pvc.yaml @@ -0,0 +1,20 @@ +{{- if .Values.database.enabled }} +{{- $ns := include "cloudhost-app.namespace" . -}} +{{- $dbName := include "cloudhost-app.dbDeploymentName" . -}} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ $dbName }} + namespace: {{ $ns }} + labels: + app: {{ $dbName }} + {{- include "cloudhost-app.labels" . | nindent 4 }} + annotations: + "helm.sh/resource-policy": keep +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.database.storageSize | quote }} +{{- end }} diff --git a/backend/helm/cloudhost-app/templates/db-secret.yaml b/backend/helm/cloudhost-app/templates/db-secret.yaml new file mode 100644 index 0000000..3834f1b --- /dev/null +++ b/backend/helm/cloudhost-app/templates/db-secret.yaml @@ -0,0 +1,17 @@ +{{- if .Values.database.enabled }} +{{- $name := include "cloudhost-app.name" . -}} +{{- $ns := include "cloudhost-app.namespace" . -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ $name }}-db-secret + namespace: {{ $ns }} + labels: + {{- include "cloudhost-app.labels" . | nindent 4 }} + annotations: + "helm.sh/resource-policy": keep +type: Opaque +data: + username: {{ .Values.database.username | b64enc | quote }} + password: {{ .Values.database.password | b64enc | quote }} +{{- end }} diff --git a/backend/helm/cloudhost-app/templates/db-service.yaml b/backend/helm/cloudhost-app/templates/db-service.yaml new file mode 100644 index 0000000..77ee18f --- /dev/null +++ b/backend/helm/cloudhost-app/templates/db-service.yaml @@ -0,0 +1,21 @@ +{{- if .Values.database.enabled }} +{{- $ns := include "cloudhost-app.namespace" . -}} +{{- $dbName := include "cloudhost-app.dbDeploymentName" . -}} +{{- $dbPort := include "cloudhost-app.dbPort" . -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ $dbName }} + namespace: {{ $ns }} + labels: + app: {{ $dbName }} + {{- include "cloudhost-app.labels" . | nindent 4 }} +spec: + type: ClusterIP + selector: + app: {{ $dbName }} + ports: + - port: {{ include "cloudhost-app.dbPort" . | int }} + targetPort: {{ include "cloudhost-app.dbPort" . | int }} + protocol: TCP +{{- end }} diff --git a/backend/helm/cloudhost-app/templates/deployment.yaml b/backend/helm/cloudhost-app/templates/deployment.yaml new file mode 100644 index 0000000..24718d7 --- /dev/null +++ b/backend/helm/cloudhost-app/templates/deployment.yaml @@ -0,0 +1,124 @@ +{{- $name := include "cloudhost-app.name" . -}} +{{- $ns := include "cloudhost-app.namespace" . -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ $name }} + namespace: {{ $ns }} + labels: + {{- include "cloudhost-app.labels" . | nindent 4 }} + annotations: + kubernetes.io/change-cause: {{ .Values.changeCause | default (printf "Deploy %s" .Values.app.image) | quote }} +spec: + revisionHistoryLimit: 10 + replicas: {{ .Values.app.replicas }} + selector: + matchLabels: + {{- include "cloudhost-app.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "cloudhost-app.labels" . | nindent 8 }} + spec: + imagePullSecrets: + - name: registry-pull-secret + containers: + - name: {{ $name }} + image: {{ .Values.app.image | quote }} + imagePullPolicy: Always + ports: + - containerPort: {{ .Values.app.port }} + {{- if and .Values.envVars (gt (len .Values.envVars) 0) }} + envFrom: + - secretRef: + name: {{ $name }}-env + {{- end }} + env: + {{- if and .Values.database.enabled (eq .Values.database.type "postgresql") }} + - name: DB_HOST + value: {{ include "cloudhost-app.dbDeploymentName" . }} + - name: DB_PORT + value: "5432" + - name: DB_NAME + value: {{ include "cloudhost-app.dbName" . }} + - name: DB_USER + valueFrom: + secretKeyRef: + name: {{ $name }}-db-secret + key: username + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $name }}-db-secret + key: password + - name: DATABASE_URL + value: "postgresql://$(DB_USER):$(DB_PASSWORD)@{{ include "cloudhost-app.dbDeploymentName" . }}:5432/{{ include "cloudhost-app.dbName" . }}" + {{- end }} + {{- if and .Values.database.enabled (eq .Values.database.type "mysql") }} + - name: DB_HOST + value: {{ include "cloudhost-app.dbDeploymentName" . }} + - name: DB_PORT + value: "3306" + - name: DB_NAME + value: {{ include "cloudhost-app.dbName" . }} + - name: DB_USER + valueFrom: + secretKeyRef: + name: {{ $name }}-db-secret + key: username + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $name }}-db-secret + key: password + - name: DATABASE_URL + value: "mysql://$(DB_USER):$(DB_PASSWORD)@{{ include "cloudhost-app.dbDeploymentName" . }}:3306/{{ include "cloudhost-app.dbName" . }}" + {{- end }} + {{- /* WordPress-specific env vars (official image expects these) */}} + {{- if and .Values.wordpress.enabled .Values.database.enabled }} + - name: WORDPRESS_DB_HOST + value: "{{ include "cloudhost-app.dbDeploymentName" . }}:{{ include "cloudhost-app.dbPort" . }}" + - name: WORDPRESS_DB_NAME + value: {{ include "cloudhost-app.dbName" . }} + - name: WORDPRESS_DB_USER + valueFrom: + secretKeyRef: + name: {{ $name }}-db-secret + key: username + - name: WORDPRESS_DB_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $name }}-db-secret + key: password + - name: WORDPRESS_TABLE_PREFIX + value: "wp_" + {{- end }} + resources: + requests: + cpu: {{ .Values.resources.cpuRequest | quote }} + memory: {{ .Values.resources.memoryRequest | quote }} + limits: + cpu: {{ .Values.resources.cpuLimit | quote }} + memory: {{ .Values.resources.memoryLimit | quote }} + readinessProbe: + tcpSocket: + port: {{ .Values.app.port }} + initialDelaySeconds: 5 + periodSeconds: 5 + livenessProbe: + tcpSocket: + port: {{ .Values.app.port }} + initialDelaySeconds: 15 + periodSeconds: 10 + failureThreshold: 5 + {{- if .Values.wordpress.enabled }} + volumeMounts: + - name: wp-content + mountPath: /var/www/html/wp-content + {{- end }} + {{- if .Values.wordpress.enabled }} + volumes: + - name: wp-content + persistentVolumeClaim: + claimName: {{ $name }}-wp-content + {{- end }} diff --git a/backend/helm/cloudhost-app/templates/ingress.yaml b/backend/helm/cloudhost-app/templates/ingress.yaml new file mode 100644 index 0000000..ed1167f --- /dev/null +++ b/backend/helm/cloudhost-app/templates/ingress.yaml @@ -0,0 +1,31 @@ +{{- if .Values.ingress.enabled }} +{{- $name := include "cloudhost-app.name" . -}} +{{- $ns := include "cloudhost-app.namespace" . -}} +{{- $host := printf "%s.%s" (default $name .Values.ingress.subdomain) .Values.ingress.domain -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $name }} + namespace: {{ $ns }} + labels: + {{- include "cloudhost-app.labels" . | nindent 4 }} + annotations: + cert-manager.io/cluster-issuer: {{ .Values.ingress.clusterIssuer | quote }} +spec: + ingressClassName: nginx + rules: + - host: {{ $host }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ $name }} + port: + number: 80 + tls: + - hosts: + - {{ $host }} + secretName: {{ $name }}-tls +{{- end }} diff --git a/backend/helm/cloudhost-app/templates/registry-pull-secret.yaml b/backend/helm/cloudhost-app/templates/registry-pull-secret.yaml new file mode 100644 index 0000000..2ef9f22 --- /dev/null +++ b/backend/helm/cloudhost-app/templates/registry-pull-secret.yaml @@ -0,0 +1,13 @@ +{{- $name := include "cloudhost-app.name" . -}} +{{- $ns := include "cloudhost-app.namespace" . -}} +{{- $registryUrl := .Values.registry.url | default "localhost:30500" -}} +apiVersion: v1 +kind: Secret +metadata: + name: registry-pull-secret + namespace: {{ $ns }} + labels: + {{- include "cloudhost-app.labels" . | nindent 4 }} +type: kubernetes.io/dockerconfigjson +data: + .dockerconfigjson: {{ printf `{"auths":{"%s":{"auth":""}}}` $registryUrl | b64enc | quote }} diff --git a/backend/helm/cloudhost-app/templates/secret.yaml b/backend/helm/cloudhost-app/templates/secret.yaml new file mode 100644 index 0000000..71e283f --- /dev/null +++ b/backend/helm/cloudhost-app/templates/secret.yaml @@ -0,0 +1,16 @@ +{{- if and .Values.envVars (gt (len .Values.envVars) 0) }} +{{- $name := include "cloudhost-app.name" . -}} +{{- $ns := include "cloudhost-app.namespace" . -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ $name }}-env + namespace: {{ $ns }} + labels: + {{- include "cloudhost-app.labels" . | nindent 4 }} +type: Opaque +data: + {{- range $key, $val := .Values.envVars }} + {{ $key }}: {{ $val | b64enc | quote }} + {{- end }} +{{- end }} diff --git a/backend/helm/cloudhost-app/templates/service.yaml b/backend/helm/cloudhost-app/templates/service.yaml new file mode 100644 index 0000000..56c53fb --- /dev/null +++ b/backend/helm/cloudhost-app/templates/service.yaml @@ -0,0 +1,17 @@ +{{- $name := include "cloudhost-app.name" . -}} +{{- $ns := include "cloudhost-app.namespace" . -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ $name }} + namespace: {{ $ns }} + labels: + {{- include "cloudhost-app.labels" . | nindent 4 }} +spec: + type: ClusterIP + selector: + {{- include "cloudhost-app.selectorLabels" . | nindent 4 }} + ports: + - port: 80 + targetPort: {{ .Values.app.port }} + protocol: TCP diff --git a/backend/helm/cloudhost-app/templates/wp-pvc.yaml b/backend/helm/cloudhost-app/templates/wp-pvc.yaml new file mode 100644 index 0000000..0e4860b --- /dev/null +++ b/backend/helm/cloudhost-app/templates/wp-pvc.yaml @@ -0,0 +1,19 @@ +{{- if .Values.wordpress.enabled }} +{{- $name := include "cloudhost-app.name" . -}} +{{- $ns := include "cloudhost-app.namespace" . -}} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ $name }}-wp-content + namespace: {{ $ns }} + labels: + {{- include "cloudhost-app.labels" . | nindent 4 }} + annotations: + "helm.sh/resource-policy": keep +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.wordpress.wpContentStorageSize | quote }} +{{- end }} diff --git a/backend/helm/cloudhost-app/values.yaml b/backend/helm/cloudhost-app/values.yaml new file mode 100644 index 0000000..d01c62b --- /dev/null +++ b/backend/helm/cloudhost-app/values.yaml @@ -0,0 +1,59 @@ +# ──────────────────────────────────────────────────────────── +# CloudHost Application — default Helm values +# These are overridden per-app at install/upgrade time. +# ──────────────────────────────────────────────────────────── + +# ── Application ────────────────────────────────────────── +app: + name: my-app + namespace: user-default + runtime: nodejs # nodejs | laravel | wordpress + image: "" # e.g. registry.example.com/my-app:v1 + port: 3000 + replicas: 1 + +# ── Resources ──────────────────────────────────────────── +resources: + cpuRequest: "100m" + cpuLimit: "500m" + memoryRequest: "128Mi" + memoryLimit: "512Mi" + +# ── Environment variables (injected via Secret) ───────── +envVars: {} + # KEY: value + +# ── Ingress ────────────────────────────────────────────── +ingress: + enabled: true + subdomain: "" # . + domain: "apps.cloudhost.ir" + clusterIssuer: letsencrypt-prod + +# ── Database (MySQL or PostgreSQL) ─────────────────────── +database: + enabled: false + type: postgresql # mysql | postgresql + version: "16" # postgres version or mysql version + image: "" # auto-computed if empty + port: 5432 # 5432 for pg, 3306 for mysql + username: appuser + password: "" + storageSize: "1Gi" + resources: + cpuRequest: "100m" + cpuLimit: "500m" + memoryRequest: "256Mi" + memoryLimit: "512Mi" + +# ── WordPress-specific ─────────────────────────────────── +wordpress: + enabled: false + wpContentStorageSize: "2Gi" + +# ── Change metadata ───────────────────────────────────── +changeCause: "" + +# ── Registry (for imagePullSecret) ────────────────────── +registry: + url: "localhost:30500"