CtrlK
BlogDocsLog inGet started
Tessl Logo

pantheon-ai/helm-generator

Comprehensive toolkit for generating best practice Helm charts and resources following current standards and conventions. Use this skill when creating new Helm charts, implementing Helm templates, or building Helm projects from scratch.

Overall
score

93%

Does it follow best practices?

Validation for skill structure

Overview
Skills
Evals
Files

generate_chart_structure.shscripts/

#!/bin/bash

# Script to scaffold basic Helm chart directory structure
# Usage: bash generate_chart_structure.sh <chart-name> <output-directory> [options]
#
# Options:
#   --force             Overwrite existing chart without prompting
#   --image <repo>      Set image repository WITHOUT tag (default: nginx)
#   --tag <tag>         Set image tag (default: uses chart appVersion)
#   --port <number>     Set service port (default: 80)
#   --type <type>       Set workload type: deployment, statefulset, daemonset (default: deployment)
#   --with-templates    Generate basic resource templates (deployment.yaml, service.yaml, etc.)
#   --with-ingress      Include ingress template (implies --with-templates)
#   --with-hpa          Include HPA template (implies --with-templates)
#
# Note: If --image contains a colon (e.g., redis:7-alpine), it will be automatically
#       split into repository and tag

set -e

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# Default values
FORCE=false
IMAGE_REPO="nginx"
IMAGE_TAG=""
SERVICE_PORT=80
WORKLOAD_TYPE="deployment"
WITH_TEMPLATES=false
WITH_INGRESS=false
WITH_HPA=false

# Function to print error and exit
error_exit() {
    echo -e "${RED}ERROR: $1${NC}" >&2
    exit 1
}

# Function to print warning
warn() {
    echo -e "${YELLOW}WARNING: $1${NC}" >&2
}

# Function to validate chart name (DNS-1123 subdomain)
# Must be lowercase, alphanumeric, hyphens allowed (not at start/end), max 63 chars
validate_chart_name() {
    local name="$1"

    # Check if empty
    if [ -z "$name" ]; then
        error_exit "Chart name cannot be empty"
    fi

    # Check length (max 63 characters for DNS-1123)
    if [ ${#name} -gt 63 ]; then
        error_exit "Chart name '${name}' exceeds 63 characters (DNS-1123 limit)"
    fi

    # Check for valid characters (lowercase alphanumeric and hyphens)
    if ! echo "$name" | grep -qE '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'; then
        error_exit "Chart name '${name}' is invalid. Must:
  - Start with a lowercase letter or number
  - Contain only lowercase letters, numbers, and hyphens
  - End with a lowercase letter or number
  - Not contain consecutive hyphens
Examples: myapp, my-app, app1, my-cool-app"
    fi

    # Check for consecutive hyphens
    if echo "$name" | grep -q '\-\-'; then
        error_exit "Chart name '${name}' contains consecutive hyphens (not allowed)"
    fi
}

# Function to validate port number
validate_port() {
    local port="$1"
    if ! [[ "$port" =~ ^[0-9]+$ ]] || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then
        error_exit "Invalid port number '${port}'. Must be between 1 and 65535"
    fi
}

# Function to validate workload type
validate_workload_type() {
    local type="$1"
    case "$type" in
        deployment|statefulset|daemonset)
            return 0
            ;;
        *)
            error_exit "Invalid workload type '${type}'. Must be: deployment, statefulset, or daemonset"
            ;;
    esac
}

# Function to show usage
show_usage() {
    cat << EOF
Usage: $0 <chart-name> <output-directory> [options]

Arguments:
  chart-name        Name of the Helm chart (must be DNS-1123 compliant)
  output-directory  Directory where chart will be created

Options:
  --force           Overwrite existing chart without prompting
  --image <repo>    Set image repository (default: nginx)
                    Note: If image contains ':' (e.g., redis:7-alpine), it will be
                    automatically split into repository and tag
  --tag <tag>       Set image tag (default: uses chart appVersion)
  --port <number>   Set service port (default: 80)
  --type <type>     Set workload type: deployment, statefulset, daemonset (default: deployment)
  --with-templates  Generate basic resource templates (deployment.yaml, service.yaml, etc.)
  --with-ingress    Include ingress template (implies --with-templates)
  --with-hpa        Include HPA template (implies --with-templates)
  -h, --help        Show this help message

Examples:
  $0 myapp ./charts
  $0 my-service ./charts --image myregistry/myapp --port 8080
  $0 my-service ./charts --image redis --tag 7-alpine
  $0 my-db ./charts --type statefulset --force
  $0 myapp ./charts --with-templates --with-ingress

Chart name requirements (DNS-1123 subdomain):
  - Maximum 63 characters
  - Lowercase letters, numbers, and hyphens only
  - Must start and end with alphanumeric character
  - No consecutive hyphens
EOF
    exit 0
}

