Files
kubernetes/init-app.sh
2025-10-23 14:17:39 +02:00

218 lines
8.1 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# init-app.sh
# Creates clusters/<cluster>/apps/<application> relative to this script, renders templates,
# updates parent clusters/<cluster>/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 (AZ, az), numbers (09), 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