diff --git a/.gitea/workflows/homelab-main.yml b/.gitea/workflows/homelab-main.yml index 9d10479..7695865 100644 --- a/.gitea/workflows/homelab-main.yml +++ b/.gitea/workflows/homelab-main.yml @@ -174,9 +174,11 @@ jobs: deploy_dir="${HOMELAB_DEPLOY_DIR:-/home/jv/my-homelab-configs}" test -d "${deploy_dir}/.git" - git -C "${deploy_dir}" remote set-url gitea https://lab2025.duckdns.org/git/jv/my-homelab-configs.git || \ - git -C "${deploy_dir}" remote add gitea https://lab2025.duckdns.org/git/jv/my-homelab-configs.git - git -C "${deploy_dir}" fetch gitea main + gitea_ssh_url="${GITEA_SSH_URL:-ssh://git@192.168.100.89:32222/jv/my-homelab-configs.git}" + gitea_ssh_command="${GITEA_SSH_COMMAND:-ssh -i /home/jv/.ssh/id_ed25519 -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new}" + git -C "${deploy_dir}" remote set-url gitea "${gitea_ssh_url}" || \ + git -C "${deploy_dir}" remote add gitea "${gitea_ssh_url}" + GIT_SSH_COMMAND="${gitea_ssh_command}" git -C "${deploy_dir}" fetch gitea main git -C "${deploy_dir}" checkout main git -C "${deploy_dir}" reset --hard "${{ gitea.sha }}" git -C "${deploy_dir}" remote set-url local-bootstrap /home/jv/git-server/my-homelab-configs.git || \ diff --git a/README.md b/README.md index facfef2..6f752d9 100644 --- a/README.md +++ b/README.md @@ -414,6 +414,14 @@ changes still require a manual Debian run. Lower-risk app changes proceed to `./lab.sh apps` after validation passes, which skips Gitea, Pimox, cluster, platform, and edge changes. +`./lab.sh bootstrap-gitea-repo` also registers the Debian host SSH public key +with the Gitea repository and switches the Debian working copy's `gitea` remote +to `ssh://git@192.168.100.89:32222/jv/my-homelab-configs.git`. The default key +is `/home/jv/.ssh/id_ed25519.pub`; set `LAB_GITEA_REPO_SSH_KEY_PATH` to use a +different Debian-host key, or `LAB_GITEA_REPO_SSH_BOOTSTRAP=false` to leave SSH +access unchanged. The Actions deploy job fetches the persistent Debian checkout +through that SSH endpoint. + Enable Actions for the repository in Gitea, then create a repository-level runner token from: diff --git a/lab.sh b/lab.sh index e840e43..4a6bedd 100755 --- a/lab.sh +++ b/lab.sh @@ -1447,6 +1447,136 @@ PY "${api_base}/user/repos" >/dev/null } +gitea_public_key_registered() { + local api_base="$1" + local auth_user="$2" + local auth_password="$3" + local owner="$4" + local repo_name="$5" + local public_key_path="$6" + local repo_keys + local user_keys + + user_keys="$(curl -fsS -u "${auth_user}:${auth_password}" "${api_base}/user/keys?limit=100")" + repo_keys="$(curl -fsS -u "${auth_user}:${auth_password}" "${api_base}/repos/${owner}/${repo_name}/keys?limit=100")" + + GITEA_PUBLIC_KEY="$(<"${public_key_path}")" \ + GITEA_USER_KEYS="${user_keys}" \ + GITEA_REPO_KEYS="${repo_keys}" \ + python3 - <<'PY' +import json +import os +import sys + +public_key = os.environ["GITEA_PUBLIC_KEY"].strip() +for env_name in ("GITEA_USER_KEYS", "GITEA_REPO_KEYS"): + for key in json.loads(os.environ[env_name]) or []: + if key.get("key", "").strip() == public_key: + sys.exit(0) +sys.exit(1) +PY +} + +create_gitea_repo_deploy_key() { + local api_base="$1" + local auth_user="$2" + local auth_password="$3" + local owner="$4" + local repo_name="$5" + local title="$6" + local public_key_path="$7" + local read_only="$8" + local payload + + payload="$( + GITEA_DEPLOY_KEY_TITLE="${title}" \ + GITEA_PUBLIC_KEY="$(<"${public_key_path}")" \ + GITEA_DEPLOY_KEY_READ_ONLY="${read_only}" \ + python3 - <<'PY' +import json +import os + +print(json.dumps({ + "title": os.environ["GITEA_DEPLOY_KEY_TITLE"], + "key": os.environ["GITEA_PUBLIC_KEY"].strip(), + "read_only": os.environ["GITEA_DEPLOY_KEY_READ_ONLY"] == "true", +})) +PY + )" + + curl -fsS \ + -u "${auth_user}:${auth_password}" \ + -H "Content-Type: application/json" \ + -X POST \ + -d "${payload}" \ + "${api_base}/repos/${owner}/${repo_name}/keys" >/dev/null +} + +ensure_gitea_repo_ssh_access() { + local api_base="$1" + local auth_user="$2" + local auth_password="$3" + local owner="$4" + local repo_name="$5" + local ssh_host="$6" + local ssh_port="$7" + local key_path="$8" + local key_title="$9" + local key_read_only="${10}" + local key_dir + local known_hosts + local public_key_path + local read_only_json="false" + local ssh_repo_url + + if [[ "${key_path}" =~ [[:space:]] || "${key_path}" == *"'"* ]]; then + echo "LAB_GITEA_REPO_SSH_KEY_PATH cannot contain whitespace or single quotes." >&2 + exit 1 + fi + + key_dir="$(dirname "${key_path}")" + public_key_path="${key_path}.pub" + mkdir -p "${key_dir}" + chmod 0700 "${key_dir}" + + if [[ ! -s "${key_path}" && ! -s "${public_key_path}" ]]; then + ssh-keygen -t ed25519 -N "" -f "${key_path}" -C "${key_title}" >/dev/null + elif [[ -s "${key_path}" && ! -s "${public_key_path}" ]]; then + ssh-keygen -y -f "${key_path}" >"${public_key_path}" + elif [[ ! -s "${key_path}" ]]; then + echo "Public key ${public_key_path} exists, but private key ${key_path} is missing." >&2 + exit 1 + fi + + chmod 0600 "${key_path}" + chmod 0644 "${public_key_path}" + + if truthy "${key_read_only}"; then + read_only_json="true" + fi + + if gitea_public_key_registered "${api_base}" "${auth_user}" "${auth_password}" "${owner}" "${repo_name}" "${public_key_path}"; then + echo "Gitea already has Debian host SSH key ${public_key_path}." + else + create_gitea_repo_deploy_key "${api_base}" "${auth_user}" "${auth_password}" "${owner}" "${repo_name}" "${key_title}" "${public_key_path}" "${read_only_json}" + echo "Added Debian host SSH key ${public_key_path} to ${owner}/${repo_name}." + fi + + known_hosts="${HOME}/.ssh/known_hosts" + touch "${known_hosts}" + chmod 0644 "${known_hosts}" + if ! ssh-keygen -F "[${ssh_host}]:${ssh_port}" -f "${known_hosts}" >/dev/null 2>&1; then + ssh-keyscan -p "${ssh_port}" "${ssh_host}" >>"${known_hosts}" 2>/dev/null + fi + + ssh_repo_url="ssh://git@${ssh_host}:${ssh_port}/${owner}/${repo_name}.git" + git -C "${REPO_ROOT}" remote set-url gitea "${ssh_repo_url}" 2>/dev/null || + git -C "${REPO_ROOT}" remote add gitea "${ssh_repo_url}" + git -C "${REPO_ROOT}" config core.sshCommand "ssh -i ${key_path} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new" + git -C "${REPO_ROOT}" ls-remote gitea HEAD >/dev/null + echo "Gitea SSH remote: ${ssh_repo_url}" +} + bootstrap_gitea_repo() { local mode="${LAB_GITEA_REPO_BOOTSTRAP:-true}" local gitea_host="${LAB_GITEA_HOST:-${LAB_RASPBERRY_HOST:-192.168.100.89}}" @@ -1454,6 +1584,7 @@ bootstrap_gitea_repo() { local gitea_key="${LAB_GITEA_SSH_KEY_PATH:-${LAB_RASPBERRY_SSH_KEY_PATH:-/home/jv/.ssh/id_ed25519}}" local container_name="${LAB_GITEA_CONTAINER_NAME:-homelab-gitea}" local http_port="${LAB_GITEA_HTTP_PORT:-3000}" + local ssh_port="${LAB_GITEA_SSH_PORT:-32222}" local root_url="${LAB_GITEA_ROOT_URL:-https://lab2025.duckdns.org/git/}" local repo_owner="${LAB_GITEA_REPO_OWNER:-jv}" local repo_name="${LAB_GITEA_REPO_NAME:-my-homelab-configs}" @@ -1463,6 +1594,10 @@ bootstrap_gitea_repo() { local credentials_file="${LAB_GITEA_BOOTSTRAP_CREDENTIALS_FILE:-${HOME}/.config/homelab/gitea-bootstrap.env}" local bootstrap_password="${LAB_GITEA_BOOTSTRAP_PASSWORD:-}" local allow_dirty="${LAB_GITEA_BOOTSTRAP_ALLOW_DIRTY:-false}" + local ssh_bootstrap="${LAB_GITEA_REPO_SSH_BOOTSTRAP:-true}" + local ssh_key_path="${LAB_GITEA_REPO_SSH_KEY_PATH:-/home/jv/.ssh/id_ed25519}" + local ssh_key_title="${LAB_GITEA_REPO_DEPLOY_KEY_TITLE:-debian-host-${repo_name}}" + local ssh_key_read_only="${LAB_GITEA_REPO_DEPLOY_KEY_READ_ONLY:-false}" local api_base local public_repo_url local direct_repo_url @@ -1490,6 +1625,10 @@ bootstrap_gitea_repo() { echo "LAB_GITEA_BOOTSTRAP_EMAIL cannot contain a single quote." >&2 exit 1 fi + if ! [[ "${ssh_port}" =~ ^[0-9]+$ ]]; then + echo "LAB_GITEA_SSH_PORT must be numeric." >&2 + exit 1 + fi if [[ -z "${bootstrap_password}" && -r "${credentials_file}" ]]; then # shellcheck disable=SC1090 @@ -1625,6 +1764,19 @@ ASKPASS_EOT remote_status="$(git -C "${REPO_ROOT}" remote get-url gitea)" echo "Gitea remote: ${remote_status}" + if ! disabled_value "${ssh_bootstrap}"; then + ensure_gitea_repo_ssh_access \ + "${api_base}" \ + "${bootstrap_user}" \ + "${bootstrap_password}" \ + "${repo_owner}" \ + "${repo_name}" \ + "${gitea_host}" \ + "${ssh_port}" \ + "${ssh_key_path}" \ + "${ssh_key_title}" \ + "${ssh_key_read_only}" + fi } install_gitea_backup_timer() {