#!/usr/bin/env bash # init-app.sh # Creates clusters//apps/, renders templates (APPLICATION_NAME, DESCRIPTION, CLUSTER_NAME), # appends the app to the end of the parent clusters//apps/kustomization.yaml list (removing trailing blank line if present), # creates a placeholder under manifests//, # and stages changes in Git (including the placeholder file). set -euo pipefail resolve_script_dir() { local src="${BASH_SOURCE[0]:-$0}" while [ -h "$src" ]; do local dir dir="$(cd -P "$(dirname "$src")" && pwd)" src="$(readlink "$src")" [[ "$src" != /* ]] && src="$dir/$src" done cd -P "$(dirname "$src")" >/dev/null 2>&1 pwd } SCRIPT_DIR="$(resolve_script_dir)" # Application name read -r -p "Application name (lowercase, letters/numbers/dashes): " APPLICATION_NAME APPLICATION_NAME="$(echo "$APPLICATION_NAME" | awk '{$1=$1;print}')" if [[ -z "$APPLICATION_NAME" ]]; then echo "Error: Application name cannot be empty." >&2; exit 1 fi if ! [[ "$APPLICATION_NAME" =~ ^[a-z0-9-]+$ ]]; then echo "Error: Application name must be lowercase and contain only letters (a–z), numbers (0–9), or dashes (-)." >&2; exit 1 fi if [[ "$APPLICATION_NAME" == *"/"* || "$APPLICATION_NAME" == *"\\"* ]]; then echo "Error: Application name must not contain path separators (/ or \\)." >&2; exit 1 fi # Cluster name DEFAULT_CLUSTER="artemis" read -r -p "Cluster name [${DEFAULT_CLUSTER}] (lowercase, letters/numbers/dashes): " CLUSTER_NAME CLUSTER_NAME="${CLUSTER_NAME:-$DEFAULT_CLUSTER}" CLUSTER_NAME="$(echo "$CLUSTER_NAME" | awk '{$1=$1;print}')" if [[ -z "$CLUSTER_NAME" ]]; then echo "Error: Cluster name cannot be empty." >&2; exit 1 fi if ! [[ "$CLUSTER_NAME" =~ ^[a-z0-9-]+$ ]]; then echo "Error: Cluster name must be lowercase and contain only letters (a–z), numbers (0–9), or dashes (-)." >&2; exit 1 fi if [[ "$CLUSTER_NAME" == *"/"* || "$CLUSTER_NAME" == *"\\"* ]]; then echo "Error: Cluster name must not contain path separators (/ or \\)." >&2; exit 1 fi # Description read -r -p "Description (spaces, letters, numbers, dots, commas): " DESCRIPTION DESCRIPTION="$(echo "$DESCRIPTION" | sed 's/^[[:space:]]\+//; s/[[:space:]]\+$//')" if [ -z "$DESCRIPTION" ]; then echo "Error: Description cannot be empty." >&2; exit 1 fi if ! echo "$DESCRIPTION" | grep -Eq '^[A-Za-z0-9 .,]+$'; then echo "Error: Description may only contain spaces, letters (A–Z, a–z), numbers (0–9), dots (.), and commas (,)." >&2; exit 1 fi # Paths TARGET_DIR="${SCRIPT_DIR}/clusters/${CLUSTER_NAME}/apps/${APPLICATION_NAME}" MANIFESTS_DIR="${SCRIPT_DIR}/manifests/${CLUSTER_NAME}/${APPLICATION_NAME}" PARENT_KUSTOMIZATION="${SCRIPT_DIR}/clusters/${CLUSTER_NAME}/apps/kustomization.yaml" # Abort if clusters target already exists if [[ -d "$TARGET_DIR" ]]; then echo "Error: Directory already exists: $TARGET_DIR" >&2; exit 1 fi # Templates TEMPLATE_DIR="${SCRIPT_DIR}/.templates" APP_PROJECT_SRC="${TEMPLATE_DIR}/app-project.yaml" APPLICATION_SRC="${TEMPLATE_DIR}/application.yaml" KUSTOMIZATION_SRC="${TEMPLATE_DIR}/kustomization.yaml" missing=false for f in "$APP_PROJECT_SRC" "$APPLICATION_SRC" "$KUSTOMIZATION_SRC"; do if [[ ! -f "$f" ]]; then echo "Error: Template not found: ${f}" >&2; missing=true; fi done if [[ "$missing" == true ]]; then exit 1; fi # Create target directories mkdir -p "$TARGET_DIR" mkdir -p "$MANIFESTS_DIR" # Prepare safe replacements safe_app_name="${APPLICATION_NAME//\\/\\\\}"; safe_app_name="${safe_app_name//&/\\&}" safe_description="${DESCRIPTION//\\/\\\\}"; safe_description="${safe_description//&/\\&}" safe_cluster_name="${CLUSTER_NAME//\\/\\\\}"; safe_cluster_name="${safe_cluster_name//&/\\&}" # Temp files (init for trap) tmp1=""; tmp2="" trap 'rm -f "$tmp1" "$tmp2"' EXIT # Render app-project.yaml tmp1="$(mktemp)" APP_PROJECT_DEST="${TARGET_DIR}/app-project.yaml" sed -e "s/\${APPLICATION_NAME}/${safe_app_name}/g" \ -e "s/\${DESCRIPTION}/${safe_description}/g" \ "$APP_PROJECT_SRC" > "$tmp1" mv "$tmp1" "$APP_PROJECT_DEST" # Render application.yaml (APPLICATION_NAME + CLUSTER_NAME) tmp2="$(mktemp)" APPLICATION_DEST="${TARGET_DIR}/application.yaml" sed -e "s/\${APPLICATION_NAME}/${safe_app_name}/g" \ -e "s/\${CLUSTER_NAME}/${safe_cluster_name}/g" \ "$APPLICATION_SRC" > "$tmp2" mv "$tmp2" "$APPLICATION_DEST" # Copy kustomization.yaml as-is KUSTOMIZATION_DEST="${TARGET_DIR}/kustomization.yaml" cp "$KUSTOMIZATION_SRC" "$KUSTOMIZATION_DEST" # Create placeholder in manifests// PLACEHOLDER_FILE="${MANIFESTS_DIR}/.placeholder" if [[ ! -f "$PLACEHOLDER_FILE" ]]; then printf "Placeholder for %s/%s\n" "$CLUSTER_NAME" "$APPLICATION_NAME" > "$PLACEHOLDER_FILE" fi echo "Created: $TARGET_DIR" echo "Wrote: $APP_PROJECT_DEST (APPLICATION_NAME='${APPLICATION_NAME}', DESCRIPTION set)" echo "Wrote: $APPLICATION_DEST (APPLICATION_NAME='${APPLICATION_NAME}', CLUSTER_NAME='${CLUSTER_NAME}')" echo "Wrote: $KUSTOMIZATION_DEST (copied as-is)" echo "Created: $PLACEHOLDER_FILE" # Parent kustomization.yaml: remove trailing blank line if present, then append new item if [[ ! -f "$PARENT_KUSTOMIZATION" ]]; then echo "Error: Parent kustomization not found: ${PARENT_KUSTOMIZATION}" >&2; exit 1 fi # Remove trailing empty last line (BSD sed) sed -i '' -e '${/^$/d;}' "$PARENT_KUSTOMIZATION" # Append the new application entry at end (two-space indent) printf " - %s\n" "$APPLICATION_NAME" >> "$PARENT_KUSTOMIZATION" echo "Updated parent kustomization.yaml: appended '${APPLICATION_NAME}' at end of resources list." # Stage changes in Git (including placeholder file) if git -C "$SCRIPT_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then REPO_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel)" REL_TARGET="${TARGET_DIR#$REPO_ROOT/}" REL_PARENT="${PARENT_KUSTOMIZATION#$REPO_ROOT/}" REL_MANIFESTS_DIR="${MANIFESTS_DIR#$REPO_ROOT/}" REL_PLACEHOLDER="${PLACEHOLDER_FILE#$REPO_ROOT/}" git -C "$REPO_ROOT" add "$REL_TARGET" "$REL_PARENT" "$REL_MANIFESTS_DIR" "$REL_PLACEHOLDER" echo "Git: staged new app dir, parent kustomization, manifests dir, and placeholder:" echo " $REL_TARGET" echo " $REL_PARENT" echo " $REL_MANIFESTS_DIR" echo " $REL_PLACEHOLDER" echo "Next: commit with a message, e.g.:" echo " git -C \"$REPO_ROOT\" commit -m \"feat(${CLUSTER_NAME}): add ${APPLICATION_NAME} app (\"${DESCRIPTION}\") and manifests placeholder; update kustomization\"" else echo "Note: Not inside a Git repository (no .git found). Skipping git add." fi