# Parse arguments
POSITIONAL_ARGS=()

while [[ $# -gt 0 ]]; do
    case $1 in
        -h|--help)
            show_usage
            ;;
        --force)
            FORCE=true
            shift
            ;;
        --image)
            IMAGE_REPO="$2"
            shift 2
            ;;
        --tag)
            IMAGE_TAG="$2"
            shift 2
            ;;
        --port)
            SERVICE_PORT="$2"
            shift 2
            ;;
        --type)
            WORKLOAD_TYPE="$2"
            shift 2
            ;;
        --with-templates)
            WITH_TEMPLATES=true
            shift
            ;;
        --with-ingress)
            WITH_TEMPLATES=true
            WITH_INGRESS=true
            shift
            ;;
        --with-hpa)
            WITH_TEMPLATES=true
            WITH_HPA=true
            shift
            ;;
        -*)
            error_exit "Unknown option: $1. Use --help for usage information."
            ;;
        *)
            POSITIONAL_ARGS+=("$1")
            shift
            ;;
    esac
done

# Restore positional parameters
set -- "${POSITIONAL_ARGS[@]}"

# Check required arguments
if [ $# -lt 2 ]; then
    echo "Usage: $0 <chart-name> <output-directory> [options]"
    echo "Example: $0 myapp ./charts"
    echo "Use --help for more information"
    exit 1
fi

CHART_NAME="$1"
OUTPUT_DIR="$2"
CHART_DIR="${OUTPUT_DIR}/${CHART_NAME}"

# Validate inputs
validate_chart_name "$CHART_NAME"
validate_port "$SERVICE_PORT"
validate_workload_type "$WORKLOAD_TYPE"

# Auto-split image:tag if colon is present and --tag wasn't explicitly set
if [[ "$IMAGE_REPO" == *":"* ]] && [ -z "$IMAGE_TAG" ]; then
    # Split on the last colon (to handle registry:port/image:tag format)
    IMAGE_TAG="${IMAGE_REPO##*:}"
    IMAGE_REPO="${IMAGE_REPO%:*}"
    echo "  Note: Auto-split image into repository '${IMAGE_REPO}' and tag '${IMAGE_TAG}'"
fi

# Check if chart directory already exists
if [ -d "$CHART_DIR" ]; then
    if [ "$FORCE" = true ]; then
        warn "Overwriting existing chart at ${CHART_DIR}"
        rm -rf "$CHART_DIR"
    else
        echo -e "${YELLOW}Chart directory already exists: ${CHART_DIR}${NC}"
        read -p "Do you want to overwrite it? [y/N] " -n 1 -r
        echo
        if [[ ! $REPLY =~ ^[Yy]$ ]]; then
            echo "Aborted. Use --force to overwrite without prompting."
            exit 1
        fi
        rm -rf "$CHART_DIR"
    fi
fi

echo "Creating Helm chart structure for: ${CHART_NAME}"
echo "  Output directory: ${CHART_DIR}"
echo "  Image: ${IMAGE_REPO}"
echo "  Port: ${SERVICE_PORT}"
echo "  Workload type: ${WORKLOAD_TYPE}"

# Create directories
mkdir -p "${CHART_DIR}/templates"
mkdir -p "${CHART_DIR}/charts"

# Create Chart.yaml
cat > "${CHART_DIR}/Chart.yaml" <<EOF
apiVersion: v2
name: ${CHART_NAME}
description: A Helm chart for Kubernetes
type: application

# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0

# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
appVersion: "1.0.0"
EOF

# Create values.yaml with customized settings
cat > "${CHART_DIR}/values.yaml" <<EOF
# Default values for ${CHART_NAME}.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.

# Workload type: ${WORKLOAD_TYPE}
replicaCount: 1

image:
  repository: ${IMAGE_REPO}
  pullPolicy: IfNotPresent
  # Overrides the image tag whose default is the chart appVersion.
  tag: "${IMAGE_TAG}"

imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""

serviceAccount:
  # Specifies whether a service account should be created
  create: true
  # Automatically mount a ServiceAccount's API credentials?
  automount: true
  # Annotations to add to the service account
  annotations: {}
  # The name of the service account to use.
  # If not set and create is true, a name is generated using the fullname template
  name: ""

podAnnotations: {}
podLabels: {}

podSecurityContext:
  runAsNonRoot: true
  runAsUser: 1000
  fsGroup: 1000

securityContext:
  allowPrivilegeEscalation: false
  capabilities:
    drop:
      - ALL
  readOnlyRootFilesystem: true

service:
  type: ClusterIP
  port: ${SERVICE_PORT}
  targetPort: 8080
  # -- Port name used in service and container port definitions
  # Customize for non-HTTP services (e.g., redis, mysql, grpc)
  portName: http

# -- Liveness probe configuration
livenessProbe:
  httpGet:
    path: /healthz
    port: http
  initialDelaySeconds: 30
  periodSeconds: 10

# -- Readiness probe configuration
readinessProbe:
  httpGet:
    path: /ready
    port: http
  initialDelaySeconds: 5
  periodSeconds: 5

# -- Startup probe configuration (for slow-starting containers)
startupProbe: {}
  # httpGet:
  #   path: /healthz
  #   port: http
  # initialDelaySeconds: 10
  # periodSeconds: 10
  # failureThreshold: 30

ingress:
  enabled: false
  className: ""
  annotations: {}
    # kubernetes.io/ingress.class: nginx
    # kubernetes.io/tls-acme: "true"
  hosts:
    - host: ${CHART_NAME}.local
      paths:
        - path: /
          pathType: ImplementationSpecific
  tls: []
  #  - secretName: ${CHART_NAME}-tls
  #    hosts:
  #      - ${CHART_NAME}.local

resources:
  limits:
    cpu: 100m
    memory: 128Mi
  requests:
    cpu: 100m
    memory: 128Mi

autoscaling:
  enabled: false
  minReplicas: 1
  maxReplicas: 100
  targetCPUUtilizationPercentage: 80
  # targetMemoryUtilizationPercentage: 80
EOF

# Add StatefulSet-specific values if workload type is statefulset
if [ "$WORKLOAD_TYPE" = "statefulset" ]; then
    cat >> "${CHART_DIR}/values.yaml" <<EOF

# StatefulSet specific configuration
persistence:
  enabled: true
  storageClass: ""
  accessModes:
    - ReadWriteOnce
  size: 1Gi
  mountPath: /data
EOF
fi

# Add DaemonSet-specific values if workload type is daemonset
if [ "$WORKLOAD_TYPE" = "daemonset" ]; then
    cat >> "${CHART_DIR}/values.yaml" <<EOF

# DaemonSet specific configuration
updateStrategy:
  type: RollingUpdate
  rollingUpdate:
    maxUnavailable: 1

# Host networking (for node-level daemons)
hostNetwork: false
hostPID: false
EOF
fi

# Add common trailing values
cat >> "${CHART_DIR}/values.yaml" <<EOF

# -- Environment variables
env: []
# - name: ENV_VAR_NAME
#   value: "value"

# -- Environment variables from ConfigMaps or Secrets
envFrom: []
# - configMapRef:
#     name: my-configmap
# - secretRef:
#     name: my-secret

# -- Volume mounts
volumeMounts: []
# - name: config
#   mountPath: /etc/config

# -- Volumes
volumes: []
# - name: config
#   configMap:
#     name: my-config

nodeSelector: {}

tolerations: []

affinity: {}
EOF

# Create .helmignore
cat > "${CHART_DIR}/.helmignore" <<EOF
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
# CI/CD
.github/
.gitlab-ci.yml
# Testing
test/
tests/
*.test
# Documentation
README.md
CONTRIBUTING.md
EOF

# Create NOTES.txt
cat > "${CHART_DIR}/templates/NOTES.txt" <<EOF
Thank you for installing {{ .Chart.Name }}.

Your release is named {{ .Release.Name }}.

To get the status of your release:

  helm status {{ .Release.Name }} -n {{ .Release.Namespace }}

To connect to your service:
{{- if and .Values.ingress (eq .Values.ingress.enabled true) }}
{{- range \$host := .Values.ingress.hosts }}
  {{- range .paths }}
  http{{ if \$.Values.ingress.tls }}s{{ end }}://{{ \$host.host }}{{ .path }}
  {{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
  export NODE_PORT=\$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "${CHART_NAME}.fullname" . }})
  export NODE_IP=\$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
  echo "Service available at: \$NODE_IP:\$NODE_PORT"
{{- else if contains "LoadBalancer" .Values.service.type }}
  NOTE: It may take a few minutes for the LoadBalancer IP to be available.
  kubectl get svc --namespace {{ .Release.Namespace }} -w {{ include "${CHART_NAME}.fullname" . }}

  export SERVICE_IP=\$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "${CHART_NAME}.fullname" . }} --template "{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}")
  echo "Service available at: \$SERVICE_IP:{{ .Values.service.port }}"
{{- else if contains "ClusterIP" .Values.service.type }}
  kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ include "${CHART_NAME}.fullname" . }} {{ .Values.service.port }}:{{ .Values.service.port }}

  Then access via: localhost:{{ .Values.service.port }}
{{- end }}
EOF

