#!/usr/bin/env bash # init-app.sh # Creates clusters//apps/ relative to this script, renders templates, # updates parent clusters//apps/kustomization.yaml to include the new application, # and stages changes in Git. 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 (allows spaces, letters upper/lower, digits, dot, comma) 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 # Portable validation using grep -Eq (works under sh, dash, bash) # Allowed chars: A-Za-z0-9 space dot comma 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 TARGET_DIR="${SCRIPT_DIR}/clusters/${CLUSTER_NAME}/apps/${APPLICATION_NAME}" # Abort if target directory 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 directory mkdir -p "$TARGET_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//&/\\&}" # Render app-project.yaml APP_PROJECT_DEST="${TARGET_DIR}/app-project.yaml" tmp1="$(mktemp)" trap 'rm -f "$tmp1" "$tmp2" "$tmp3" "$tmp_parent" "$tmp_norm" "$tmp_slice"' EXIT 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 (replace APPLICATION_NAME and CLUSTER_NAME) APPLICATION_DEST="${TARGET_DIR}/application.yaml" tmp2="$(mktemp)" 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" 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)" # Update parent kustomization.yaml one level above the new folder PARENT_KUSTOMIZATION="${SCRIPT_DIR}/clusters/${CLUSTER_NAME}/apps/kustomization.yaml" if [[ ! -f "$PARENT_KUSTOMIZATION" ]]; then echo "Error: Parent kustomization not found: ${PARENT_KUSTOMIZATION}" >&2 echo "Hint: Ensure a kustomization.yaml exists in clusters/${CLUSTER_NAME}/apps/" >&2 exit 1 fi # Normalize CRLF line endings if present if file "$PARENT_KUSTOMIZATION" | grep -q "CRLF"; then tmp_norm="$(mktemp)" tr -d '\r' < "$PARENT_KUSTOMIZATION" > "$tmp_norm" mv "$tmp_norm" "$PARENT_KUSTOMIZATION" fi # If already present, skip if grep -Eq "^[[:space:]]*-[[:space:]]*${APPLICATION_NAME}[[:space:]]*$" "$PARENT_KUSTOMIZATION"; then echo "Parent kustomization.yaml already contains resource '${APPLICATION_NAME}', skipping insertion." else # Determine indentation from the first existing resource item (fallback two spaces) indent="$(grep -E '^[[:space:]]*-[[:space:]]' "$PARENT_KUSTOMIZATION" | sed -n '1s/^\([[:space:]]*\)-.*/\1/p')" [[ -z "${indent:-}" ]] && indent=" " # Work within the most recent 'resources:' block (if any) last_res_start_line="$(nl -ba "$PARENT_KUSTOMIZATION" | awk '/^[[:space:]]*[0-9]+\s+resources:[[:space:]]*$/ { line=$1 } END { if (line!="") print line }')" if [[ -n "${last_res_start_line:-}" ]]; then # Slice parent from resources start to EOF tmp_slice="$(mktemp)" tail -n +"$last_res_start_line" "$PARENT_KUSTOMIZATION" > "$tmp_slice" # In the slice, find last '-' resource line number slice_last_line="$(nl -ba "$tmp_slice" \ | awk '/^[[:space:]]*[0-9]+\s+[[:space:]]*-[[:space:]]/ { print $1 }' \ | tail -n1)" if [[ -n "${slice_last_line:-}" ]]; then # Map slice line back to parent absolute line abs_last_line=$(( last_res_start_line + slice_last_line - 1 )) # Build new file via tmp_parent; avoid arithmetic in command args tmp_parent="$(mktemp)" head -n "$abs_last_line" "$PARENT_KUSTOMIZATION" > "$tmp_parent" printf "%s- %s\n" "$indent" "$APPLICATION_NAME" >> "$tmp_parent" next_line=$(( abs_last_line + 1 )) tail -n +"$next_line" "$PARENT_KUSTOMIZATION" >> "$tmp_parent" mv "$tmp_parent" "$PARENT_KUSTOMIZATION" echo "Updated parent kustomization.yaml: appended '${APPLICATION_NAME}' to resources." else # No items yet → insert directly after the resources key line tmp_parent="$(mktemp)" head -n "$last_res_start_line" "$PARENT_KUSTOMIZATION" > "$tmp_parent" printf "%s- %s\n" "$indent" "$APPLICATION_NAME" >> "$tmp_parent" next_line=$(( last_res_start_line + 1 )) tail -n +"$next_line" "$PARENT_KUSTOMIZATION" >> "$tmp_parent" mv "$tmp_parent" "$PARENT_KUSTOMIZATION" echo "Updated parent kustomization.yaml: added first resource '${APPLICATION_NAME}' under resources." fi rm -f "$tmp_slice" else # No resources key exists at all → create section at the end ONCE via tmp_parent tmp_parent="$(mktemp)" cat "$PARENT_KUSTOMIZATION" > "$tmp_parent" printf "\nresources:\n%s- %s\n" "$indent" "$APPLICATION_NAME" >> "$tmp_parent" mv "$tmp_parent" "$PARENT_KUSTOMIZATION" echo "Updated parent kustomization.yaml: created resources section with '${APPLICATION_NAME}'." fi fi # Stage changes in Git (if inside a Git repo) 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/}" git -C "$REPO_ROOT" add "$REL_TARGET" "$REL_PARENT" echo "Git: staged new directory/files and parent kustomization: $REL_TARGET, $REL_PARENT" echo "Next: commit with a message, e.g.:" echo " git -C \"$REPO_ROOT\" commit -m \"feat(${CLUSTER_NAME}): add ${APPLICATION_NAME} app (\"${DESCRIPTION}\") and update kustomization\"" else echo "Note: Not inside a Git repository (no .git found). Skipping git add." fi