diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..aff8284 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,32 @@ +name: Test + +on: + push: + branches: + - main + - master + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.12"] + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: uv sync --frozen --dev + + - name: Run tests + run: uv run pytest tests -q --tb=short diff --git a/.gitmodules b/.gitmodules index 05bb5b2..3100dc1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,7 @@ [submodule "opencloud-compose"] path = opencloud-compose url = https://github.com/opencloud-eu/opencloud-compose.git +[submodule "easydeploy-lib"] + path = easydeploy-lib + url = https://github.com/nordwestt/easy-deploy-lib.git + branch = main diff --git a/README.md b/README.md index 7898047..c85011c 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,18 @@ This replaces the official test installer (`curl -L https://opencloud.eu/install **Requirements:** Linux VPS, Docker Compose v2, DNS pointing at the server, ports 80/443 open. ```bash -git clone https://github.com/your-org/opencloud-easy-deploy.git +git clone --recurse-submodules https://github.com/your-org/opencloud-easy-deploy.git cd opencloud-easy-deploy -bash ensure-dependencies.sh # Docker, uv, submodule, Python deps +bash ensure-dependencies.sh # Docker, uv, submodules, Python deps bash wizard.sh # interactive: writes deploy.yaml and deploys ``` +If you already cloned without submodules: + +```bash +git submodule update --init --recursive +``` + Or manually: ```bash diff --git a/apply.sh b/apply.sh index 3548c19..dce41e7 100755 --- a/apply.sh +++ b/apply.sh @@ -3,6 +3,9 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=scripts/lib.sh +source "${SCRIPT_DIR}/scripts/lib.sh" +cd "${SCRIPT_DIR}" ensure_dependencies="false" python_args=() @@ -22,4 +25,4 @@ if [[ "$ensure_dependencies" == "true" ]]; then bash "${SCRIPT_DIR}/ensure-dependencies.sh" fi -uv run python "${SCRIPT_DIR}/scripts/apply.py" "${python_args[@]}" +exec uv run python -m scripts.apply "${python_args[@]}" diff --git a/backup-bundle.sh b/backup-bundle.sh index eaf7631..93e382e 100755 --- a/backup-bundle.sh +++ b/backup-bundle.sh @@ -5,9 +5,10 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=scripts/lib.sh source "${SCRIPT_DIR}/scripts/lib.sh" +cd "${SCRIPT_DIR}" if [[ -f "${SCRIPT_DIR}/deploy.yaml" ]] && docker inspect opencloud &>/dev/null 2>&1; then warn "OpenCloud is running — for a consistent backup, run: bash stop.sh" fi -exec uv run python "${SCRIPT_DIR}/scripts/bundle.py" create "$@" +exec uv run python -m scripts.bundle create "$@" diff --git a/backup.sh b/backup.sh index 5de0a0a..6a8ca5d 100755 --- a/backup.sh +++ b/backup.sh @@ -3,4 +3,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -exec uv run python "${SCRIPT_DIR}/scripts/backup.py" run "$@" +# shellcheck source=scripts/lib.sh +source "${SCRIPT_DIR}/scripts/lib.sh" +cd "${SCRIPT_DIR}" +exec uv run python -m scripts.backup run "$@" diff --git a/diagnose.sh b/diagnose.sh index 7e8d7ee..70023ec 100755 --- a/diagnose.sh +++ b/diagnose.sh @@ -5,6 +5,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=scripts/lib.sh source "${SCRIPT_DIR}/scripts/lib.sh" +cd "${SCRIPT_DIR}" COMPOSE_ENV="${SCRIPT_DIR}/opencloud-compose/.env" DEPLOY_YAML="${SCRIPT_DIR}/deploy.yaml" diff --git a/easydeploy-lib b/easydeploy-lib new file mode 160000 index 0000000..9285f8e --- /dev/null +++ b/easydeploy-lib @@ -0,0 +1 @@ +Subproject commit 9285f8efa0e3d6ce022604708d78c95c3b5bd933 diff --git a/ensure-dependencies.sh b/ensure-dependencies.sh index dbf814e..920a26b 100755 --- a/ensure-dependencies.sh +++ b/ensure-dependencies.sh @@ -5,27 +5,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=scripts/lib.sh source "${SCRIPT_DIR}/scripts/lib.sh" - -export PATH="${HOME}/.local/bin:${PATH}" - -run_as_root() { - if [[ "${EUID}" -eq 0 ]]; then - "$@" - elif command -v sudo &>/dev/null; then - sudo "$@" - else - die "Need root privileges for: $* — re-run with sudo or as root" - fi -} - -docker_usable() { - command -v docker &>/dev/null || return 1 - docker info &>/dev/null 2>&1 -} - -compose_usable() { - docker compose version &>/dev/null 2>&1 || command -v docker-compose &>/dev/null -} +# shellcheck source=scripts/deps_config.sh +source "${SCRIPT_DIR}/scripts/deps_config.sh" ensure_git() { if command -v git &>/dev/null; then @@ -33,80 +14,35 @@ ensure_git() { return fi info "Installing git…" - if command -v apt-get &>/dev/null; then - run_as_root apt-get update -qq - run_as_root apt-get install -y git - elif command -v dnf &>/dev/null; then - run_as_root dnf install -y git - elif command -v pacman &>/dev/null; then - run_as_root pacman -Sy --noconfirm git + local manager + manager="$(detect_supported_package_manager)" || die "git is required but no supported package manager was found" + install_missing_dependencies "$manager" git + if command -v git &>/dev/null; then + success "git installed" else die "git is required but not installed — install git and re-run" fi - success "git installed" } -ensure_submodule() { +ensure_submodules() { if [[ ! -d "${SCRIPT_DIR}/.git" ]]; then warn "Not a git checkout — skipping submodule update" if [[ ! -f "${SCRIPT_DIR}/opencloud-compose/docker-compose.yml" ]]; then die "opencloud-compose is missing — clone the repo with submodules or copy opencloud-compose manually" fi + if [[ ! -f "${SCRIPT_DIR}/easydeploy-lib/lib/init.sh" ]]; then + die "easydeploy-lib is missing — clone the repo with submodules" + fi return fi - if [[ -f "${SCRIPT_DIR}/opencloud-compose/docker-compose.yml" ]]; then - info "Updating opencloud-compose submodule…" - else - info "Initializing opencloud-compose submodule…" - fi + info "Initializing git submodules (opencloud-compose, easydeploy-lib)…" git -C "${SCRIPT_DIR}" submodule update --init --recursive - success "opencloud-compose submodule ready" -} - -ensure_docker() { - if docker_usable; then - success "Docker present ($(docker --version | head -1))" - else - if command -v docker &>/dev/null; then - warn "Docker is installed but the daemon is not reachable — start docker and re-run" - die "Try: sudo systemctl enable --now docker" - fi - info "Installing Docker (get.docker.com)…" - curl -fsSL https://get.docker.com | run_as_root sh - if ! docker_usable; then - if [[ "${EUID}" -ne 0 ]] && ! groups | grep -q '\bdocker\b'; then - warn "Your user is not in the docker group — log out/in after:" - warn " sudo usermod -aG docker ${USER}" - warn "Or run docker commands with sudo until then." - fi - if ! docker_usable && [[ "${EUID}" -ne 0 ]]; then - warn "Verifying Docker with sudo…" - if sudo docker info &>/dev/null 2>&1; then - success "Docker installed (use sudo docker until group membership applies)" - else - die "Docker install finished but daemon is not running" - fi - else - die "Docker install finished but daemon is not running" - fi - else - success "Docker installed" - fi - fi - - if compose_usable; then - if docker compose version &>/dev/null 2>&1; then - success "Docker Compose present ($(docker compose version --short 2>/dev/null || docker compose version | head -1))" - else - success "docker-compose present" - fi - else - die "Docker Compose v2 is required — reinstall Docker or install the compose plugin" - fi + success "Git submodules ready" } ensure_uv() { + export PATH="${HOME}/.local/bin:${PATH}" if command -v uv &>/dev/null; then success "uv present ($(uv --version))" return @@ -121,6 +57,7 @@ ensure_uv() { } ensure_python_deps() { + export PATH="${HOME}/.local/bin:${PATH}" info "Syncing Python dependencies (uv sync --dev)…" uv sync --dev --directory "${SCRIPT_DIR}" success "Python dependencies ready" @@ -132,8 +69,8 @@ main() { echo ensure_git - ensure_submodule - ensure_docker + ensure_submodules + ensure_dependencies_installed ensure_uv ensure_python_deps diff --git a/restore-borg.sh b/restore-borg.sh index e40b114..7b5225d 100755 --- a/restore-borg.sh +++ b/restore-borg.sh @@ -34,7 +34,8 @@ info "Installing dependencies…" bash "${SCRIPT_DIR}/ensure-dependencies.sh" info "Restoring from Borg repository…" -uv run python "${SCRIPT_DIR}/scripts/backup.py" fresh-restore --latest "$@" +cd "${SCRIPT_DIR}" +uv run python -m scripts.backup fresh-restore --latest "$@" info "Starting OpenCloud stack…" bash "${SCRIPT_DIR}/apply.sh" diff --git a/restore-bundle.sh b/restore-bundle.sh index 1da99a5..fb94517 100755 --- a/restore-bundle.sh +++ b/restore-bundle.sh @@ -29,7 +29,8 @@ info "Installing dependencies…" bash "${SCRIPT_DIR}/ensure-dependencies.sh" info "Restoring files from ${BUNDLE}…" -uv run python "${SCRIPT_DIR}/scripts/bundle.py" restore "$BUNDLE" --skip-apply +cd "${SCRIPT_DIR}" +uv run python -m scripts.bundle restore "$BUNDLE" --skip-apply info "Starting OpenCloud stack…" bash "${SCRIPT_DIR}/apply.sh" diff --git a/restore.sh b/restore.sh index bcf6045..3ca20bd 100755 --- a/restore.sh +++ b/restore.sh @@ -3,15 +3,18 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=scripts/lib.sh +source "${SCRIPT_DIR}/scripts/lib.sh" +cd "${SCRIPT_DIR}" if [[ "${1:-}" == "--list" ]]; then shift - exec uv run python "${SCRIPT_DIR}/scripts/backup.py" list "$@" + exec uv run python -m scripts.backup list "$@" fi if [[ "${1:-}" == "--latest" ]]; then shift - exec uv run python "${SCRIPT_DIR}/scripts/backup.py" restore --latest "$@" + exec uv run python -m scripts.backup restore --latest "$@" fi if [[ "${1:-}" == "--archive" ]]; then @@ -19,12 +22,12 @@ if [[ "${1:-}" == "--archive" ]]; then archive="${1:-}" [[ -n "$archive" ]] || { echo "Usage: restore.sh --archive NAME [--dry-run]" >&2; exit 1; } shift - exec uv run python "${SCRIPT_DIR}/scripts/backup.py" restore --archive "$archive" "$@" + exec uv run python -m scripts.backup restore --archive "$archive" "$@" fi if [[ "${1:-}" == "--dry-run" ]]; then shift - exec uv run python "${SCRIPT_DIR}/scripts/backup.py" restore --dry-run "$@" + exec uv run python -m scripts.backup restore --dry-run "$@" fi cat <${RESET} $*"; } -success() { echo -e "${GREEN} [ok]${RESET} $*"; } -warn() { echo -e "${YELLOW} [!]${RESET} $*"; } -error() { echo -e "${RED} [ERR]${RESET} $*" >&2; } -die() { error "$*"; exit 1; } +# shellcheck source=easydeploy-lib/lib/init.sh +source "${_ocd_project_root}/easydeploy-lib/lib/init.sh" -load_deploy_env() { - local deploy_env="$1" - [[ -f "$deploy_env" ]] || return 0 - - while IFS='=' read -r key value || [[ -n "$key" ]]; do - [[ -z "$key" || "$key" == \#* ]] && continue - [[ "$key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue - value="${value%%#*}" - value="${value%"${value##*[![:space:]]}"}" - export "${key}=${value}" - done < "$deploy_env" -} - -ask() { - local _var="$1" - local _prompt="$2" - local _default="${3:-}" - - if [[ -n "$_default" ]]; then - echo -ne "${BOLD} ${_prompt}${RESET} ${CYAN}[${_default}]${RESET}: " - else - echo -ne "${BOLD} ${_prompt}${RESET}: " - fi - - local _input - read -r _input - printf -v "$_var" '%s' "${_input:-$_default}" -} - -ask_secret() { - local _var="$1" - local _prompt="$2" - - echo -ne "${BOLD} ${_prompt}${RESET}: " - local _input - read -rs _input - echo - printf -v "$_var" '%s' "$_input" -} - -ask_yn() { - local _var="$1" - local _prompt="$2" - local _default="${3:-n}" - - local suffix="y/N" - [[ "$_default" == "y" ]] && suffix="Y/n" - - echo -ne "${BOLD} ${_prompt}${RESET} ${CYAN}[${suffix}]${RESET}: " - local _input - read -r _input - _input="${_input:-$_default}" - case "${_input,,}" in - y|yes) printf -v "$_var" '%s' "y" ;; - *) printf -v "$_var" '%s' "n" ;; - esac -} - -docker_compose_cmd() { - if docker compose version &>/dev/null; then - echo "docker compose" - return 0 - fi - if command -v docker-compose &>/dev/null; then - echo "docker-compose" - return 0 - fi - die "Docker Compose v2 is required (docker compose)" -} - -ensure_docker_network() { - local network_name="$1" - if ! docker network inspect "$network_name" &>/dev/null; then - info "Creating Docker network ${network_name}…" - docker network create "$network_name" - fi -} - -base_domain_from_host() { - local host="$1" - local parts=(${host//./ }) - local count=${#parts[@]} - if (( count >= 2 )); then - echo "${parts[count-2]}.${parts[count-1]}" - else - echo "$host" - fi -} +export PATH="${HOME}/.local/bin:${PATH}" diff --git a/wizard.sh b/wizard.sh index bac11ed..a6560cf 100755 --- a/wizard.sh +++ b/wizard.sh @@ -97,6 +97,7 @@ gather_config() { exit 0 } + cd "${SCRIPT_DIR}" uv run python - <