# Generate _helpers.tpl using the standard helpers script
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [ -f "${SCRIPT_DIR}/generate_standard_helpers.sh" ]; then
    bash "${SCRIPT_DIR}/generate_standard_helpers.sh" "${CHART_NAME}" "${CHART_DIR}" 2>/dev/null || true
fi

# Generate resource templates if requested
if [ "$WITH_TEMPLATES" = true ]; then
    echo "  Generating resource templates..."

    # Generate ServiceAccount template
    cat > "${CHART_DIR}/templates/serviceaccount.yaml" <<SAEOF
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
  name: {{ include "${CHART_NAME}.serviceAccountName" . }}
  labels:
    {{- include "${CHART_NAME}.labels" . | nindent 4 }}
  {{- with .Values.serviceAccount.annotations }}
  annotations:
    {{- toYaml . | nindent 4 }}
  {{- end }}
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
{{- end }}
SAEOF

    # Generate Service template
    cat > "${CHART_DIR}/templates/service.yaml" <<SVCEOF
apiVersion: v1
kind: Service
metadata:
  name: {{ include "${CHART_NAME}.fullname" . }}
  labels:
    {{- include "${CHART_NAME}.labels" . | nindent 4 }}
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: {{ .Values.service.portName | default "http" }}
      protocol: TCP
      name: {{ .Values.service.portName | default "http" }}
  selector:
    {{- include "${CHART_NAME}.selectorLabels" . | nindent 4 }}
