Files

341 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "${script_dir}/../.." && pwd)"
env_file="${repo_root}/.env.devprod"
compose_file=""
output_mode="json"
usage() {
cat <<USAGE
Usage: ./scripts/topology/discover-cloud-topology.sh [options]
Detect running cloud topology and print install profile recommendation.
Options:
--repo-root <path> Repository root (default: auto)
--env-file <path> Env file to inspect (default: .env.devprod)
--compose-file <path> Compose file override
--json JSON output (default)
--text Human-readable output
-h, --help Show this help
USAGE
}
json_escape() {
local input="${1:-}"
input="${input//\\/\\\\}"
input="${input//\"/\\\"}"
input="${input//$'\n'/\\n}"
input="${input//$'\r'/}"
input="${input//$'\t'/\\t}"
printf '%s' "${input}"
}
bool_json() {
if [[ "${1:-0}" == "1" ]]; then
printf 'true'
else
printf 'false'
fi
}
run_timeout_cmd() {
if command -v timeout >/dev/null 2>&1; then
timeout 20 "$@"
else
"$@"
fi
}
host_occ_runner() {
if [[ -z "${host_occ_path:-}" || ! -f "${host_occ_path:-}" ]]; then
return 1
fi
if [[ -z "$(command -v php || true)" ]]; then
return 1
fi
if [[ "${EUID:-$(id -u)}" -eq 0 ]]; then
if command -v sudo >/dev/null 2>&1; then
run_timeout_cmd sudo -u www-data php "${host_occ_path}" "$@"
return $?
fi
if command -v runuser >/dev/null 2>&1; then
run_timeout_cmd runuser -u www-data -- php "${host_occ_path}" "$@"
return $?
fi
fi
run_timeout_cmd php "${host_occ_path}" "$@"
}
read_kv() {
local key="$1"
local file="$2"
local line
line="$(grep -m1 -E "^${key}=" "${file}" 2>/dev/null || true)"
if [[ -z "${line}" ]]; then
return 1
fi
printf '%s' "${line#*=}"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--repo-root)
repo_root="${2:-}"
shift 2
;;
--repo-root=*)
repo_root="${1#*=}"
shift
;;
--env-file)
env_file="${2:-}"
shift 2
;;
--env-file=*)
env_file="${1#*=}"
shift
;;
--compose-file)
compose_file="${2:-}"
shift 2
;;
--compose-file=*)
compose_file="${1#*=}"
shift
;;
--json)
output_mode="json"
shift
;;
--text)
output_mode="text"
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1"
usage
exit 1
;;
esac
done
if [[ "${repo_root}" != /* ]]; then
repo_root="$(cd "${repo_root}" 2>/dev/null && pwd)"
fi
if [[ "${env_file}" != /* ]]; then
env_file="${repo_root}/${env_file}"
fi
if [[ -z "${compose_file}" ]]; then
if [[ -f "${repo_root}/docker-compose.devprod.nossl.yml" ]]; then
compose_file="${repo_root}/docker-compose.devprod.nossl.yml"
elif [[ -f "${repo_root}/docker-compose.devprod.yml" ]]; then
compose_file="${repo_root}/docker-compose.devprod.yml"
else
compose_file="${repo_root}/docker-compose.devprod.nossl.yml"
fi
fi
if [[ "${compose_file}" != /* ]]; then
compose_file="${repo_root}/${compose_file}"
fi
has_docker_cli="0"
has_compose_plugin="0"
docker_daemon_reachable="0"
env_exists="0"
compose_exists="0"
compose_config_ok="0"
compose_has_app_service="0"
compose_occ_ok="0"
compose_services_count="0"
host_occ_exists="0"
host_occ_status_ok="0"
host_occ_path=""
if command -v docker >/dev/null 2>&1; then
has_docker_cli="1"
if docker compose version >/dev/null 2>&1; then
has_compose_plugin="1"
fi
if docker info >/dev/null 2>&1; then
docker_daemon_reachable="1"
fi
fi
if [[ -f "${env_file}" ]]; then
env_exists="1"
fi
if [[ -f "${compose_file}" ]]; then
compose_exists="1"
fi
services_list=""
if [[ "${has_compose_plugin}" == "1" && "${compose_exists}" == "1" && "${env_exists}" == "1" ]]; then
if services_list="$(docker compose -f "${compose_file}" --env-file "${env_file}" config --services 2>/dev/null || true)"; then
if [[ -n "${services_list}" ]]; then
compose_config_ok="1"
compose_services_count="$(printf '%s\n' "${services_list}" | sed '/^$/d' | wc -l | awk '{print $1}')"
if printf '%s\n' "${services_list}" | grep -q '^app$'; then
compose_has_app_service="1"
fi
fi
fi
fi
if [[ "${compose_has_app_service}" == "1" && "${docker_daemon_reachable}" == "1" ]]; then
if docker compose -f "${compose_file}" --env-file "${env_file}" exec -T app php occ status >/dev/null 2>&1; then
compose_occ_ok="1"
fi
fi
occ_candidates=(
"/var/www/nextcloud/occ"
"/var/www/html/nextcloud/occ"
"/var/www/html/occ"
"/srv/www/nextcloud/occ"
"/var/lib/nextcloud/occ"
)
for candidate in "${occ_candidates[@]}"; do
if [[ -f "${candidate}" ]]; then
host_occ_exists="1"
host_occ_path="${candidate}"
break
fi
done
if [[ "${host_occ_exists}" == "1" && -n "${host_occ_path}" && -x "$(command -v php || true)" ]]; then
if host_occ_runner status >/dev/null 2>&1; then
host_occ_status_ok="1"
fi
fi
topology_id="unknown"
topology_confidence="low"
topology_reason="No known Nextcloud runtime detected."
recommended_profile="manual"
recommended_command=""
recommended_notes=()
if [[ "${compose_occ_ok}" == "1" ]]; then
topology_id="repo_docker_devprod"
topology_confidence="high"
topology_reason="Detected Docker Compose stack with working Nextcloud OCC in app container."
recommended_profile="repo-devprod-bootstrap"
recommended_command="./scripts/topology/apply-topology-profile.sh --profile repo-devprod-bootstrap --repo-root \"${repo_root}\" --env-file \"${env_file}\" --compose-file \"${compose_file}\""
recommended_notes+=("Best path for this topology: run recreate + finish setup using repository scripts.")
elif [[ "${compose_config_ok}" == "1" && "${has_docker_cli}" == "1" ]]; then
topology_id="docker_compose_generic"
topology_confidence="medium"
topology_reason="Detected Docker Compose config, but Nextcloud OCC in app container is not currently healthy."
recommended_profile="repo-devprod-bootstrap"
recommended_command="./scripts/topology/apply-topology-profile.sh --profile repo-devprod-bootstrap --repo-root \"${repo_root}\" --env-file \"${env_file}\" --compose-file \"${compose_file}\""
recommended_notes+=("Run profile apply to attempt full container recreate and repair.")
elif [[ "${host_occ_exists}" == "1" ]]; then
if [[ "${host_occ_status_ok}" == "1" && "${docker_daemon_reachable}" == "1" ]]; then
topology_id="host_nextcloud_plus_docker"
topology_confidence="high"
topology_reason="Detected host-level Nextcloud OCC with working status check and a reachable Docker daemon."
recommended_notes+=("This is the primary existing-instance adoption target: keep host Nextcloud in place and add NuQloud sidecars around it.")
recommended_notes+=("The host-hybrid installer now handles bootstrap, app wiring, and lifecycle helpers for this topology.")
recommended_notes+=("If Talk/Office/notify_push are already configured on the host, prefer reusing them instead of replacing them.")
else
topology_id="host_nextcloud"
topology_confidence="medium"
topology_reason="Detected host-level Nextcloud OCC path outside repository Docker stack."
fi
recommended_profile="host-nextcloud-hybrid"
recommended_command="./scripts/install-host-nextcloud-hybrid.sh --repo-root \"${repo_root}\" --occ-path \"${host_occ_path}\""
recommended_notes+=("Host-level integration now has a dedicated installer plus restart/stop/update wrappers.")
fi
if [[ "${output_mode}" == "text" ]]; then
echo "Topology ID: ${topology_id}"
echo "Confidence: ${topology_confidence}"
echo "Reason: ${topology_reason}"
echo
echo "Capabilities:"
echo " docker_cli: ${has_docker_cli}"
echo " docker_compose_plugin: ${has_compose_plugin}"
echo " docker_daemon_reachable: ${docker_daemon_reachable}"
echo " env_file_exists: ${env_exists}"
echo " compose_file_exists: ${compose_exists}"
echo " compose_config_ok: ${compose_config_ok}"
echo " compose_has_app_service: ${compose_has_app_service}"
echo " compose_occ_ok: ${compose_occ_ok}"
echo " host_occ_exists: ${host_occ_exists}"
echo " host_occ_status_ok: ${host_occ_status_ok}"
echo
echo "Recommendation:"
echo " profile: ${recommended_profile}"
if [[ -n "${recommended_command}" ]]; then
echo " command: ${recommended_command}"
fi
if [[ "${#recommended_notes[@]}" -gt 0 ]]; then
echo " notes:"
for note in "${recommended_notes[@]}"; do
echo " - ${note}"
done
fi
exit 0
fi
notes_json=""
if [[ "${#recommended_notes[@]}" -gt 0 ]]; then
note_first="1"
for note in "${recommended_notes[@]}"; do
if [[ "${note_first}" == "1" ]]; then
notes_json="${notes_json}\"$(json_escape "${note}")\""
note_first="0"
else
notes_json="${notes_json},\"$(json_escape "${note}")\""
fi
done
fi
generated_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
printf '{\n'
printf ' "ok": true,\n'
printf ' "generatedAt": "%s",\n' "$(json_escape "${generated_at}")"
printf ' "topology": {\n'
printf ' "id": "%s",\n' "$(json_escape "${topology_id}")"
printf ' "confidence": "%s",\n' "$(json_escape "${topology_confidence}")"
printf ' "reason": "%s"\n' "$(json_escape "${topology_reason}")"
printf ' },\n'
printf ' "paths": {\n'
printf ' "repoRoot": "%s",\n' "$(json_escape "${repo_root}")"
printf ' "envFile": "%s",\n' "$(json_escape "${env_file}")"
printf ' "composeFile": "%s",\n' "$(json_escape "${compose_file}")"
printf ' "hostOccPath": "%s"\n' "$(json_escape "${host_occ_path}")"
printf ' },\n'
printf ' "capabilities": {\n'
printf ' "dockerCli": %s,\n' "$(bool_json "${has_docker_cli}")"
printf ' "dockerComposePlugin": %s,\n' "$(bool_json "${has_compose_plugin}")"
printf ' "dockerDaemonReachable": %s,\n' "$(bool_json "${docker_daemon_reachable}")"
printf ' "envFileExists": %s,\n' "$(bool_json "${env_exists}")"
printf ' "composeFileExists": %s,\n' "$(bool_json "${compose_exists}")"
printf ' "composeConfigOk": %s,\n' "$(bool_json "${compose_config_ok}")"
printf ' "composeHasAppService": %s,\n' "$(bool_json "${compose_has_app_service}")"
printf ' "composeOccOk": %s,\n' "$(bool_json "${compose_occ_ok}")"
printf ' "composeServicesCount": %s,\n' "$(json_escape "${compose_services_count}")"
printf ' "hostOccExists": %s,\n' "$(bool_json "${host_occ_exists}")"
printf ' "hostOccStatusOk": %s\n' "$(bool_json "${host_occ_status_ok}")"
printf ' },\n'
printf ' "recommendation": {\n'
printf ' "profile": "%s",\n' "$(json_escape "${recommended_profile}")"
printf ' "command": "%s",\n' "$(json_escape "${recommended_command}")"
printf ' "notes": [%s]\n' "${notes_json}"
printf ' }\n'
printf '}\n'