#!/usr/bin/env bash set -euo pipefail usage() { cat <<'USAGE' Usage: scripts/init-template \ --repo-name my-app Optional: --dry-run --repo-url https://git.olb42.com/olb42/my-app.git --namespace my-app --release-name my-app --hostname my-app.example.com USAGE } die() { echo "Error: $*" >&2 exit 1 } add_error() { errors+=("$*") } add_warning() { warnings+=("$*") } validate_dns_label() { local value="$1" [[ ${#value} -le 63 && "$value" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]] } validate_hostname() { local value="$1" [[ ${#value} -le 253 && "$value" != .* && "$value" != *. ]] || return 1 local old_ifs="$IFS" local labels=() IFS='.' read -r -a labels <<< "$value" IFS="$old_ifs" local label for label in "${labels[@]}"; do validate_dns_label "$label" || return 1 done } print_preflight() { local status="$1" echo "Preflight checks for $repo_name" echo " template: $template_dir_real" echo " target: $target_dir" echo " repo URL: $repo_url" echo " namespace: $namespace" echo " hostname: $hostname" echo if [[ ${#warnings[@]} -gt 0 ]]; then echo "Warnings:" local warning for warning in "${warnings[@]}"; do echo " - $warning" done echo fi if [[ ${#errors[@]} -gt 0 ]]; then echo "Failures:" local error for error in "${errors[@]}"; do echo " - $error" done echo echo "$status" return 1 fi echo "$status" } run_preflight() { local include_remote_check="${1:-false}" errors=() warnings=() command -v git >/dev/null 2>&1 || add_error "git is required" command -v python3 >/dev/null 2>&1 || add_error "python3 is required" command -v cp >/dev/null 2>&1 || add_error "cp is required" if [[ -z "$repo_name" ]]; then add_error "--repo-name is required" elif [[ ! "$repo_name" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then add_error "--repo-name must be kebab-case using lowercase letters, numbers, and hyphens" fi if [[ -z "$template_dir_real" ]]; then add_error "template directory not found: $template_dir" elif [[ "$current_dir/" != "$template_dir_real"/* ]]; then add_error "run this script from $template_dir or one of its subdirectories" elif [[ ! -d "$template_dir_real/.git" ]]; then add_error "template directory is not a git repository: $template_dir_real" else local branch branch="$(git -C "$template_dir_real" branch --show-current 2>/dev/null || true)" [[ "$branch" == "init" ]] || add_warning "template branch is '$branch', expected 'init'" fi if [[ -d "$apps_dir" ]]; then [[ -w "$apps_dir" ]] || add_error "apps directory is not writable: $apps_dir" else local apps_parent apps_parent="$(dirname "$apps_dir")" [[ -d "$apps_parent" && -w "$apps_parent" ]] || add_error "cannot create apps directory under: $apps_parent" fi [[ -e "$target_dir" ]] && add_error "target directory already exists: $target_dir" validate_dns_label "$namespace" || add_error "--namespace must be a valid Kubernetes DNS label" validate_dns_label "$release_name" || add_error "--release-name must be a valid Helm release name" validate_hostname "$hostname" || add_error "--hostname must be a valid DNS hostname" [[ "$repo_url" == *$'\n'* || "$repo_url" == *$'\r'* ]] && add_error "--repo-url must not contain newlines" [[ "$repo_url" =~ ^https://git\.olb42\.com/olb42/[a-z0-9]([-a-z0-9]*[a-z0-9])?\.git$ ]] || add_error "--repo-url must match https://git.olb42.com/olb42/.git" [[ "$repo_url" == "https://git.olb42.com/olb42/$repo_name.git" ]] || add_warning "--repo-url does not match --repo-name" if [[ "$include_remote_check" == true && ${#errors[@]} -eq 0 ]]; then local remote_output remote_output="$(GIT_TERMINAL_PROMPT=0 git ls-remote --heads "$repo_url" main init 2>&1 || true)" if printf '%s\n' "$remote_output" | grep -q 'refs/heads/'; then add_error "remote already has main/init branches at $repo_url" else local remote_check_output remote_check_output="$(GIT_TERMINAL_PROMPT=0 git ls-remote "$repo_url" 2>&1 || true)" if printf '%s\n' "$remote_check_output" | grep -qi '^fatal:'; then add_error "remote repository is not reachable without prompting: $repo_url ($remote_check_output)" fi fi fi } repo_name="" repo_url="" namespace="" release_name="" hostname="" dry_run=false errors=() warnings=() while [[ $# -gt 0 ]]; do case "$1" in --repo-name) [[ $# -ge 2 ]] || die "--repo-name requires a value"; repo_name="$2"; shift 2 ;; --repo-url) [[ $# -ge 2 ]] || die "--repo-url requires a value"; repo_url="$2"; shift 2 ;; --namespace) [[ $# -ge 2 ]] || die "--namespace requires a value"; namespace="$2"; shift 2 ;; --release-name) [[ $# -ge 2 ]] || die "--release-name requires a value"; release_name="$2"; shift 2 ;; --hostname) [[ $# -ge 2 ]] || die "--hostname requires a value"; hostname="$2"; shift 2 ;; --dry-run) dry_run=true; shift ;; -h|--help) usage; exit 0 ;; *) echo "Unknown argument: $1"; usage; exit 1 ;; esac done template_dir="$HOME/devops/infra/templates/template" apps_dir="$HOME/devops/apps" target_dir="$apps_dir/$repo_name" current_dir="$(pwd -P)" template_dir_real="$(cd "$template_dir" 2>/dev/null && pwd -P || true)" repo_url="${repo_url:-https://git.olb42.com/olb42/$repo_name.git}" release_name="${release_name:-$repo_name}" hostname="${hostname:-$repo_name.olb42.com}" namespace="${namespace:-$repo_name}" run_preflight "$dry_run" if [[ "$dry_run" == true ]]; then if [[ ${#errors[@]} -gt 0 ]]; then print_preflight "Dry run failed. No files were copied." exit 1 fi print_preflight "Dry run passed. No files were copied." exit 0 fi if [[ ${#errors[@]} -gt 0 ]]; then usage print_preflight "Template initialization failed before copying files." >&2 exit 1 fi mkdir -p "$apps_dir" cp -a "$template_dir_real" "$target_dir" cd "$target_dir" git reset --mixed >/dev/null python3 - "$repo_name" "$repo_url" "$namespace" "$release_name" "$hostname" <<'PY' from pathlib import Path import sys repo_name, repo_url, namespace, release_name, hostname = sys.argv[1:] replacements = { "${REPO_NAME_KEBAB}": repo_name, "${REPO_HTTPS_URL}": repo_url, "${APP_NAMESPACE}": namespace, "${HELM_RELEASE_NAME}": release_name, "${APP_HOSTNAME}": hostname, } for path in Path(".").rglob("*"): if not path.is_file(): continue if ".git" in path.parts or path.name == "init-template": continue try: text = path.read_text() except UnicodeDecodeError: continue original = text for old, new in replacements.items(): text = text.replace(old, new) if text != original: path.write_text(text) PY git branch -M main while IFS= read -r remote; do git remote remove "$remote" done < <(git remote) git remote add origin "$repo_url" echo "Template initialized for $repo_name" echo "Created $target_dir" echo "Remote origin: $repo_url" echo "Next: review manifest/overlays/production and push main"