SVCEOF

    # Generate headless service for StatefulSet
    if [ "$WORKLOAD_TYPE" = "statefulset" ]; then
        cat > "${CHART_DIR}/templates/service-headless.yaml" <<HLSVCEOF
apiVersion: v1
kind: Service
metadata:
  name: {{ include "${CHART_NAME}.fullname" . }}-headless
  labels:
    {{- include "${CHART_NAME}.labels" . | nindent 4 }}
spec:
  type: ClusterIP
  clusterIP: None
  ports:
    - port: {{ .Values.service.port }}
      targetPort: {{ .Values.service.portName | default "http" }}
      protocol: TCP
      name: {{ .Values.service.portName | default "http" }}
  selector:
    {{- include "${CHART_NAME}.selectorLabels" . | nindent 4 }}
HLSVCEOF
    fi

    # Generate workload template based on type
    case "$WORKLOAD_TYPE" in
        deployment)
            cat > "${CHART_DIR}/templates/deployment.yaml" <<DEPEOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "${CHART_NAME}.fullname" . }}
  labels:
    {{- include "${CHART_NAME}.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "${CHART_NAME}.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      annotations:
        {{- with .Values.podAnnotations }}
        {{- toYaml . | nindent 8 }}
        {{- end }}
        {{- if .Values.configMap }}
        {{- if .Values.configMap.enabled }}
        checksum/config: {{ include (print \$.Template.BasePath "/configmap.yaml") . | sha256sum }}
        {{- end }}
        {{- end }}
        {{- if .Values.secret }}
        {{- if .Values.secret.enabled }}
        checksum/secret: {{ include (print \$.Template.BasePath "/secret.yaml") . | sha256sum }}
        {{- end }}
        {{- end }}
      labels:
        {{- include "${CHART_NAME}.labels" . | nindent 8 }}
        {{- with .Values.podLabels }}
        {{- toYaml . | nindent 8 }}
        {{- end }}
    spec:
      {{- with .Values.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      serviceAccountName: {{ include "${CHART_NAME}.serviceAccountName" . }}
      securityContext:
        {{- toYaml .Values.podSecurityContext | nindent 8 }}
      containers:
        - name: {{ .Chart.Name }}
          securityContext:
            {{- toYaml .Values.securityContext | nindent 12 }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: {{ .Values.service.portName | default "http" }}
              containerPort: {{ .Values.service.targetPort }}
              protocol: TCP
          {{- with .Values.livenessProbe }}
          livenessProbe:
            {{- toYaml . | nindent 12 }}
          {{- end }}
          {{- with .Values.readinessProbe }}
          readinessProbe:
            {{- toYaml . | nindent 12 }}
          {{- end }}
          {{- with .Values.startupProbe }}
          startupProbe:
            {{- toYaml . | nindent 12 }}
          {{- end }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          {{- with .Values.env }}
          env:
            {{- toYaml . | nindent 12 }}
          {{- end }}
          {{- with .Values.envFrom }}
          envFrom:
            {{- toYaml . | nindent 12 }}
          {{- end }}
          {{- with .Values.volumeMounts }}
          volumeMounts:
            {{- toYaml . | nindent 12 }}
          {{- end }}
      {{- with .Values.volumes }}
      volumes:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.nodeSelector }}
      nodeSelector:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.affinity }}
      affinity:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.tolerations }}
      tolerations:
        {{- toYaml . | nindent 8 }}
      {{- end }}
