From 3a22da6572ab31f735151355194f3c205f172185 Mon Sep 17 00:00:00 2001 From: Rishang Date: Fri, 6 Mar 2026 15:40:42 +0530 Subject: [PATCH 1/9] feat: Add Job and CronJob configuration options to `values.yaml` and document their usage. --- .../docs/pages/configuration/job.mdx | 48 ++ .../templates/_podtemplate.yaml | 409 +++++++++++++ .../component-chart/templates/deployment.yaml | 538 ++++-------------- charts/component-chart/values.yaml | 17 +- 4 files changed, 583 insertions(+), 429 deletions(-) create mode 100644 charts/component-chart/docs/pages/configuration/job.mdx create mode 100644 charts/component-chart/templates/_podtemplate.yaml diff --git a/charts/component-chart/docs/pages/configuration/job.mdx b/charts/component-chart/docs/pages/configuration/job.mdx new file mode 100644 index 0000000..1f765e7 --- /dev/null +++ b/charts/component-chart/docs/pages/configuration/job.mdx @@ -0,0 +1,48 @@ +--- +title: Job & CronJob Configuration +--- + +The `component-chart` fully supports setting up background tasks using Kubernetes `Job` or `CronJob` resources instead of the standard `Deployment`. + +It automatically evaluates the API `kind` based on the specified values. Providing a `job` object or a `job.schedule` string signals the chart to generate `Job` or `CronJob` resources, respectively. + +## Deploying a Job + +To deploy a one-off `Job`, you can define `job` settings. Any specific configurations under `job` will transform the component into a `Job`. + +```yaml +job: + backoffLimit: 4 + activeDeadlineSeconds: 100 + ttlSecondsAfterFinished: 100 +``` + +## Deploying a CronJob + +To deploy a periodic `CronJob`, define the `schedule` string. The component chart will dynamically wrap the pod template into a `jobTemplate`. + +```yaml +job: + schedule: "*/5 * * * *" + concurrencyPolicy: Allow + failedJobsHistoryLimit: 1 + successfulJobsHistoryLimit: 3 + startingDeadlineSeconds: 100 + suspend: false + # You can also apply Job parameters to the instantiated jobs + backoffLimit: 4 +``` + +### Configuration Options + +The `job` configuration block accepts fields mapped to the Kubernetes `JobSpec` and `CronJobSpec` specifications: + +- **`job.schedule`**: (String) The cron schedule (e.g., `* * * * *`). **Triggers CronJob mode.** +- **`job.backoffLimit`**: (Int) Specifies the number of retries before marking a job as failed. +- **`job.activeDeadlineSeconds`**: (Int) Specifies the duration in seconds relative to the startTime that the job may be continuously active before the system tries to terminate it. +- **`job.ttlSecondsAfterFinished`**: (Int) TTL mechanism to clean up finished jobs. +- **`job.concurrencyPolicy`**: (String) Specifies how to treat concurrent executions of a Job created by a CronJob. (`Allow`, `Forbid`, `Replace`) +- **`job.failedJobsHistoryLimit`**: (Int) The number of failed finished jobs to retain. +- **`job.successfulJobsHistoryLimit`**: (Int) The number of successful finished jobs to retain. +- **`job.startingDeadlineSeconds`**: (Int) Optional deadline in seconds for starting the job if it misses scheduled time for any reason. +- **`job.suspend`**: (Bool) This flag tells the controller to suspend subsequent executions. diff --git a/charts/component-chart/templates/_podtemplate.yaml b/charts/component-chart/templates/_podtemplate.yaml new file mode 100644 index 0000000..77e0510 --- /dev/null +++ b/charts/component-chart/templates/_podtemplate.yaml @@ -0,0 +1,409 @@ +{{/* +Renders the full pod template (metadata + spec). +Usage: {{ include "component.podTemplate" $ }} +*/}} +{{- define "component.podTemplate" -}} +{{- $allContainers := list }} +{{- range $idx, $container := $.Values.containers }} + {{- $allContainers = append $allContainers $container }} +{{- end }} +{{- range $idx, $initContainer := $.Values.initContainers }} + {{- $allContainers = append $allContainers $initContainer }} +{{- end -}} +metadata: + labels: + "app.kubernetes.io/name": "devspace-app" + "app.kubernetes.io/component": {{ $.Release.Name | quote }} + "app.kubernetes.io/managed-by": {{ $.Release.Service | quote }} + {{- range $labelName, $labelValue := $.Values.labels }} + {{ $labelName | quote }}: {{ $labelValue | quote }} + {{- end }} + annotations: + "helm.sh/chart": "{{ $.Chart.Name }}-{{ $.Chart.Version }}" + {{- range $annotationName, $annotationValue := $.Values.annotations }} + {{ $annotationName | quote }}: {{ $annotationValue | quote }} + {{- end }} +spec: + imagePullSecrets: + {{- range $secretIndex, $secretName := $.Values.pullSecrets }} + - name: {{ $secretName | quote }} + {{- end }} + nodeSelector: +{{ toYaml $.Values.nodeSelector | indent 4 }} + nodeName: +{{ toYaml $.Values.nodeName | indent 4 }} + affinity: +{{ toYaml $.Values.affinity | indent 4 }} + tolerations: +{{ toYaml $.Values.tolerations | indent 4 }} + dnsConfig: +{{ toYaml $.Values.dnsConfig | indent 4 }} + hostAliases: +{{ toYaml $.Values.hostAliases | indent 4 }} + overhead: +{{ toYaml $.Values.overhead | indent 4 }} + readinessGates: +{{ toYaml $.Values.readinessGates | indent 4 }} + securityContext: +{{ toYaml $.Values.securityContext | indent 4 }} + topologySpreadConstraints: +{{ toYaml $.Values.topologySpreadConstraints | indent 4 }} + {{- if hasKey $.Values "activeDeadlineSeconds" }} + activeDeadlineSeconds: {{ $.Values.activeDeadlineSeconds }} + {{- end }} + {{- if hasKey $.Values "automountServiceAccountToken" }} + automountServiceAccountToken: {{ $.Values.automountServiceAccountToken }} + {{- end }} + {{- if $.Values.dnsPolicy }} + dnsPolicy: {{ $.Values.dnsPolicy }} + {{- end }} + {{- if hasKey $.Values "enableServiceLinks" }} + enableServiceLinks: {{ $.Values.enableServiceLinks }} + {{- end }} + {{- if $.Values.hostIPC }} + hostIPC: {{ $.Values.hostIPC }} + {{- end }} + {{- if $.Values.hostNetwork }} + hostNetwork: {{ $.Values.hostNetwork }} + {{- end }} + {{- if $.Values.hostPID }} + hostPID: {{ $.Values.hostPID }} + {{- end }} + {{- if $.Values.hostname }} + hostname: {{ $.Values.hostname }} + {{- end }} + {{- if $.Values.preemptionPolicy }} + preemptionPolicy: {{ $.Values.preemptionPolicy }} + {{- end }} + {{- if $.Values.priority }} + priority: {{ $.Values.priority }} + {{- end }} + {{- if $.Values.priorityClassName }} + priorityClassName: {{ $.Values.priorityClassName }} + {{- end }} + {{- if $.Values.restartPolicy }} + restartPolicy: {{ $.Values.restartPolicy }} + {{- else if $.Values.job }} + restartPolicy: OnFailure + {{- end }} + {{- if $.Values.runtimeClassName }} + runtimeClassName: {{ $.Values.runtimeClassName }} + {{- end }} + {{- if $.Values.schedulerName }} + schedulerName: {{ $.Values.schedulerName }} + {{- end }} + {{- if $.Values.serviceaccount.name }} + serviceAccountName: {{ $.Values.serviceaccount.name }} + {{- else if $.Values.serviceAccountName }} + serviceAccountName: {{ $.Values.serviceAccountName }} + {{- end }} + {{- if $.Values.setHostnameAsFQDN }} + setHostnameAsFQDN: {{ $.Values.setHostnameAsFQDN }} + {{- end }} + {{- if $.Values.shareProcessNamespace }} + shareProcessNamespace: {{ $.Values.shareProcessNamespace }} + {{- end }} + {{- if $.Values.subdomain }} + subdomain: {{ $.Values.subdomain }} + {{- end }} + {{- if hasKey $.Values "terminationGracePeriodSeconds" }} + terminationGracePeriodSeconds: {{ $.Values.terminationGracePeriodSeconds }} + {{- else }} + terminationGracePeriodSeconds: 5 + {{- end }} + ephemeralContainers: +{{ toYaml $.Values.ephemeralContainers | indent 4 }} + containers: + {{- range $containerIndex, $container := $.Values.containers }} + - image: "{{ $container.image }}:{{ $container.imageTag | default "latest" }}" + {{- if $container.name }} + name: {{ $container.name | quote }} + {{- else }} + name: "container-{{ $containerIndex }}" + {{- end }} + {{- if $container.imagePullPolicy }} + imagePullPolicy: {{ $container.imagePullPolicy }} + {{- end }} + {{- if $container.workingDir }} + workingDir: {{ $container.workingDir }} + {{- end }} + {{- if hasKey $container "stdin" }} + stdin: {{ $container.stdin }} + {{- end }} + {{- if hasKey $container "stdinOnce" }} + stdinOnce: {{ $container.stdinOnce }} + {{- end }} + {{- if hasKey $container "tty" }} + tty: {{ $container.tty }} + {{- end }} + command: + {{- range $commandIndex, $command := $container.command }} + - {{ $command | quote }} + {{- end }} + args: + {{- range $argIndex, $arg := $container.args }} + - {{ $arg | quote }} + {{- end }} + {{- if $container.terminationMessagePath }} + terminationMessagePath: {{ $container.terminationMessagePath }} + {{- end }} + {{- if $container.terminationMessagePolicy }} + terminationMessagePolicy: {{ $container.terminationMessagePolicy }} + {{- end }} + env: +{{ toYaml $container.env | indent 8 }} + envFrom: +{{ toYaml $container.envFrom | indent 8 }} + {{- if $container.ports }} + ports: + {{- range $portMapIndex, $portMap := $container.ports }} + {{- if $portMap.name }} + - name: {{ $portMap.name }} + {{- else }} + - name: "port-{{ $portMapIndex }}" + {{- end }} + {{- $_ := unset $portMap "name" }} +{{ toYaml $portMap | indent 10 }} + {{- end }} + {{- end }} + securityContext: +{{ toYaml $container.securityContext | indent 8 }} + lifecycle: +{{ toYaml $container.lifecycle | indent 8 }} + livenessProbe: +{{ toYaml $container.livenessProbe | indent 8 }} + readinessProbe: +{{ toYaml $container.readinessProbe | indent 8 }} + startupProbe: +{{ toYaml $container.startupProbe | indent 8 }} + volumeDevices: +{{ toYaml $container.volumeDevices | indent 8 }} + volumeMounts: + {{- range $volumeMountIndex, $volumeMount := $container.volumeMounts }} + - mountPath: {{ $volumeMount.containerPath | quote }} + name: {{ $volumeMount.volume.name | quote }} + {{- if $volumeMount.volume.subPath }} + subPath: {{ trimAll "/" $volumeMount.volume.subPath | default "" | quote }} + {{- end }} + readOnly: {{ $volumeMount.volume.readOnly | default false }} + {{- end }} + {{- if $container.resources }} + resources: + {{- with $container.resources.limits }} + limits: + {{- if .cpu }} + cpu: {{ .cpu | quote }} + {{- end }} + {{- if .gpu }} + nvidia.com/gpu: {{ .gpu | quote }} + {{- end }} + {{- if .memory }} + memory: {{ .memory | quote }} + {{- end }} + {{- if .ephemeralStorage }} + ephemeral-storage: {{ .ephemeralStorage | quote }} + {{- end }} + {{- end }} + {{- if $container.resources.requests }} + {{- with $container.resources.requests }} + requests: + {{- if .cpu }} + cpu: {{ .cpu | quote }} + {{- else }} + {{- if $container.resources.limits }} + {{- if $container.resources.limits.cpu }} + cpu: "0" + {{- end }} + {{- end }} + {{- end }} + {{- if .memory }} + memory: {{ .memory | quote }} + {{- else }} + {{- if $container.resources.limits }} + {{- if $container.resources.limits.memory }} + memory: "0" + {{- end }} + {{- end }} + {{- end }} + {{- if .ephemeralStorage }} + ephemeral-storage: {{ .ephemeralStorage | quote }} + {{- else }} + {{- if $container.resources.limits }} + {{- if $container.resources.limits.ephemeralStorage }} + ephemeral-storage: "0" + {{- end }} + {{- end }} + {{- end }} + {{- end }} + {{- else }} + {{- if $container.resources.limits }} + requests: + {{- if $container.resources.limits.cpu }} + cpu: "0" + {{- end }} + {{- if $container.resources.limits.memory }} + memory: "0" + {{- end }} + {{- if $container.resources.limits.ephemeralStorage }} + ephemeral-storage: "0" + {{- end }} + {{- end }} + {{- end }} + {{- end }} + {{- end }} + initContainers: + {{- range $containerIndex, $container := $.Values.initContainers }} + - image: "{{ $container.image }}:{{ $container.imageTag | default "latest" }}" + {{- if $container.name }} + name: {{ $container.name | quote }} + {{- else }} + name: "init-container-{{ $containerIndex }}" + {{- end }} + {{- if $container.imagePullPolicy }} + imagePullPolicy: {{ $container.imagePullPolicy }} + {{- end }} + {{- if $container.workingDir }} + workingDir: {{ $container.workingDir }} + {{- end }} + {{- if hasKey $container "stdin" }} + stdin: {{ $container.stdin }} + {{- end }} + {{- if hasKey $container "stdinOnce" }} + stdinOnce: {{ $container.stdinOnce }} + {{- end }} + {{- if hasKey $container "tty" }} + tty: {{ $container.tty }} + {{- end }} + command: + {{- range $commandIndex, $command := $container.command }} + - {{ $command | quote }} + {{- end }} + args: + {{- range $argIndex, $arg := $container.args }} + - {{ $arg | quote }} + {{- end }} + {{- if $container.terminationMessagePath }} + terminationMessagePath: {{ $container.terminationMessagePath }} + {{- end }} + {{- if $container.terminationMessagePolicy }} + terminationMessagePolicy: {{ $container.terminationMessagePolicy }} + {{- end }} + env: +{{ toYaml $container.env | indent 8 }} + envFrom: +{{ toYaml $container.envFrom | indent 8 }} + securityContext: +{{ toYaml $container.securityContext | indent 8 }} + lifecycle: +{{ toYaml $container.lifecycle | indent 8 }} + livenessProbe: +{{ toYaml $container.livenessProbe | indent 8 }} + readinessProbe: +{{ toYaml $container.readinessProbe | indent 8 }} + startupProbe: +{{ toYaml $container.startupProbe | indent 8 }} + volumeDevices: +{{ toYaml $container.volumeDevices | indent 8 }} + volumeMounts: + {{- range $volumeMountIndex, $volumeMount := $container.volumeMounts }} + - mountPath: {{ $volumeMount.containerPath | quote }} + name: {{ $volumeMount.volume.name | quote }} + {{- if $volumeMount.volume.subPath }} + subPath: {{ trimAll "/" $volumeMount.volume.subPath | default "" | quote }} + {{- end }} + readOnly: {{ $volumeMount.volume.readOnly | default false }} + {{- end }} + {{- if $container.resources }} + resources: + {{- with $container.resources.limits }} + limits: + {{- if .cpu }} + cpu: {{ .cpu | quote }} + {{- end }} + {{- if .memory }} + memory: {{ .memory | quote }} + {{- end }} + {{- if .ephemeralStorage }} + ephemeral-storage: {{ .ephemeralStorage | quote }} + {{- end }} + {{- end }} + {{- if $container.resources.requests }} + {{- with $container.resources.requests }} + requests: + {{- if .cpu }} + cpu: {{ .cpu | quote }} + {{- else }} + {{- if $container.resources.limits }} + {{- if $container.resources.limits.cpu }} + cpu: "0" + {{- end }} + {{- end }} + {{- end }} + {{- if .memory }} + memory: {{ .memory | quote }} + {{- else }} + {{- if $container.resources.limits }} + {{- if $container.resources.limits.memory }} + memory: "0" + {{- end }} + {{- end }} + {{- end }} + {{- if .ephemeralStorage }} + ephemeral-storage: {{ .ephemeralStorage | quote }} + {{- else }} + {{- if $container.resources.limits }} + {{- if $container.resources.limits.ephemeralStorage }} + ephemeral-storage: "0" + {{- end }} + {{- end }} + {{- end }} + {{- end }} + {{- else }} + {{- if $container.resources.limits }} + requests: + {{- if $container.resources.limits.cpu }} + cpu: "0" + {{- end }} + {{- if $container.resources.limits.memory }} + memory: "0" + {{- end }} + {{- if $container.resources.limits.ephemeralStorage }} + ephemeral-storage: "0" + {{- end }} + {{- end }} + {{- end }} + {{- end }} + {{- end }} + volumes: + {{- range $volumeIndex, $volume := $.Values.volumes }} + {{- $isVolumeOfThisComponent := false }} + {{- range $containerIndex, $container := $allContainers }} + {{- range $volumeMountIndex, $volumeMount := $container.volumeMounts }} + {{- if or $volumeMount.volume.readOnly $volumeMount.volume.shared $volume.secret $volume.configMap (hasKey $volume "emptyDir") (hasKey $volume "hostPath") }} + {{- if eq $volume.name $volumeMount.volume.name }} + {{- $isVolumeOfThisComponent = true }} + {{- end }} + {{- end }} + {{- end }} + {{- end }} + {{- if $isVolumeOfThisComponent }} + - name: {{ $volume.name | quote }} + {{- if $volume.secret }} + secret: +{{ toYaml $volume.secret | indent 8 }} + {{- else if $volume.configMap }} + configMap: +{{ toYaml $volume.configMap | indent 8 }} + {{- else if hasKey $volume "emptyDir" }} + emptyDir: +{{ toYaml $volume.emptyDir | indent 8 }} + {{- else if hasKey $volume "hostPath" }} + hostPath: +{{ toYaml $volume.hostPath | indent 8 }} + {{- else }} + persistentVolumeClaim: + claimName: {{ $volume.name | quote }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/charts/component-chart/templates/deployment.yaml b/charts/component-chart/templates/deployment.yaml index e5231e6..fd6b3ea 100644 --- a/charts/component-chart/templates/deployment.yaml +++ b/charts/component-chart/templates/deployment.yaml @@ -1,21 +1,40 @@ {{- range $containerIndex, $containerUnused := $.Values.containers }} {{- if eq $containerIndex 0 }} -apiVersion: apps/v1 + +{{/* + ─── Determine resource kind ─────────────────────────────────────────────── + Priority: + 1. CronJob – $.Values.job is set AND job.schedule is non-empty + 2. Job – $.Values.job is set (no schedule) + 3. StatefulSet – any container has a non-readOnly volumeMount backed by a + PVC (i.e. not secret / configMap / emptyDir / hostPath) + 4. Deployment – everything else +*/}} + {{- $kind := "Deployment" }} -{{- range $containerIndex, $container := $.Values.containers }} - {{- range $volumeMountIndex, $volumeMount := $container.volumeMounts }} - {{- if $volumeMount.volume.readOnly }} - {{- else }} - {{- range $volumeIndex, $volume := $.Values.volumes }} - {{- if not (or (hasKey $volume "secret") (hasKey $volume "configMap") (hasKey $volume "emptyDir") (hasKey $volume "hostPath") ) }} - {{- if eq $volumeMount.volume.name $volume.name }} - {{- $kind = "StatefulSet" }} - {{- end }} + +{{- if $.Values.job }} + {{- if $.Values.job.schedule }} + {{- $kind = "CronJob" }} + {{- else }} + {{- $kind = "Job" }} + {{- end }} +{{- else }} + {{- range $containerIndex, $container := $.Values.containers }} + {{- range $volumeMountIndex, $volumeMount := $container.volumeMounts }} + {{- if not $volumeMount.volume.readOnly }} + {{- range $volumeIndex, $volume := $.Values.volumes }} + {{- if not (or (hasKey $volume "secret") (hasKey $volume "configMap") (hasKey $volume "emptyDir") (hasKey $volume "hostPath")) }} + {{- if eq $volumeMount.volume.name $volume.name }} + {{- $kind = "StatefulSet" }} + {{- end }} + {{- end }} {{- end }} {{- end }} {{- end }} {{- end }} {{- end }} + {{- $allContainers := list }} {{- range $idx, $container := $.Values.containers }} {{- $allContainers = append $allContainers $container }} @@ -23,6 +42,8 @@ apiVersion: apps/v1 {{- range $idx, $initContainer := $.Values.initContainers }} {{- $allContainers = append $allContainers $initContainer }} {{- end }} + +apiVersion: {{ if eq $kind "CronJob" }}batch/v1{{ else if eq $kind "Job" }}batch/v1{{ else }}apps/v1{{ end }} kind: {{ $kind }} metadata: name: {{ $.Release.Name | quote }} @@ -39,14 +60,80 @@ metadata: {{ $annotationName | quote }}: {{ $annotationValue | quote }} {{- end }} spec: + {{/* + ── CronJob spec ────────────────────────────────────────────────────────── + */}} + {{- if eq $kind "CronJob" }} + schedule: {{ $.Values.job.schedule | quote }} + {{- if $.Values.job.concurrencyPolicy }} + concurrencyPolicy: {{ $.Values.job.concurrencyPolicy }} + {{- end }} + {{- if hasKey $.Values.job "suspend" }} + suspend: {{ $.Values.job.suspend }} + {{- end }} + {{- if hasKey $.Values.job "startingDeadlineSeconds" }} + startingDeadlineSeconds: {{ $.Values.job.startingDeadlineSeconds }} + {{- end }} + {{- if hasKey $.Values.job "failedJobsHistoryLimit" }} + failedJobsHistoryLimit: {{ $.Values.job.failedJobsHistoryLimit }} + {{- end }} + {{- if hasKey $.Values.job "successfulJobsHistoryLimit" }} + successfulJobsHistoryLimit: {{ $.Values.job.successfulJobsHistoryLimit }} + {{- end }} + jobTemplate: + metadata: + labels: + "app.kubernetes.io/name": "devspace-app" + "app.kubernetes.io/component": {{ $.Release.Name | quote }} + "app.kubernetes.io/managed-by": {{ $.Release.Service | quote }} + {{- range $labelName, $labelValue := $.Values.labels }} + {{ $labelName | quote }}: {{ $labelValue | quote }} + {{- end }} + annotations: + "helm.sh/chart": "{{ $.Chart.Name }}-{{ $.Chart.Version }}" + {{- range $annotationName, $annotationValue := $.Values.annotations }} + {{ $annotationName | quote }}: {{ $annotationValue | quote }} + {{- end }} + spec: + {{- if hasKey $.Values.job "backoffLimit" }} + backoffLimit: {{ $.Values.job.backoffLimit }} + {{- end }} + {{- if hasKey $.Values.job "activeDeadlineSeconds" }} + activeDeadlineSeconds: {{ $.Values.job.activeDeadlineSeconds }} + {{- end }} + {{- if hasKey $.Values.job "ttlSecondsAfterFinished" }} + ttlSecondsAfterFinished: {{ $.Values.job.ttlSecondsAfterFinished }} + {{- end }} + template: +{{ include "component.podTemplate" $ | indent 8 }} + {{- end }}{{/* end CronJob */}} + {{/* + ── Job spec ────────────────────────────────────────────────────────────── + */}} + {{- if eq $kind "Job" }} + {{- if hasKey $.Values.job "backoffLimit" }} + backoffLimit: {{ $.Values.job.backoffLimit }} + {{- end }} + {{- if hasKey $.Values.job "activeDeadlineSeconds" }} + activeDeadlineSeconds: {{ $.Values.job.activeDeadlineSeconds }} + {{- end }} + {{- if hasKey $.Values.job "ttlSecondsAfterFinished" }} + ttlSecondsAfterFinished: {{ $.Values.job.ttlSecondsAfterFinished }} + {{- end }} + template: +{{ include "component.podTemplate" $ | indent 4 }} + {{- end }}{{/* end Job */}} + {{/* + ── Deployment / StatefulSet spec ───────────────────────────────────────── + */}} + {{- if or (eq $kind "Deployment") (eq $kind "StatefulSet") }} {{- if not $.Values.autoScaling }} replicas: {{ $.Values.replicas | default 1 }} {{- end }} revisionHistoryLimit: {{ $.Values.revisionHistoryLimit | default 3 }} {{- if eq $kind "Deployment" }} strategy: - {{- if $.Values.rollingUpdate }} - {{- if $.Values.rollingUpdate.enabled }} + {{- if and $.Values.rollingUpdate $.Values.rollingUpdate.enabled }} type: RollingUpdate rollingUpdate: maxSurge: {{ $.Values.rollingUpdate.maxSurge | default "25%" }} @@ -54,19 +141,14 @@ spec: {{- else }} type: Recreate {{- end }} - {{- else }} - type: Recreate - {{- end }} {{- end }} {{- if eq $kind "StatefulSet" }} - {{- if $.Values.rollingUpdate }} - {{- if $.Values.rollingUpdate.enabled }} + {{- if and $.Values.rollingUpdate $.Values.rollingUpdate.enabled }} updateStrategy: type: RollingUpdate rollingUpdate: partition: {{ $.Values.rollingUpdate.partition | default 0 }} {{- end }} - {{- end }} podManagementPolicy: {{ $.Values.podManagementPolicy | default "OrderedReady" | quote }} {{- if $.Values.serviceName }} serviceName: {{ $.Values.serviceName | quote }} @@ -83,416 +165,15 @@ spec: {{ $labelName | quote }}: {{ $labelValue | quote }} {{- end }} template: - metadata: - labels: - "app.kubernetes.io/name": "devspace-app" - "app.kubernetes.io/component": {{ $.Release.Name | quote }} - "app.kubernetes.io/managed-by": {{ $.Release.Service | quote }} - {{- range $labelName, $labelValue := $.Values.labels }} - {{ $labelName | quote }}: {{ $labelValue | quote }} - {{- end }} - annotations: - "helm.sh/chart": "{{ $.Chart.Name }}-{{ $.Chart.Version }}" - {{- range $annotationName, $annotationValue := $.Values.annotations }} - {{ $annotationName | quote }}: {{ $annotationValue | quote }} - {{- end }} - spec: - imagePullSecrets: - {{- range $secretIndex, $secretName := $.Values.pullSecrets }} - - name: {{ $secretName | quote }} - {{- end }} - nodeSelector: -{{ toYaml $.Values.nodeSelector | indent 8 }} - nodeName: -{{ toYaml $.Values.nodeName | indent 8 }} - affinity: -{{ toYaml $.Values.affinity | indent 8 }} - tolerations: -{{ toYaml $.Values.tolerations | indent 8 }} - dnsConfig: -{{ toYaml $.Values.dnsConfig | indent 8 }} - hostAliases: -{{ toYaml $.Values.hostAliases | indent 8 }} - overhead: -{{ toYaml $.Values.overhead | indent 8 }} - readinessGates: -{{ toYaml $.Values.readinessGates | indent 8 }} - securityContext: -{{ toYaml $.Values.securityContext | indent 8 }} - topologySpreadConstraints: -{{ toYaml $.Values.topologySpreadConstraints | indent 8 }} - {{- if hasKey $.Values "activeDeadlineSeconds" }} - activeDeadlineSeconds: {{ $.Values.activeDeadlineSeconds }} - {{- end }} - {{- if hasKey $.Values "automountServiceAccountToken" }} - automountServiceAccountToken: {{ $.Values.automountServiceAccountToken }} - {{- end }} - {{- if $.Values.dnsPolicy }} - dnsPolicy: {{ $.Values.dnsPolicy }} - {{- end }} - {{- if hasKey $.Values "enableServiceLinks" }} - enableServiceLinks: {{ $.Values.enableServiceLinks }} - {{- end }} - {{- if $.Values.hostIPC }} - hostIPC: {{ $.Values.hostIPC }} - {{- end }} - {{- if $.Values.hostNetwork }} - hostNetwork: {{ $.Values.hostNetwork }} - {{- end }} - {{- if $.Values.hostPID }} - hostPID: {{ $.Values.hostPID }} - {{- end }} - {{- if $.Values.hostname }} - hostname: {{ $.Values.hostname }} - {{- end }} - {{- if $.Values.preemptionPolicy }} - preemptionPolicy: {{ $.Values.preemptionPolicy }} - {{- end }} - {{- if $.Values.priority }} - priority: {{ $.Values.priority }} - {{- end }} - {{- if $.Values.priorityClassName }} - priorityClassName: {{ $.Values.priorityClassName }} - {{- end }} - {{- if $.Values.restartPolicy }} - restartPolicy: {{ $.Values.restartPolicy }} - {{- end }} - {{- if $.Values.runtimeClassName }} - runtimeClassName: {{ $.Values.runtimeClassName }} - {{- end }} - {{- if $.Values.schedulerName }} - schedulerName: {{ $.Values.schedulerName }} - {{- end }} - {{- if $.Values.serviceaccount.name }} - serviceAccountName: {{ $.Values.serviceaccount.name }} - {{- else if $.Values.serviceAccountName }} - serviceAccountName: {{ $.Values.serviceAccountName }} - {{- end }} - {{- if $.Values.setHostnameAsFQDN }} - setHostnameAsFQDN: {{ $.Values.setHostnameAsFQDN }} - {{- end }} - {{- if $.Values.shareProcessNamespace }} - shareProcessNamespace: {{ $.Values.shareProcessNamespace }} - {{- end }} - {{- if $.Values.subdomain }} - subdomain: {{ $.Values.subdomain }} - {{- end }} - {{- if hasKey $.Values "terminationGracePeriodSeconds" }} - terminationGracePeriodSeconds: {{ $.Values.terminationGracePeriodSeconds }} - {{- else }} - terminationGracePeriodSeconds: 5 - {{- end }} - ephemeralContainers: -{{ toYaml $.Values.ephemeralContainers | indent 8 }} - containers: - {{- range $containerIndex, $container := $.Values.containers }} - - image: "{{ $container.image }}:{{ $container.imageTag | default "latest" }}" - {{- if $container.name }} - name: {{ $container.name | quote }} - {{- else }} - name: "container-{{ $containerIndex }}" - {{- end }} - {{- if $container.imagePullPolicy }} - imagePullPolicy: {{ $container.imagePullPolicy }} - {{- end }} - {{- if $container.workingDir }} - workingDir: {{ $container.workingDir }} - {{- end }} - {{- if hasKey $container "stdin" }} - stdin: {{ $container.stdin }} - {{- end }} - {{- if hasKey $container "stdinOnce" }} - stdinOnce: {{ $container.stdinOnce }} - {{- end }} - {{- if hasKey $container "tty" }} - tty: {{ $container.tty }} - {{- end }} - command: - {{- range $commandIndex, $command := $container.command }} - - {{ $command | quote }} - {{- end }} - args: - {{- range $argIndex, $arg := $container.args }} - - {{ $arg | quote }} - {{- end }} - {{- if $container.terminationMessagePath }} - terminationMessagePath: {{ $container.terminationMessagePath }} - {{- end }} - {{- if $container.terminationMessagePolicy }} - terminationMessagePolicy: {{ $container.terminationMessagePolicy }} - {{- end }} - env: -{{ toYaml $container.env | indent 12 }} - envFrom: -{{ toYaml $container.envFrom | indent 12 }} - {{- if $container.ports }} - ports: - {{- range $portMapIndex, $portMap := $container.ports }} - {{- if $portMap.name }} - - name: {{ $portMap.name }} - {{- else }} - - name: "port-{{ $portMapIndex }}" - {{- end }} - {{- $_ := unset $portMap "name" }} -{{ toYaml $portMap | indent 14 }} - {{- end }} - {{- end }} - securityContext: -{{ toYaml $container.securityContext | indent 12 }} - lifecycle: -{{ toYaml $container.lifecycle | indent 12 }} - livenessProbe: -{{ toYaml $container.livenessProbe | indent 12 }} - readinessProbe: -{{ toYaml $container.readinessProbe | indent 12 }} - startupProbe: -{{ toYaml $container.startupProbe | indent 12 }} - volumeDevices: -{{ toYaml $container.volumeDevices | indent 12 }} - volumeMounts: - {{- range $volumeMountIndex, $volumeMount := $container.volumeMounts }} - - mountPath: {{ $volumeMount.containerPath | quote }} - name: {{ $volumeMount.volume.name | quote }} - {{- if $volumeMount.volume.subPath }} - subPath: {{ trimAll "/" $volumeMount.volume.subPath | default "" | quote }} - {{- end }} - readOnly: {{ $volumeMount.volume.readOnly | default false }} - {{- end }} - {{- if $container.resources }} - resources: - {{- with $container.resources.limits }} - limits: - {{- if .cpu }} - cpu: {{ .cpu | quote }} - {{- end }} - {{- if .gpu }} - nvidia.com/gpu: {{ .gpu | quote }} - {{- end }} - {{- if .memory }} - memory: {{ .memory | quote }} - {{- end }} - {{- if .ephemeralStorage }} - ephemeral-storage: {{ .ephemeralStorage | quote }} - {{- end }} - {{- end }} - {{- if $container.resources.requests }} - {{- with $container.resources.requests }} - requests: - {{- if .cpu }} - cpu: {{ .cpu | quote }} - {{- else }} - {{- if $container.resources.limits }} - {{- if $container.resources.limits.cpu }} - cpu: "0" - {{- end }} - {{- end }} - {{- end }} - {{- if .memory }} - memory: {{ .memory | quote }} - {{- else }} - {{- if $container.resources.limits }} - {{- if $container.resources.limits.memory }} - memory: "0" - {{- end }} - {{- end }} - {{- end }} - {{- if .ephemeralStorage }} - ephemeral-storage: {{ .ephemeralStorage | quote }} - {{- else }} - {{- if $container.resources.limits }} - {{- if $container.resources.limits.ephemeralStorage }} - ephemeral-storage: "0" - {{- end }} - {{- end }} - {{- end }} - {{- end }} - {{- else }} - {{- if $container.resources.limits }} - requests: - {{- if $container.resources.limits.cpu }} - cpu: "0" - {{- end }} - {{- if $container.resources.limits.memory }} - memory: "0" - {{- end }} - {{- if $container.resources.limits.ephemeralStorage }} - ephemeral-storage: "0" - {{- end }} - {{- end }} - {{- end }} - {{- end }} - {{- end }} - initContainers: - {{- range $containerIndex, $container := $.Values.initContainers }} - - image: "{{ $container.image }}:{{ $container.imageTag | default "latest" }}" - {{- if $container.name }} - name: {{ $container.name | quote }} - {{- else }} - name: "init-container-{{ $containerIndex }}" - {{- end }} - {{- if $container.imagePullPolicy }} - imagePullPolicy: {{ $container.imagePullPolicy }} - {{- end }} - {{- if $container.workingDir }} - workingDir: {{ $container.workingDir }} - {{- end }} - {{- if hasKey $container "stdin" }} - stdin: {{ $container.stdin }} - {{- end }} - {{- if hasKey $container "stdinOnce" }} - stdinOnce: {{ $container.stdinOnce }} - {{- end }} - {{- if hasKey $container "tty" }} - tty: {{ $container.tty }} - {{- end }} - command: - {{- range $commandIndex, $command := $container.command }} - - {{ $command | quote }} - {{- end }} - args: - {{- range $argIndex, $arg := $container.args }} - - {{ $arg | quote }} - {{- end }} - {{- if $container.terminationMessagePath }} - terminationMessagePath: {{ $container.terminationMessagePath }} - {{- end }} - {{- if $container.terminationMessagePolicy }} - terminationMessagePolicy: {{ $container.terminationMessagePolicy }} - {{- end }} - env: -{{ toYaml $container.env | indent 12 }} - envFrom: -{{ toYaml $container.envFrom | indent 12 }} - securityContext: -{{ toYaml $container.securityContext | indent 12 }} - lifecycle: -{{ toYaml $container.lifecycle | indent 12 }} - livenessProbe: -{{ toYaml $container.livenessProbe | indent 12 }} - readinessProbe: -{{ toYaml $container.readinessProbe | indent 12 }} - startupProbe: -{{ toYaml $container.startupProbe | indent 12 }} - volumeDevices: -{{ toYaml $container.volumeDevices | indent 12 }} - volumeMounts: - {{- range $volumeMountIndex, $volumeMount := $container.volumeMounts }} - - mountPath: {{ $volumeMount.containerPath | quote }} - name: {{ $volumeMount.volume.name | quote }} - {{- if $volumeMount.volume.subPath }} - subPath: {{ trimAll "/" $volumeMount.volume.subPath | default "" | quote }} - {{- end }} - readOnly: {{ $volumeMount.volume.readOnly | default false }} - {{- end }} - {{- if $container.resources }} - resources: - {{- with $container.resources.limits }} - limits: - {{- if .cpu }} - cpu: {{ .cpu | quote }} - {{- end }} - {{- if .memory }} - memory: {{ .memory | quote }} - {{- end }} - {{- if .ephemeralStorage }} - ephemeral-storage: {{ .ephemeralStorage | quote }} - {{- end }} - {{- end }} - {{- if $container.resources.requests }} - {{- with $container.resources.requests }} - requests: - {{- if .cpu }} - cpu: {{ .cpu | quote }} - {{- else }} - {{- if $container.resources.limits }} - {{- if $container.resources.limits.cpu }} - cpu: "0" - {{- end }} - {{- end }} - {{- end }} - {{- if .memory }} - memory: {{ .memory | quote }} - {{- else }} - {{- if $container.resources.limits }} - {{- if $container.resources.limits.memory }} - memory: "0" - {{- end }} - {{- end }} - {{- end }} - {{- if .ephemeralStorage }} - ephemeral-storage: {{ .ephemeralStorage | quote }} - {{- else }} - {{- if $container.resources.limits }} - {{- if $container.resources.limits.ephemeralStorage }} - ephemeral-storage: "0" - {{- end }} - {{- end }} - {{- end }} - {{- end }} - {{- else }} - {{- if $container.resources.limits }} - requests: - {{- if $container.resources.limits.cpu }} - cpu: "0" - {{- end }} - {{- if $container.resources.limits.memory }} - memory: "0" - {{- end }} - {{- if $container.resources.limits.ephemeralStorage }} - ephemeral-storage: "0" - {{- end }} - {{- end }} - {{- end }} - {{- end }} - {{- end }} - volumes: - {{- range $volumeIndex, $volume := $.Values.volumes }} - {{- $isVolumeOfThisComponent := false }} - {{- range $containerIndex, $container := $allContainers }} - {{- range $volumeMountIndex, $volumeMount := $container.volumeMounts }} - {{- if or $volumeMount.volume.readOnly $volumeMount.volume.shared $volume.secret $volume.configMap (hasKey $volume "emptyDir") (hasKey $volume "hostPath") }} - {{- if eq $volume.name $volumeMount.volume.name }} - {{- $isVolumeOfThisComponent = true }} - {{- end }} - {{- end }} - {{- end }} - {{- end }} - {{- if $isVolumeOfThisComponent }} - - name: {{ $volume.name | quote }} - {{- if $volume.secret }} - secret: -{{ toYaml $volume.secret | indent 12 }} - {{- else }} - {{- if $volume.configMap }} - configMap: -{{ toYaml $volume.configMap | indent 12 }} - {{- else }} - {{- if hasKey $volume "emptyDir" }} - emptyDir: -{{ toYaml $volume.emptyDir | indent 12 }} - {{- else }} - {{- if hasKey $volume "hostPath" }} - hostPath: -{{ toYaml $volume.hostPath | indent 12 }} - {{- else }} - persistentVolumeClaim: - claimName: {{ $volume.name | quote }} - {{- end }} - {{- end }} - {{- end }} - {{- end }} - {{- end }} - {{- end }} +{{ include "component.podTemplate" $ | indent 4 }} {{- if eq $kind "StatefulSet" }} volumeClaimTemplates: {{- range $volumeIndex, $volume := $.Values.volumes }} - {{- if or $volume.secret $volume.configMap (hasKey $volume "emptyDir") (hasKey $volume "hostPath") }} - {{- else }} + {{- if not (or $volume.secret $volume.configMap (hasKey $volume "emptyDir") (hasKey $volume "hostPath")) }} {{- $isVolumeTemplateOfThisComponent := false }} {{- range $containerIndex, $container := $allContainers }} {{- range $volumeMountIndex, $volumeMount := $container.volumeMounts }} - {{- if or $volumeMount.volume.readOnly $volumeMount.volume.shared }} - {{- else }} + {{- if not (or $volumeMount.volume.readOnly $volumeMount.volume.shared) }} {{- if eq $volume.name $volumeMount.volume.name }} {{- $isVolumeTemplateOfThisComponent = true }} {{- end }} @@ -540,10 +221,12 @@ spec: {{- end }} {{- end }} {{- end }} - {{- end }} + {{- end }}{{/* end StatefulSet volumeClaimTemplates */}} + {{- end }}{{/* end Deployment / StatefulSet */}} + --- -# Create headless service for StatefulSet +{{/* Create headless service for StatefulSet */}} {{- if eq $kind "StatefulSet" }} apiVersion: v1 kind: Service @@ -578,6 +261,7 @@ spec: {{ $labelName | quote }}: {{ $labelValue | quote }} {{- end }} --- -{{- end }} -{{- end }} -{{- end }} +{{- end }}{{/* end headless Service */}} + +{{- end }}{{/* end if containerIndex == 0 */}} +{{- end }}{{/* end range containers */}} \ No newline at end of file diff --git a/charts/component-chart/values.yaml b/charts/component-chart/values.yaml index 1afdfee..6a3b9c1 100644 --- a/charts/component-chart/values.yaml +++ b/charts/component-chart/values.yaml @@ -11,6 +11,20 @@ initContainers: [] # Defines a number of replicas for the component replicas: 1 +# Job or CronJob specific configuration +job: {} + # A schedule for a CronJob. If set, this component will be a CronJob instead of a Deployment else it will be a Job. + # "*/5 * * * *" + # schedule: '' + # backoffLimit: 4 + # activeDeadlineSeconds: 100 + # ttlSecondsAfterFinished: 100 + # concurrencyPolicy: Allow + # failedJobsHistoryLimit: 1 + # successfulJobsHistoryLimit: 3 + # startingDeadlineSeconds: 100 + # suspend: false + # Defines a service for the component service: type: ClusterIP @@ -71,7 +85,6 @@ httpRoutes: [] # - name: my-service # port: 80 - # External secrets for the component externalSecrets: [] # - name: my-external-secret @@ -89,4 +102,4 @@ externalSecrets: [] # key: /path/to/parameter # Extra objects for the component -extraObjects: [] \ No newline at end of file +extraObjects: [] From 97ca699a37fad48592904c55be0ce309b83df8c2 Mon Sep 17 00:00:00 2001 From: Rishang Date: Fri, 6 Mar 2026 15:45:08 +0530 Subject: [PATCH 2/9] add: test --- Taskfile.yaml | 5 +++++ charts/component-chart/test/values.yaml | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/Taskfile.yaml b/Taskfile.yaml index bf75817..237d572 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -16,8 +16,13 @@ tasks: desc: Validate Helm charts cmds: - | + NAME={{.CLI_ARGS}} + for chart in charts/*; do chart_name=$(basename "$chart") + if [ "$chart_name" != "$NAME" ]; then + continue + fi ( cd "$chart" || exit 1 echo "Validating $chart_name..." diff --git a/charts/component-chart/test/values.yaml b/charts/component-chart/test/values.yaml index 7fa3798..6565bbf 100644 --- a/charts/component-chart/test/values.yaml +++ b/charts/component-chart/test/values.yaml @@ -36,6 +36,10 @@ containers: initialDelaySeconds: 30 timeoutSeconds: 5 failureThreshold: 3 + +# job: +# schedule: "*/5 * * * *" + service: ports: - port: 80 From 2ed75ae23a5602f54c33c251672b97d240765f0d Mon Sep 17 00:00:00 2001 From: Rishang Date: Fri, 6 Mar 2026 16:04:24 +0530 Subject: [PATCH 3/9] fmt --- charts/component-chart/templates/deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/component-chart/templates/deployment.yaml b/charts/component-chart/templates/deployment.yaml index fd6b3ea..bf5fb5c 100644 --- a/charts/component-chart/templates/deployment.yaml +++ b/charts/component-chart/templates/deployment.yaml @@ -43,7 +43,7 @@ {{- $allContainers = append $allContainers $initContainer }} {{- end }} -apiVersion: {{ if eq $kind "CronJob" }}batch/v1{{ else if eq $kind "Job" }}batch/v1{{ else }}apps/v1{{ end }} +apiVersion: {{ if has $kind (list "CronJob" "Job") }}batch/v1{{ else }}apps/v1{{ end }} kind: {{ $kind }} metadata: name: {{ $.Release.Name | quote }} From f831e0005ea151debdc8c795fcabd081d5f87d4a Mon Sep 17 00:00:00 2001 From: Rishang Date: Wed, 11 Mar 2026 15:57:40 +0530 Subject: [PATCH 4/9] refactor: Simplify deployment logic and add job.yaml template for Job and CronJob configurations --- .../component-chart/templates/deployment.yaml | 102 +++--------------- charts/component-chart/templates/job.yaml | 100 +++++++++++++++++ 2 files changed, 113 insertions(+), 89 deletions(-) create mode 100644 charts/component-chart/templates/job.yaml diff --git a/charts/component-chart/templates/deployment.yaml b/charts/component-chart/templates/deployment.yaml index bf5fb5c..5dfe7d9 100644 --- a/charts/component-chart/templates/deployment.yaml +++ b/charts/component-chart/templates/deployment.yaml @@ -1,33 +1,24 @@ {{- range $containerIndex, $containerUnused := $.Values.containers }} {{- if eq $containerIndex 0 }} +{{- if not $.Values.job }} {{/* ─── Determine resource kind ─────────────────────────────────────────────── Priority: - 1. CronJob – $.Values.job is set AND job.schedule is non-empty - 2. Job – $.Values.job is set (no schedule) - 3. StatefulSet – any container has a non-readOnly volumeMount backed by a + 1. StatefulSet – any container has a non-readOnly volumeMount backed by a PVC (i.e. not secret / configMap / emptyDir / hostPath) - 4. Deployment – everything else + 2. Deployment – everything else */}} {{- $kind := "Deployment" }} -{{- if $.Values.job }} - {{- if $.Values.job.schedule }} - {{- $kind = "CronJob" }} - {{- else }} - {{- $kind = "Job" }} - {{- end }} -{{- else }} - {{- range $containerIndex, $container := $.Values.containers }} - {{- range $volumeMountIndex, $volumeMount := $container.volumeMounts }} - {{- if not $volumeMount.volume.readOnly }} - {{- range $volumeIndex, $volume := $.Values.volumes }} - {{- if not (or (hasKey $volume "secret") (hasKey $volume "configMap") (hasKey $volume "emptyDir") (hasKey $volume "hostPath")) }} - {{- if eq $volumeMount.volume.name $volume.name }} - {{- $kind = "StatefulSet" }} - {{- end }} +{{- range $containerIndex, $container := $.Values.containers }} + {{- range $volumeMountIndex, $volumeMount := $container.volumeMounts }} + {{- if not $volumeMount.volume.readOnly }} + {{- range $volumeIndex, $volume := $.Values.volumes }} + {{- if not (or (hasKey $volume "secret") (hasKey $volume "configMap") (hasKey $volume "emptyDir") (hasKey $volume "hostPath")) }} + {{- if eq $volumeMount.volume.name $volume.name }} + {{- $kind = "StatefulSet" }} {{- end }} {{- end }} {{- end }} @@ -43,7 +34,7 @@ {{- $allContainers = append $allContainers $initContainer }} {{- end }} -apiVersion: {{ if has $kind (list "CronJob" "Job") }}batch/v1{{ else }}apps/v1{{ end }} +apiVersion: apps/v1 kind: {{ $kind }} metadata: name: {{ $.Release.Name | quote }} @@ -60,73 +51,6 @@ metadata: {{ $annotationName | quote }}: {{ $annotationValue | quote }} {{- end }} spec: - {{/* - ── CronJob spec ────────────────────────────────────────────────────────── - */}} - {{- if eq $kind "CronJob" }} - schedule: {{ $.Values.job.schedule | quote }} - {{- if $.Values.job.concurrencyPolicy }} - concurrencyPolicy: {{ $.Values.job.concurrencyPolicy }} - {{- end }} - {{- if hasKey $.Values.job "suspend" }} - suspend: {{ $.Values.job.suspend }} - {{- end }} - {{- if hasKey $.Values.job "startingDeadlineSeconds" }} - startingDeadlineSeconds: {{ $.Values.job.startingDeadlineSeconds }} - {{- end }} - {{- if hasKey $.Values.job "failedJobsHistoryLimit" }} - failedJobsHistoryLimit: {{ $.Values.job.failedJobsHistoryLimit }} - {{- end }} - {{- if hasKey $.Values.job "successfulJobsHistoryLimit" }} - successfulJobsHistoryLimit: {{ $.Values.job.successfulJobsHistoryLimit }} - {{- end }} - jobTemplate: - metadata: - labels: - "app.kubernetes.io/name": "devspace-app" - "app.kubernetes.io/component": {{ $.Release.Name | quote }} - "app.kubernetes.io/managed-by": {{ $.Release.Service | quote }} - {{- range $labelName, $labelValue := $.Values.labels }} - {{ $labelName | quote }}: {{ $labelValue | quote }} - {{- end }} - annotations: - "helm.sh/chart": "{{ $.Chart.Name }}-{{ $.Chart.Version }}" - {{- range $annotationName, $annotationValue := $.Values.annotations }} - {{ $annotationName | quote }}: {{ $annotationValue | quote }} - {{- end }} - spec: - {{- if hasKey $.Values.job "backoffLimit" }} - backoffLimit: {{ $.Values.job.backoffLimit }} - {{- end }} - {{- if hasKey $.Values.job "activeDeadlineSeconds" }} - activeDeadlineSeconds: {{ $.Values.job.activeDeadlineSeconds }} - {{- end }} - {{- if hasKey $.Values.job "ttlSecondsAfterFinished" }} - ttlSecondsAfterFinished: {{ $.Values.job.ttlSecondsAfterFinished }} - {{- end }} - template: -{{ include "component.podTemplate" $ | indent 8 }} - {{- end }}{{/* end CronJob */}} - {{/* - ── Job spec ────────────────────────────────────────────────────────────── - */}} - {{- if eq $kind "Job" }} - {{- if hasKey $.Values.job "backoffLimit" }} - backoffLimit: {{ $.Values.job.backoffLimit }} - {{- end }} - {{- if hasKey $.Values.job "activeDeadlineSeconds" }} - activeDeadlineSeconds: {{ $.Values.job.activeDeadlineSeconds }} - {{- end }} - {{- if hasKey $.Values.job "ttlSecondsAfterFinished" }} - ttlSecondsAfterFinished: {{ $.Values.job.ttlSecondsAfterFinished }} - {{- end }} - template: -{{ include "component.podTemplate" $ | indent 4 }} - {{- end }}{{/* end Job */}} - {{/* - ── Deployment / StatefulSet spec ───────────────────────────────────────── - */}} - {{- if or (eq $kind "Deployment") (eq $kind "StatefulSet") }} {{- if not $.Values.autoScaling }} replicas: {{ $.Values.replicas | default 1 }} {{- end }} @@ -222,7 +146,6 @@ spec: {{- end }} {{- end }} {{- end }}{{/* end StatefulSet volumeClaimTemplates */}} - {{- end }}{{/* end Deployment / StatefulSet */}} --- @@ -263,5 +186,6 @@ spec: --- {{- end }}{{/* end headless Service */}} +{{- end }}{{/* end if not job */}} {{- end }}{{/* end if containerIndex == 0 */}} -{{- end }}{{/* end range containers */}} \ No newline at end of file +{{- end }}{{/* end range containers */}} diff --git a/charts/component-chart/templates/job.yaml b/charts/component-chart/templates/job.yaml new file mode 100644 index 0000000..5e4ab98 --- /dev/null +++ b/charts/component-chart/templates/job.yaml @@ -0,0 +1,100 @@ +{{- if $.Values.job }} +{{- range $containerIndex, $containerUnused := $.Values.containers }} +{{- if eq $containerIndex 0 }} + +{{/* + ─── Determine resource kind ─────────────────────────────────────────────── + Priority: + 1. CronJob – $.Values.job.schedule is non-empty + 2. Job – $.Values.job is set (no schedule) +*/}} + +{{- $kind := "Job" }} +{{- if $.Values.job.schedule }} + {{- $kind = "CronJob" }} +{{- end }} + +apiVersion: batch/v1 +kind: {{ $kind }} +metadata: + name: {{ $.Release.Name | quote }} + labels: + "app.kubernetes.io/name": "devspace-app" + "app.kubernetes.io/component": {{ $.Release.Name | quote }} + "app.kubernetes.io/managed-by": {{ $.Release.Service | quote }} + {{- range $labelName, $labelValue := $.Values.labels }} + {{ $labelName | quote }}: {{ $labelValue | quote }} + {{- end }} + annotations: + "helm.sh/chart": "{{ $.Chart.Name }}-{{ $.Chart.Version }}" + {{- range $annotationName, $annotationValue := $.Values.annotations }} + {{ $annotationName | quote }}: {{ $annotationValue | quote }} + {{- end }} +spec: + {{/* + ── CronJob spec ────────────────────────────────────────────────────────── + */}} + {{- if eq $kind "CronJob" }} + schedule: {{ $.Values.job.schedule | quote }} + {{- if $.Values.job.concurrencyPolicy }} + concurrencyPolicy: {{ $.Values.job.concurrencyPolicy }} + {{- end }} + {{- if hasKey $.Values.job "suspend" }} + suspend: {{ $.Values.job.suspend }} + {{- end }} + {{- if hasKey $.Values.job "startingDeadlineSeconds" }} + startingDeadlineSeconds: {{ $.Values.job.startingDeadlineSeconds }} + {{- end }} + {{- if hasKey $.Values.job "failedJobsHistoryLimit" }} + failedJobsHistoryLimit: {{ $.Values.job.failedJobsHistoryLimit }} + {{- end }} + {{- if hasKey $.Values.job "successfulJobsHistoryLimit" }} + successfulJobsHistoryLimit: {{ $.Values.job.successfulJobsHistoryLimit }} + {{- end }} + jobTemplate: + metadata: + labels: + "app.kubernetes.io/name": "devspace-app" + "app.kubernetes.io/component": {{ $.Release.Name | quote }} + "app.kubernetes.io/managed-by": {{ $.Release.Service | quote }} + {{- range $labelName, $labelValue := $.Values.labels }} + {{ $labelName | quote }}: {{ $labelValue | quote }} + {{- end }} + annotations: + "helm.sh/chart": "{{ $.Chart.Name }}-{{ $.Chart.Version }}" + {{- range $annotationName, $annotationValue := $.Values.annotations }} + {{ $annotationName | quote }}: {{ $annotationValue | quote }} + {{- end }} + spec: + {{- if hasKey $.Values.job "backoffLimit" }} + backoffLimit: {{ $.Values.job.backoffLimit }} + {{- end }} + {{- if hasKey $.Values.job "activeDeadlineSeconds" }} + activeDeadlineSeconds: {{ $.Values.job.activeDeadlineSeconds }} + {{- end }} + {{- if hasKey $.Values.job "ttlSecondsAfterFinished" }} + ttlSecondsAfterFinished: {{ $.Values.job.ttlSecondsAfterFinished }} + {{- end }} + template: +{{ include "component.podTemplate" $ | indent 8 }} + {{- end }}{{/* end CronJob */}} + {{/* + ── Job spec ────────────────────────────────────────────────────────────── + */}} + {{- if eq $kind "Job" }} + {{- if hasKey $.Values.job "backoffLimit" }} + backoffLimit: {{ $.Values.job.backoffLimit }} + {{- end }} + {{- if hasKey $.Values.job "activeDeadlineSeconds" }} + activeDeadlineSeconds: {{ $.Values.job.activeDeadlineSeconds }} + {{- end }} + {{- if hasKey $.Values.job "ttlSecondsAfterFinished" }} + ttlSecondsAfterFinished: {{ $.Values.job.ttlSecondsAfterFinished }} + {{- end }} + template: +{{ include "component.podTemplate" $ | indent 4 }} + {{- end }}{{/* end Job */}} + +{{- end }}{{/* end if containerIndex == 0 */}} +{{- end }}{{/* end range containers */}} +{{- end }}{{/* end if job */}} From 8529f932304e610fe25afccd8164d1e9c4d9860c Mon Sep 17 00:00:00 2001 From: Dishant Pandya Date: Wed, 11 Mar 2026 20:19:55 +0530 Subject: [PATCH 5/9] chore: configure helm tests --- .github/workflows/validate.yaml | 9 ++- Taskfile.yaml | 33 +++++++++- .../component-chart/tests/configmap_test.yaml | 20 ++++++ .../tests/deployment_test.yaml | 62 +++++++++++++++++++ charts/component-chart/tests/job_test.yaml | 55 ++++++++++++++++ charts/component-chart/tests/secret_test.yaml | 26 ++++++++ .../component-chart/tests/service_test.yaml | 32 ++++++++++ .../tests/serviceaccount_test.yaml | 20 ++++++ .../tests/statefulset_test.yaml | 40 ++++++++++++ .../tests/values/statefulset.yaml | 14 +++++ 10 files changed, 308 insertions(+), 3 deletions(-) create mode 100644 charts/component-chart/tests/configmap_test.yaml create mode 100644 charts/component-chart/tests/deployment_test.yaml create mode 100644 charts/component-chart/tests/job_test.yaml create mode 100644 charts/component-chart/tests/secret_test.yaml create mode 100644 charts/component-chart/tests/service_test.yaml create mode 100644 charts/component-chart/tests/serviceaccount_test.yaml create mode 100644 charts/component-chart/tests/statefulset_test.yaml create mode 100644 charts/component-chart/tests/values/statefulset.yaml diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index 5d435f5..d5c35f3 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -16,4 +16,11 @@ jobs: version: 3.x repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Validate Helm charts - run: task helm-validate \ No newline at end of file + run: task helm-validate + - name: Install helm-unittest plugin + run: | + if ! helm plugin list | awk '{print $1}' | grep -qx unittest; then + helm plugin install https://github.com/helm-unittest/helm-unittest + fi + - name: Run chart unit tests + run: task helm-unittest diff --git a/Taskfile.yaml b/Taskfile.yaml index 237d572..eea6e7e 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -20,7 +20,7 @@ tasks: for chart in charts/*; do chart_name=$(basename "$chart") - if [ "$chart_name" != "$NAME" ]; then + if [ -n "$NAME" ] && [ "$chart_name" != "$NAME" ]; then continue fi ( @@ -39,6 +39,36 @@ tasks: ) || exit 1 done + helm-unittest: + desc: Run Helm unit tests (helm-unittest plugin) + cmds: + - | + NAME={{.CLI_ARGS}} + + if ! helm unittest -h >/dev/null 2>&1; then + echo "❌ helm-unittest plugin is not installed." + echo "Install: helm plugin install https://github.com/helm-unittest/helm-unittest" + exit 1 + fi + + for chart in charts/*; do + chart_name=$(basename "$chart") + if [ -n "$NAME" ] && [ "$chart_name" != "$NAME" ]; then + continue + fi + + if [ ! -d "$chart/tests" ] || [ -z "$(ls -A "$chart/tests" 2>/dev/null)" ]; then + echo "⏭️ Skipping $chart_name (no unit tests)" + continue + fi + + ( + cd "$chart" || exit 1 + echo "Running unit tests for $chart_name..." + helm unittest . + ) || exit 1 + done + helm-package-index: desc: Package Helm charts and update repository index cmds: @@ -64,4 +94,3 @@ tasks: - echo "✅ Charts packaged successfully!" - echo "📦 Files created in web/static/charts/" - ls -lh web/static/charts/ - diff --git a/charts/component-chart/tests/configmap_test.yaml b/charts/component-chart/tests/configmap_test.yaml new file mode 100644 index 0000000..4409736 --- /dev/null +++ b/charts/component-chart/tests/configmap_test.yaml @@ -0,0 +1,20 @@ +suite: component-chart / configmap +release: + name: demo + namespace: default +values: + - test/values.yaml +templates: + - templates/config-map.yaml +tests: + - it: renders a ConfigMap with data + asserts: + - isKind: + of: ConfigMap + - equal: + path: metadata.name + value: demo + - equal: + path: data.KEY + value: value + diff --git a/charts/component-chart/tests/deployment_test.yaml b/charts/component-chart/tests/deployment_test.yaml new file mode 100644 index 0000000..2e33b4c --- /dev/null +++ b/charts/component-chart/tests/deployment_test.yaml @@ -0,0 +1,62 @@ +suite: component-chart / deployment +release: + name: demo + namespace: default +values: + - test/values.yaml +templates: + - templates/deployment.yaml +tests: + - it: renders a Deployment with expected labels and container spec + asserts: + - isKind: + of: Deployment + - equal: + path: metadata.name + value: demo + - equal: + path: metadata.labels."azure.workload.identity/use" + value: "true" + - equal: + path: spec.replicas + value: 1 + - equal: + path: spec.strategy.type + value: RollingUpdate + - equal: + path: spec.strategy.rollingUpdate.maxSurge + value: 25% + - equal: + path: spec.strategy.rollingUpdate.maxUnavailable + value: 0% + - equal: + path: spec.selector.matchLabels."app.kubernetes.io/component" + value: demo + - equal: + path: spec.template.spec.containers[0].name + value: demo + - equal: + path: spec.template.spec.containers[0].image + value: nginx:latest + - equal: + path: spec.template.spec.containers[0].imagePullPolicy + value: Always + - equal: + path: spec.template.spec.containers[0].envFrom[0].secretRef.name + value: demo + - equal: + path: spec.template.spec.containers[0].envFrom[1].configMapRef.name + value: demo + - equal: + path: spec.template.spec.containers[0].env[0].name + value: NAME + - equal: + path: spec.template.spec.containers[0].env[0].value + value: demo + - equal: + path: spec.template.spec.containers[0].readinessProbe.httpGet.path + value: / + - equal: + path: spec.template.spec.containers[0].readinessProbe.httpGet.port + value: 80 + diff --git a/charts/component-chart/tests/job_test.yaml b/charts/component-chart/tests/job_test.yaml new file mode 100644 index 0000000..cec828f --- /dev/null +++ b/charts/component-chart/tests/job_test.yaml @@ -0,0 +1,55 @@ +suite: component-chart / job +release: + name: demo + namespace: default +templates: + - templates/job.yaml +tests: + - it: renders a Job when job is set without schedule + set: + containers[0].name: worker + containers[0].image: busybox + containers[0].imageTag: "1.36" + job.backoffLimit: 2 + asserts: + - isKind: + of: Job + - equal: + path: metadata.name + value: demo + - equal: + path: spec.backoffLimit + value: 2 + - equal: + path: spec.template.spec.restartPolicy + value: OnFailure + - equal: + path: spec.template.spec.containers[0].name + value: worker + - equal: + path: spec.template.spec.containers[0].image + value: busybox:1.36 + + - it: renders a CronJob when job.schedule is set + set: + containers[0].name: worker + containers[0].image: busybox + containers[0].imageTag: "1.36" + job.backoffLimit: 2 + job.schedule: "*/5 * * * *" + asserts: + - isKind: + of: CronJob + - equal: + path: metadata.name + value: demo + - equal: + path: spec.schedule + value: "*/5 * * * *" + - equal: + path: spec.jobTemplate.spec.backoffLimit + value: 2 + - equal: + path: spec.jobTemplate.spec.template.spec.restartPolicy + value: OnFailure + diff --git a/charts/component-chart/tests/secret_test.yaml b/charts/component-chart/tests/secret_test.yaml new file mode 100644 index 0000000..a0bc847 --- /dev/null +++ b/charts/component-chart/tests/secret_test.yaml @@ -0,0 +1,26 @@ +suite: component-chart / secret +release: + name: demo + namespace: default +values: + - test/values.yaml +templates: + - templates/config-secrets.yaml +tests: + - it: renders an Opaque Secret with stringData and annotations + asserts: + - isKind: + of: Secret + - equal: + path: metadata.name + value: demo + - equal: + path: type + value: Opaque + - equal: + path: metadata.annotations."avp.kubernetes.io/path" + value: app + - equal: + path: stringData.SECRET + value: "" + diff --git a/charts/component-chart/tests/service_test.yaml b/charts/component-chart/tests/service_test.yaml new file mode 100644 index 0000000..33bc7a9 --- /dev/null +++ b/charts/component-chart/tests/service_test.yaml @@ -0,0 +1,32 @@ +suite: component-chart / service +release: + name: demo + namespace: default +values: + - test/values.yaml +templates: + - templates/service.yaml +tests: + - it: renders a ClusterIP service with expected ports and selectors + asserts: + - isKind: + of: Service + - equal: + path: metadata.name + value: demo + - equal: + path: spec.type + value: ClusterIP + - equal: + path: spec.ports[0].port + value: 80 + - equal: + path: spec.ports[0].targetPort + value: 80 + - equal: + path: spec.selector."app.kubernetes.io/name" + value: devspace-app + - equal: + path: spec.selector."app.kubernetes.io/component" + value: demo + diff --git a/charts/component-chart/tests/serviceaccount_test.yaml b/charts/component-chart/tests/serviceaccount_test.yaml new file mode 100644 index 0000000..6c00ef4 --- /dev/null +++ b/charts/component-chart/tests/serviceaccount_test.yaml @@ -0,0 +1,20 @@ +suite: component-chart / serviceaccount +release: + name: demo + namespace: default +values: + - test/values.yaml +templates: + - templates/service-account.yaml +tests: + - it: renders a ServiceAccount with the configured name + asserts: + - isKind: + of: ServiceAccount + - equal: + path: metadata.name + value: storage-reader + - equal: + path: metadata.labels."app.kubernetes.io/name" + value: demo + diff --git a/charts/component-chart/tests/statefulset_test.yaml b/charts/component-chart/tests/statefulset_test.yaml new file mode 100644 index 0000000..b948af1 --- /dev/null +++ b/charts/component-chart/tests/statefulset_test.yaml @@ -0,0 +1,40 @@ +suite: component-chart / statefulset detection +release: + name: demo + namespace: default +values: + - tests/values/statefulset.yaml +templates: + - templates/deployment.yaml +tests: + - it: renders a StatefulSet when a non-readOnly PVC-backed volumeMount exists + asserts: + - isKind: + of: StatefulSet + - equal: + path: metadata.name + value: demo + - equal: + path: spec.serviceName + value: demo-headless + - equal: + path: spec.volumeClaimTemplates[0].metadata.name + value: data + - equal: + path: spec.volumeClaimTemplates[0].spec.resources.requests.storage + value: 1Gi + + - it: renders a headless Service for the StatefulSet + asserts: + - isKind: + of: Service + documentIndex: 1 + - equal: + path: metadata.name + value: demo-headless + documentIndex: 1 + - equal: + path: spec.clusterIP + value: None + documentIndex: 1 + diff --git a/charts/component-chart/tests/values/statefulset.yaml b/charts/component-chart/tests/values/statefulset.yaml new file mode 100644 index 0000000..d7db151 --- /dev/null +++ b/charts/component-chart/tests/values/statefulset.yaml @@ -0,0 +1,14 @@ +containers: + - name: app + image: nginx + imageTag: "1.27" + volumeMounts: + - containerPath: /data + volume: + name: data + readOnly: false + +volumes: + - name: data + size: 1Gi + From b62538b88243403d0e1dd3c4dc13710258cef97b Mon Sep 17 00:00:00 2001 From: Dishant Pandya Date: Wed, 11 Mar 2026 20:41:58 +0530 Subject: [PATCH 6/9] test: add update test cases and test values --- .../component-chart/tests/configmap_test.yaml | 3 +- .../tests/deployment_test.yaml | 7 +-- .../tests/job_cronjob_test.yaml | 26 +++++++++ .../component-chart/tests/job_job_test.yaml | 29 ++++++++++ charts/component-chart/tests/job_test.yaml | 55 ------------------- charts/component-chart/tests/secret_test.yaml | 5 +- .../component-chart/tests/service_test.yaml | 7 +-- .../tests/serviceaccount_test.yaml | 5 +- .../tests/statefulset_test.yaml | 8 +-- .../tests/values/configmap.yaml | 4 ++ .../component-chart/tests/values/cronjob.yaml | 8 +++ .../tests/values/deployment.yaml | 40 ++++++++++++++ charts/component-chart/tests/values/job.yaml | 7 +++ .../component-chart/tests/values/secret.yaml | 6 ++ .../component-chart/tests/values/service.yaml | 5 ++ .../tests/values/serviceaccount.yaml | 3 + 16 files changed, 142 insertions(+), 76 deletions(-) create mode 100644 charts/component-chart/tests/job_cronjob_test.yaml create mode 100644 charts/component-chart/tests/job_job_test.yaml delete mode 100644 charts/component-chart/tests/job_test.yaml create mode 100644 charts/component-chart/tests/values/configmap.yaml create mode 100644 charts/component-chart/tests/values/cronjob.yaml create mode 100644 charts/component-chart/tests/values/deployment.yaml create mode 100644 charts/component-chart/tests/values/job.yaml create mode 100644 charts/component-chart/tests/values/secret.yaml create mode 100644 charts/component-chart/tests/values/service.yaml create mode 100644 charts/component-chart/tests/values/serviceaccount.yaml diff --git a/charts/component-chart/tests/configmap_test.yaml b/charts/component-chart/tests/configmap_test.yaml index 4409736..a7bb719 100644 --- a/charts/component-chart/tests/configmap_test.yaml +++ b/charts/component-chart/tests/configmap_test.yaml @@ -3,7 +3,7 @@ release: name: demo namespace: default values: - - test/values.yaml + - values/configmap.yaml templates: - templates/config-map.yaml tests: @@ -17,4 +17,3 @@ tests: - equal: path: data.KEY value: value - diff --git a/charts/component-chart/tests/deployment_test.yaml b/charts/component-chart/tests/deployment_test.yaml index 2e33b4c..ee6e691 100644 --- a/charts/component-chart/tests/deployment_test.yaml +++ b/charts/component-chart/tests/deployment_test.yaml @@ -3,7 +3,7 @@ release: name: demo namespace: default values: - - test/values.yaml + - values/deployment.yaml templates: - templates/deployment.yaml tests: @@ -15,7 +15,7 @@ tests: path: metadata.name value: demo - equal: - path: metadata.labels."azure.workload.identity/use" + path: metadata.labels["azure.workload.identity/use"] value: "true" - equal: path: spec.replicas @@ -30,7 +30,7 @@ tests: path: spec.strategy.rollingUpdate.maxUnavailable value: 0% - equal: - path: spec.selector.matchLabels."app.kubernetes.io/component" + path: spec.selector.matchLabels["app.kubernetes.io/component"] value: demo - equal: path: spec.template.spec.containers[0].name @@ -59,4 +59,3 @@ tests: - equal: path: spec.template.spec.containers[0].readinessProbe.httpGet.port value: 80 - diff --git a/charts/component-chart/tests/job_cronjob_test.yaml b/charts/component-chart/tests/job_cronjob_test.yaml new file mode 100644 index 0000000..e9420ef --- /dev/null +++ b/charts/component-chart/tests/job_cronjob_test.yaml @@ -0,0 +1,26 @@ +suite: component-chart / cronjob +release: + name: demo + namespace: default +values: + - values/cronjob.yaml +templates: + - templates/job.yaml +tests: + - it: renders a CronJob when job.schedule is set + asserts: + - isKind: + of: CronJob + - equal: + path: metadata.name + value: demo + - equal: + path: spec.schedule + value: "*/5 * * * *" + - equal: + path: spec.jobTemplate.spec.backoffLimit + value: 2 + - equal: + path: spec.jobTemplate.spec.template.spec.restartPolicy + value: OnFailure + diff --git a/charts/component-chart/tests/job_job_test.yaml b/charts/component-chart/tests/job_job_test.yaml new file mode 100644 index 0000000..fb987df --- /dev/null +++ b/charts/component-chart/tests/job_job_test.yaml @@ -0,0 +1,29 @@ +suite: component-chart / job +release: + name: demo + namespace: default +values: + - values/job.yaml +templates: + - templates/job.yaml +tests: + - it: renders a Job when job is set without schedule + asserts: + - isKind: + of: Job + - equal: + path: metadata.name + value: demo + - equal: + path: spec.backoffLimit + value: 2 + - equal: + path: spec.template.spec.restartPolicy + value: OnFailure + - equal: + path: spec.template.spec.containers[0].name + value: worker + - equal: + path: spec.template.spec.containers[0].image + value: busybox:1.36 + diff --git a/charts/component-chart/tests/job_test.yaml b/charts/component-chart/tests/job_test.yaml deleted file mode 100644 index cec828f..0000000 --- a/charts/component-chart/tests/job_test.yaml +++ /dev/null @@ -1,55 +0,0 @@ -suite: component-chart / job -release: - name: demo - namespace: default -templates: - - templates/job.yaml -tests: - - it: renders a Job when job is set without schedule - set: - containers[0].name: worker - containers[0].image: busybox - containers[0].imageTag: "1.36" - job.backoffLimit: 2 - asserts: - - isKind: - of: Job - - equal: - path: metadata.name - value: demo - - equal: - path: spec.backoffLimit - value: 2 - - equal: - path: spec.template.spec.restartPolicy - value: OnFailure - - equal: - path: spec.template.spec.containers[0].name - value: worker - - equal: - path: spec.template.spec.containers[0].image - value: busybox:1.36 - - - it: renders a CronJob when job.schedule is set - set: - containers[0].name: worker - containers[0].image: busybox - containers[0].imageTag: "1.36" - job.backoffLimit: 2 - job.schedule: "*/5 * * * *" - asserts: - - isKind: - of: CronJob - - equal: - path: metadata.name - value: demo - - equal: - path: spec.schedule - value: "*/5 * * * *" - - equal: - path: spec.jobTemplate.spec.backoffLimit - value: 2 - - equal: - path: spec.jobTemplate.spec.template.spec.restartPolicy - value: OnFailure - diff --git a/charts/component-chart/tests/secret_test.yaml b/charts/component-chart/tests/secret_test.yaml index a0bc847..c14d8ec 100644 --- a/charts/component-chart/tests/secret_test.yaml +++ b/charts/component-chart/tests/secret_test.yaml @@ -3,7 +3,7 @@ release: name: demo namespace: default values: - - test/values.yaml + - values/secret.yaml templates: - templates/config-secrets.yaml tests: @@ -18,9 +18,8 @@ tests: path: type value: Opaque - equal: - path: metadata.annotations."avp.kubernetes.io/path" + path: metadata.annotations["avp.kubernetes.io/path"] value: app - equal: path: stringData.SECRET value: "" - diff --git a/charts/component-chart/tests/service_test.yaml b/charts/component-chart/tests/service_test.yaml index 33bc7a9..7a5a718 100644 --- a/charts/component-chart/tests/service_test.yaml +++ b/charts/component-chart/tests/service_test.yaml @@ -3,7 +3,7 @@ release: name: demo namespace: default values: - - test/values.yaml + - values/service.yaml templates: - templates/service.yaml tests: @@ -24,9 +24,8 @@ tests: path: spec.ports[0].targetPort value: 80 - equal: - path: spec.selector."app.kubernetes.io/name" + path: spec.selector["app.kubernetes.io/name"] value: devspace-app - equal: - path: spec.selector."app.kubernetes.io/component" + path: spec.selector["app.kubernetes.io/component"] value: demo - diff --git a/charts/component-chart/tests/serviceaccount_test.yaml b/charts/component-chart/tests/serviceaccount_test.yaml index 6c00ef4..4f00475 100644 --- a/charts/component-chart/tests/serviceaccount_test.yaml +++ b/charts/component-chart/tests/serviceaccount_test.yaml @@ -3,7 +3,7 @@ release: name: demo namespace: default values: - - test/values.yaml + - values/serviceaccount.yaml templates: - templates/service-account.yaml tests: @@ -15,6 +15,5 @@ tests: path: metadata.name value: storage-reader - equal: - path: metadata.labels."app.kubernetes.io/name" + path: metadata.labels["app.kubernetes.io/name"] value: demo - diff --git a/charts/component-chart/tests/statefulset_test.yaml b/charts/component-chart/tests/statefulset_test.yaml index b948af1..97a4db3 100644 --- a/charts/component-chart/tests/statefulset_test.yaml +++ b/charts/component-chart/tests/statefulset_test.yaml @@ -3,11 +3,12 @@ release: name: demo namespace: default values: - - tests/values/statefulset.yaml + - values/statefulset.yaml templates: - templates/deployment.yaml tests: - it: renders a StatefulSet when a non-readOnly PVC-backed volumeMount exists + documentIndex: 0 asserts: - isKind: of: StatefulSet @@ -25,16 +26,13 @@ tests: value: 1Gi - it: renders a headless Service for the StatefulSet + documentIndex: 1 asserts: - isKind: of: Service - documentIndex: 1 - equal: path: metadata.name value: demo-headless - documentIndex: 1 - equal: path: spec.clusterIP value: None - documentIndex: 1 - diff --git a/charts/component-chart/tests/values/configmap.yaml b/charts/component-chart/tests/values/configmap.yaml new file mode 100644 index 0000000..8cd46b5 --- /dev/null +++ b/charts/component-chart/tests/values/configmap.yaml @@ -0,0 +1,4 @@ +configmap: + data: + KEY: value + diff --git a/charts/component-chart/tests/values/cronjob.yaml b/charts/component-chart/tests/values/cronjob.yaml new file mode 100644 index 0000000..5f2fd27 --- /dev/null +++ b/charts/component-chart/tests/values/cronjob.yaml @@ -0,0 +1,8 @@ +containers: + - name: worker + image: busybox + imageTag: "1.36" +job: + backoffLimit: 2 + schedule: "*/5 * * * *" + diff --git a/charts/component-chart/tests/values/deployment.yaml b/charts/component-chart/tests/values/deployment.yaml new file mode 100644 index 0000000..0a2790e --- /dev/null +++ b/charts/component-chart/tests/values/deployment.yaml @@ -0,0 +1,40 @@ +labels: + azure.workload.identity/use: "true" + +containers: + - name: demo + image: nginx + imageTag: latest + imagePullPolicy: Always + envFrom: + - secretRef: + name: demo + - configMapRef: + name: demo + env: + - name: NAME + value: demo + resources: + limits: + cpu: 10m + memory: 64Mi + requests: + cpu: 1m + memory: 10Mi + readinessProbe: + httpGet: + path: / + port: 80 + periodSeconds: 30 + initialDelaySeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + livenessProbe: + httpGet: + path: / + port: 80 + periodSeconds: 30 + initialDelaySeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + diff --git a/charts/component-chart/tests/values/job.yaml b/charts/component-chart/tests/values/job.yaml new file mode 100644 index 0000000..7fdb965 --- /dev/null +++ b/charts/component-chart/tests/values/job.yaml @@ -0,0 +1,7 @@ +containers: + - name: worker + image: busybox + imageTag: "1.36" +job: + backoffLimit: 2 + diff --git a/charts/component-chart/tests/values/secret.yaml b/charts/component-chart/tests/values/secret.yaml new file mode 100644 index 0000000..eaeed93 --- /dev/null +++ b/charts/component-chart/tests/values/secret.yaml @@ -0,0 +1,6 @@ +secrets: + annotations: + avp.kubernetes.io/path: app + stringData: + SECRET: + diff --git a/charts/component-chart/tests/values/service.yaml b/charts/component-chart/tests/values/service.yaml new file mode 100644 index 0000000..5e4418b --- /dev/null +++ b/charts/component-chart/tests/values/service.yaml @@ -0,0 +1,5 @@ +service: + type: ClusterIP + ports: + - port: 80 + diff --git a/charts/component-chart/tests/values/serviceaccount.yaml b/charts/component-chart/tests/values/serviceaccount.yaml new file mode 100644 index 0000000..1e2f169 --- /dev/null +++ b/charts/component-chart/tests/values/serviceaccount.yaml @@ -0,0 +1,3 @@ +serviceaccount: + name: storage-reader + From 121ad6ba65be5d2e7374de55a2d15c6c9c39891b Mon Sep 17 00:00:00 2001 From: Dishant Pandya Date: Wed, 11 Mar 2026 20:45:27 +0530 Subject: [PATCH 7/9] ci: add update workflow for tests --- .github/workflows/validate.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index d5c35f3..1603fa6 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -15,6 +15,10 @@ jobs: with: version: 3.x repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Setup Helm + uses: azure/setup-helm@v4 + with: + version: v3.19.0 - name: Validate Helm charts run: task helm-validate - name: Install helm-unittest plugin From 4fa6980da2a629a05d6dd54c7002d7d949d2268f Mon Sep 17 00:00:00 2001 From: Rishang Date: Thu, 19 Mar 2026 12:56:42 +0530 Subject: [PATCH 8/9] feat: Enhance external secrets configuration in values.yaml and documentation --- .../pages/configuration/external-secrets.mdx | 42 +++++++++++-------- .../templates/external-secret.yaml | 29 +++++++++++-- charts/component-chart/values.yaml | 38 ++++++++++------- 3 files changed, 74 insertions(+), 35 deletions(-) diff --git a/charts/component-chart/docs/pages/configuration/external-secrets.mdx b/charts/component-chart/docs/pages/configuration/external-secrets.mdx index 0547fa2..468516b 100644 --- a/charts/component-chart/docs/pages/configuration/external-secrets.mdx +++ b/charts/component-chart/docs/pages/configuration/external-secrets.mdx @@ -14,24 +14,32 @@ Create one or more `ExternalSecret` resources (External Secrets Operator). Rendered by: `templates/external-secret.yaml` ```yaml -externalSecrets: # struct[] | Optional list of ExternalSecrets - - name: my-external-secret # string | Required +externalSecrets: # struct | Optional external-secrets config + # ref: https://external-secrets.io/latest/api/secretstore/ + secretstore: # struct | Optional SecretStore to create + name: my-secret-store # string | Default: release name + labels: {} # map | Optional labels annotations: {} # map | Optional annotations - refreshInterval: "0s" # string | "0s" disables auto-refresh - secretStoreRef: # struct | Required - name: my-secret-store # string - kind: SecretStore # string | Default: SecretStore - target: # struct | Optional target config - name: my-k8s-secret # string | Default: external secret name - creationPolicy: Owner # string | Default: Owner - template: {} # any | Optional target template - data: # struct[] | Optional per-key mapping - - secretKey: MY_SECRET_KEY # string - remoteRef: - key: /path/to/parameter # string - property: someProperty # string | Optional - version: latest # string | Optional - dataFrom: [] # any | Optional passthrough list + provider: {} # any | Required when secretstore is set + secrets: # struct[] | Optional list of ExternalSecrets + # ref: https://external-secrets.io/latest/api/externalsecret/ + - name: my-external-secret # string | Required + annotations: {} # map | Optional annotations + refreshInterval: "0s" # string | "0s" disables auto-refresh + secretStoreRef: # struct | Optional + name: my-secret-store # string | Default order: secretStoreRef.name -> externalSecrets.secretstore.name -> release name + kind: SecretStore # string | Default: SecretStore + target: # struct | Optional target config + name: my-k8s-secret # string | Default: external secret name + creationPolicy: Owner # string | Default: Owner + template: {} # any | Optional target template + data: # struct[] | Optional per-key mapping + - secretKey: MY_SECRET_KEY # string + remoteRef: + key: /path/to/parameter # string + property: someProperty # string | Optional + version: latest # string | Optional + dataFrom: [] # any | Optional passthrough list ``` diff --git a/charts/component-chart/templates/external-secret.yaml b/charts/component-chart/templates/external-secret.yaml index eced8fc..2e05664 100644 --- a/charts/component-chart/templates/external-secret.yaml +++ b/charts/component-chart/templates/external-secret.yaml @@ -1,5 +1,28 @@ {{- if .Values.externalSecrets }} -{{- range $externalSecrets := .Values.externalSecrets }} +{{- if .Values.externalSecrets.secretstore }} +--- +apiVersion: external-secrets.io/v1 +kind: SecretStore +metadata: + name: {{ .Values.externalSecrets.secretstore.name | default $.Release.Name | quote }} + labels: + "app.kubernetes.io/name": {{ $.Release.Name | quote }} + "app.kubernetes.io/managed-by": {{ $.Release.Service | quote }} + {{- range $labelName, $labelValue := .Values.externalSecrets.secretstore.labels }} + {{ $labelName | quote }}: {{ $labelValue | quote }} + {{- end }} + {{- if .Values.externalSecrets.secretstore.annotations }} + annotations: + {{- range $annotationName, $annotationValue := .Values.externalSecrets.secretstore.annotations }} + {{ $annotationName | quote }}: {{ $annotationValue | quote }} + {{- end }} + {{- end }} +spec: + provider: + {{- toYaml .Values.externalSecrets.secretstore.provider | nindent 4 }} + +{{- end }} +{{- range $externalSecrets := .Values.externalSecrets.secrets }} --- apiVersion: external-secrets.io/v1 kind: ExternalSecret @@ -15,9 +38,9 @@ metadata: {{- end }} {{- end }} spec: - refreshInterval: {{ $externalSecrets.refreshInterval | default "0" | quote }} + refreshInterval: {{ $externalSecrets.refreshInterval | default "0s" | quote }} secretStoreRef: - name: {{ $externalSecrets.secretStoreRef.name | quote }} + name: {{ coalesce $externalSecrets.secretStoreRef.name $.Values.externalSecrets.secretstore.name $.Release.Name | quote }} kind: {{ $externalSecrets.secretStoreRef.kind | default "SecretStore" }} target: name: {{ $externalSecrets.target.name | default $externalSecrets.name | quote }} diff --git a/charts/component-chart/values.yaml b/charts/component-chart/values.yaml index 6a3b9c1..10498f1 100644 --- a/charts/component-chart/values.yaml +++ b/charts/component-chart/values.yaml @@ -86,20 +86,28 @@ httpRoutes: [] # port: 80 # External secrets for the component -externalSecrets: [] - # - name: my-external-secret +externalSecrets: {} + # secretstore: # Optional SecretStore resource to create + # # ref: https://external-secrets.io/latest/api/secretstore/ + # name: my-secret-store # Default: release name + # labels: {} # annotations: {} - # refreshInterval: "0s" # Set to "0s" to disable auto-refresh, or "1h", "5m", etc. - # secretStoreRef: - # name: my-secret-store - # kind: SecretStore - # target: - # name: my-external-secret - # creationPolicy: Owner - # data: - # - secretKey: MY_SECRET_KEY - # remoteRef: - # key: /path/to/parameter - -# Extra objects for the component + # provider: {} # Required when secretstore is set + # secrets: # List of ExternalSecret resources + # # ref: https://external-secrets.io/latest/api/externalsecret/ + # - name: my-external-secret + # annotations: {} + # refreshInterval: "0s" # Set to "0s" to disable auto-refresh, or "1h", "5m", etc. + # secretStoreRef: + # name: my-secret-store # Default order: secretStoreRef.name -> externalSecrets.secretstore.name -> release name + # kind: SecretStore # Default: SecretStore + # target: + # name: my-external-secret + # creationPolicy: Owner + # data: + # - secretKey: MY_SECRET_KEY + # remoteRef: + # key: /path/to/parameter + +# List of any extra Kubernetes manifests (CRDs) needed for the component extraObjects: [] From 5fd71907ffbf82ee62667c20ffcc392e1dbfb482 Mon Sep 17 00:00:00 2001 From: Rishang Date: Thu, 19 Mar 2026 12:57:05 +0530 Subject: [PATCH 9/9] fmt --- charts/component-chart/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/component-chart/values.yaml b/charts/component-chart/values.yaml index 10498f1..0d52cfd 100644 --- a/charts/component-chart/values.yaml +++ b/charts/component-chart/values.yaml @@ -97,7 +97,7 @@ externalSecrets: {} # # ref: https://external-secrets.io/latest/api/externalsecret/ # - name: my-external-secret # annotations: {} - # refreshInterval: "0s" # Set to "0s" to disable auto-refresh, or "1h", "5m", etc. + # refreshInterval: "0s" # Default: "0s" to disable auto-refresh, or "1h", "5m", etc. # secretStoreRef: # name: my-secret-store # Default order: secretStoreRef.name -> externalSecrets.secretstore.name -> release name # kind: SecretStore # Default: SecretStore