From c7c32d3594e23ae20cac8c1cbfc2f4edd39c66b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Groothuis?= Date: Thu, 23 Oct 2025 14:17:39 +0200 Subject: [PATCH] chore(bootstrap): Added templating --- init-app.sh | 217 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) diff --git a/init-app.sh b/init-app.sh index e69de29..ca1c651 100755 --- a/init-app.sh +++ b/init-app.sh @@ -0,0 +1,217 @@ +#!/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 + 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 + tmp_slice="$(mktemp)" + tail -n +"$last_res_start_line" "$PARENT_KUSTOMIZATION" > "$tmp_slice" + + slice_last_line="$(nl -ba "$tmp_slice" \ + | awk '/^[[:space:]]*[0-9]+\s+[[:space:]]*-[[:space:]]/ { print $1 }' \ + | tail -n1)" + + if [[ -n "${slice_last_line:-}" ]]; then + abs_last_line=$(( last_res_start_line + slice_last_line - 1 )) + tmp_parent="$(mktemp)" + { + head -n "$abs_last_line" "$PARENT_KUSTOMIZATION" + printf "%s- %s\n" "$indent" "$APPLICATION_NAME" + tail -n +"$((abs_last_line + 1))" "$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" + printf "%s- %s\n" "$indent" "$APPLICATION_NAME" + tail -n +"$((last_res_start_line + 1))" "$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 + tmp_parent="$(mktemp)" + { + cat "$PARENT_KUSTOMIZATION" + 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