DEPEOF
            ;;
        statefulset)
            cat > "${CHART_DIR}/templates/statefulset.yaml" <<STSEOF
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: {{ include "${CHART_NAME}.fullname" . }}
  labels:
    {{- include "${CHART_NAME}.labels" . | nindent 4 }}
spec:
  serviceName: {{ include "${CHART_NAME}.fullname" . }}-headless
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "${CHART_NAME}.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      annotations:
        {{- with .Values.podAnnotations }}
        {{- toYaml . | nindent 8 }}
        {{- end }}
        {{- if .Values.configMap }}
        {{- if .Values.configMap.enabled }}
        checksum/config: {{ include (print \$.Template.BasePath "/configmap.yaml") . | sha256sum }}
        {{- end }}
        {{- end }}
        {{- if .Values.secret }}
        {{- if .Values.secret.enabled }}
        checksum/secret: {{ include (print \$.Template.BasePath "/secret.yaml") . | sha256sum }}
        {{- end }}
        {{- end }}
      labels:
        {{- include "${CHART_NAME}.labels" . | nindent 8 }}
        {{- with .Values.podLabels }}
        {{- toYaml . | nindent 8 }}
        {{- end }}
    spec:
      {{- with .Values.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      serviceAccountName: {{ include "${CHART_NAME}.serviceAccountName" . }}
      securityContext:
        {{- toYaml .Values.podSecurityContext | nindent 8 }}
      containers:
        - name: {{ .Chart.Name }}
          securityContext:
            {{- toYaml .Values.securityContext | nindent 12 }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: {{ .Values.service.portName | default "http" }}
              containerPort: {{ .Values.service.targetPort }}
              protocol: TCP
          {{- with .Values.livenessProbe }}
          livenessProbe:
            {{- toYaml . | nindent 12 }}
          {{- end }}
          {{- with .Values.readinessProbe }}
          readinessProbe:
            {{- toYaml . | nindent 12 }}
          {{- end }}
          {{- with .Values.startupProbe }}
          startupProbe:
            {{- toYaml . | nindent 12 }}
          {{- end }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          {{- with .Values.env }}
          env:
            {{- toYaml . | nindent 12 }}
          {{- end }}
          {{- with .Values.envFrom }}
          envFrom:
            {{- toYaml . | nindent 12 }}
          {{- end }}
          volumeMounts:
            {{- if .Values.persistence.enabled }}
            - name: data
              mountPath: {{ .Values.persistence.mountPath }}
            {{- end }}
            {{- with .Values.volumeMounts }}
            {{- toYaml . | nindent 12 }}
            {{- end }}
      {{- with .Values.volumes }}
      volumes:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.nodeSelector }}
      nodeSelector:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.affinity }}
      affinity:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.tolerations }}
      tolerations:
        {{- toYaml . | nindent 8 }}
      {{- end }}
  {{- if .Values.persistence.enabled }}
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes:
          {{- range .Values.persistence.accessModes }}
          - {{ . | quote }}
          {{- end }}
        {{- if .Values.persistence.storageClass }}
        storageClassName: {{ .Values.persistence.storageClass | quote }}
        {{- end }}
        resources:
          requests:
            storage: {{ .Values.persistence.size | quote }}
  {{- end }}
