341 lines
11 KiB
Bash
Executable File
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'
|