#!/usr/bin/env bash
set -Eeuo pipefail
readonly GIT_USER=git
readonly GIT_GROUP=git
readonly STATE_DIR="${GITOLITE_HOME:-/var/lib/gitolite}"
readonly REPOSITORIES_DIR="${STATE_DIR}/repositories"
readonly HOST_KEY_DIR="${STATE_DIR}/.ssh-host-keys"
readonly RUNTIME_DIR=/run/git-browser-appliance
readonly ADMIN_KEY_FILE="${GITOLITE_ADMIN_KEY_FILE:-/run/secrets/gitolite_admin_key}"
readonly ADMIN_NAME="${GITOLITE_ADMIN_NAME:-admin}"
log() {
printf '[git-browser] %s\n' "$*" >&2
}
die() {
log "error: $*"
exit 1
}
run_as_git() {
runuser -u "${GIT_USER}" -- env \
HOME="${STATE_DIR}" \
USER="${GIT_USER}" \
LOGNAME="${GIT_USER}" \
PATH="${STATE_DIR}/bin:/usr/local/bin:/usr/bin:/bin" \
"$@"
}
prepare_state_directory() {
install -d -o "${GIT_USER}" -g "${GIT_GROUP}" -m 0750 "${STATE_DIR}"
case "${GITOLITE_FIX_PERMISSIONS:-false}" in
1|true|TRUE|yes|YES)
log "repairing ownership under ${STATE_DIR}"
find "${STATE_DIR}" -xdev -path "${HOST_KEY_DIR}" -prune -o \
-exec chown "${GIT_USER}:${GIT_GROUP}" {} +
;;
0|false|FALSE|no|NO|'') ;;
*) die "GITOLITE_FIX_PERMISSIONS must be true or false" ;;
esac
run_as_git test -w "${STATE_DIR}" || die \
"${STATE_DIR} is not writable by uid 1000; fix the volume ownership or start once with GITOLITE_FIX_PERMISSIONS=true"
run_as_git mkdir -p "${STATE_DIR}/bin" "${STATE_DIR}/.ssh"
chmod 0700 "${STATE_DIR}/.ssh"
touch "${STATE_DIR}/.ssh/authorized_keys"
chown "${GIT_USER}:${GIT_GROUP}" "${STATE_DIR}/.ssh/authorized_keys"
chmod 0600 "${STATE_DIR}/.ssh/authorized_keys"
}
install_gitolite() {
log "installing Gitolite into persistent state"
run_as_git /opt/gitolite/install -to "${STATE_DIR}/bin"
}
is_initialized() {
[[ -d "${REPOSITORIES_DIR}/gitolite-admin.git" && -f "${STATE_DIR}/.gitolite/conf/gitolite.conf" ]]
}
has_partial_state() {
[[ -e "${REPOSITORIES_DIR}/gitolite-admin.git" || -e "${STATE_DIR}/.gitolite" || -e "${STATE_DIR}/.gitolite.rc" ]]
}
validate_admin_key() {
[[ "${ADMIN_NAME}" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || die \
"GITOLITE_ADMIN_NAME must contain only letters, digits, dot, underscore, and hyphen"
[[ -f "${ADMIN_KEY_FILE}" && -r "${ADMIN_KEY_FILE}" ]] || die \
"first boot requires a readable public key at ${ADMIN_KEY_FILE}"
grep -Eq '^(ssh-(ed25519|rsa)|ecdsa-sha2-nistp(256|384|521)|sk-(ssh-ed25519|ecdsa-sha2-nistp256)@openssh\.com)[[:space:]]' \
"${ADMIN_KEY_FILE}" || die "${ADMIN_KEY_FILE} does not look like an OpenSSH public key"
ssh-keygen -l -f "${ADMIN_KEY_FILE}" >/dev/null 2>&1 || die \
"${ADMIN_KEY_FILE} is not a valid OpenSSH public key"
}
initialize_gitolite() {
validate_admin_key
local setup_dir setup_key
setup_dir="$(mktemp -d /run/gitolite-setup.XXXXXX)"
setup_key="${setup_dir}/${ADMIN_NAME}.pub"
install -o "${GIT_USER}" -g "${GIT_GROUP}" -m 0600 "${ADMIN_KEY_FILE}" "${setup_key}"
log "initializing Gitolite administrator ${ADMIN_NAME}"
if ! run_as_git "${STATE_DIR}/bin/gitolite" setup -pk "${setup_key}"; then
rm -rf -- "${setup_dir}"
die "Gitolite setup failed"
fi
rm -rf -- "${setup_dir}"
}
configure_gitolite() {
if is_initialized; then
log "existing Gitolite state found; refreshing generated files and hooks"
run_as_git "${STATE_DIR}/bin/gitolite" setup
return
fi
if has_partial_state; then
die "partial Gitolite state found in ${STATE_DIR}; restore a complete volume or clear it intentionally"
fi
initialize_gitolite
}
prepare_ssh() {
install -d -o root -g root -m 0700 "${HOST_KEY_DIR}"
if [[ ! -s "${HOST_KEY_DIR}/ssh_host_ed25519_key" ]]; then
log "generating persistent ED25519 SSH host key"
ssh-keygen -q -t ed25519 -N '' -f "${HOST_KEY_DIR}/ssh_host_ed25519_key"
fi
if [[ ! -s "${HOST_KEY_DIR}/ssh_host_rsa_key" ]]; then
log "generating persistent RSA SSH host key"
ssh-keygen -q -t rsa -b 3072 -N '' -f "${HOST_KEY_DIR}/ssh_host_rsa_key"
fi
chown -R root:root "${HOST_KEY_DIR}"
chmod 0700 "${HOST_KEY_DIR}"
chmod 0600 "${HOST_KEY_DIR}"/ssh_host_*_key
chmod 0644 "${HOST_KEY_DIR}"/ssh_host_*_key.pub
install -d -m 0755 /run/sshd "${RUNTIME_DIR}"
/usr/sbin/sshd -t -f /etc/ssh/sshd_config
}
start_services() {
local -a browser_args
browser_args=(
-root "${REPOSITORIES_DIR}"
-listen "${GIT_BROWSER_LISTEN:-0.0.0.0:8080}"
)
if [[ -n "${GIT_BROWSER_CLONE_SSH_PREFIX:-}" ]]; then
browser_args+=(-clone-ssh-prefix "${GIT_BROWSER_CLONE_SSH_PREFIX}")
fi
if [[ -n "${GIT_BROWSER_CLONE_HTTPS_PREFIX:-}" ]]; then
browser_args+=(-clone-https-prefix "${GIT_BROWSER_CLONE_HTTPS_PREFIX}")
fi
if [[ -n "${GIT_BROWSER_HIDE:-}" ]]; then
browser_args+=(-hide "${GIT_BROWSER_HIDE}")
fi
log "starting SSH on :22"
/usr/sbin/sshd -D -e -f /etc/ssh/sshd_config &
sshd_pid=$!
printf '%s\n' "${sshd_pid}" >"${RUNTIME_DIR}/sshd.pid"
log "starting git-browser on ${GIT_BROWSER_LISTEN:-0.0.0.0:8080}"
run_as_git /usr/local/bin/git-browser "${browser_args[@]}" &
browser_pid=$!
printf '%s\n' "${browser_pid}" >"${RUNTIME_DIR}/git-browser.pid"
}
stop_services() {
local signal="${1:-TERM}"
trap - TERM INT
log "stopping services"
kill -s "${signal}" "${browser_pid:-}" "${sshd_pid:-}" 2>/dev/null || true
wait "${browser_pid:-}" 2>/dev/null || true
wait "${sshd_pid:-}" 2>/dev/null || true
}
supervise_services() {
local status=0 stopped=
trap 'stop_services TERM; exit 0' TERM INT
set +e
wait -n "${sshd_pid}" "${browser_pid}"
status=$?
set -e
if ! kill -0 "${sshd_pid}" 2>/dev/null; then
stopped=sshd
elif ! kill -0 "${browser_pid}" 2>/dev/null; then
stopped=git-browser
else
stopped='a service'
fi
log "${stopped} exited unexpectedly with status ${status}"
stop_services TERM
exit "${status}"
}
main() {
[[ "${STATE_DIR}" = /var/lib/gitolite ]] || die \
"GITOLITE_HOME is fixed at /var/lib/gitolite in this image"
prepare_state_directory
install_gitolite
configure_gitolite
prepare_ssh
start_services
supervise_services
}
main "$@"