STSEOF
            ;;
        daemonset)
            cat > "${CHART_DIR}/templates/daemonset.yaml" <<DSEOF
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: {{ include "${CHART_NAME}.fullname" . }}
  labels:
    {{- include "${CHART_NAME}.labels" . | nindent 4 }}
spec:
  selector:
    matchLabels:
      {{- include "${CHART_NAME}.selectorLabels" . | nindent 6 }}
  {{- with .Values.updateStrategy }}
  updateStrategy:
    {{- toYaml . | nindent 4 }}
  {{- end }}
  template:
    metadata:
      annotations:
        {{- with .Values.podAnnotations }}
        {{- toYaml . | nindent 8 }}
        {{- end }}
        {{- if .Values.configMap }}
        {{- if .Values.configMap.enabled }}
        checksum/config: {{ include (print \$.Template.BasePath "/configmap.yaml") . | sha256sum }}
        {{- end }}
        {{- end }}
        {{- if .Values.secret }}
        {{- if .Values.secret.enabled }}
        checksum/secret: {{ include (print \$.Template.BasePath "/secret.yaml") . | sha256sum }}
        {{- end }}
        {{- end }}
      labels:
        {{- include "${CHART_NAME}.labels" . | nindent 8 }}
        {{- with .Values.podLabels }}
        {{- toYaml . | nindent 8 }}
        {{- end }}
    spec:
      {{- with .Values.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      serviceAccountName: {{ include "${CHART_NAME}.serviceAccountName" . }}
      securityContext:
        {{- toYaml .Values.podSecurityContext | nindent 8 }}
      hostNetwork: {{ .Values.hostNetwork | default false }}
      {{- if .Values.hostPID }}
      hostPID: true
      {{- end }}
      containers:
        - name: {{ .Chart.Name }}
          securityContext:
            {{- toYaml .Values.securityContext | nindent 12 }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: {{ .Values.service.portName | default "http" }}
              containerPort: {{ .Values.service.targetPort }}
              protocol: TCP
          {{- with .Values.livenessProbe }}
          livenessProbe:
            {{- toYaml . | nindent 12 }}
          {{- end }}
          {{- with .Values.readinessProbe }}
          readinessProbe:
            {{- toYaml . | nindent 12 }}
          {{- end }}
          {{- with .Values.startupProbe }}
          startupProbe:
            {{- toYaml . | nindent 12 }}
          {{- end }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          {{- with .Values.env }}
          env:
            {{- toYaml . | nindent 12 }}
          {{- end }}
          {{- with .Values.envFrom }}
          envFrom:
            {{- toYaml . | nindent 12 }}
          {{- end }}
          {{- with .Values.volumeMounts }}
          volumeMounts:
            {{- toYaml . | nindent 12 }}
          {{- end }}
      {{- with .Values.volumes }}
      volumes:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.nodeSelector }}
      nodeSelector:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.tolerations }}
      tolerations:
        {{- toYaml . | nindent 8 }}
      {{- end }}
DSEOF
            ;;
    esac

    # Generate Ingress template if requested
    if [ "$WITH_INGRESS" = true ]; then
        cat > "${CHART_DIR}/templates/ingress.yaml" <<'INGEOF'
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ include "CHART_PLACEHOLDER.fullname" . }}
  labels:
    {{- include "CHART_PLACEHOLDER.labels" . | nindent 4 }}
  {{- with .Values.ingress.annotations }}
  annotations:
    {{- toYaml . | nindent 4 }}
  {{- end }}
