270 lines
8.0 KiB
Bash
270 lines
8.0 KiB
Bash
#!/usr/bin/env bash
|
|
# tools/create_railiance_repo.sh
|
|
#
|
|
# Railiance — sanitized bootstrap script for creating a Gitea repository.
|
|
#
|
|
# PURPOSE:
|
|
# Creates a repo (default: railiance-cluster) in your Gitea (user or org),
|
|
# scaffolds a minimal layout, commits, and pushes the initial content.
|
|
#
|
|
# SAFETY NOTES:
|
|
# - This script NEVER writes your credentials file and NEVER prints your token.
|
|
# - It requires ~/.railiance_gitea.conf to exist (see example below).
|
|
# - It relies on your local Git credential helper or SSH for the initial push.
|
|
# (Recommended: `git config --global credential.helper cache` or `store`)
|
|
#
|
|
# REQUIRED CONFIG (~/.railiance_gitea.conf):
|
|
# GITEA_URL="https://git.example.com" # no trailing slash; include subpath if any
|
|
# GITEA_USER="your-username" # your Gitea login (for user-owned repos)
|
|
# GITEA_ORG="" # optional; set to org name to create under org
|
|
# GITEA_TOKEN="xxxxxxxxxxxxxxxx" # personal access token with read:user + write:repository
|
|
#
|
|
# USAGE:
|
|
# ./tools/create_railiance_repo.sh
|
|
# ./tools/create_railiance_repo.sh --repo my-repo --desc "My desc" --public
|
|
# ./tools/create_railiance_repo.sh --org coulomb # override org from config
|
|
#
|
|
# FLAGS:
|
|
# --repo <name> : repository name (default: railiance-cluster)
|
|
# --desc <text> : description (default provided)
|
|
# --org <orgname> : create under this organization (overrides GITEA_ORG)
|
|
# --public : create as public repo (default: private)
|
|
# --no-scaffold : skip creating local scaffold (useful if repo already has content)
|
|
# -h|--help : show help
|
|
#
|
|
# EXITS non-zero on failure.
|
|
|
|
set -euo pipefail
|
|
|
|
# ----------------------------- parse args ------------------------------------
|
|
REPO_NAME="railiance-cluster"
|
|
REPO_DESC="Bootstrap repo for Railiance IaC. Recreates Coulomb infra from scratch."
|
|
IS_PRIVATE=true
|
|
OVERRIDE_ORG=""
|
|
DO_SCAFFOLD=true
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--repo) REPO_NAME="${2:?}"; shift 2 ;;
|
|
--desc) REPO_DESC="${2:?}"; shift 2 ;;
|
|
--org) OVERRIDE_ORG="${2:?}"; shift 2 ;;
|
|
--public) IS_PRIVATE=false; shift ;;
|
|
--no-scaffold)DO_SCAFFOLD=false; shift ;;
|
|
-h|--help)
|
|
sed -n '1,120p' "$0" | sed -n '1,/^set -euo pipefail/p'
|
|
exit 0
|
|
;;
|
|
*) echo "Unknown arg: $1" >&2; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
# ----------------------------- load config -----------------------------------
|
|
CONFIG_FILE="${HOME}/.railiance_gitea.conf"
|
|
if [[ ! -f "${CONFIG_FILE}" ]]; then
|
|
echo "ERROR: Missing config ${CONFIG_FILE}"
|
|
echo "Create it with:"
|
|
cat <<'EOT'
|
|
GITEA_URL="https://git.example.com"
|
|
GITEA_USER="your-username"
|
|
GITEA_ORG="" # or set to your org
|
|
GITEA_TOKEN="xxxxxxxxxxxxxxxx"
|
|
EOT
|
|
exit 1
|
|
fi
|
|
|
|
# shellcheck disable=SC1090
|
|
source "${CONFIG_FILE}"
|
|
|
|
: "${GITEA_URL:?Set GITEA_URL in ${CONFIG_FILE}}"
|
|
: "${GITEA_USER:?Set GITEA_USER in ${CONFIG_FILE}}"
|
|
: "${GITEA_TOKEN:?Set GITEA_TOKEN in ${CONFIG_FILE}}"
|
|
: "${GITEA_ORG:=}"
|
|
|
|
if [[ -n "${OVERRIDE_ORG}" ]]; then
|
|
GITEA_ORG="${OVERRIDE_ORG}"
|
|
fi
|
|
|
|
# ----------------------------- helpers ---------------------------------------
|
|
need_cmd() { command -v "$1" >/dev/null 2>&1 || { echo "Missing: $1" >&2; exit 1; }; }
|
|
need_cmd curl
|
|
need_cmd git
|
|
|
|
json_escape() {
|
|
# minimal escape for description text
|
|
printf '%s' "$1" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))'
|
|
}
|
|
|
|
curl_code() {
|
|
# prints only HTTP code; follows redirects; uses token header
|
|
curl -sS -o /dev/null -w "%{http_code}" -L \
|
|
-H "Authorization: token ${GITEA_TOKEN}" "$@"
|
|
}
|
|
|
|
repo_exists() {
|
|
local owner="$1" repo="$2" code
|
|
code=$(curl_code "${GITEA_URL}/api/v1/repos/${owner}/${repo}")
|
|
[[ "${code}" == "200" ]]
|
|
}
|
|
|
|
create_repo_user() {
|
|
local repo="$1" private="$2" desc_json="$3"
|
|
curl_code -X POST "${GITEA_URL}/api/v1/user/repos" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"name\":\"${repo}\",\"private\":${private},\"description\":${desc_json}}"
|
|
}
|
|
|
|
create_repo_org() {
|
|
local org="$1" repo="$2" private="$3" desc_json="$4"
|
|
curl_code -X POST "${GITEA_URL}/api/v1/orgs/${org}/repos" \
|
|
-H "Content-Type: application/json" \
|
|
-d "{\"name\":\"${repo}\",\"private\":${private},\"description\":${desc_json}}"
|
|
}
|
|
|
|
scaffold_repo() {
|
|
local dir="$1"
|
|
mkdir -p "${dir}"/{ansible,helm,k8s,tests,docs}
|
|
cat > "${dir}/README.md" <<EOF
|
|
# ${REPO_NAME}
|
|
|
|
**Railiance Bootstrap** — opinionated, reproducible IaC to rebuild Coulomb infra from scratch.
|
|
|
|
## Goals
|
|
- Two Linux machines + this Git repo + credentials ⇒ full rebuild
|
|
- GitOps-first with ArgoCD/Flux
|
|
- OODA (Observe→Orient→Decide→Act) encoded as pipelines
|
|
- Tests define success at every step
|
|
|
|
## Layout
|
|
\`\`\`
|
|
ansible/ # host bootstrap
|
|
helm/ # charts & values
|
|
k8s/ # raw manifests/CRDs
|
|
tests/ # expectation tests (bash/py)
|
|
docs/ # operator notes, OODA SOPs
|
|
\`\`\`
|
|
EOF
|
|
|
|
cat > "${dir}/.gitignore" <<'EOF'
|
|
# OS
|
|
.DS_Store
|
|
|
|
# Python
|
|
__pycache__/
|
|
*.pyc
|
|
|
|
# Ansible artifacts
|
|
*.retry
|
|
|
|
# Helm build
|
|
charts/*/charts/
|
|
charts/*/tmp/
|
|
|
|
# Local secrets (do not commit)
|
|
secrets/
|
|
*.enc
|
|
EOF
|
|
|
|
cat > "${dir}/ansible/bootstrap.yml" <<'EOF'
|
|
---
|
|
- name: Railiance host bootstrap (example)
|
|
hosts: all
|
|
become: true
|
|
tasks:
|
|
- name: Ensure base packages
|
|
apt:
|
|
name:
|
|
- curl
|
|
- git
|
|
- jq
|
|
update_cache: yes
|
|
state: present
|
|
EOF
|
|
|
|
cat > "${dir}/tests/smoke_kube.sh" <<'EOF'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
echo "[placeholder] Add kube API checks here (kubectl get nodes/ns)"
|
|
EOF
|
|
chmod +x "${dir}/tests/smoke_kube.sh"
|
|
|
|
cat > "${dir}/docs/OODA.md" <<'EOF'
|
|
# OODA in Railiance
|
|
- Observe: telemetry & test results
|
|
- Orient: AI analyzes deltas & proposes remediations
|
|
- Decide: specify → review → authorize (Git PR)
|
|
- Act: GitOps applies automatically (human independent)
|
|
EOF
|
|
}
|
|
|
|
git_init_and_push() {
|
|
local dir="$1" remote_url="$2"
|
|
(
|
|
cd "${dir}"
|
|
git init
|
|
git add .
|
|
git commit -m "railiance: initial bootstrap scaffold"
|
|
git branch -M main
|
|
git remote add origin "${remote_url}"
|
|
# relies on credential helper or SSH setup; avoids embedding token in URL
|
|
git push -u origin main
|
|
)
|
|
}
|
|
|
|
# ----------------------------- main ------------------------------------------
|
|
DESC_JSON=$(json_escape "${REPO_DESC}")
|
|
PRIVATE_JSON=$([[ "${IS_PRIVATE}" == true ]] && echo true || echo false)
|
|
|
|
OWNER="${GITEA_USER}"
|
|
REMOTE_URL="${GITEA_URL}/${OWNER}/${REPO_NAME}.git"
|
|
|
|
if [[ -n "${GITEA_ORG}" ]]; then
|
|
OWNER="${GITEA_ORG}"
|
|
REMOTE_URL="${GITEA_URL}/${OWNER}/${REPO_NAME}.git"
|
|
fi
|
|
|
|
# Basic API reachability check (no auth needed)
|
|
VER_CODE=$(curl_code "${GITEA_URL}/api/v1/version")
|
|
if [[ "${VER_CODE}" != "200" ]]; then
|
|
echo "ERROR: Gitea API not reachable at ${GITEA_URL}/api/v1/version (HTTP ${VER_CODE})" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if repo_exists "${OWNER}" "${REPO_NAME}"; then
|
|
echo "Repo ${OWNER}/${REPO_NAME} already exists. Skipping creation."
|
|
else
|
|
if [[ -n "${GITEA_ORG}" ]]; then
|
|
echo "Creating repo under org '${GITEA_ORG}'..."
|
|
CODE=$(create_repo_org "${GITEA_ORG}" "${REPO_NAME}" "${PRIVATE_JSON}" "${DESC_JSON}")
|
|
if [[ "${CODE}" != "201" ]]; then
|
|
echo "ERROR: Failed to create repo in org (HTTP ${CODE}). Do you have org permissions?" >&2
|
|
exit 1
|
|
fi
|
|
else
|
|
echo "Creating repo under user '${GITEA_USER}'..."
|
|
CODE=$(create_repo_user "${REPO_NAME}" "${PRIVATE_JSON}" "${DESC_JSON}")
|
|
if [[ "${CODE}" != "201" ]]; then
|
|
echo "ERROR: Failed to create user repo (HTTP ${CODE}). Check token scopes." >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
echo "Repo created: ${OWNER}/${REPO_NAME}"
|
|
fi
|
|
|
|
if [[ "${DO_SCAFFOLD}" == true ]]; then
|
|
if [[ -d "${REPO_NAME}" ]]; then
|
|
echo "Local dir '${REPO_NAME}' exists. Skipping scaffold."
|
|
else
|
|
echo "Scaffolding '${REPO_NAME}'..."
|
|
mkdir -p "${REPO_NAME}"
|
|
scaffold_repo "${REPO_NAME}"
|
|
fi
|
|
echo "Pushing initial commit to ${REMOTE_URL} ..."
|
|
git_init_and_push "${REPO_NAME}" "${REMOTE_URL}"
|
|
else
|
|
echo "Skipping scaffold as requested (--no-scaffold)."
|
|
fi
|
|
|
|
echo "✅ Done."
|
|
echo "Repository: ${OWNER}/${REPO_NAME}"
|
|
echo "Remote URL: ${REMOTE_URL}"
|