#!/usr/bin/env bash # integrate-into-nodered.sh — add Collabora nodes to EXISTING nextcloud-node-red (:1880) # Called by install.sh / bootstrap-install.sh (default mode). set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PKG_SRC="${PKG_SRC:-$(cd "${SCRIPT_DIR}/.." && pwd)}" NODERED_DIR="${NODERED_DIR:-/home/ncdeploy/nextcloud-node-red}" NODERED_CONTAINER="${NODERED_CONTAINER:-nextcloud-node-red}" NODERED_PORT="${NODERED_PORT:-1880}" CODE_CONTAINER="${CODE_CONTAINER:-code}" COLLABORA_IP="${COLLABORA_IP:-}" NEXTCLOUD_URL="${NEXTCLOUD_URL:-https://cloud.domain.name}" MARKER="nextcloud-collabora-nodered" log() { printf '[integrate] %s\n' "$*"; } warn() { printf '[integrate] WARNING: %s\n' "$*" >&2; } die() { printf '[integrate] ERROR: %s\n' "$*" >&2; exit 1; } run_as_root() { if [ "$(id -u)" -eq 0 ]; then "$@" elif command -v sudo >/dev/null 2>&1; then sudo "$@" else die "sudo/root required for: $*" fi } runtime_user() { if [ -n "${INSTALL_USER:-}" ]; then echo "${INSTALL_USER}" elif id ncdeploy >/dev/null 2>&1; then echo "ncdeploy" else id -un fi } # Docker build context — .dockerignore + rättigheter (undviker permission denied) DOCKERIGNORE_MARKER="nextcloud-collabora-nodered" patch_dockerignore() { local di="${NODERED_DIR}/.dockerignore" local snippet="${PKG_SRC}/integrate/dockerignore.snippet" if [ ! -f "${di}" ]; then log "Creating .dockerignore" touch "${di}" fi if ! grep -q "${DOCKERIGNORE_MARKER}" "${di}" 2>/dev/null; then log "Patching .dockerignore (exclude unreadable local artifacts)" { echo "" echo "# ${DOCKERIGNORE_MARKER}" if [ -f "${snippet}" ]; then cat "${snippet}" else echo "nodes/odds-scraper/odds-scraper" echo ".git" echo ".vscode" echo "**/*.deb" echo "winboat*" echo "**/node_modules" fi } >> "${di}" fi # Dynamiskt: alla filer under nodes/ som inte ens root kan läsa → ignorera local unreadable while IFS= read -r relpath; do [ -n "${relpath}" ] || continue if ! grep -qxF "${relpath}" "${di}" 2>/dev/null; then log " + .dockerignore: ${relpath} (unreadable)" echo "${relpath}" >> "${di}" fi done < <(run_as_root find "${NODERED_DIR}/nodes" -type f ! -readable -printf 'nodes/%P\n' 2>/dev/null || true) } fix_nodered_build_permissions() { local user group user="$(runtime_user)" group="$(id -gn "${user}" 2>/dev/null || echo "${user}")" if [ ! -d "${NODERED_DIR}/nodes" ]; then return 0 fi log "Fixing ownership and read permissions for ${NODERED_DIR} (content unchanged)" run_as_root chown -R "${user}:${group}" "${NODERED_DIR}" run_as_root find "${NODERED_DIR}/nodes" -type d -exec chmod u+rwx,go+rx {} + 2>/dev/null || true run_as_root find "${NODERED_DIR}/nodes" -type f -exec chmod u+rw,go+r {} + 2>/dev/null || true # Binärer som ska vara körbara run_as_root find "${NODERED_DIR}/nodes" -type f \( -name "*.sh" -o -name "odds-scraper" \) -exec chmod u+rx,go+rx {} + 2>/dev/null || true } # Simulera Docker build context — måste lyckas innan build verify_build_context() { local di="${NODERED_DIR}/.dockerignore" local errfile errfile="$(mktemp)" log "Verifying Docker build context..." patch_dockerignore if ! run_as_root tar -cf /dev/null \ --exclude-from="${di}" \ -C "${NODERED_DIR}" . 2>"${errfile}"; then warn "Build context error:" sed 's/^/[integrate] /' "${errfile}" >&2 || true # Sista utväg: tvinga bort kända problemfil if [ -f "${NODERED_DIR}/nodes/odds-scraper/odds-scraper" ]; then echo "nodes/odds-scraper/odds-scraper" >> "${di}" log "Forced nodes/odds-scraper/odds-scraper into .dockerignore" fi if ! run_as_root tar -cf /dev/null \ --exclude-from="${di}" \ -C "${NODERED_DIR}" . 2>"${errfile}"; then rm -f "${errfile}" die "Build context still unreadable after .dockerignore — see ${di}" fi fi rm -f "${errfile}" log "Build context OK" } compose_cmd() { if docker compose version >/dev/null 2>&1; then echo "docker compose" else echo "docker-compose" fi } # Alltid root för compose build — samma användare som kan läsa hela trädet + .dockerignore compose_run() { local compose compose="$(compose_cmd)" cd "${NODERED_DIR}" run_as_root env DOCKER_BUILDKIT=1 ${compose} "$@" } code_bridge_ip() { docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{if eq $k "bridge"}}{{$v.IPAddress}}{{end}}{{end}}' "${CODE_CONTAINER}" 2>/dev/null } # Host-LAN-IP för extra_hosts (detektera; fallback 192.168.1.10) detect_host_lan_ip() { local ip="" if command -v hostname >/dev/null 2>&1; then ip="$(hostname -I 2>/dev/null | tr ' ' '\n' \ | grep -E '^(192\.168|10)\.[0-9]+\.[0-9]+\.[0-9]+$' | head -n1 || true)" fi if [ -z "${ip}" ] && command -v ip >/dev/null 2>&1; then ip="$(ip -4 addr show scope global 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1 \ | grep -E '^(192\.168|10)\.' | head -n1 || true)" fi if [ -z "${ip}" ]; then warn "Kunde inte detektera host-LAN-IP — fallback 192.168.1.10" ip="192.168.1.10" fi HOST_LAN_IP="${ip}" log "Host-LAN-IP: ${HOST_LAN_IP}" } # Skriv NC_HOST_IP i nextcloud-node-red/.env så compose-substitutionen plockar upp den write_nodered_env_ip() { local envfile="${NODERED_DIR}/.env" local existing="" if [ -f "${envfile}" ]; then existing="$(grep -E '^NC_HOST_IP=[[:alnum:].:]+' "${envfile}" 2>/dev/null | tail -n1 | cut -d= -f2 || true)" fi if [ -n "${existing}" ]; then log "${envfile} har redan NC_HOST_IP=${existing} — behåller" HOST_LAN_IP="${existing}" return 0 fi if [ -f "${envfile}" ] && grep -q '^NC_HOST_IP=' "${envfile}" 2>/dev/null; then run_as_root sed -i "s|^NC_HOST_IP=.*|NC_HOST_IP=${HOST_LAN_IP}|" "${envfile}" else run_as_root bash -c "echo 'NC_HOST_IP=${HOST_LAN_IP}' >> '${envfile}'" fi log "NC_HOST_IP=${HOST_LAN_IP} → ${envfile}" } detect_code_ip() { local ip ip="$(code_bridge_ip)" if [ -n "${ip}" ]; then COLLABORA_IP="${ip}" log "CODE bridge-IP: ${COLLABORA_IP} (container ${CODE_CONTAINER})" return 0 fi if [ -n "${COLLABORA_IP}" ]; then warn "Could not read bridge IP — using COLLABORA_IP=${COLLABORA_IP}" return 0 fi die "CODE bridge IP missing — set COLLABORA_IP or ensure ${CODE_CONTAINER} runs on Docker bridge" } detect_nodered_dir() { if [ -d "${NODERED_DIR}" ] && [ -f "${NODERED_DIR}/Dockerfile" ]; then return 0 fi local from_compose from_compose="$(docker inspect -f '{{index .Config.Labels "com.docker.compose.project.working_dir"}}' "${NODERED_CONTAINER}" 2>/dev/null || true)" if [ -n "${from_compose}" ] && [ -f "${from_compose}/Dockerfile" ]; then NODERED_DIR="${from_compose}" log "Node-RED directory from compose label: ${NODERED_DIR}" return 0 fi for candidate in \ "/home/ncdeploy/nextcloud-node-red" \ "/opt/nextcloud-node-red"; do if [ -f "${candidate}/Dockerfile" ]; then NODERED_DIR="${candidate}" log "Node-RED directory found: ${NODERED_DIR}" return 0 fi done die "NODERED_DIR missing or invalid — set path to existing nextcloud-node-red (Dockerfile + entrypoint.sh)" } resolve_paths() { if [ -f "${PKG_SRC}/.env" ]; then set -a # shellcheck disable=SC1091 . "${PKG_SRC}/.env" set +a fi if [ "$(id -u)" -eq 0 ] && [ -d "/home/ncdeploy/nextcloud-node-red" ]; then NODERED_DIR="${NODERED_DIR:-/home/ncdeploy/nextcloud-node-red}" PKG_SRC="${PKG_SRC:-/home/ncdeploy/nextcloud-collabora-nodered}" fi detect_nodered_dir detect_code_ip detect_host_lan_ip write_nodered_env_ip } stop_sidecar_if_any() { if docker ps -a --format '{{.Names}}' | grep -qx "nextcloud-collabora-nodered"; then warn "Stopping stray sidecar container nextcloud-collabora-nodered (should not exist)" docker stop nextcloud-collabora-nodered 2>/dev/null || true docker rm nextcloud-collabora-nodered 2>/dev/null || true fi } preflight() { [ -d "${PKG_SRC}" ] || die "Package source missing: ${PKG_SRC}" [ -f "${PKG_SRC}/package.json" ] || die "package.json missing in ${PKG_SRC}" [ -d "${NODERED_DIR}" ] || die "nextcloud-node-red missing: ${NODERED_DIR}" [ -f "${NODERED_DIR}/Dockerfile" ] || die "Dockerfile missing in ${NODERED_DIR}" [ -f "${NODERED_DIR}/entrypoint.sh" ] || die "entrypoint.sh missing in ${NODERED_DIR}" if ! docker ps --format '{{.Names}}' | grep -qx "${NODERED_CONTAINER}"; then die "Container '${NODERED_CONTAINER}' is not running — start existing nextcloud-node-red first" fi if ! docker ps --format '{{.Names}}' | grep -qx "${CODE_CONTAINER}"; then die "CODE container '${CODE_CONTAINER}' is not running" fi } assert_package_layout() { local dest="$1" local missing=0 if [ ! -f "${dest}/package.json" ]; then warn "Missing: ${dest}/package.json" missing=1 fi if [ ! -d "${dest}/lib" ] || [ -z "$(ls -A "${dest}/lib" 2>/dev/null)" ]; then warn "Missing or empty: ${dest}/lib/" missing=1 fi if [ ! -d "${dest}/nodes" ] || [ -z "$(ls -A "${dest}/nodes" 2>/dev/null)" ]; then warn "Missing or empty: ${dest}/nodes/" missing=1 fi if [ "${missing}" -ne 0 ]; then die "Wrong package layout after copy — expected package.json + lib/ + nodes/ under ${dest}" fi } copy_node_package() { local dest="${NODERED_DIR}/nodes/nextcloud-collabora" log "Copying package → ${dest} (lib/ + nodes/, not flat)" rm -rf "${dest}" mkdir -p "${dest}/lib" "${dest}/nodes" cp -a "${PKG_SRC}/package.json" "${dest}/" cp -a "${PKG_SRC}/lib/." "${dest}/lib/" cp -a "${PKG_SRC}/nodes/." "${dest}/nodes/" if [ -d "${PKG_SRC}/locales" ] && [ -n "$(ls -A "${PKG_SRC}/locales" 2>/dev/null)" ]; then mkdir -p "${dest}/locales" cp -a "${PKG_SRC}/locales/." "${dest}/locales/" fi assert_package_layout "${dest}" log "Package layout OK: ${dest}/{package.json,lib/,nodes/}" } patch_dockerfile() { local df="${NODERED_DIR}/Dockerfile" if grep -q "${MARKER}" "${df}"; then log "Dockerfile already patched" return 0 fi log "Patching Dockerfile" cat >> "${df}" <<'EOF' # nextcloud-collabora-nodered — Collabora/Office-noder (WebDAV + CODE REST) RUN mkdir -p /opt/nextcloud-nodes/node-red-contrib-nextcloud-collabora COPY nodes/nextcloud-collabora/package.json /opt/nextcloud-nodes/node-red-contrib-nextcloud-collabora/ COPY nodes/nextcloud-collabora/lib /opt/nextcloud-nodes/node-red-contrib-nextcloud-collabora/lib COPY nodes/nextcloud-collabora/nodes /opt/nextcloud-nodes/node-red-contrib-nextcloud-collabora/nodes RUN cd /opt/nextcloud-nodes/node-red-contrib-nextcloud-collabora && npm install --omit=dev EOF } patch_entrypoint() { local ep="${NODERED_DIR}/entrypoint.sh" # Rensa trasig tidigare patch (rå cp-rader utan install_nodes) if grep -q 'cp -a /opt/nextcloud-nodes/node-red-contrib-nextcloud-collabora' "${ep}"; then log "Removing broken collabora patch from entrypoint.sh" sed -i '/cp -a \/opt\/nextcloud-nodes\/node-red-contrib-nextcloud-collabora/d' "${ep}" fi if grep -q 'install_nodes.*Nextcloud Collabora' "${ep}"; then log "entrypoint.sh already patched (Collabora install_nodes)" return 0 fi log "Patching entrypoint.sh (install_nodes for Collabora)" if grep -q 'install_nodes.*Nextcloud OCS' "${ep}"; then sed -i '/install_nodes.*Nextcloud OCS/a\ install_nodes "/opt/nextcloud-nodes/node-red-contrib-nextcloud-collabora" "/data/node_modules/node-red-contrib-nextcloud-collabora" "Nextcloud Collabora"' "${ep}" elif grep -q 'install_nodes.*Odds Scraper' "${ep}"; then sed -i '/install_nodes.*Odds Scraper/a\ install_nodes "/opt/nextcloud-nodes/node-red-contrib-nextcloud-collabora" "/data/node_modules/node-red-contrib-nextcloud-collabora" "Nextcloud Collabora"' "${ep}" else cat >> "${ep}" <<'EOF' # nextcloud-collabora-nodered install_nodes "/opt/nextcloud-nodes/node-red-contrib-nextcloud-collabora" "/data/node_modules/node-red-contrib-nextcloud-collabora" "Nextcloud Collabora" EOF fi } # Krav: docker network connect code — ignorerar already connected connect_code_network() { local net net="$(docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{printf "%s\n" $k}}{{end}}' "${NODERED_CONTAINER}" 2>/dev/null \ | grep -v '^bridge$' | head -n1)" [ -n "${net}" ] || die "No Node-RED compose network on ${NODERED_CONTAINER}" if docker network inspect "${net}" --format '{{range .Containers}}{{.Name}} {{end}}' 2>/dev/null | grep -qw "${CODE_CONTAINER}"; then log "Already connected: ${CODE_CONTAINER} on ${net}" return 0 fi log "Connecting ${CODE_CONTAINER} → ${net}" if ! docker network connect "${net}" "${CODE_CONTAINER}" 2>/dev/null; then if docker network inspect "${net}" --format '{{range .Containers}}{{.Name}} {{end}}' 2>/dev/null | grep -qw "${CODE_CONTAINER}"; then log "Already connected (race): ${CODE_CONTAINER} on ${net}" else warn "Could not connect ${CODE_CONTAINER} → ${net} — COLLABORA_URL fallar tillbaka till CODE-IP" fi fi } compose_service_name() { local svc svc="$(docker inspect -f '{{index .Config.Labels "com.docker.compose.service"}}' "${NODERED_CONTAINER}" 2>/dev/null || true)" echo "${svc:-node-red}" } # Recreate utan 'docker compose down' — volymer (node-red-data) bevaras. # ExtraHosts måste vara ifyllda efteråt. recreate_nodered_service() { local service extra service="$(compose_service_name)" log "Recreating ${service} (--no-deps --force-recreate, volymer bevaras)..." if ! compose_run up -d --no-deps --force-recreate "${service}"; then warn "force-recreate misslyckades — rm + up --no-deps (volymen node-red-data ligger kvar)" run_as_root docker rm -f "${NODERED_CONTAINER}" 2>/dev/null || true compose_run up -d --no-deps "${service}" fi for _ in $(seq 1 10); do extra="$(docker inspect -f '{{len .HostConfig.ExtraHosts}}' "${NODERED_CONTAINER}" 2>/dev/null || echo 0)" [ "${extra}" != "0" ] && break sleep 1 done if [ -z "${extra:-0}" ] || [ "${extra}" = "0" ]; then die "ExtraHosts är [] på ${NODERED_CONTAINER} efter recreate — kontrollera extra_hosts i ${NODERED_DIR}/docker-compose.yml" fi log "ExtraHosts (${extra}): $(docker inspect -f '{{json .HostConfig.ExtraHosts}}' "${NODERED_CONTAINER}")" } # COLLABORA_URL: intern CODE är HTTP — http://code:9980 primärt, # fallback http://:9980. # Aldrig http://127.0.0.1:9980 eller http://host.docker.internal:9980. resolve_collabora_url() { local url="${COLLABORA_URL:-http://code:9980}" case "${url}" in http://127.0.0.1:*|http://localhost:*|http://host.docker.internal:*|\ https://127.0.0.1:*|https://localhost:*|https://host.docker.internal:*) warn "Otillåten COLLABORA_URL '${url}' — tvingar http://code:9980" url="http://code:9980" ;; esac # Fallback till CODE-IP om 'code' inte kan resolvas från nod-containern if ! docker exec "${NODERED_CONTAINER}" getent hosts code >/dev/null 2>&1; then local ip ip="$(docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}} {{end}}' "${CODE_CONTAINER}" 2>/dev/null | awk '{print $1}')" [ -n "${ip}" ] || ip="${COLLABORA_IP:-}" if [ -n "${ip}" ]; then warn "'code' inte resolvbart från ${NODERED_CONTAINER} — fallback http://${ip}:9980" url="http://${ip}:9980" else die "Varken 'code'-DNS eller CODE-IP tillgänglig — kan inte sätta COLLABORA_URL" fi fi COLLABORA_URL="${url}" log "COLLABORA_URL=${COLLABORA_URL}" } rebuild_nodered() { log "Rebuilding ${NODERED_CONTAINER} (same instance, port ${NODERED_PORT})..." fix_nodered_build_permissions verify_build_context if [ "${FORCE_REBUILD:-0}" = "1" ]; then compose_run build --no-cache else compose_run build fi recreate_nodered_service } wait_nodered() { log "Waiting for Node-RED :${NODERED_PORT}..." for i in $(seq 1 30); do if curl -sf "http://127.0.0.1:${NODERED_PORT}" >/dev/null 2>&1; then log "Node-RED responding at http://127.0.0.1:${NODERED_PORT}" return 0 fi sleep 2 done warn "Node-RED did not respond within 60 s" } verify_code_from_nodered() { # Krav 7: getent i containern != 127.0.0.1/127.0.1.1 + curl mot CODE och Nextcloud local fail=0 h ip_out for h in cloud.domain.name docs.domain.name host.docker.internal code; do ip_out="$(docker exec "${NODERED_CONTAINER}" getent hosts "${h}" 2>/dev/null | awk '{print $1}' | head -n1 || true)" if [ -z "${ip_out}" ]; then warn "getent ${h}: INGET SVAR"; fail=1 elif [ "${ip_out#127.}" != "${ip_out}" ] || [ "${ip_out}" = "::1" ]; then warn "getent ${h}: loopback (${ip_out}) — extra_hosts/translering trasig"; fail=1 else log "getent ${h}: ${ip_out}" fi done if docker exec "${NODERED_CONTAINER}" curl -sfk "${COLLABORA_URL}/hosting/capabilities" >/dev/null 2>&1; then log "OK: CODE nås från Node-RED: ${COLLABORA_URL}/hosting/capabilities" else warn "CODE inte nåbar: ${COLLABORA_URL}/hosting/capabilities" warn "Kör: docker network connect \$(docker inspect -f '{{range \$k,\$_ := .NetworkSettings.Networks}}{{printf \"%s\\n\" \$k}}{{end}}' ${NODERED_CONTAINER} | head -n1) ${CODE_CONTAINER}" fail=1 fi if docker exec "${NODERED_CONTAINER}" curl -sfk https://cloud.domain.name/status.php >/dev/null 2>&1; then log "OK: Nextcloud nås från Node-RED: https://cloud.domain.name/status.php" else warn "Nextcloud inte nåbar från Node-RED via https://cloud.domain.name/status.php" fail=1 fi return "${fail}" } main() { resolve_paths log "Integrating Collabora nodes into existing nextcloud-node-red" log "Package: ${PKG_SRC}" log "Node-RED: ${NODERED_DIR} (container ${NODERED_CONTAINER})" stop_sidecar_if_any preflight copy_node_package patch_dockerfile patch_entrypoint rebuild_nodered connect_code_network resolve_collabora_url wait_nodered if ! verify_code_from_nodered; then warn "Verifiering hade fel — se varningarna ovan" fi local host_ip nc_url host_ip="$(hostname -I 2>/dev/null | awk '{print $1}' || echo "127.0.0.1")" nc_url="${NEXTCLOUD_URL:-https://cloud.domain.name}" cat <