spec:
  {{- if .Values.ingress.className }}
  ingressClassName: {{ .Values.ingress.className }}
  {{- end }}
  {{- if .Values.ingress.tls }}
  tls:
    {{- range .Values.ingress.tls }}
    - hosts:
        {{- range .hosts }}
        - {{ . | quote }}
        {{- end }}
      secretName: {{ .secretName }}
    {{- end }}
  {{- end }}
  rules:
    {{- range .Values.ingress.hosts }}
    - host: {{ .host | quote }}
      http:
        paths:
          {{- range .paths }}
          - path: {{ .path }}
            pathType: {{ .pathType }}
            backend:
              service:
                name: {{ include "CHART_PLACEHOLDER.fullname" $ }}
                port:
                  number: {{ $.Values.service.port }}
          {{- end }}
    {{- end }}
{{- end }}
INGEOF
        sed -i.bak "s/CHART_PLACEHOLDER/${CHART_NAME}/g" "${CHART_DIR}/templates/ingress.yaml"
        rm -f "${CHART_DIR}/templates/ingress.yaml.bak"
    fi

    # Generate HPA template if requested (only for Deployment and StatefulSet)
    if [ "$WITH_HPA" = true ] && [ "$WORKLOAD_TYPE" != "daemonset" ]; then
        # Determine the workload kind for HPA
        HPA_KIND="Deployment"
        if [ "$WORKLOAD_TYPE" = "statefulset" ]; then
            HPA_KIND="StatefulSet"
        fi
        cat > "${CHART_DIR}/templates/hpa.yaml" <<HPAEOF
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: {{ include "${CHART_NAME}.fullname" . }}
  labels:
    {{- include "${CHART_NAME}.labels" . | nindent 4 }}
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: ${HPA_KIND}
    name: {{ include "${CHART_NAME}.fullname" . }}
  minReplicas: {{ .Values.autoscaling.minReplicas }}
  maxReplicas: {{ .Values.autoscaling.maxReplicas }}
  metrics:
    {{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
    {{- end }}
    {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
    {{- end }}
{{- end }}
HPAEOF
    elif [ "$WITH_HPA" = true ] && [ "$WORKLOAD_TYPE" = "daemonset" ]; then
        warn "HPA is not applicable for DaemonSet workloads - skipping hpa.yaml"
    fi
    echo "  ✅ Resource templates generated"
fi

echo ""
echo -e "${GREEN}✅ Chart structure created successfully!${NC}"
echo ""
echo "   📁 ${CHART_DIR}/"
echo "   ├── Chart.yaml"
echo "   ├── values.yaml"
echo "   ├── .helmignore"
echo "   ├── templates/"
if [ -f "${CHART_DIR}/templates/_helpers.tpl" ]; then
    echo "   │   ├── _helpers.tpl"
fi
echo "   │   ├── NOTES.txt"
if [ "$WITH_TEMPLATES" = true ]; then
    echo "   │   ├── serviceaccount.yaml"
    echo "   │   ├── service.yaml"
    if [ "$WORKLOAD_TYPE" = "statefulset" ]; then
        echo "   │   ├── service-headless.yaml"
        echo "   │   ├── statefulset.yaml"
    elif [ "$WORKLOAD_TYPE" = "daemonset" ]; then
        echo "   │   ├── daemonset.yaml"
    else
        echo "   │   ├── deployment.yaml"
    fi
    if [ "$WITH_INGRESS" = true ]; then
        echo "   │   ├── ingress.yaml"
    fi
    if [ "$WITH_HPA" = true ] && [ "$WORKLOAD_TYPE" != "daemonset" ]; then
        echo "   │   └── hpa.yaml"
    fi
fi
echo "   └── charts/"
echo ""
echo "Next steps:"
if [ "$WITH_TEMPLATES" = true ]; then
    echo "1. Customize values in ${CHART_DIR}/values.yaml"
    echo "2. Validate with: helm lint ${CHART_DIR}"
    echo "3. Test rendering: helm template test ${CHART_DIR}"
else
    echo "1. Generate templates or use --with-templates flag"
    echo "2. Customize values in ${CHART_DIR}/values.yaml"
    echo "3. Validate with: helm lint ${CHART_DIR}"
fi

Install with Tessl CLI

npx tessl i pantheon-ai/helm-generator@0.1.0

SKILL.md

tile.json