diff --git a/.gitmodules b/.gitmodules index df23a469..e0709140 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,3 @@ -[submodule "lib/sp-vm-tools"] - path = lib/sp-vm-tools - url = https://github.com/super-protocol/sp-vm-tools [submodule "swarm-cloud"] path = src/repos/swarm-cloud url = git@github.com:Super-Protocol/swarm-cloud.git -[submodule "swarm-db"] - path = src/repos/swarm-db - url = git@github.com:Super-Protocol/swarm-db.git \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..14a02e89 --- /dev/null +++ b/LICENSE @@ -0,0 +1,66 @@ +Business Source License 1.1 + +License text copyright (c) 2024 Super Protocol, All Rights Reserved. +"Business Source License" is a trademark of MariaDB Corporation Ab. + +----------------------------------------------------------------------------- + +Parameters + +- Licensor: + Super Protocol + +- Licensed Work: + Super Protocol Software + The Licensed Work is (c) 2024 Super Protocol + +- Additional Use Grant: + None + +- Change Date: + 2028-01-01 + +- Change License: + GNU General Public License v2.0 or later + +----------------------------------------------------------------------------- + +Terms + +The Licensor hereby grants you the right to copy, modify, create derivative works, redistribute, and make non-production use of the Licensed Work. The Licensor may make an Additional Use Grant permitting limited production use. + +Effective on the Change Date, or the fourth anniversary of the first publicly available distribution of a specific version of the Licensed Work under this License, whichever comes first, the Licensor hereby grants you rights under the terms of the Change License, and the rights granted in the paragraph above terminate. + +If your use of the Licensed Work does not comply with the requirements currently in effect as described in this License, you must purchase a commercial license from the Licensor, its affiliated entities, or authorized resellers, or you must refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works of the Licensed Work, are subject to this License. This License applies separately for each version of the Licensed Work and the Change Date may vary for each version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy of the Licensed Work. If you receive the Licensed Work in original or modified form from a third party, the terms and conditions set forth in this License apply to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically terminate your rights under this License for the current and all other versions of the Licensed Work. + +This License does not grant you any right in any trademark or logo of Licensor or its affiliates (provided that you may use a trademark or logo of Licensor as expressly required by this License). + +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND TITLE. + +MariaDB hereby grants you permission to use this License’s text to license your works, and to refer to it using the trademark "Business Source License," as long as you comply with the Covenants of Licensor below. + +----------------------------------------------------------------------------- + +Covenants of Licensor + +In consideration of the right to use this License’s text and the "Business Source License" name and trademark, Licensor covenants to MariaDB, and to all other recipients of the licensed work to be provided by Licensor: + +1. To specify as the Change License the GPL Version 2.0 or any later version, or a license that is compatible with GPL Version 2.0 or a later version, where "compatible" means that software provided under the Change License can be included in a program with software provided under GPL Version 2.0 or a later version. Licensor may specify additional Change Licenses without limitation. + +2. To either: (a) specify an additional grant of rights to use that does not impose any additional restriction on the rights granted in this License, as the Additional Use Grant; or (b) insert the text "None." + +3. To specify a Change Date. + +4. Not to modify this License in any other way. + +----------------------------------------------------------------------------- + +Notice + +The Business Source License (this document, or the "License") is not an Open Source license. However, the Licensed Work will eventually be made available under an Open Source License, as stated in this License. \ No newline at end of file diff --git a/signatures/tdx/pre-release/mrenclave-005859454c42f7d2bf2143b24cbb8623ca1655d3b694bb69b7645cd4e568183a.json b/signatures/tdx/pre-release/mrenclave-005859454c42f7d2bf2143b24cbb8623ca1655d3b694bb69b7645cd4e568183a.json new file mode 100644 index 00000000..c2fe6443 --- /dev/null +++ b/signatures/tdx/pre-release/mrenclave-005859454c42f7d2bf2143b24cbb8623ca1655d3b694bb69b7645cd4e568183a.json @@ -0,0 +1,7 @@ +{ + "mrenclave": "005859454c42f7d2bf2143b24cbb8623ca1655d3b694bb69b7645cd4e568183a", + "signature": "S2cBl3yovLVALEvV4YMMw0wDRLGJJ0baquerS4HQELCbMvDZ3oSz35Bo3v2L5x5+H6/zPjXmTyfg/Nu8duarSP9xRnSCog6+cKmAsCoT0jCqez7yphlbpjrO2imQw8Zp2Hq2cBbof+jwLTeOwzv2dk+1fMTe6oWxFCj45g0E41dSLWl5Ox2oofAEMzW/QFkhyh3/XlttgY1+dxHxbtpspmBg7ZFALTrBaK2U60mWJlsSUxhnlnpqpwVfhri1L4cUR9Cds+wmZ296iUWgFasIh6qTsx73BiFwTLCPEAApgDY+ETHUX6tStuIHX1FXpkzFFOWtWXW17KOD1Q49PjvSLcshZEtNZZD5zFU69oHuu1dfdicRXr2cBLLxZB4SYWRackxYToQ9sJdZ1vkxRjcad4zW2GY5HAhMYOSUgebfzydT0ixnEfsvaC+LnRC5d0bCf5NtVZA9F3KFTJ/gMbn7+nzjt5VV6ebCf/5mzf3EscBx52QE3h3kx7AliVn/9qai", + "build": "build-295", + "description": "test/debug/swarm", + "creationDate": "2026-01-05T12:00:10.685Z" +} diff --git a/signatures/tdx/pre-release/mrenclave-dbf91a376403a78483473da4c78c170cdb30daf62ed7269b692c3f25d86d833f.json b/signatures/tdx/pre-release/mrenclave-dbf91a376403a78483473da4c78c170cdb30daf62ed7269b692c3f25d86d833f.json new file mode 100644 index 00000000..475c94f6 --- /dev/null +++ b/signatures/tdx/pre-release/mrenclave-dbf91a376403a78483473da4c78c170cdb30daf62ed7269b692c3f25d86d833f.json @@ -0,0 +1,7 @@ +{ + "mrenclave": "dbf91a376403a78483473da4c78c170cdb30daf62ed7269b692c3f25d86d833f", + "signature": "POPSfX3b6mbWSo7bekIG/hSqAX/5kK0BskADHh/7Sro6E4PO4mSum7CL9xhdv2V/mpmp7zu1zjlT/wpBvi8VnAhpTHBysm7XITS2vofvwo192ow/IQJAXRlNmammObVD+tRuLaBIG6jjLii0acaM2ukKKFC+Lv8mIVT3MV5vsrmxK6ojZhCX8hMM9hB1D0aq4t/SbJcMNNh/q2aKiVbaVFb7czJkjYUM/1DjtzjTzZn1DMeOXLb8ZSaoMKgatLKRrN4iRmg6hI7gXXhcL2iR0IqjuDEZAoEBq88eS83fJWCia/VjoKPB3jHue8ikD3EG4YaZADEp77xv/RY4IgM2Mq1vwuId6qsONS6uEO9BGOtc79gwZ9a9Au+CMoWUDw/nEGT7NRsfmKxNNHMaKzLUwixLu1HAC+/XZkAbRNCAI9j7KPX5zWvnXUBmfqXBrMeZEK6kmBJn38buM0hkkULtlOVTxyuWEnKqfGU5wOXnOyAemfGc2u+vAXWQ22dsgb4/", + "build": "build-286", + "description": "argo_branch=main argo_sp_env=develop sp-debug=true", + "creationDate": "2026-01-23T09:39:01.919Z" +} diff --git a/src/Dockerfile b/src/Dockerfile index a886c489..97571e90 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -99,37 +99,6 @@ ADD initramfs/files/init.sh /initramfs-root/sbin/init.sh RUN gen_init_cpio /initramfs-root/initramfs.list | gzip -9 -n > /initramfs-root/initramfs.cpio.gz ### End ### -### Start certs ### -FROM ubuntu:24.04 AS certs_builder -RUN apt update && apt install -y openssl - -WORKDIR /buildroot -ARG SUPER_REGISTRY_HOST=registry.superprotocol.local -RUN openssl genrsa \ - -out "/buildroot/${SUPER_REGISTRY_HOST}.ca.key" 2048; -RUN openssl req -x509 -new -nodes \ - -key "/buildroot/${SUPER_REGISTRY_HOST}.ca.key" \ - -sha256 -days 3650 \ - -out "/buildroot/${SUPER_REGISTRY_HOST}.ca.crt" \ - -subj "/ST=Milk Galaxy/L=Planet Earth/O=SuperProtocol/OU=MyUnit/CN=SuperProtocol.com" -RUN openssl genrsa \ - -out "/buildroot/${SUPER_REGISTRY_HOST}.key" 2048; -RUN printf "[req]\ndefault_bits = 2048\nprompt = no\ndistinguished_name = req_distinguished_name\nreq_extensions = req_ext\n[req_distinguished_name]\nC = US\nST = Milk Galaxy\nL = Planet Earth\nO = SuperProtocol\nOU = MyUnit\nCN = ${SUPER_REGISTRY_HOST}\n[req_ext]\nsubjectAltName = @alt_names\n[alt_names]\nDNS.1 = ${SUPER_REGISTRY_HOST}\n[v3_ext]\nsubjectAltName = @alt_names\nbasicConstraints = CA:FALSE\nkeyUsage = digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment\n" > "/buildroot/san.cnf" -RUN openssl req -new \ - -key "/buildroot/${SUPER_REGISTRY_HOST}.key" \ - -out "/buildroot/${SUPER_REGISTRY_HOST}.csr" \ - -config /buildroot/san.cnf -RUN openssl x509 -req \ - -in "/buildroot/${SUPER_REGISTRY_HOST}.csr" \ - -CA "/buildroot/${SUPER_REGISTRY_HOST}.ca.crt" \ - -CAkey "/buildroot/${SUPER_REGISTRY_HOST}.ca.key" \ - -CAcreateserial \ - -out "/buildroot/${SUPER_REGISTRY_HOST}.crt" \ - -days 3650 -sha256 \ - -extfile "/buildroot/san.cnf" \ - -extensions v3_ext -### End certs ### - ### Start kernel ### FROM ubuntu:24.04 AS kernel_builder RUN apt-get update && apt-get install -y wget gcc make build-essential curl libssl-dev bc elfutils libelf-dev bison flex cpio kmod rsync debhelper @@ -152,13 +121,6 @@ ADD kernel/files/scripts/build-kernel.sh /buildroot/files/scripts/ RUN /buildroot/files/scripts/build-kernel.sh ### End kernel ### -### Swarm DB start -FROM golang:1.25.3 AS swarm-db-build -COPY repos/swarm-db /app/swarm/swarm-db -WORKDIR /app/swarm/swarm-db -RUN make -RUN make build-linux-amd64 -### Swarm DB finish ### Start rootfs ### FROM ubuntu:noble-20250714 AS rootfs_builder @@ -186,13 +148,6 @@ RUN --security=insecure /buildroot/files/scripts/install_nvidia.sh ADD rootfs/files/scripts/install_gve.sh /buildroot/files/scripts/install_gve.sh RUN --security=insecure /buildroot/files/scripts/install_gve.sh -# RKE2: clean default installation inside rootfs (no legacy scripts) -ARG RKE2_VERSION=v1.32.8+rke2r1 -ARG RKE2_INSTALL_SHA256=2d24db2184dd6b1a5e281fa45cc9a8234c889394721746f89b5fe953fdaaf40a -ENV INSTALL_RKE2_VERSION=${RKE2_VERSION} -ENV RKE2_INSTALL_SHA256=${RKE2_INSTALL_SHA256} -ADD rootfs/files/scripts/install_rke2.sh /buildroot/files/scripts/install_rke2.sh -RUN --security=insecure /buildroot/files/scripts/install_rke2.sh RUN mkdir -p "${OUTPUTDIR}/etc/super/var/lib/rancher/rke2/server/manifests" ADD rootfs/files/configs/etc/super/var/lib/rancher/rke2/server/manifests/k8s-swarm.yaml \ @@ -222,26 +177,10 @@ ADD rootfs/files/configs/tdx-attest.conf ${OUTPUTDIR}/etc/ ADD rootfs/files/configs/chrony/chrony.conf ${OUTPUTDIR}/etc/chrony/ ADD rootfs/files/configs/chrony/chrony.service ${OUTPUTDIR}/lib/systemd/system/ -RUN sed -i '1 s|^.*$|-:root:ALL|' "${OUTPUTDIR}/etc/security/access.conf" -RUN sed -i '1 s|^.*$|account required pam_access.so|' "${OUTPUTDIR}/etc/pam.d/login" +# RUN sed -i '1 s|^.*$|-:root:ALL|' "${OUTPUTDIR}/etc/security/access.conf" +# RUN sed -i '1 s|^.*$|account required pam_access.so|' "${OUTPUTDIR}/etc/pam.d/login" ADD rootfs/files/configs/nvidia-persistenced.service ${OUTPUTDIR}/usr/lib/systemd/system/ -RUN mkdir -p "${OUTPUTDIR}/etc/super/certs" -COPY --from=certs_builder /buildroot/registry.superprotocol.local.ca.crt ${OUTPUTDIR}/usr/local/share/ca-certificates/registry.superprotocol.local.ca.crt -COPY --from=certs_builder /buildroot/registry.superprotocol.local.ca.crt ${OUTPUTDIR}/etc/super/certs/registry.superprotocol.local.ca.crt -COPY --from=certs_builder /buildroot/registry.superprotocol.local.key ${OUTPUTDIR}/etc/super/certs/registry.superprotocol.local.key -COPY --from=certs_builder /buildroot/registry.superprotocol.local.crt ${OUTPUTDIR}/etc/super/certs/registry.superprotocol.local.crt -# ADD rootfs/files/configs/cert/superprotocol-certs.sh ${OUTPUTDIR}/usr/local/bin/ -# ADD rootfs/files/configs/cert/superprotocol-certs.service ${OUTPUTDIR}/etc/systemd/system -# RUN ln -s /etc/systemd/system/superprotocol-certs.service ${OUTPUTDIR}/etc/systemd/system/multi-user.target.wants/superprotocol-certs.service -ADD rootfs/files/scripts/refresh_ca_certs.sh /buildroot/files/scripts/refresh_ca_certs.sh -RUN --security=insecure /buildroot/files/scripts/refresh_ca_certs.sh - -# check for presence of trusted self-signed CA from Super Protocol -RUN awk -v cmd='openssl x509 -noout -subject' '/BEGIN/{close(cmd)};{print | cmd}' < ${OUTPUTDIR}/etc/ssl/certs/ca-certificates.crt | grep -i 'super' - -ARG SUPER_REGISTRY_HOST=registry.superprotocol.local -RUN echo "127.0.0.1 $SUPER_REGISTRY_HOST $LOCAL_REGISTRY_HOST" >> "${OUTPUTDIR}/etc/hosts" RUN echo "sp-$(petname)" > "${OUTPUTDIR}/etc/hostname" ADD rootfs/files/configs/etc/sysctl.d/99-zzz-override_cilium.conf ${OUTPUTDIR}/etc/sysctl.d/99-zzz-override_cilium.conf ADD rootfs/files/configs/etc/resolv.conf ${OUTPUTDIR}/etc/resolv.conf @@ -272,10 +211,6 @@ ADD rootfs/files/configs/etc/multipath.conf.append /buildroot/files/configs/etc/ ADD rootfs/files/configs/etc/sysctl.conf.append /buildroot/files/configs/etc/sysctl.conf.append RUN mkdir -p "${OUTPUTDIR}/sp" -# Enable only the timer; service will be triggered by it -ADD rootfs/files/configs/etc/systemd/system/local-registry.service ${OUTPUTDIR}/etc/systemd/system/local-registry.service -ADD rootfs/files/configs/usr/local/bin/local-registry.sh ${OUTPUTDIR}/usr/local/bin/local-registry.sh -RUN ln -sf /etc/systemd/system/local-registry.service "${OUTPUTDIR}/etc/systemd/system/multi-user.target.wants/local-registry.service" ADD rootfs/files/configs/etc/systemd/system/hardening-vm.service ${OUTPUTDIR}/etc/systemd/system/hardening-vm.service ADD rootfs/files/configs/usr/local/bin/hardening-vm.sh ${OUTPUTDIR}/usr/local/bin/hardening-vm.sh RUN ln -sf /etc/systemd/system/hardening-vm.service "${OUTPUTDIR}/etc/systemd/system/multi-user.target.wants/hardening-vm.service" @@ -285,19 +220,33 @@ ADD rootfs/files/configs/etc/systemd/system/swarm-db.service ${OUTPUTDIR}/etc/sy RUN ln -sf /etc/systemd/system/swarm-db.service "${OUTPUTDIR}/etc/systemd/system/multi-user.target.wants/swarm-db.service" ADD rootfs/files/configs/usr/local/bin/swarm-cloud-api.sh ${OUTPUTDIR}/usr/local/bin/swarm-cloud-api.sh ADD rootfs/files/configs/etc/systemd/system/swarm-node.service ${OUTPUTDIR}/etc/systemd/system/swarm-node.service -ADD rootfs/files/configs/usr/local/bin/swarm-node.sh ${OUTPUTDIR}/usr/local/bin/swarm-node.sh RUN ln -sf /etc/systemd/system/swarm-node.service "${OUTPUTDIR}/etc/systemd/system/multi-user.target.wants/swarm-node.service" -RUN chmod +x ${OUTPUTDIR}/usr/local/bin/swarm-cloud-api.sh ${OUTPUTDIR}/usr/local/bin/swarm-node.sh +RUN chmod +x ${OUTPUTDIR}/usr/local/bin/swarm-cloud-api.sh # run-state directories are prepared in state_disk_mount.sh; bind mounts via fstab ADD rootfs/files/configs/etc/securetty "${OUTPUTDIR}/etc/securetty" +# swarm-init: downloads binaries and seeds swarm-db (restarts until MySQL is available) +# generate-swarm-db-config: generates /etc/swarm-db/config.yaml at swarm-db start (ExecStartPre) +# configuration is read from /sp/swarm/config.yaml (attached via provider config disk at boot) +RUN mkdir -p "${OUTPUTDIR}/etc/swarm" "${OUTPUTDIR}/etc/swarm-db" "${OUTPUTDIR}/etc/swarm-node" +ADD rootfs/files/configs/etc/swarm-node/config.yaml ${OUTPUTDIR}/etc/swarm-node/config.yaml +ADD rootfs/files/configs/usr/local/bin/swarm-init.sh ${OUTPUTDIR}/usr/local/bin/swarm-init.sh +ADD rootfs/files/configs/usr/local/bin/generate-swarm-db-config.sh ${OUTPUTDIR}/usr/local/bin/generate-swarm-db-config.sh +RUN chmod +x ${OUTPUTDIR}/usr/local/bin/swarm-init.sh \ + ${OUTPUTDIR}/usr/local/bin/generate-swarm-db-config.sh +ADD rootfs/files/configs/etc/systemd/system/swarm-init.service ${OUTPUTDIR}/etc/systemd/system/swarm-init.service +RUN ln -sf /etc/systemd/system/swarm-init.service "${OUTPUTDIR}/etc/systemd/system/multi-user.target.wants/swarm-init.service" + +# swarm-host-agent: placeholder service file (binary + real service installed by swarm-init at boot) +COPY rootfs/files/configs/etc/systemd/system/swarm-host-agent.service ${OUTPUTDIR}/etc/systemd/system/swarm-host-agent.service + +RUN mkdir -p "${OUTPUTDIR}/etc/swarm-service-launchers" +COPY swarm-scripts ${OUTPUTDIR}/etc/swarm-service-launchers/ + # swarm one-shot services runner ADD rootfs/files/configs/etc/systemd/system/swarm-services.service ${OUTPUTDIR}/etc/systemd/system/swarm-services.service ADD rootfs/files/configs/usr/local/bin/swarm-services.sh ${OUTPUTDIR}/usr/local/bin/swarm-services.sh RUN chmod +x ${OUTPUTDIR}/usr/local/bin/swarm-services.sh -RUN ln -sf /etc/systemd/system/swarm-services.service "${OUTPUTDIR}/etc/systemd/system/multi-user.target.wants/swarm-services.service" -ADD rootfs/files/configs/etc/systemd/system/download-sp-swarm-services.service ${OUTPUTDIR}/etc/systemd/system/download-sp-swarm-services.service -RUN ln -sf /etc/systemd/system/download-sp-swarm-services.service "${OUTPUTDIR}/etc/systemd/system/multi-user.target.wants/download-sp-swarm-services.service" # disabling serial getty ADD rootfs/files/configs/usr/lib/systemd/system/serial-getty@.service "${OUTPUTDIR}/usr/lib/systemd/system/serial-getty@.service" @@ -305,35 +254,15 @@ RUN ln -sf /dev/null "${OUTPUTDIR}/etc/systemd/system/getty@ttyS0.service" # swarm binaries into VM rootfs RUN mkdir -p "${OUTPUTDIR}/usr/local/lib/swarm-cloud" "${OUTPUTDIR}/usr/local/bin" -COPY --from=swarm-db-build /app/swarm/swarm-db/swarm-db-linux-amd64 ${OUTPUTDIR}/usr/local/bin/swarm-db-linux-amd64 -COPY repos/swarm-cloud ${OUTPUTDIR}/opt/swarm-cloud -RUN mkdir -p ${OUTPUTDIR}/etc/swarm-cloud -RUN cp -r ${OUTPUTDIR}/opt/swarm-cloud/services ${OUTPUTDIR}/etc/swarm-cloud/services -RUN cp -r ${OUTPUTDIR}/opt/swarm-cloud/provision-plugin-sdk ${OUTPUTDIR}/etc/swarm-cloud/provision-plugin-sdk -RUN chmod +x ${OUTPUTDIR}/usr/local/bin/swarm-db-linux-amd64 +ADD rootfs/files/configs/usr/local/bin/swarm-cloud-ui.sh ${OUTPUTDIR}/usr/local/bin/swarm-cloud-ui.sh +RUN chmod +x ${OUTPUTDIR}/usr/local/bin/swarm-cloud-ui.sh ADD rootfs/files/configs/usr/local/bin/kubectl ${OUTPUTDIR}/usr/local/bin/kubectl RUN chmod +x ${OUTPUTDIR}/usr/local/bin/kubectl -RUN mkdir -p ${OUTPUTDIR}/etc/swarm-db -# COPY repos/swarm-db/schema.yaml ${OUTPUTDIR}/etc/swarm-db/schema.yaml +# swarm-db binary is downloaded at runtime by swarm-init (tags.swarm_db in /sp/swarm/config.yaml) +# provision plugins at /etc/swarm-cloud/services are mounted into the swarm-node container at runtime +RUN mkdir -p "${OUTPUTDIR}/etc/swarm-db" "${OUTPUTDIR}/etc/swarm-services" COPY repos/swarm-cloud/apps/swarm-node-e2e/fixtures/schema.yaml ${OUTPUTDIR}/etc/swarm-db/schema.yaml -# install Node.js -ADD rootfs/files/scripts/install_nodejs.sh /buildroot/files/scripts/ -RUN --security=insecure /buildroot/files/scripts/install_nodejs.sh - -# Services Downloader: stage files and install -ADD rootfs/files/configs/usr/local/lib/services-downloader ${OUTPUTDIR}/usr/local/lib/services-downloader -ADD rootfs/files/scripts/install_services_downloader.sh /buildroot/files/scripts/ -RUN chmod +x /buildroot/files/scripts/install_services_downloader.sh -RUN --security=insecure bash /buildroot/files/scripts/install_services_downloader.sh -## no standalone wrapper; script uses Node CLI directly -ADD rootfs/files/configs/usr/local/bin/download-sp-swarm-services.sh ${OUTPUTDIR}/usr/local/bin/download-sp-swarm-services.sh -RUN chmod +x ${OUTPUTDIR}/usr/local/bin/download-sp-swarm-services.sh - -# install pnpm and build Node.js applications inside rootfs via script -ADD rootfs/files/scripts/build_swarm_cloud.sh /buildroot/files/scripts/ -RUN chmod +x /buildroot/files/scripts/build_swarm_cloud.sh -RUN --security=insecure /buildroot/files/scripts/build_swarm_cloud.sh # make /opt/swarm-cloud-api point to built swarm-cloud artifacts RUN mkdir -p ${OUTPUTDIR}/opt && ln -s /usr/local/lib/swarm-cloud-api ${OUTPUTDIR}/opt/swarm-cloud-api @@ -346,6 +275,10 @@ RUN chmod +x \ ${OUTPUTDIR}/usr/local/lib/swarm-cloud-api/swarm-cloud-api \ ${OUTPUTDIR}/usr/local/lib/swarm-cloud-api/schema-sync +# install Node.js (required by PCCS npm install and swarm-cloud-api) +ADD rootfs/files/scripts/install_nodejs.sh /buildroot/files/scripts/ +RUN --security=insecure bash /buildroot/files/scripts/install_nodejs.sh + # install PCCS ADD rootfs/files/scripts/install_pccs.sh /buildroot/files/scripts/ ADD rootfs/files/configs/pccs-init/ /buildroot/files/configs/pccs-init/ @@ -357,68 +290,32 @@ RUN ln -s /usr/lib/systemd/system/pccs-init.service "${OUTPUTDIR}/etc/systemd/sy # Custom swarm services RUN mkdir -p ${OUTPUTDIR}/etc/swarm-services/ -COPY services/apps/ ${OUTPUTDIR}/etc/swarm-services/ -COPY swarm-scripts ${OUTPUTDIR}/etc/swarm-service-launchers/ -# provision plugins from original swarm-cloud repo -# COPY repos/swarm-cloud/services/swarm-cloud-api ${OUTPUTDIR}/etc/swarm-services/swarm-cloud-api -RUN chmod +x ${OUTPUTDIR}/etc/swarm-services/*/main.py -RUN chmod +x ${OUTPUTDIR}/etc/swarm-cloud/services/*/main.py # tools needed at runtime (prevent daemons from starting in chroot) and runtime setup ADD rootfs/files/scripts/setup_runtime_tools.sh /buildroot/files/scripts/ RUN chmod +x /buildroot/files/scripts/setup_runtime_tools.sh RUN --security=insecure /buildroot/files/scripts/setup_runtime_tools.sh -# MongoDB (install official mongodb-org 7.0 via Jammy repository inside VM rootfs) -ADD rootfs/files/scripts/install_mongodb.sh /buildroot/files/scripts/ -RUN --security=insecure bash /buildroot/files/scripts/install_mongodb.sh -# disable autostart without requiring systemd during build (remove enable symlinks for MongoDB) -RUN rm -f ${OUTPUTDIR}/etc/systemd/system/multi-user.target.wants/mongod.service \ - && rm -f ${OUTPUTDIR}/etc/systemd/system/default.target.wants/mongod.service \ - && rm -f ${OUTPUTDIR}/etc/systemd/system/multi-user.target.wants/mongodb.service \ - && rm -f ${OUTPUTDIR}/etc/systemd/system/default.target.wants/mongodb.service -# NATS (install nats-server 2.12.2 into VM rootfs) -ADD rootfs/files/scripts/install_nats.sh /buildroot/files/scripts/ -RUN --security=insecure bash /buildroot/files/scripts/install_nats.sh -# disable autostart without requiring systemd during build (ensure no enable symlinks for nats) -RUN rm -f ${OUTPUTDIR}/etc/systemd/system/multi-user.target.wants/nats-server.service \ - && rm -f ${OUTPUTDIR}/etc/systemd/system/default.target.wants/nats-server.service || true -# CockroachDB (install cockroach binary into VM rootfs) -ADD rootfs/files/scripts/install_cockroachdb.sh /buildroot/files/scripts/ -RUN --security=insecure bash /buildroot/files/scripts/install_cockroachdb.sh -# Knot DNS (install knot into VM rootfs) -ADD rootfs/files/scripts/install_knot.sh /buildroot/files/scripts/ -RUN --security=insecure bash /buildroot/files/scripts/install_knot.sh -# disable autostart without requiring systemd during build (ensure no enable symlinks for knot) -RUN rm -f ${OUTPUTDIR}/etc/systemd/system/multi-user.target.wants/knot.service \ - && rm -f ${OUTPUTDIR}/etc/systemd/system/default.target.wants/knot.service || true -# OpenResty (install into VM rootfs so provisioner only configures and manages it) -ADD rootfs/files/scripts/install_openresty.sh /buildroot/files/scripts/ -RUN --security=insecure bash /buildroot/files/scripts/install_openresty.sh -# disable autostart without requiring systemd during build (ensure no enable symlinks for openresty) -RUN rm -f ${OUTPUTDIR}/etc/systemd/system/multi-user.target.wants/openresty.service \ - && rm -f ${OUTPUTDIR}/etc/systemd/system/default.target.wants/openresty.service || true -RUN mkdir -p ${OUTPUTDIR}/etc/resty-auto-ssl/storage \ - && chown -R www-data:www-data ${OUTPUTDIR}/etc/resty-auto-ssl -# disable autostart without requiring systemd during build (remove enable symlinks) -RUN rm -f ${OUTPUTDIR}/etc/systemd/system/multi-user.target.wants/redis-server.service \ - && rm -f ${OUTPUTDIR}/etc/systemd/system/default.target.wants/redis-server.service \ - && rm -f ${OUTPUTDIR}/etc/systemd/system/multi-user.target.wants/redis-sentinel.service \ - && rm -f ${OUTPUTDIR}/etc/systemd/system/default.target.wants/redis-sentinel.service -# ensure swarm-cloud-api, cockroachdb and wireguard are disabled by default (no systemd enable symlinks) -RUN rm -f ${OUTPUTDIR}/etc/systemd/system/multi-user.target.wants/cockroachdb.service \ - && rm -f ${OUTPUTDIR}/etc/systemd/system/multi-user.target.wants/wg-quick@wg0.service -# cleanup apt lists and policy-rc.d -RUN rm -f ${OUTPUTDIR}/usr/sbin/policy-rc.d \ - && rm -rf ${OUTPUTDIR}/var/lib/apt/lists/* -ADD rootfs/files/scripts/install_provision_plugin_sdk.sh /buildroot/files/scripts/ -RUN --security=insecure bash /buildroot/files/scripts/install_provision_plugin_sdk.sh + +# Python dependencies required by provision plugins (redis-py for rke2/redis plugins, podman-compose for container plugins) +ADD rootfs/files/scripts/install_python_deps.sh /buildroot/files/scripts/ +RUN --security=insecure bash /buildroot/files/scripts/install_python_deps.sh + +# Extra system packages required by provision plugins at runtime +# mysql-client: bootstrap-services.sh and provision plugins use mysql CLI to talk to SwarmDB +# unzip: download-services.sh extracts service archives +# netcat-openbsd: provision plugins use nc for port readiness checks (e.g. cockroachdb plugin) +# dnsutils: DNS tools (nsupdate) used by rke2 plugin for Knot DNS updates +ADD rootfs/files/scripts/install_extra_packages.sh /buildroot/files/scripts/ +RUN --security=insecure bash /buildroot/files/scripts/install_extra_packages.sh # Build info ARG SP_VM_IMAGE_VERSION RUN bash -c '[[ -n "$SP_VM_IMAGE_VERSION" ]] && echo "$SP_VM_IMAGE_VERSION" > "${OUTPUTDIR}/etc/sp-release"' # after all! +# cleanup apt lists and policy-rc.d +RUN rm -f ${OUTPUTDIR}/usr/sbin/policy-rc.d ADD rootfs/files/scripts/cleanup_rootfs.sh /buildroot/files/scripts/ RUN --security=insecure /buildroot/files/scripts/cleanup_rootfs.sh ### End rootfs ### diff --git a/src/repos/swarm-cloud b/src/repos/swarm-cloud index 25ce9fda..f02d46ca 160000 --- a/src/repos/swarm-cloud +++ b/src/repos/swarm-cloud @@ -1 +1 @@ -Subproject commit 25ce9fda182b1a148fe96a6d57573b807aea986a +Subproject commit f02d46ca579a5f7b07282b914c3ac2dd0b9d8ceb diff --git a/src/repos/swarm-db b/src/repos/swarm-db deleted file mode 160000 index bc8d0afb..00000000 --- a/src/repos/swarm-db +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bc8d0afbb78ac2153443677e7fdf4969ae29f119 diff --git a/src/rootfs/files/configs/etc/resolv.conf b/src/rootfs/files/configs/etc/resolv.conf index 861a2b7e..16ef5206 100644 --- a/src/rootfs/files/configs/etc/resolv.conf +++ b/src/rootfs/files/configs/etc/resolv.conf @@ -2,4 +2,3 @@ nameserver 127.0.0.53 nameserver 1.1.1.1 nameserver 8.8.8.8 options edns0 trust-ad -search diff --git a/src/rootfs/files/configs/etc/swarm-node/config.yaml b/src/rootfs/files/configs/etc/swarm-node/config.yaml new file mode 100644 index 00000000..a132b1ee --- /dev/null +++ b/src/rootfs/files/configs/etc/swarm-node/config.yaml @@ -0,0 +1,27 @@ +port: 4001 +host: "0.0.0.0" + +db: + host: "127.0.0.1" + port: 3306 + username: "root" + password: "" + database: "swarmdb" + synchronize: false + autoLoadEntities: true + +leaderElection: + enabled: true + leaseMs: 60000 + guardMs: 10000 + renewGuardMs: 30000 + propagationGuardMs: 5000 + electionIntervalMs: 1000 + leaderScore: 1.0 + graceDownMs: 5000 + +provision: + enabled: true + swarmDbApiUrl: "http://127.0.0.1:8080" + servicesDir: "/etc/swarm-services" + localDbPath: "/var/lib/swarm-node/provision.db" diff --git a/src/rootfs/files/configs/etc/systemd/system/download-sp-swarm-services.service b/src/rootfs/files/configs/etc/systemd/system/download-sp-swarm-services.service deleted file mode 100644 index aecc0eda..00000000 --- a/src/rootfs/files/configs/etc/systemd/system/download-sp-swarm-services.service +++ /dev/null @@ -1,18 +0,0 @@ -[Unit] -Description=Download and stage Swarm services pack (run once) -After=network-online.target -Wants=network-online.target -ConditionPathExists=!/etc/sp-swarm-services/.downloaded - -[Service] -Type=oneshot -User=root -ExecStart=/usr/local/bin/download-sp-swarm-services.sh -RemainAfterExit=yes -Restart=on-failure -StandardOutput=append:/var/log/download-sp-swarm-services.log -StandardError=append:/var/log/download-sp-swarm-services-err.log -RestartSec=5min - -[Install] -WantedBy=multi-user.target diff --git a/src/rootfs/files/configs/etc/systemd/system/local-registry.service b/src/rootfs/files/configs/etc/systemd/system/local-registry.service deleted file mode 100644 index b3a2f3d3..00000000 --- a/src/rootfs/files/configs/etc/systemd/system/local-registry.service +++ /dev/null @@ -1,13 +0,0 @@ -[Unit] -Description=Run local registry & fileserver by using Hauler -After=network.target -Before=rke2-server.service - -[Service] -Type=simple -ExecStart=/usr/local/bin/local-registry.sh -Restart=always -RestartSec=5 - -[Install] -WantedBy=multi-user.target diff --git a/src/rootfs/files/configs/etc/systemd/system/swarm-cloud-api.service b/src/rootfs/files/configs/etc/systemd/system/swarm-cloud-api.service deleted file mode 100644 index 412fe3a5..00000000 --- a/src/rootfs/files/configs/etc/systemd/system/swarm-cloud-api.service +++ /dev/null @@ -1,18 +0,0 @@ -[Unit] -Description=Swarm Cloud API service -After=network-online.target swarm-db.service -Wants=network-online.target -Requires=swarm-db.service - -[Service] -Type=simple -WorkingDirectory=/usr/local/lib/swarm-cloud -ExecStart=/usr/local/bin/swarm-cloud-api.sh -StandardOutput=append:/var/log/swarm-cloud-api.log -StandardError=append:/var/log/swarm-cloud-api-err.log -Restart=always -RestartSec=5 -Environment=NODE_ENV=production - -[Install] -WantedBy=multi-user.target diff --git a/src/rootfs/files/configs/etc/systemd/system/swarm-db.service b/src/rootfs/files/configs/etc/systemd/system/swarm-db.service index 5a621707..80b4edf6 100644 --- a/src/rootfs/files/configs/etc/systemd/system/swarm-db.service +++ b/src/rootfs/files/configs/etc/systemd/system/swarm-db.service @@ -1,16 +1,15 @@ [Unit] Description=Swarm DB service -After=network-online.target local-fs.target -Wants=network-online.target -RequiresMountsFor=/var /var/lib /var/lib/swarm-db +After=network-online.target local-fs.target swarm-init.service +Wants=network-online.target swarm-init.service swarm-services.service ConditionPathExists=/usr/local/bin/swarm-db-linux-amd64 -ConditionPathExists=/sp/swarm/node-db.yaml [Service] Type=simple WorkingDirectory=/ ExecStartPre=mkdir -p /var/lib/swarm-db/data -ExecStart=/usr/local/bin/swarm-db-linux-amd64 -config /sp/swarm/node-db.yaml +ExecStartPre=/bin/bash /usr/local/bin/generate-swarm-db-config.sh +ExecStart=/usr/local/bin/swarm-db-linux-amd64 -config /etc/swarm-db/config.yaml StandardOutput=append:/var/log/swarm-db.log StandardError=append:/var/log/swarm-db-err.log Restart=always diff --git a/src/rootfs/files/configs/etc/systemd/system/swarm-host-agent.service b/src/rootfs/files/configs/etc/systemd/system/swarm-host-agent.service new file mode 100644 index 00000000..445f12de --- /dev/null +++ b/src/rootfs/files/configs/etc/systemd/system/swarm-host-agent.service @@ -0,0 +1,16 @@ +[Unit] +Description=Swarm Host Agent +After=network-online.target swarm-init.service +Requires=swarm-init.service + +[Service] +Type=simple +EnvironmentFile=/etc/swarm/swarm-host-agent.env +ExecStart=/usr/local/bin/swarm-host-agent +Restart=always +RestartSec=5 +StandardOutput=append:/var/log/swarm-host-agent.log +StandardError=append:/var/log/swarm-host-agent.log + +[Install] +WantedBy=multi-user.target diff --git a/src/rootfs/files/configs/etc/systemd/system/swarm-init.service b/src/rootfs/files/configs/etc/systemd/system/swarm-init.service new file mode 100644 index 00000000..ef368dec --- /dev/null +++ b/src/rootfs/files/configs/etc/systemd/system/swarm-init.service @@ -0,0 +1,15 @@ +[Unit] +Description=Swarm Initialization (download binaries, configure services, seed swarm-db) +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=/bin/bash /usr/local/bin/swarm-init.sh +Restart=on-failure +RestartSec=10 +StandardOutput=append:/var/log/swarm-init.log +StandardError=append:/var/log/swarm-init.log + +[Install] +WantedBy=multi-user.target diff --git a/src/rootfs/files/configs/etc/systemd/system/swarm-node.service b/src/rootfs/files/configs/etc/systemd/system/swarm-node.service index 36bb9d83..bb1c58a4 100644 --- a/src/rootfs/files/configs/etc/systemd/system/swarm-node.service +++ b/src/rootfs/files/configs/etc/systemd/system/swarm-node.service @@ -1,19 +1,39 @@ [Unit] -Description=Swarm Node service -After=network-online.target swarm-db.service +Description=Swarm Node Service (Podman Container) +After=network-online.target swarm-db.service swarm-host-agent.service Wants=network-online.target -Requires=swarm-db.service +Requires=swarm-db.service swarm-host-agent.service [Service] Type=simple -WorkingDirectory=/usr/local/lib/swarm-cloud -ExecStartPre=mkdir -p /var/lib/swarm-cloud-api/data -ExecStart=/usr/local/bin/swarm-node.sh +EnvironmentFile=-/etc/swarm/swarm-node.env +ExecStartPre=-/usr/bin/podman stop swarm-node +ExecStartPre=-/usr/bin/podman rm swarm-node +ExecStartPre=mkdir -p /var/lib/swarm-node +ExecStartPre=chown -R 1001:1001 /var/lib/swarm-node +ExecStart=/bin/bash -c '\ + test -n "${SWARM_NODE_TAG}" || { echo "SWARM_NODE_TAG not set, waiting for swarm-init"; exit 1; }; \ + exec /usr/bin/podman run \ + --name swarm-node \ + --rm \ + --network host \ + -v /etc/swarm-node:/etc/swarm-node:ro \ + -v /etc/swarm-services:/etc/swarm-services:ro \ + -v /var/lib/swarm-node:/var/lib/swarm-node \ + -v /var/run/swarm-agent.sock:/var/run/swarm-agent.sock \ + -e NODE_ENV=production \ + -e SWC_NODE_CONFIG_PATH=/etc/swarm-node/config.yaml \ + -e SWARM_CLOUD_API_TAG="${SWARM_CLOUD_API_TAG}" \ + -e SWARM_CLOUD_UI_TAG="${SWARM_CLOUD_UI_TAG}" \ + -e AUTH_SERVICE_TAG="${AUTH_SERVICE_TAG}" \ + -e SWARM_HOST_AGENT_SOCKET=/var/run/swarm-agent.sock \ + "ghcr.io/super-protocol/swarm-cloud/swarm-node:${SWARM_NODE_TAG}" \ + apps/swarm-node/dist/main.js' +ExecStop=/usr/bin/podman stop swarm-node +Restart=always +RestartSec=10 StandardOutput=append:/var/log/swarm-node-api.log StandardError=append:/var/log/swarm-node-api-err.log -Restart=always -RestartSec=5 -Environment=NODE_ENV=production SWC_NODE_CONFIG_PATH=/sp/swarm/api.yaml [Install] WantedBy=multi-user.target diff --git a/src/rootfs/files/configs/etc/systemd/system/swarm-services.service b/src/rootfs/files/configs/etc/systemd/system/swarm-services.service index 456995d2..9382e712 100644 --- a/src/rootfs/files/configs/etc/systemd/system/swarm-services.service +++ b/src/rootfs/files/configs/etc/systemd/system/swarm-services.service @@ -1,8 +1,8 @@ [Unit] Description=Run Swarm setup scripts from /etc/swarm-service-launchers -After=network-online.target swarm-db.service swarm-node.service download-sp-swarm-services.service +After=network-online.target swarm-db.service swarm-init.service Wants=network-online.target -Requires=swarm-db.service swarm-node.service +Requires=swarm-db.service swarm-init.service ConditionPathExists=/etc/swarm-service-launchers [Service] diff --git a/src/rootfs/files/configs/sp/swarm/config.yaml b/src/rootfs/files/configs/sp/swarm/config.yaml new file mode 100644 index 00000000..aa0f410b --- /dev/null +++ b/src/rootfs/files/configs/sp/swarm/config.yaml @@ -0,0 +1,21 @@ +github: + token: "" # GitHub personal access token (required for private repos and ghcr.io) + +tags: + swarm_db: "" # e.g. "v0.1.0" — downloads and replaces built-in binary; empty = use built-in + host_agent: "" # e.g. "host-agent-v1.0.0" — required; downloads binary + service + config + swarm_node: "" # e.g. "v1.2.3" — Docker image tag for ghcr.io/.../swarm-node + sdk: "" # e.g. "v1.2.3" — downloads and replaces built-in SDK; empty = use built-in + services: "" # e.g. "v1.2.3" — downloads all service .zip archives into /etc/swarm-services + swarm_cloud_api: "" # passed as env var to swarm-node container + swarm_cloud_ui: "" # passed as env var to swarm-node container + auth_service: "" # passed as env var to swarm-node container + +swarm_db: + node_name: "" # defaults to hostname + advertise_addr: "" # defaults to auto-detected external IP + join_addresses: [] # e.g. ["192.168.1.2:7946", "192.168.1.3:7946"] + +powerdns_api_url: "" # e.g. "http://ns1.example.com:8081" +powerdns_api_key: "" # PowerDNS API key +base_domain: "" # e.g. "example.com" diff --git a/src/rootfs/files/configs/usr/local/bin/download-sp-swarm-services.sh b/src/rootfs/files/configs/usr/local/bin/download-sp-swarm-services.sh deleted file mode 100644 index f0d1d8e8..00000000 --- a/src/rootfs/files/configs/usr/local/bin/download-sp-swarm-services.sh +++ /dev/null @@ -1,212 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# Download and stage Swarm service pack into /etc/sp-swarm-services. -# - If /sp/swarm/gatekeeper-keys.yaml exists, extract TLS key/cert to -# /etc/super/certs/gatekeeper.key and /etc/super/certs/gatekeeper.crt -# - Read BRANCH from sp-swarm-services.yaml (fallback to $BRANCH_NAME or "main") -# - Invoke Node CLI to fetch resource "sp-swarm-services" and --unpack -# - Merge content of /etc/sp-swarm-services/swarm-service-pluggins/ into /etc - -YAML_PATH="${YAML_PATH:-/sp/swarm/gatekeeper-keys.yaml}" # for cert extraction -SP_SWARM_SERVICES_YAML_PATH="${SP_SWARM_SERVICES_YAML_PATH:-/sp/swarm/sp-swarm-services.yaml}" -SSL_CERT_PATH="${SSL_CERT_PATH:-/etc/super/certs/gatekeeper.crt}" -SSL_KEY_PATH="${SSL_KEY_PATH:-/etc/super/certs/gatekeeper.key}" -GK_ENV="${GATEKEEPER_ENV:-mainnet}" -TARGET_DIR="${TARGET_DIR:-/etc/sp-swarm-services}" -RESOURCE_NAME="sp-swarm-services" - -log() { - local ts - ts="$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || date '+%Y-%m-%dT%H:%M:%S%z')" - printf "[%s] [download-sp-swarm-services] %s\n" "$ts" "$*" >&2; -} - -is_cloud_mode() { - # Cloud images are built with SP_VM_IMAGE_VERSION like: cloud-build- - if [[ -f /etc/sp-release ]] && grep -q "cloud-build" /etc/sp-release 2>/dev/null; then - return 0 - fi - # Also exposed via kernel cmdline as: build= - if [[ -r /proc/cmdline ]] && grep -q "build=cloud-build" /proc/cmdline 2>/dev/null; then - return 0 - fi - return 1 -} - -# Helpers: YAML block extraction and PEM normalization -list_top_keys() { - awk 'BEGIN{FS":"} /^[A-Za-z0-9_.-]+:[[:space:]]*/{print $1}' "$YAML_PATH" | sort -u || true -} - -extract_block_from_yaml() { - # $1: key name (e.g., key, cert) - local keyname="$1" - local awk_program - read -r -d '' awk_program <<'AWK' -function ltrim(s){ sub(/^\r?/, "", s); return s } -BEGIN{ inblk=0; found=0 } -{ - sub("\r$", "", $0) - if (inblk==0 && $0 ~ "^" KEY "[[:space:]]*:") { - idx = index($0, ":") - rest = substr($0, idx+1) - gsub(/^[[:space:]]+/, "", rest) - if (rest ~ /^\|[+-]?([[:space:]]*)?$/ || rest ~ /^$/) { - inblk=1; found=1; next - } else { - inline=rest - gsub(/[[:space:]]+$/, "", inline) - if (inline ~ /^".*"$/ || inline ~ /^'.*'$/) { inline=substr(inline,2,length(inline)-2); gsub(/\\n/, "\n", inline) } - print inline; exit - } - } else if (inblk==1) { - if ($0 ~ /^[A-Za-z0-9_.-]+:[[:space:]]*/) { exit } - print $0 - } -} -END { } -AWK - awk -v KEY="$keyname" "$awk_program" "$YAML_PATH" -} - -deindent_block() { - awk ' - { sub("\r$", "", $0); line=$0; match(line, /^[[:space:]]*/); ind=RLENGTH; if (min=="" || ind0) print substr(lines[i], min+1); else print lines[i] } } - ' -} - -trim_to_pem() { - awk ' - BEGIN{begin=0} - { sub("\r$", "", $0); if (begin==0) { if ($0 ~ /^-----BEGIN[[:space:]]/) { begin=1; print $0 } } else { print $0 } } - ' | awk 'NF>0' -} - - -## TODO: temporary solution. Need to use subroot cert and key -ensure_gatekeeper_certs_from_yaml() { - # If outputs already exist, skip - if [[ -f "$SSL_KEY_PATH" && -f "$SSL_CERT_PATH" ]]; then - log "TLS key/cert already present — skipping extraction"; - return 0 - fi - - if [[ ! -f "$YAML_PATH" ]]; then - log "YAML not found: $YAML_PATH — skipping cert extraction"; - return 0 - fi - - install -d "$(dirname "$SSL_CERT_PATH")" - - # Extract raw content for key and cert from YAML (supports block scalar and inline) - local key_content cert_content - key_content="$(extract_block_from_yaml key || true)" - cert_content="$(extract_block_from_yaml cert || true)" - - if [[ -z "${key_content//[[:space:]]/}" ]]; then - log "ERROR: key block not found in $YAML_PATH"; - log "Top-level keys: $(list_top_keys | tr '\n' ' ')"; - return 1 - fi - if [[ -z "${cert_content//[[:space:]]/}" ]]; then - log "ERROR: cert block not found in $YAML_PATH"; - log "Top-level keys: $(list_top_keys | tr '\n' ' ')"; - return 1 - fi - - # Normalize: deindent and trim strictly to PEM BEGIN..END - printf "%s\n" "$key_content" | deindent_block | trim_to_pem > "$SSL_KEY_PATH" - printf "%s\n" "$cert_content" | deindent_block | trim_to_pem > "$SSL_CERT_PATH" - - # Sanity checks - if ! grep -q "^-----BEGIN PRIVATE KEY" "$SSL_KEY_PATH"; then - log "ERROR: key PEM header not found after extraction"; return 1 - fi - if ! grep -q "^-----BEGIN CERTIFICATE" "$SSL_CERT_PATH"; then - log "ERROR: cert PEM header not found after extraction"; return 1 - fi - - # Optional openssl validation - if command -v openssl >/dev/null 2>&1; then - if ! openssl pkey -in "$SSL_KEY_PATH" -noout >/dev/null 2>&1; then - log "ERROR: openssl failed to parse key: $SSL_KEY_PATH"; return 1 - fi - if ! openssl x509 -in "$SSL_CERT_PATH" -noout >/dev/null 2>&1; then - log "ERROR: openssl failed to parse cert: $SSL_CERT_PATH"; return 1 - fi - fi - - chmod 600 "$SSL_KEY_PATH" || true - chmod 644 "$SSL_CERT_PATH" || true - if [[ $(id -u) -eq 0 ]]; then - chown root:root "$SSL_KEY_PATH" "$SSL_CERT_PATH" || true - fi - log "Wrote key to $SSL_KEY_PATH and cert to $SSL_CERT_PATH" -} - -parse_branch_name() { - local branch="" - if [[ -f "$SP_SWARM_SERVICES_YAML_PATH" ]]; then - # Read only 'branch' key (expected in sp-swarm-services.yaml) - branch=$(awk ' - BEGIN{br=""} - /^[[:space:]]*branch[[:space:]]*:/ { sub(/^[[:space:]]*branch[[:space:]]*:[[:space:]]*/, "", $0); br=$0; gsub(/[\r\n\t\f]+/, "", br); print br; exit } - ' "$SP_SWARM_SERVICES_YAML_PATH") - fi - - # Trim quotes and whitespace - branch="${branch//\"/}" - branch="${branch//\'/}" - branch="$(printf "%s" "$branch" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g')" - - if [[ -z "$branch" ]]; then - branch="${BRANCH_NAME:-main}" - log "Branch not found in YAML; using: $branch" - else - log "Using branch from YAML: $branch" - fi - - printf "%s" "$branch" -} - -main() { - # In cloud mode this unit/script must be a no-op. - if is_cloud_mode; then - log "Cloud mode detected — exiting" - exit 0 - fi - - # If sp-swarm-services YAML is missing, do nothing and exit 0 - if [[ ! -f "$SP_SWARM_SERVICES_YAML_PATH" ]]; then - log "sp-swarm-services.yaml not found: $SP_SWARM_SERVICES_YAML_PATH — exiting" - exit 0 - fi - - ensure_gatekeeper_certs_from_yaml || exit 1 - - install -d "$TARGET_DIR" - local branch - branch="$(parse_branch_name)" - - log "Running Node services-downloader for $RESOURCE_NAME (branch=$branch)" - if ! /usr/bin/env node /usr/local/lib/services-downloader/src/index.js \ - --resource-name "$RESOURCE_NAME" \ - --branch-name "$branch" \ - --ssl-cert-path "$SSL_CERT_PATH" \ - --ssl-key-path "$SSL_KEY_PATH" \ - --environment "$GK_ENV" \ - --unpack-with-absolute-path; then - log "ERROR: services-downloader failed"; exit 1 - fi - - # Mark as completed to stop future retries - mkdir -p "$TARGET_DIR" - touch "$TARGET_DIR/.downloaded" - log "Marked as completed: $TARGET_DIR/.downloaded" - - log "Done" -} - -main "$@" diff --git a/src/rootfs/files/configs/usr/local/bin/generate-swarm-db-config.sh b/src/rootfs/files/configs/usr/local/bin/generate-swarm-db-config.sh new file mode 100644 index 00000000..f79b1a47 --- /dev/null +++ b/src/rootfs/files/configs/usr/local/bin/generate-swarm-db-config.sh @@ -0,0 +1,84 @@ +#!/bin/bash +set -euo pipefail + +CONFIG="/sp/swarm/config.yaml" + +log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [generate-swarm-db-config] $*"; } + +cfg() { + python3 -c " +import yaml +c = yaml.safe_load(open('$CONFIG')) or {} +v = c +for k in '$1'.split('.'): + v = v.get(k) if isinstance(v, dict) else None +print('' if v is None else v)" +} + +NODE_NAME=$(cfg "swarm_db.node_name") +ADVERTISE_ADDR=$(cfg "swarm_db.advertise_addr") + +[ -z "$NODE_NAME" ] && NODE_NAME=$(hostname) + +if [ -z "$ADVERTISE_ADDR" ]; then + log "auto-detecting external IP..." + ADVERTISE_ADDR=$(curl -sf --max-time 5 https://myip.wtf/json \ + | python3 -c "import sys,json; print(json.load(sys.stdin).get('YourFuckingIPAddress',''))" 2>/dev/null || true) + [ -z "$ADVERTISE_ADDR" ] && \ + ADVERTISE_ADDR=$(curl -sf --max-time 5 https://api.ipify.org 2>/dev/null || true) + if [ -z "$ADVERTISE_ADDR" ]; then + log "WARNING: could not detect external IP, using 127.0.0.1" + ADVERTISE_ADDR="127.0.0.1" + fi + log "detected advertise_addr: $ADVERTISE_ADDR" +fi + +log "generating /etc/swarm-db/config.yaml (node=$NODE_NAME, advertise=$ADVERTISE_ADDR)..." +mkdir -p /etc/swarm-db /var/lib/swarm-db + +NODE_NAME_VAL="$NODE_NAME" ADVERTISE_ADDR_VAL="$ADVERTISE_ADDR" \ +python3 - << 'PYEOF' +import yaml, os + +with open('/sp/swarm/config.yaml') as f: + swarm_cfg = yaml.safe_load(f) or {} + +join_addresses = (swarm_cfg.get('swarm_db') or {}).get('join_addresses') or [] + +config = { + 'node': { + 'name': os.environ['NODE_NAME_VAL'], + 'host': '0.0.0.0', + 'port': 8001, + 'data_dir': '/var/lib/swarm-db', + 'schema_file': '/etc/swarm-db/schema.yaml', + }, + 'memberlist': { + 'bind_addr': '0.0.0.0', + 'bind_port': 7946, + 'advertise_addr': os.environ['ADVERTISE_ADDR_VAL'], + 'advertise_port': 7946, + 'join_addresses': join_addresses, + 'gossip_interval': '200ms', + 'probe_interval': '1s', + 'probe_timeout': '500ms', + 'suspicion_max_time_multiplier': 6, + }, + 'sql': { + 'enabled': True, + 'host': '0.0.0.0', + 'port': 3306, + 'system_database': 'swarmdb', + }, + 'jq': { + 'enabled': True, + 'host': '0.0.0.0', + 'port': 8080, + }, +} + +with open('/etc/swarm-db/config.yaml', 'w') as f: + yaml.dump(config, f, default_flow_style=False) +PYEOF + +log "swarm-db config generated" diff --git a/src/rootfs/files/configs/usr/local/bin/local-registry.sh b/src/rootfs/files/configs/usr/local/bin/local-registry.sh deleted file mode 100755 index d325dc67..00000000 --- a/src/rootfs/files/configs/usr/local/bin/local-registry.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash - -set -euo pipefail; - -SUPER_REGISTRY_HOST="registry.superprotocol.local"; -SUPER_CERTS_DIR="/opt/super/certs"; -SUPER_CERT_FILEPATH="${SUPER_CERTS_DIR}/${SUPER_REGISTRY_HOST}"; - -pkill hauler || true; - -sleep 3; # enterprise delay - -mkdir -p "/opt/hauler/.hauler"; - -find /etc/super/opt/hauler -type f -name "*.zst" | xargs /usr/local/bin/hauler store load --store /opt/hauler/store; - -nohup /usr/local/bin/hauler store serve fileserver --store /opt/hauler/store --directory /opt/hauler/registry & - -/usr/local/bin/hauler \ - store serve registry \ - --store /opt/hauler/store \ - --directory /opt/hauler/registry \ - --tls-cert="${SUPER_CERT_FILEPATH}.crt" \ - --tls-key="${SUPER_CERT_FILEPATH}.key"; diff --git a/src/services/apps/swarm-cloud-ui.sh b/src/rootfs/files/configs/usr/local/bin/swarm-cloud-ui.sh similarity index 55% rename from src/services/apps/swarm-cloud-ui.sh rename to src/rootfs/files/configs/usr/local/bin/swarm-cloud-ui.sh index 75e800b2..c6599328 100644 --- a/src/services/apps/swarm-cloud-ui.sh +++ b/src/rootfs/files/configs/usr/local/bin/swarm-cloud-ui.sh @@ -4,28 +4,31 @@ set -euo pipefail # This script starts the swarm-cloud-ui frontend in the same layout that the VM image uses. # According to build_swarm_cloud.sh and the Dockerfile, the built UI is published to: -# /usr/local/lib/swarm-cloud/dist/apps/swarm-cloud-ui +# /usr/local/lib/swarm-cloud/apps/swarm-cloud-ui # All dependencies are installed at image build time in build_swarm_cloud.sh; this script # MUST NOT run pnpm install or modify node_modules at runtime. SWARM_CLOUD_ROOT="/usr/local/lib/swarm-cloud" -SWARM_CLOUD_UI_DIR="${SWARM_CLOUD_ROOT}/dist/apps/swarm-cloud-ui" +SWARM_CLOUD_UI_DIR="${SWARM_CLOUD_ROOT}/apps/swarm-cloud-ui" cd "${SWARM_CLOUD_UI_DIR}" if ! command -v node >/dev/null 2>&1; then - echo "Node.js is not installed or not in PATH. Please install Node.js first." >&2 + echo "Node.js is not installed or not in PATH." >&2 exit 1 fi LISTEN_INTERFACE="${LISTEN_INTERFACE:-0.0.0.0}" SWARM_CLOUD_UI_PORT="${SWARM_CLOUD_UI_PORT:-3000}" -echo "Starting swarm-cloud-ui in development mode with Next.js (pnpm deploy layout)..." +echo "Starting swarm-cloud-ui in production mode (Next standalone)..." echo " Host: ${LISTEN_INTERFACE}" echo " Port: ${SWARM_CLOUD_UI_PORT}" -NODE_ENV=development exec node \ - node_modules/next/dist/bin/next dev \ - --hostname "${LISTEN_INTERFACE}" \ - --port "${SWARM_CLOUD_UI_PORT}" +if [[ ! -f "apps/swarm-cloud-ui/server.js" ]]; then + echo "Expected standalone server entrypoint not found: ${SWARM_CLOUD_UI_DIR}/apps/swarm-cloud-ui/server.js" >&2 + exit 1 +fi + +NODE_ENV=production HOSTNAME="${LISTEN_INTERFACE}" PORT="${SWARM_CLOUD_UI_PORT}" exec node \ + apps/swarm-cloud-ui/server.js diff --git a/src/rootfs/files/configs/usr/local/bin/swarm-init.sh b/src/rootfs/files/configs/usr/local/bin/swarm-init.sh new file mode 100644 index 00000000..8174d8c7 --- /dev/null +++ b/src/rootfs/files/configs/usr/local/bin/swarm-init.sh @@ -0,0 +1,227 @@ +#!/bin/bash +set -euo pipefail + +CONFIG="/sp/swarm/config.yaml" + +log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [swarm-init] $*"; } + +log "starting swarm initialization" + +# Read a scalar value from /sp/swarm/config.yaml via python3+pyyaml +cfg() { + python3 -c " +import yaml +c = yaml.safe_load(open('$CONFIG')) or {} +v = c +for k in '$1'.split('.'): + v = v.get(k) if isinstance(v, dict) else None +print('' if v is None else v)" +} + +GITHUB_TOKEN=$(cfg "github.token") +SWARM_DB_TAG=$(cfg "tags.swarm_db") +HOST_AGENT_TAG=$(cfg "tags.host_agent") +SWARM_NODE_TAG=$(cfg "tags.swarm_node") +SDK_TAG=$(cfg "tags.sdk") +SERVICES_TAG=$(cfg "tags.services") +SWARM_CLOUD_API_TAG=$(cfg "tags.swarm_cloud_api") +SWARM_CLOUD_UI_TAG=$(cfg "tags.swarm_cloud_ui") +AUTH_SERVICE_TAG=$(cfg "tags.auth_service") + +# Download a GitHub release asset to a local file path +# Usage: download_github_asset +download_github_asset() { + local owner="$1" repo="$2" tag="$3" filename="$4" dest="$5" + local auth_args=() + [ -n "$GITHUB_TOKEN" ] && auth_args=(-H "Authorization: token $GITHUB_TOKEN") + + local rel_file; rel_file=$(mktemp) + if ! curl -sf "${auth_args[@]}" \ + "https://api.github.com/repos/$owner/$repo/releases/tags/$tag" \ + -o "$rel_file"; then + rm -f "$rel_file" + log "ERROR: failed to fetch release info for $owner/$repo@$tag" + return 1 + fi + + local asset_id + asset_id=$(python3 -c " +import json +with open('$rel_file') as f: + data = json.load(f) +for a in data.get('assets', []): + if a['name'] == '$filename': + print(a['id']); break +" 2>/dev/null || true) + rm -f "$rel_file" + + if [ -z "$asset_id" ]; then + log "ERROR: asset '$filename' not found in $owner/$repo@$tag" + return 1 + fi + + curl -sfL "${auth_args[@]}" \ + -H "Accept: application/octet-stream" \ + -o "$dest" \ + "https://api.github.com/repos/$owner/$repo/releases/assets/$asset_id" +} + +# Install swarm-db binary from GitHub Releases (idempotent: skip if already installed) +if [ -n "$SWARM_DB_TAG" ]; then + if [ -f "/usr/local/bin/swarm-db-linux-amd64" ]; then + log "swarm-db already installed, skipping" + else + log "installing swarm-db $SWARM_DB_TAG..." + FILENAME="swarm-db-${SWARM_DB_TAG}-linux-amd64.tar.gz" + TMP=$(mktemp -d) + download_github_asset "Super-Protocol" "swarm-db" "$SWARM_DB_TAG" "$FILENAME" "$TMP/swarm-db.tar.gz" + tar xzf "$TMP/swarm-db.tar.gz" -C "$TMP" + install -m 755 "$TMP/swarm-db" /usr/local/bin/swarm-db-linux-amd64 + rm -rf "$TMP" + log "swarm-db $SWARM_DB_TAG installed" + fi +else + log "tags.swarm_db not set, using built-in swarm-db binary" +fi + +# Install provision-plugin-sdk from GitHub Releases (pip install is idempotent) +if [ -n "$SDK_TAG" ]; then + log "installing provision-plugin-sdk $SDK_TAG..." + FILENAME="provision-plugin-sdk-${SDK_TAG}.tar.gz" + TMP=$(mktemp -d) + download_github_asset "Super-Protocol" "swarm-cloud" "$SDK_TAG" "$FILENAME" "$TMP/sdk.tar.gz" + tar xzf "$TMP/sdk.tar.gz" -C "$TMP" + pip3 install --break-system-packages --quiet "$TMP" + rm -rf "$TMP" + log "provision-plugin-sdk $SDK_TAG installed" +else + log "tags.sdk not set, using built-in provision-plugin-sdk" +fi + +# Download swarm-services from GitHub Release into /etc/swarm-services (always overwrite) +if [ -n "$SERVICES_TAG" ]; then + log "downloading swarm-services $SERVICES_TAG..." + TMP=$(mktemp -d) + REL_FILE=$(mktemp) + auth_curl_args=() + [ -n "$GITHUB_TOKEN" ] && auth_curl_args=(-H "Authorization: token $GITHUB_TOKEN") + + if ! curl -sf "${auth_curl_args[@]}" \ + "https://api.github.com/repos/Super-Protocol/swarm-cloud/releases/tags/$SERVICES_TAG" \ + -o "$REL_FILE"; then + rm -f "$REL_FILE" + log "ERROR: failed to fetch release info for swarm-services $SERVICES_TAG" + exit 1 + fi + + GITHUB_TOKEN="$GITHUB_TOKEN" REL_FILE="$REL_FILE" TMP_DIR="$TMP" \ + python3 - << 'PYEOF' +import json, os, subprocess, re, zipfile + +github_token = os.environ.get('GITHUB_TOKEN', '') +rel_file = os.environ['REL_FILE'] +tmp_dir = os.environ['TMP_DIR'] +services_dir = '/etc/swarm-services' + +with open(rel_file) as f: + data = json.load(f) +os.unlink(rel_file) + +os.makedirs(services_dir, exist_ok=True) +auth_headers = ['-H', f'Authorization: token {github_token}'] if github_token else [] + +for asset in data.get('assets', []): + name = asset['name'] + if not name.endswith('.zip'): + continue + asset_id = asset['id'] + service_name = re.sub(r'^(.+?)-v[\d][^/]*\.zip$', r'\1', name) + dest = os.path.join(tmp_dir, name) + + subprocess.run( + ['curl', '-sfL'] + auth_headers + [ + '-H', 'Accept: application/octet-stream', + '-o', dest, + f'https://api.github.com/repos/Super-Protocol/swarm-cloud/releases/assets/{asset_id}', + ], + check=True, + ) + + svc_dir = os.path.join(services_dir, service_name) + os.makedirs(svc_dir, exist_ok=True) + with zipfile.ZipFile(dest, 'r') as zf: + zf.extractall(svc_dir) + + if not os.path.exists(os.path.join(svc_dir, 'manifest.yaml')): + print(f'ERROR: manifest.yaml not found in {service_name}', flush=True) + raise SystemExit(1) + + main_py = os.path.join(svc_dir, 'main.py') + if os.path.exists(main_py): + os.chmod(main_py, 0o755) + + print(f'installed service: {service_name}', flush=True) +PYEOF + rm -rf "$TMP" + log "swarm-services $SERVICES_TAG installed" +else + log "tags.services not set, skipping swarm-services download" +fi + +# Install swarm-host-agent from GitHub Releases (idempotent: skip if already installed) +# Tag format: "host-agent-vX.Y.Z" → release tag "release-vX.Y.Z" +if [ -n "$HOST_AGENT_TAG" ]; then + if [ -f "/usr/local/bin/swarm-host-agent" ]; then + log "swarm-host-agent already installed, skipping" + else + log "installing swarm-host-agent $HOST_AGENT_TAG..." + if [[ "$HOST_AGENT_TAG" == release-* ]]; then + RELEASE_TAG="$HOST_AGENT_TAG" + elif [[ "$HOST_AGENT_TAG" == host-agent-* ]]; then + VERSION="${HOST_AGENT_TAG#host-agent-}" + RELEASE_TAG="release-$VERSION" + else + RELEASE_TAG="release-$HOST_AGENT_TAG" + fi + FILENAME="swarm-host-agent-${RELEASE_TAG}-linux-amd64.tar.gz" + TMP=$(mktemp -d) + download_github_asset "Super-Protocol" "swarm-cloud" "$RELEASE_TAG" "$FILENAME" "$TMP/host-agent.tar.gz" + tar xzf "$TMP/host-agent.tar.gz" -C "$TMP" + EXTRACT_DIR=$(ls -1 "$TMP" | grep -v 'host-agent\.tar\.gz' | head -1) + install -m 755 "$TMP/$EXTRACT_DIR/swarm-host-agent" /usr/local/bin/swarm-host-agent + mkdir -p /etc/swarm + cp "$TMP/$EXTRACT_DIR/host-agent.yaml" /etc/swarm/host-agent.yaml + rm -rf "$TMP" + log "swarm-host-agent $RELEASE_TAG installed" + systemctl enable swarm-host-agent.service + fi +else + log "ERROR: tags.host_agent is required" + exit 1 +fi + +# Authenticate to ghcr.io for pulling swarm-node container image (idempotent) +if [ -n "$GITHUB_TOKEN" ]; then + log "authenticating to ghcr.io..." + echo "$GITHUB_TOKEN" | podman login ghcr.io -u oauth2 --password-stdin + log "ghcr.io login successful" +else + log "WARNING: github.token not set, skipping ghcr.io login (image must be publicly accessible)" +fi + +# Generate /etc/swarm/swarm-node.env for swarm-node.service EnvironmentFile (idempotent) +log "generating /etc/swarm/swarm-node.env..." +mkdir -p /etc/swarm +cat > /etc/swarm/swarm-node.env << EOF +SWARM_NODE_TAG=${SWARM_NODE_TAG} +EOF + +# Generate /etc/swarm/swarm-host-agent.env for swarm-host-agent.service EnvironmentFile (idempotent) +log "generating /etc/swarm/swarm-host-agent.env..." +cat > /etc/swarm/swarm-host-agent.env << EOF +SWARM_CLOUD_API_TAG=${SWARM_CLOUD_API_TAG} +SWARM_CLOUD_UI_TAG=${SWARM_CLOUD_UI_TAG} +AUTH_SERVICE_TAG=${AUTH_SERVICE_TAG} +EOF + +log "swarm-init completed successfully" diff --git a/src/rootfs/files/configs/usr/local/bin/swarm-node.sh b/src/rootfs/files/configs/usr/local/bin/swarm-node.sh deleted file mode 100644 index df702e73..00000000 --- a/src/rootfs/files/configs/usr/local/bin/swarm-node.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -cd /usr/local/lib/swarm-cloud - -exec node ./apps/swarm-node/dist/main.js diff --git a/src/rootfs/files/configs/usr/local/lib/services-downloader/package-lock.json b/src/rootfs/files/configs/usr/local/lib/services-downloader/package-lock.json deleted file mode 100644 index 0dc13562..00000000 --- a/src/rootfs/files/configs/usr/local/lib/services-downloader/package-lock.json +++ /dev/null @@ -1,131 +0,0 @@ -{ - "name": "services-downloader", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "services-downloader", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "@super-protocol/sp-files-addon": "^0.13.2", - "proper-lockfile": "^4.1.2" - }, - "bin": { - "sp-services-downloader": "src/index.js" - } - }, - "node_modules/@super-protocol/sp-files-addon": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/@super-protocol/sp-files-addon/-/sp-files-addon-0.13.2.tgz", - "integrity": "sha512-IZhxPu7TLlrlPS5bzVIvYnDDoGqtkm3fQjm/xuovXG3PM6ErGPRzRn0xpqCbkvXhRM04mTXoUmdT86TyhCh3qA==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "optionalDependencies": { - "@super-protocol/sp-files-addon-darwin-arm64": "0.13.2", - "@super-protocol/sp-files-addon-darwin-x64": "0.13.2", - "@super-protocol/sp-files-addon-linux-x64-gnu": "0.13.2", - "@super-protocol/sp-files-addon-win32-x64-msvc": "0.13.2" - } - }, - "node_modules/@super-protocol/sp-files-addon-darwin-arm64": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/@super-protocol/sp-files-addon-darwin-arm64/-/sp-files-addon-darwin-arm64-0.13.2.tgz", - "integrity": "sha512-lIZrgqG8fIVAo9ZmTUZaWWsmRsstGBKdjel7IkRA9uQIPxa4vsbItjQEtySj4nIxlvOjbfE1YAAOeOIxrXfAmw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@super-protocol/sp-files-addon-darwin-x64": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/@super-protocol/sp-files-addon-darwin-x64/-/sp-files-addon-darwin-x64-0.13.2.tgz", - "integrity": "sha512-tMlsBcAZgfraen+J2cmgaqXrM6rRSxBwLjv9sc3bnnTd8yELT2zBDRLm6+lykQpiwHAS/oPZrMe52oDmPCuV+A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 16" - } - }, - "node_modules/@super-protocol/sp-files-addon-linux-x64-gnu": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/@super-protocol/sp-files-addon-linux-x64-gnu/-/sp-files-addon-linux-x64-gnu-0.13.2.tgz", - "integrity": "sha512-eJ22VQ9Irx3Oh7uCvQ/CYKDfYWi+buEju9XN4EXBUV6WTmenUSWpUd0WWoyPIBe+Gx3XtmpETQ2VQ8ZU+XeQcA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 16" - } - }, - "node_modules/@super-protocol/sp-files-addon-win32-x64-msvc": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/@super-protocol/sp-files-addon-win32-x64-msvc/-/sp-files-addon-win32-x64-msvc-0.13.2.tgz", - "integrity": "sha512-p13EjHkh+ipHIjyowLMj6+0LBB49C4DP7xNuXRWlmrPJjSuGhnMXqqq8DvRQWqz+h+EubxBK/Lyt0zEOmllewg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 16" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - } - } -} diff --git a/src/rootfs/files/configs/usr/local/lib/services-downloader/package.json b/src/rootfs/files/configs/usr/local/lib/services-downloader/package.json deleted file mode 100644 index 2271cdb6..00000000 --- a/src/rootfs/files/configs/usr/local/lib/services-downloader/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "services-downloader", - "version": "1.0.0", - "main": "index.js", - "bin": { - "sp-services-downloader": "src/index.js" - }, - "author": "Super Protocol", - "license": "ISC", - "description": "", - "dependencies": { - "@super-protocol/sp-files-addon": "^0.13.2", - "proper-lockfile": "^4.1.2" - } -} diff --git a/src/rootfs/files/configs/usr/local/lib/services-downloader/src/downloader.js b/src/rootfs/files/configs/usr/local/lib/services-downloader/src/downloader.js deleted file mode 100644 index bdfb56da..00000000 --- a/src/rootfs/files/configs/usr/local/lib/services-downloader/src/downloader.js +++ /dev/null @@ -1,64 +0,0 @@ -const path = require('path'); -const fs = require('fs/promises'); -const { download } = require('@super-protocol/sp-files-addon'); - -async function ensureDir(dir) { - await fs.mkdir(dir, { recursive: true }); -} - -async function resourceExists(filePath) { - try { - const stat = await fs.stat(filePath); - if (stat.isFile()) return true; - if (stat.isDirectory()) { - const entries = await fs.readdir(filePath); - return entries.length > 0; - } - return false; - } catch { - // Path does not exist or not accessible - return false; - } -} - -// Resource helpers and plain download only; orchestration happens in CLI - -/** - * Download resource using sp-files-addon. - * Performs plain download; concurrency control handled by caller. - * - * @param {Object} params - * @param {string} params.resourceName - * @param {string} params.branchName - * @param {Object} params.resource - Resource definition for sp-files-addon - * @param {Object} [params.encryption] - Optional encryption { key, iv } - * @param {string} params.targetDir - Local directory where the resource will be downloaded - * @param {number} [params.threads] - Parallelism for sp-files-addon - * @returns {Promise<{ hash: string, size: number, targetDir: string }>} download result - */ -async function downloadResource(params) { - const { resourceName, branchName, targetDir, resource, encryption } = params; - if (!resourceName || !branchName) throw new Error('resourceName and branchName are required'); - if (!targetDir) throw new Error('targetDir is required'); - if (!resource) { - throw new Error('Resource is missing in parameters'); - } - - await ensureDir(targetDir); - - const result = await download(resource, targetDir, { - encryption, - threads: params.threads, - retry: { maxRetries: 5, initialDelayMs: 1000 }, - progressCallback: ({ key, current, total }) => { - const t = typeof total === 'number' ? total : 0; - const c = typeof current === 'number' ? current : 0; - const pct = t > 0 ? Math.floor((c / t) * 100) : 0; - console.log(`${key} ${c}/${t} (${pct}%)`); - }, - }); - - return { hash: result.hash, size: result.size, targetDir }; -} - -module.exports = { downloadResource, resourceExists }; diff --git a/src/rootfs/files/configs/usr/local/lib/services-downloader/src/gatekeeper-client.js b/src/rootfs/files/configs/usr/local/lib/services-downloader/src/gatekeeper-client.js deleted file mode 100644 index 2b4d08c8..00000000 --- a/src/rootfs/files/configs/usr/local/lib/services-downloader/src/gatekeeper-client.js +++ /dev/null @@ -1,90 +0,0 @@ -const https = require('https'); -const { URL } = require('url'); - -const getResourceFromGatekeeper = async (params) => { - const { resourceName, branchName, sslKeyPem, sslCertPem } = params; - const urlString = getUrl(resourceName, branchName, params.environment || 'mainnet'); - - const agent = new https.Agent({ - key: sslKeyPem, - cert: sslCertPem, - rejectUnauthorized: true, - }); - - const buf = await new Promise((resolve, reject) => { - try { - const urlObj = new URL(urlString); - - const req = https.request( - { - protocol: urlObj.protocol, - hostname: urlObj.hostname, - port: urlObj.port, - path: urlObj.pathname + urlObj.search, - method: 'GET', - headers: { Accept: 'application/json' }, - agent, - timeout: params.timeout || 30000, - }, - (res) => { - const chunks = []; - res.on('data', (chunk) => chunks.push(chunk)); - res.on('end', () => { - const body = Buffer.concat(chunks); - const ok = res.statusCode >= 200 && res.statusCode < 300; - if (ok) { - resolve(body); - } else { - const error = new Error( - `Gatekeeper request failed: ${res.statusCode} ${ - res.statusMessage - } - ${body.toString('utf8')}`, - ); - error.statusCode = res.statusCode; - error.headers = res.headers; - error.body = body; - reject(error); - } - }); - }, - ); - - req.on('error', reject); - req.on('timeout', () => req.destroy(new Error('Request timed out'))); - req.end(); - } catch (e) { - reject(e); - } - }); - - return parseGatekeeperResourceResponse(buf); -}; - -function parseGatekeeperResourceResponse(buf) { - let responseData; - try { - responseData = JSON.parse(buf.toString('utf8')); - } catch (e) { - const sample = buf.slice(0, 256).toString('utf8'); - throw new Error(`Invalid Gatekeeper response JSON: ${e.message}. Sample: ${sample}`); - } - - // { - // resource: { type: 'STORJ', filepath: '...' }, - // encryption: { key: 'hex', iv: 'hex' } - // } - const data = responseData.data; - if (!data?.resource || !data?.encryption) { - throw new Error('Gatekeeper response is invalid - missing resource or encryption field'); - } - - return data; -} - -const getUrl = (resourceName, branchName, environment) => { - const subdomain = `secrets-gatekeeper${environment === 'mainnet' ? '' : `-${environment}`}`; - - return `https://${subdomain}.superprotocol.io:44443/resources/${resourceName}/${branchName}`; -}; - -module.exports = { getResourceFromGatekeeper }; diff --git a/src/rootfs/files/configs/usr/local/lib/services-downloader/src/index.js b/src/rootfs/files/configs/usr/local/lib/services-downloader/src/index.js deleted file mode 100755 index b40d26d3..00000000 --- a/src/rootfs/files/configs/usr/local/lib/services-downloader/src/index.js +++ /dev/null @@ -1,195 +0,0 @@ -#!/usr/bin/env node -const fs = require('fs/promises'); -const os = require('os'); -const path = require('path'); -const { downloadResource, resourceExists } = require('./downloader'); -const { getResourceFromGatekeeper } = require('./gatekeeper-client'); -const { unpackTarGz, unpackTarGzAbsolute } = require('./unarchiver'); -const { acquireResourceLock } = require('./lock'); - -function printHelp() { - const text = ` -Services Downloader CLI - -Usage: - sp-services-downloader --resource-name --branch-name - --ssl-cert-path --ssl-key-path - [--environment ] [--threads ] [--timeout ] - <--download-to | --unpack-to | --unpack-with-absolute-path> - -Required arguments: - --resource-name Logical resource name (used for locking) - --branch-name Branch name (used for locking) - --ssl-cert-path Path to client SSL certificate (PEM) - --ssl-key-path Path to client SSL private key (PEM) - -Optional arguments: - --environment Gatekeeper environment (default: mainnet) - --threads Parallel threads for download - --timeout Request timeout to Gatekeeper in ms (default: 30000) - --download-to Download resource into the specified directory (no unpack) - --unpack-to Download to temp and unpack archive contents to the specified directory - --unpack-with-absolute-path - Download to temp and unpack archive entries with absolute paths directly to '/' - (when set, no directory is required) - --help Show this help - -Examples: - sp-services-downloader --resource-name svc --branch-name main - --ssl-cert-path /secrets/client.crt --ssl-key-path /secrets/client.key - --download-to /tmp/svc - - sp-services-downloader --resource-name svc --branch-name main - --ssl-cert-path /secrets/client.crt --ssl-key-path /secrets/client.key - --unpack-to /etc/sp-swarm-services - - sp-services-downloader --resource-name svc --branch-name main - --ssl-cert-path /secrets/client.crt --ssl-key-path /secrets/client.key - --unpack-with-absolute-path`; - process.stdout.write(text); -} - -function parseArgs(argv) { - const args = {}; - for (let i = 0; i < argv.length; i++) { - const a = argv[i]; - if (!a.startsWith('--')) continue; - const key = a.slice(2); - const next = argv[i + 1]; - if (next && !next.startsWith('--')) { - args[key] = next; - i++; - } else { - args[key] = true; - } - } - return args; -} - -async function main() { - const args = parseArgs(process.argv.slice(2)); - if (args.help) { - printHelp(); - return; - } - try { - const resourceName = args['resource-name']; - const branchName = args['branch-name']; - const downloadTo = args['download-to']; - const unpackTo = args['unpack-to']; - const sslCertPath = args['ssl-cert-path']; - const sslKeyPath = args['ssl-key-path']; - const environment = args.environment || 'mainnet'; - const timeout = args.timeout ? Number(args.timeout) : 30000; - - const unpackWithAbs = !!args['unpack-with-absolute-path']; - if (!resourceName || !branchName || !sslCertPath || !sslKeyPath) { - throw new Error('Missing required arguments. See --help'); - } - // Mode selection: exactly one of the mode flags must be provided - const modeCount = [!!downloadTo, !!unpackTo, unpackWithAbs].filter(Boolean).length; - if (modeCount !== 1) { - throw new Error( - 'Specify exactly one mode: --download-to | --unpack-to | --unpack-with-absolute-path', - ); - } - if (downloadTo && typeof downloadTo !== 'string') { - throw new Error('Invalid --download-to value'); - } - if (unpackTo && typeof unpackTo !== 'string') { - throw new Error('Invalid --unpack-to value'); - } - - const [sslCertPem, sslKeyPem] = await Promise.all([ - fs.readFile(sslCertPath, 'utf8'), - fs.readFile(sslKeyPath, 'utf8'), - ]); - - console.info(`[INFO] fetching resource ${resourceName}@${branchName} env=${environment}`); - const { resource, encryption } = await getResourceFromGatekeeper({ - resourceName, - branchName, - sslKeyPem, - sslCertPem, - environment, - timeout, - }); - - const threads = args.threads ? Number(args.threads) : undefined; - - // Acquire per-resource lock - const release = await acquireResourceLock(resourceName, branchName); - console.info(`[INFO] lock acquired for ${resourceName}/${branchName}`); - - try { - // Skip if destination already populated (download-to or unpack-to) - if (downloadTo && (await resourceExists(downloadTo))) { - console.info(`[INFO] skip: target already populated -> ${downloadTo}`); - process.stdout.write( - JSON.stringify({ ok: true, hash: 'unknown', size: 0, targetDir: downloadTo }) + '\n', - ); - return; - } - if (unpackTo && (await resourceExists(unpackTo))) { - console.info(`[INFO] skip: target already populated -> ${unpackTo}`); - process.stdout.write( - JSON.stringify({ ok: true, hash: 'unknown', size: 0, targetDir: unpackTo }) + '\n', - ); - return; - } - - let downloadDir = downloadTo || unpackTo || '/'; - let tempDir; - try { - if (unpackTo || unpackWithAbs) { - const tempPrefix = path.join(os.tmpdir(), 'sp-services-downloader-'); - tempDir = await fs.mkdtemp(tempPrefix); - console.info(`[INFO] unpack enabled: downloading archive to temp -> ${tempDir}`); - downloadDir = tempDir; - } - - const result = await downloadResource({ - resourceName, - branchName, - targetDir: downloadDir, - resource, - encryption, - threads, - }); - - if (unpackWithAbs) { - console.info(`[INFO] unpack-with-absolute-path: extracting archive entries to /`); - await unpackTarGzAbsolute(downloadDir); - } else if (unpackTo) { - console.info(`[INFO] unpacking from temp to target -> ${unpackTo}`); - await unpackTarGz(downloadDir, unpackTo); - } - - const outTarget = unpackWithAbs ? '/' : downloadTo || unpackTo; - process.stdout.write( - JSON.stringify({ ok: true, hash: result.hash, size: result.size, targetDir: outTarget }) + - '\n', - ); - } finally { - if (tempDir) { - try { - await fs.rm(tempDir, { recursive: true, force: true }); - console.info(`[INFO] cleaned temp directory -> ${tempDir}`); - } catch (cleanupErr) { - console.warn(`[WARN] failed to clean temp directory ${tempDir}: ${cleanupErr.message}`); - } - } - } - } finally { - await release(); - console.info(`[INFO] lock released for ${resourceName}/${branchName}`); - } - } catch (e) { - process.stderr.write(`[ERROR] ${e.message}\n`); - process.exitCode = 1; - } -} - -if (require.main === module) { - main(); -} diff --git a/src/rootfs/files/configs/usr/local/lib/services-downloader/src/lock.js b/src/rootfs/files/configs/usr/local/lib/services-downloader/src/lock.js deleted file mode 100644 index d9e1e3ba..00000000 --- a/src/rootfs/files/configs/usr/local/lib/services-downloader/src/lock.js +++ /dev/null @@ -1,25 +0,0 @@ -const path = require('path'); -const os = require('os'); -const fs = require('fs/promises'); -const lockfile = require('proper-lockfile'); - -function buildLockName(resourceName, branchName) { - const safe = (s) => encodeURIComponent(String(s || '')); - return `${safe(resourceName)}__${safe(branchName)}`; -} - -async function acquireResourceLock(resourceName, branchName, options = {}) { - const baseDir = options.baseDir || path.join(os.tmpdir(), 'sp-services-downloader-locks'); - await fs.mkdir(baseDir, { recursive: true }); - const lockTarget = path.join(baseDir, buildLockName(resourceName, branchName)); - - const release = await lockfile.lock(lockTarget, { - stale: options.staleMs || 60_000, - retries: options.retries || { retries: 120, factor: 1, minTimeout: 500 }, - realpath: false, - }); - - return release; -} - -module.exports = { acquireResourceLock, buildLockName }; diff --git a/src/rootfs/files/configs/usr/local/lib/services-downloader/src/unarchiver.js b/src/rootfs/files/configs/usr/local/lib/services-downloader/src/unarchiver.js deleted file mode 100644 index 71b1c2b7..00000000 --- a/src/rootfs/files/configs/usr/local/lib/services-downloader/src/unarchiver.js +++ /dev/null @@ -1,97 +0,0 @@ -const path = require('path'); -const fs = require('fs/promises'); -const { execFile } = require('child_process'); - -async function hasFiles(dir) { - try { - const stat = await fs.stat(dir); - if (!stat.isDirectory()) return false; - const entries = await fs.readdir(dir); - return entries.length > 0; - } catch { - return false; - } -} - -async function ensureDir(dir) { - await fs.mkdir(dir, { recursive: true }); -} - -async function findTarLike(targetDir) { - const entries = await fs.readdir(targetDir, { withFileTypes: true }); - for (const e of entries) { - const full = path.join(targetDir, e.name); - if (e.isFile()) { - const name = e.name.toLowerCase(); - if (name.endsWith('.tar.gz') || name.endsWith('.tgz') || name.endsWith('.tar')) { - return full; - } - } else if (e.isDirectory()) { - try { - const nested = await findTarLike(full); - if (nested) return nested; - } catch {} - } - } - return null; -} - -function execTarExtract(tarFile, destDir) { - return new Promise((resolve, reject) => { - const lower = tarFile.toLowerCase(); - const args = - lower.endsWith('.tar.gz') || lower.endsWith('.tgz') - ? ['-xzf', tarFile, '-C', destDir] - : ['-xf', tarFile, '-C', destDir]; - execFile('tar', args, (err) => { - if (err) return reject(err); - resolve(); - }); - }); -} - -function execTarExtractAbsolute(tarFile) { - return new Promise((resolve, reject) => { - const lower = tarFile.toLowerCase(); - const args = - lower.endsWith('.tar.gz') || lower.endsWith('.tgz') - ? ['-xzf', tarFile, '-C', '/', '-p', '-P', '-k'] - : ['-xf', tarFile, '-C', '/', '-p', '-P', '-k']; - execFile('tar', args, (err) => { - if (err) return reject(err); - resolve(); - }); - }); -} - -async function unpackTarGz(targetDir, unpackTarTo) { - await ensureDir(unpackTarTo); - const destHasFiles = await hasFiles(unpackTarTo); - if (destHasFiles) { - console.info(`[INFO] unpack skip: destination not empty: ${unpackTarTo}`); - return false; - } - - const tarFile = await findTarLike(targetDir); - if (!tarFile) { - console.info(`[INFO] unpack skip: no archive found under ${targetDir}`); - return false; - } - - await execTarExtract(tarFile, unpackTarTo); - console.info(`[INFO] unpacked ${path.basename(tarFile)} to ${unpackTarTo}`); - return true; -} - -async function unpackTarGzAbsolute(targetDir) { - const tarFile = await findTarLike(targetDir); - if (!tarFile) { - console.info(`[INFO] unpack-with-absolute-path skip: no archive found under ${targetDir}`); - return false; - } - await execTarExtractAbsolute(tarFile); - console.info(`[INFO] unpack-with-absolute-path: unpacked ${path.basename(tarFile)} to /`); - return true; -} - -module.exports = { unpackTarGz, unpackTarGzAbsolute }; diff --git a/src/rootfs/files/scripts/build_swarm_cloud.sh b/src/rootfs/files/scripts/build_swarm_cloud.sh deleted file mode 100644 index e6cbaeee..00000000 --- a/src/rootfs/files/scripts/build_swarm_cloud.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/bin/bash - -# bash unofficial strict mode; -set -euo pipefail; - -# public, required -# OUTPUTDIR - -# private -BUILDROOT="/buildroot"; - -# init logging -source "${BUILDROOT}/files/scripts/log.sh"; - -# chroot functions -source "${BUILDROOT}/files/scripts/chroot.sh"; - -function build_swarm_cloud() { - log_info "enabling corepack inside rootfs"; - chroot "${OUTPUTDIR}" /bin/bash -lc 'corepack enable'; - - log_info "installing Node.js dependencies with pnpm"; - chroot "${OUTPUTDIR}" /bin/bash -lc 'export NX_DAEMON=false NX_ADD_PLUGINS=false NX_NO_CLOUD=true; cd /opt/swarm-cloud && pnpm install --frozen-lockfile'; - - # swarm-cloud-api - log_info "building swarm-cloud-api"; - chroot "${OUTPUTDIR}" /bin/bash -lc 'cd /opt/swarm-cloud && pnpm nx build swarm-cloud-api --output-style=stream'; - - log_info "publishing built swarm-cloud-api artifacts to /usr/local/lib/swarm-cloud"; - chroot "${OUTPUTDIR}" /bin/bash -lc 'set -e; mkdir -p /usr/local/lib/swarm-cloud/apps/swarm-cloud-api'; - chroot "${OUTPUTDIR}" /bin/bash -lc 'cp -r /opt/swarm-cloud/apps/swarm-cloud-api/{dist,node_modules} /usr/local/lib/swarm-cloud/apps/swarm-cloud-api/'; - - # swarm-node - log_info "building swarm-node"; - chroot "${OUTPUTDIR}" /bin/bash -lc 'cd /opt/swarm-cloud && pnpm nx build swarm-node --output-style=stream'; - - log_info "publishing built swarm-node artifacts to /usr/local/lib/swarm-cloud"; - chroot "${OUTPUTDIR}" /bin/bash -lc 'set -e; mkdir -p /usr/local/lib/swarm-cloud/apps/swarm-node'; - chroot "${OUTPUTDIR}" /bin/bash -lc 'cp -r /opt/swarm-cloud/apps/swarm-node/{dist,node_modules} /usr/local/lib/swarm-cloud/apps/swarm-node/'; -# -# # swarm-cloud-ui -# log_info "building swarm-cloud-ui"; -# chroot "${OUTPUTDIR}" /bin/bash -lc 'cd /opt/swarm-cloud && pnpm nx build swarm-cloud-ui --output-style=stream'; -# -# log_info "deploying swarm-cloud-ui via pnpm deploy to /usr/local/lib/swarm-cloud/apps/swarm-cloud-ui"; -# chroot "${OUTPUTDIR}" /bin/bash -lc 'set -e; mkdir -p /usr/local/lib/swarm-cloud/apps/swarm-cloud-ui'; -# chroot "${OUTPUTDIR}" /bin/bash -lc 'cp -r /opt/swarm-cloud/apps/swarm-cloud-ui/{.next,node_modules} /usr/local/lib/swarm-cloud/apps/swarm-cloud-ui/'; -# -# log_info "copying shared UI libraries to /usr/local/lib/swarm-cloud/libs"; -# chroot "${OUTPUTDIR}" /bin/bash -lc 'mkdir -p /usr/local/lib/swarm-cloud/libs'; -# chroot "${OUTPUTDIR}" /bin/bash -lc 'cp -r /opt/swarm-cloud/libs/ui /usr/local/lib/swarm-cloud/libs/ui'; - -# # In the deployed UI lib, TypeScript sources live under libs/ui/src, but some imports -# # reference sibling TS modules with a .js extension (e.g. "../lib/utils.js", "./button.js"). -# # Next + TS expect extension-less imports for TS modules. Adjust imports only in the -# # deployed copy (do not touch the original sources under src/repos). -# chroot "${OUTPUTDIR}" /bin/bash -lc "\ -# find /usr/local/lib/swarm-cloud/dist/libs/ui/src -type f \\( -name '*.ts' -o -name '*.tsx' \\) -print0 \ -# | xargs -0 sed -i 's/\\.js\\([\"'\"'\"']\\)/\\1/g'" - -# log_info "copying workspace-level Node.js dependencies and configs to /usr/local/lib/swarm-cloud"; -# chroot "${OUTPUTDIR}" /bin/bash -lc 'mkdir -p /usr/local/lib/swarm-cloud/node_modules'; -# # copy the *contents* of node_modules so that the .pnpm layout and symlink targets remain valid - - # common - chroot "${OUTPUTDIR}" /bin/bash -lc 'cp /opt/swarm-cloud/package.json /usr/local/lib/swarm-cloud/package.json'; - chroot "${OUTPUTDIR}" /bin/bash -lc 'cp /opt/swarm-cloud/pnpm-lock.yaml /usr/local/lib/swarm-cloud/pnpm-lock.yaml'; - chroot "${OUTPUTDIR}" /bin/bash -lc 'cp /opt/swarm-cloud/pnpm-workspace.yaml /usr/local/lib/swarm-cloud/pnpm-workspace.yaml'; - chroot "${OUTPUTDIR}" /bin/bash -lc 'cp -a /opt/swarm-cloud/node_modules/. /usr/local/lib/swarm-cloud/node_modules/'; - - log_info "removing sources from /opt/swarm-cloud"; - chroot "${OUTPUTDIR}" /bin/bash -lc 'rm -rf /opt/swarm-cloud || true'; -} - -chroot_init; -build_swarm_cloud; -chroot_deinit; diff --git a/src/rootfs/files/scripts/download_rke2.sh b/src/rootfs/files/scripts/download_rke2.sh deleted file mode 100755 index 83353f05..00000000 --- a/src/rootfs/files/scripts/download_rke2.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash - -# bash unofficial strict mode; -set -euo pipefail; - -# public, required -# OUTPUTDIR - -# private -BUILDROOT="/buildroot"; -RKE2_VERSION="v1.30.3+rke2r1"; -SHA_CHECKSUMS_TXT="445ead9865914fa2e6d6a59affd00babc462480efebf438d207961f740ab83a2"; -SHA_INSTALL_SH="2d24db2184dd6b1a5e281fa45cc9a8234c889394721746f89b5fe953fdaaf40a"; - -# init loggggging; -source "$BUILDROOT/files/scripts/log.sh"; - -function download_rke2() { - log_info "downloading rke2 install scripts" - mkdir -p "$OUTPUTDIR/root/rke2"; - wget \ - "https://github.com/rancher/rke2/releases/download/${RKE2_VERSION}/rke2-images.linux-amd64.tar.zst" \ - -O "$OUTPUTDIR/root/rke2/rke2-images.linux-amd64.tar.zst"; - wget \ - "https://github.com/rancher/rke2/releases/download/${RKE2_VERSION}/rke2.linux-amd64.tar.gz" \ - -O "$OUTPUTDIR/root/rke2/rke2.linux-amd64.tar.gz"; - wget \ - "https://github.com/rancher/rke2/releases/download/${RKE2_VERSION}/sha256sum-amd64.txt" \ - -O "$OUTPUTDIR/root/rke2/sha256sum-amd64.txt"; - wget \ - "https://get.rke2.io" \ - -O "$OUTPUTDIR/root/rke2/rke2-install.sh"; -} - -function validate_checksum() { - log_info "validating checksums"; - pushd "$OUTPUTDIR/root/rke2"; - echo "$SHA_CHECKSUMS_TXT sha256sum-amd64.txt" | sha256sum --check - echo "$SHA_INSTALL_SH rke2-install.sh" | sha256sum --check - popd; -} - -download_rke2; -validate_checksum; diff --git a/src/rootfs/files/scripts/install_cockroachdb.sh b/src/rootfs/files/scripts/install_cockroachdb.sh deleted file mode 100644 index e20da789..00000000 --- a/src/rootfs/files/scripts/install_cockroachdb.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash - -# bash unofficial strict mode -set -euo pipefail - -# private -BUILDROOT="/buildroot" - -# init logging -source "$BUILDROOT/files/scripts/log.sh" - -# chroot functions -source "$BUILDROOT/files/scripts/chroot.sh" - -function install_cockroachdb() { - log_info "installing CockroachDB binary inside VM rootfs" - - # Ensure required tools are present - chroot "$OUTPUTDIR" /bin/bash -lc 'set -e; apt-get update' - chroot "$OUTPUTDIR" /bin/bash -lc 'set -e; apt-get install -y --no-install-recommends wget ca-certificates tar' - - # Download and install latest CockroachDB binary - chroot "$OUTPUTDIR" /bin/bash -lc ' - set -e; - arch=$(uname -m); - case "$arch" in - x86_64) cr_arch=amd64 ;; - aarch64|arm64) cr_arch=arm64 ;; - *) cr_arch=amd64 ;; - esac; - cd /tmp; - wget -q https://binaries.cockroachdb.com/cockroach-latest.linux-${cr_arch}.tgz -O cockroach.tgz; - tar -xzf cockroach.tgz; - dir=$(tar -tzf cockroach.tgz | head -1 | cut -d/ -f1); - cp "$dir/cockroach" /usr/local/bin/cockroach; - chmod 0755 /usr/local/bin/cockroach; - rm -rf "$dir" cockroach.tgz; - ' - - chroot "$OUTPUTDIR" /bin/bash -lc 'apt-get clean' -} - -chroot_init -install_cockroachdb -chroot_deinit diff --git a/src/rootfs/files/scripts/install_extra_packages.sh b/src/rootfs/files/scripts/install_extra_packages.sh new file mode 100644 index 00000000..3a7dde0a --- /dev/null +++ b/src/rootfs/files/scripts/install_extra_packages.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# bash unofficial strict mode +set -euo pipefail + +# private +BUILDROOT="/buildroot" + +# init logging +source "$BUILDROOT/files/scripts/log.sh" + +# chroot functions +source "$BUILDROOT/files/scripts/chroot.sh" + +function install_extra_packages() { + log_info "installing extra system packages for cloud-init compatibility" + + chroot "$OUTPUTDIR" /bin/bash -lc "apt-get update" + + # podman: container runtime used by cloud-init-style swarm services + # (cloud-init runs swarm-node as a Podman container; also needed by provision plugins) + # unzip: used to extract service archives (download-services.sh) + # NOTE: mysql-client, netcat-openbsd, dnsutils are already installed by setup_runtime_tools.sh + chroot "$OUTPUTDIR" /bin/bash -lc "apt-get install -y --no-install-recommends \ + podman \ + unzip" + + chroot "$OUTPUTDIR" /bin/bash -lc "apt-get clean" + log_info "extra packages installed successfully" +} + +chroot_init +install_extra_packages +chroot_deinit diff --git a/src/rootfs/files/scripts/install_knot.sh b/src/rootfs/files/scripts/install_knot.sh deleted file mode 100644 index 6d34ce6a..00000000 --- a/src/rootfs/files/scripts/install_knot.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash - -# bash unofficial strict mode -set -euo pipefail - -# private -BUILDROOT="/buildroot" - -# init logging -source "$BUILDROOT/files/scripts/log.sh" - -# chroot functions -source "$BUILDROOT/files/scripts/chroot.sh" - -function install_knot() { - log_info "installing Knot DNS into VM rootfs" - - # Base tools and add-apt-repository support - chroot "$OUTPUTDIR" /bin/bash -lc 'set -e; apt-get update' - chroot "$OUTPUTDIR" /bin/bash -lc 'set -e; apt-get install -y --no-install-recommends software-properties-common ca-certificates' - - # Add upstream PPA and install knot - chroot "$OUTPUTDIR" /bin/bash -lc ' - set -e; - add-apt-repository -y ppa:cz.nic-labs/knot-dns; - apt-get update; - apt-get install -y knot; - ' - - chroot "$OUTPUTDIR" /bin/bash -lc 'apt-get clean' -} - -chroot_init -install_knot -chroot_deinit diff --git a/src/rootfs/files/scripts/install_mongodb.sh b/src/rootfs/files/scripts/install_mongodb.sh deleted file mode 100644 index 0f9d0ffc..00000000 --- a/src/rootfs/files/scripts/install_mongodb.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash - -# bash unofficial strict mode -set -euo pipefail - -# private -BUILDROOT="/buildroot" - -# init logging -source "$BUILDROOT/files/scripts/log.sh" - -# chroot functions -source "$BUILDROOT/files/scripts/chroot.sh" - -function install_mongodb() { - log_info "installing MongoDB (mongodb-org 7.0) inside VM rootfs" - chroot "$OUTPUTDIR" /bin/bash -lc 'set -e; apt update' - chroot "$OUTPUTDIR" /bin/bash -lc 'set -e; apt install -y --no-install-recommends gnupg curl' - chroot "$OUTPUTDIR" /bin/bash -lc 'set -e; curl -fsSL https://www.mongodb.org/static/pgp/server-7.0.asc | gpg --dearmor -o /usr/share/keyrings/mongodb-server-7.0.gpg' - chroot "$OUTPUTDIR" /bin/bash -lc 'set -e; echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" > /etc/apt/sources.list.d/mongodb-org-7.0.list' - chroot "$OUTPUTDIR" /bin/bash -lc 'set -e; apt update' - chroot "$OUTPUTDIR" /bin/bash -lc 'set -e; apt install -y --no-install-recommends mongodb-org mongodb-mongosh' - chroot "$OUTPUTDIR" /bin/bash -lc 'set -e; apt clean' - chroot "$OUTPUTDIR" /bin/bash -lc 'set -e; mkdir -p /var/lib/mongodb /var/log/mongodb' - chroot "$OUTPUTDIR" /bin/bash -lc 'set -e; chown -R mongodb:mongodb /var/lib/mongodb /var/log/mongodb || true' -} - -chroot_init -install_mongodb -chroot_deinit - - diff --git a/src/rootfs/files/scripts/install_nats.sh b/src/rootfs/files/scripts/install_nats.sh deleted file mode 100644 index ced2cf2c..00000000 --- a/src/rootfs/files/scripts/install_nats.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash - -# bash unofficial strict mode -set -euo pipefail - -# private -BUILDROOT="/buildroot" - -# init logging -source "$BUILDROOT/files/scripts/log.sh" - -# chroot functions -source "$BUILDROOT/files/scripts/chroot.sh" - -function install_nats() { - local NATS_VERSION="2.12.2" - local NATS_PKG="nats-server-v${NATS_VERSION}-linux-amd64" - local NATS_URL="https://github.com/nats-io/nats-server/releases/download/v${NATS_VERSION}/${NATS_PKG}.tar.gz" - - log_info "installing NATS (nats-server v${NATS_VERSION}) inside VM rootfs" - - # prerequisites - chroot "$OUTPUTDIR" /bin/bash -lc 'set -e; apt update' - chroot "$OUTPUTDIR" /bin/bash -lc 'set -e; apt install -y --no-install-recommends curl ca-certificates tar' - - # download and install binary - chroot "$OUTPUTDIR" /bin/bash -lc "set -e; cd /tmp && curl -fsSL '${NATS_URL}' -o ${NATS_PKG}.tar.gz" - chroot "$OUTPUTDIR" /bin/bash -lc "set -e; cd /tmp && tar -xzf ${NATS_PKG}.tar.gz" - chroot "$OUTPUTDIR" /bin/bash -lc "set -e; install -m 0755 /tmp/${NATS_PKG}/nats-server /usr/local/bin/nats-server" - - # create user/group if absent - chroot "$OUTPUTDIR" /bin/bash -lc "getent group nats >/dev/null 2>&1 || groupadd --system nats" - chroot "$OUTPUTDIR" /bin/bash -lc "id -u nats >/dev/null 2>&1 || useradd --system --no-create-home --gid nats nats" - - # directories - chroot "$OUTPUTDIR" /bin/bash -lc 'set -e; mkdir -p /etc/nats /var/lib/nats /var/log/nats' - chroot "$OUTPUTDIR" /bin/bash -lc 'set -e; chown -R nats:nats /etc/nats /var/lib/nats /var/log/nats || true' - - # systemd unit - cat > "${OUTPUTDIR}/usr/lib/systemd/system/nats-server.service" <<'UNIT' -[Unit] -Description=NATS Server -After=network-online.target wg-quick.target -Wants=network-online.target wg-quick.target - -[Service] -User=nats -Group=nats -ExecStart=/usr/local/bin/nats-server -c /etc/nats/nats-server.conf -Restart=always -RestartSec=2 -LimitNOFILE=100000 - -[Install] -WantedBy=multi-user.target -UNIT - - # cleanup - chroot "$OUTPUTDIR" /bin/bash -lc "rm -rf /tmp/${NATS_PKG} /tmp/${NATS_PKG}.tar.gz" - chroot "$OUTPUTDIR" /bin/bash -lc 'apt clean' -} - -chroot_init -install_nats -chroot_deinit diff --git a/src/rootfs/files/scripts/install_nodejs.sh b/src/rootfs/files/scripts/install_nodejs.sh old mode 100755 new mode 100644 index 2d66755d..c7c8d0b9 --- a/src/rootfs/files/scripts/install_nodejs.sh +++ b/src/rootfs/files/scripts/install_nodejs.sh @@ -18,17 +18,17 @@ source "${BUILDROOT}/files/scripts/chroot.sh"; function install_nodejs() { log_info "adding NodeSource repository"; chroot "${OUTPUTDIR}" /bin/bash -c 'curl -sL https://deb.nodesource.com/setup_22.x | bash -'; - + log_info "installing Node.js"; chroot "${OUTPUTDIR}" /bin/bash -c 'DEBIAN_FRONTEND=noninteractive apt install -y nodejs'; - + # Verify installation local NODE_VERSION=$(chroot "${OUTPUTDIR}" /bin/bash -c 'node --version' 2>/dev/null || true); if [ -z "${NODE_VERSION}" ]; then log_fail "Node.js installation failed"; return 1; fi - + log_info "Node.js ${NODE_VERSION} installed successfully"; } diff --git a/src/rootfs/files/scripts/install_openresty.sh b/src/rootfs/files/scripts/install_openresty.sh deleted file mode 100644 index 3c228279..00000000 --- a/src/rootfs/files/scripts/install_openresty.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash - -# bash unofficial strict mode -set -euo pipefail - -# private -BUILDROOT="/buildroot" - -# init logging -source "$BUILDROOT/files/scripts/log.sh" - -# chroot functions -source "$BUILDROOT/files/scripts/chroot.sh" - -function install_openresty() { - log_info "installing OpenResty and Lua tooling inside VM rootfs" - - # base prerequisites for adding GPG key and repo (per official docs) - chroot "$OUTPUTDIR" /bin/bash -lc 'set -e; apt-get update' - chroot "$OUTPUTDIR" /bin/bash -lc 'set -e; apt-get install -y --no-install-recommends wget gnupg ca-certificates lsb-release' - - # import OpenResty GPG key and create keyring (Ubuntu 22+/24+ style) - chroot "$OUTPUTDIR" /bin/bash -lc 'set -e; wget -O - https://openresty.org/package/pubkey.gpg | gpg --dearmor -o /usr/share/keyrings/openresty.gpg' - - # add OpenResty APT repository with signed-by (per https://openresty.org/en/linux-packages.html) - chroot "$OUTPUTDIR" /bin/bash -lc 'set -e; codename=$(lsb_release -sc); arch=$(dpkg --print-architecture); echo "deb [arch=${arch} signed-by=/usr/share/keyrings/openresty.gpg] http://openresty.org/package/ubuntu ${codename} main" > /etc/apt/sources.list.d/openresty.list' - - chroot "$OUTPUTDIR" /bin/bash -lc 'set -e; apt-get update' - - # install OpenResty itself (no recommends to keep image small); luarocks separately - chroot "$OUTPUTDIR" /bin/bash -lc 'set -e; apt-get install -y --no-install-recommends openresty' - chroot "$OUTPUTDIR" /bin/bash -lc 'set -e; apt-get install -y luarocks' - - # install required Lua modules (best effort – warnings only on failure) - chroot "$OUTPUTDIR" /bin/bash -lc 'set -e; for m in lua-resty-auto-ssl lua-resty-redis lua-resty-http; do echo "[*] installing $m via luarocks"; if ! luarocks install "$m"; then echo "[!] warning: failed to install $m" >&2; fi; done' - - chroot "$OUTPUTDIR" /bin/bash -lc 'set -e; apt-get clean' -} - -chroot_init -install_openresty -chroot_deinit diff --git a/src/rootfs/files/scripts/install_python_deps.sh b/src/rootfs/files/scripts/install_python_deps.sh new file mode 100644 index 00000000..c3c85ad3 --- /dev/null +++ b/src/rootfs/files/scripts/install_python_deps.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# bash unofficial strict mode +set -euo pipefail + +# private +BUILDROOT="/buildroot" + +# init logging +source "$BUILDROOT/files/scripts/log.sh" + +# chroot functions +source "$BUILDROOT/files/scripts/chroot.sh" + +function install_python_deps() { + log_info "installing Python dependencies for provision plugins" + # redis-py is already installed by setup_runtime_tools.sh, but ensure correct version range + # (setup_runtime_tools.sh installs latest; provision plugins require >=5.0.0,<6.0.0) + chroot "$OUTPUTDIR" /bin/bash -lc "pip3 install --break-system-packages 'redis>=5.0.0,<6.0.0'" + # podman-compose: required for provision plugins that orchestrate Podman containers + chroot "$OUTPUTDIR" /bin/bash -lc "pip3 install --break-system-packages podman-compose" + # pyyaml: required by swarm-init.sh to parse /etc/swarm/config.yaml at runtime + chroot "$OUTPUTDIR" /bin/bash -lc "pip3 install --break-system-packages pyyaml" + log_info "Python dependencies installed successfully" +} + +chroot_init +install_python_deps +chroot_deinit diff --git a/src/rootfs/files/scripts/install_rke2.sh b/src/rootfs/files/scripts/install_rke2.sh deleted file mode 100755 index 656df3ad..00000000 --- a/src/rootfs/files/scripts/install_rke2.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash - -# bash unofficial strict mode; -set -euo pipefail; - -# public, required -# OUTPUTDIR -# RKE2_INSTALL_SHA256 - -# private -BUILDROOT="/buildroot"; - -# init logging; -source "$BUILDROOT/files/scripts/log.sh"; - -# chroot functions -source "$BUILDROOT/files/scripts/chroot.sh"; - -function install_rke2() { - log_info "staging rke2 installer into rootfs" - mkdir -p "$OUTPUTDIR/root/rke2"; - wget -q -O "$OUTPUTDIR/root/rke2/rke2-install.sh" "https://get.rke2.io"; - - log_info "verifying rke2 installer sha256" - echo "${RKE2_INSTALL_SHA256} $OUTPUTDIR/root/rke2/rke2-install.sh" | sha256sum -c -; - - log_info "installing rke2" - chroot "$OUTPUTDIR" /bin/bash -c 'bash /root/rke2/rke2-install.sh'; - rm -rf "$OUTPUTDIR/root/rke2"; -} - -function disable_rke2_service() { - log_info "disabling rke2 services" - chroot "$OUTPUTDIR" /bin/bash -c 'systemctl disable rke2-server.service || true'; - chroot "$OUTPUTDIR" /bin/bash -c 'systemctl disable rke2-agent.service || true'; -} - -function add_aliases() { - log_info "adding kubectl aliases" - echo "export KUBECONFIG=/etc/rancher/rke2/rke2.yaml" >> "$OUTPUTDIR/etc/profile"; - echo "alias k='/usr/local/bin/kubectl'" >> "$OUTPUTDIR/etc/profile"; - echo "alias kubectl='/usr/local/bin/kubectl'" >> "$OUTPUTDIR/etc/profile"; -} - -chroot_init; -install_rke2; -disable_rke2_service; -chroot_deinit; -add_aliases; diff --git a/src/rootfs/files/scripts/install_services_downloader.sh b/src/rootfs/files/scripts/install_services_downloader.sh deleted file mode 100644 index 7a0edc5c..00000000 --- a/src/rootfs/files/scripts/install_services_downloader.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -# bash unofficial strict mode; -set -euo pipefail; - -# public, required -# OUTPUTDIR - -# private -BUILDROOT="/buildroot"; - -# init logging; -source "${BUILDROOT}/files/scripts/log.sh"; - -# chroot functions -source "${BUILDROOT}/files/scripts/chroot.sh"; - -function install_services_downloader() { - log_info "installing services-downloader dependencies (npm ci)"; - chroot "${OUTPUTDIR}" /bin/bash -c 'cd /usr/local/lib/services-downloader && npm ci'; - - # quick smoke test prints help via node directly - chroot "${OUTPUTDIR}" /bin/bash -c 'node /usr/local/lib/services-downloader/src/index.js --help >/dev/null || true'; -} - -chroot_init; -install_services_downloader; -chroot_deinit; diff --git a/src/rootfs/files/scripts/refresh_ca_certs.sh b/src/rootfs/files/scripts/refresh_ca_certs.sh deleted file mode 100755 index 8d46e848..00000000 --- a/src/rootfs/files/scripts/refresh_ca_certs.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -# bash unofficial strict mode; -set -euo pipefail; - -# private -BUILDROOT="/buildroot"; - -# init loggggging; -source "$BUILDROOT/files/scripts/log.sh"; - -# chroot functions -source "$BUILDROOT/files/scripts/chroot.sh"; - -function refresh_ca_certs() { - log_info "refreshing ca certs"; - chroot "$OUTPUTDIR" /bin/bash -c 'update-ca-certificates --fresh'; -} - -chroot_init; -refresh_ca_certs; -chroot_deinit; diff --git a/src/rootfs/files/scripts/setup_runtime_tools.sh b/src/rootfs/files/scripts/setup_runtime_tools.sh index 11f3a7a1..b256d09d 100644 --- a/src/rootfs/files/scripts/setup_runtime_tools.sh +++ b/src/rootfs/files/scripts/setup_runtime_tools.sh @@ -17,17 +17,15 @@ function setup_runtime_tools() { printf '#!/bin/sh\nexit 101\n' > "${OUTPUTDIR}/usr/sbin/policy-rc.d" chmod +x "${OUTPUTDIR}/usr/sbin/policy-rc.d" - log_info "installing runtime packages into rootfs (python3, redis, mysql client, openssl, netcat, dns tools)" + log_info "installing runtime packages into rootfs (python3, mysql client, openssl, netcat, dns tools)" chroot "${OUTPUTDIR}" /usr/bin/apt update chroot "${OUTPUTDIR}" /usr/bin/apt install -y --no-install-recommends \ - mysql-client python3 python3-pip redis-server redis-sentinel redis-tools openssl netcat-openbsd dnsutils + mysql-client python3 python3-pip openssl netcat-openbsd dnsutils nano ncurses-term chroot "${OUTPUTDIR}" /usr/bin/apt clean log_info "installing Python runtime dependencies" chroot "${OUTPUTDIR}" /bin/bash -lc 'python3 -m pip install --break-system-packages SQLAlchemy PyMySQL requests redis' - log_info "ensuring redis data/log directories exist with proper ownership" - chroot "${OUTPUTDIR}" /bin/bash -lc 'mkdir -p /var/lib/redis /var/log/redis && chown -R redis:redis /var/lib/redis /var/log/redis && chmod 0750 /var/lib/redis' } chroot_init diff --git a/src/rootfs/files/scripts/template_configs_post_rke2install.sh b/src/rootfs/files/scripts/template_configs_post_rke2install.sh deleted file mode 100755 index 801de545..00000000 --- a/src/rootfs/files/scripts/template_configs_post_rke2install.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash - -# bash unofficial strict mode; -set -euo pipefail; - -# public, required -# OUTPUTDIR -# LOCAL_REGISTRY_HOST - -# private -BUILDROOT="/buildroot"; - -# init loggggging; -source "$BUILDROOT/files/scripts/log.sh"; - -function template_rke2_configs_postinstall() { - log_info "templating rke2 configs after install"; - mkdir -p "$OUTPUTDIR/etc/super/var/lib/rancher/rke2/agent/etc/containerd"; - envsubst \ - '$LOCAL_REGISTRY_HOST' \ - < "$BUILDROOT/files/configs/etc/super/var/lib/rancher/rke2/agent/etc/containerd/config.toml.tmpl.tmpl" \ - > "$OUTPUTDIR/etc/super/var/lib/rancher/rke2/agent/etc/containerd/config.toml.tmpl"; -} - -function append_to_files() { - log_info "appending to configs after rke2 install"; - cat \ - "$BUILDROOT/files/configs/usr/local/lib/systemd/system/rke2-server.env.append" \ - >> "$OUTPUTDIR/usr/local/lib/systemd/system/rke2-server.env"; - cat \ - "$BUILDROOT/files/configs/etc/multipath.conf.append" \ - >> "$OUTPUTDIR/etc/multipath.conf"; - cat \ - "$BUILDROOT/files/configs/etc/sysctl.conf.append" \ - >> "$OUTPUTDIR/etc/sysctl.conf"; -} - -function finalize_rke2() { - log_info "finalizing rke2 install"; - mkdir -p "$OUTPUTDIR/etc/kubernetes"; - mkdir -p "$OUTPUTDIR/etc/super/etc/iscsi"; - cp -a "$OUTPUTDIR/etc/iscsi/." "$OUTPUTDIR/etc/super/etc/iscsi/"; -} - -template_rke2_configs_postinstall; -append_to_files; -finalize_rke2; diff --git a/src/rootfs/files/scripts/template_rke2_configs_preinstall.sh b/src/rootfs/files/scripts/template_rke2_configs_preinstall.sh deleted file mode 100755 index 44472ba5..00000000 --- a/src/rootfs/files/scripts/template_rke2_configs_preinstall.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash - -# bash unofficial strict mode; -set -euo pipefail; - -# public, required -# OUTPUTDIR -# LOCAL_REGISTRY_HOST -# SUPER_REGISTRY_HOST - -# private -BUILDROOT="/buildroot"; - -# init loggggging; -source "$BUILDROOT/files/scripts/log.sh"; - -function check_args() { - if [[ -z "${LOCAL_REGISTRY_HOST:-""}" ]]; then - log_fail "LOCAL_REGISTRY_HOST is required"; - fi - if [[ -z "${SUPER_REGISTRY_HOST:-""}" ]]; then - log_fail "SUPER_REGISTRY_HOST is required"; - fi -} - -function template_rke2_configs_preinstall() { - log_info "templating rke2 configs before install"; - mkdir -p "$OUTPUTDIR/etc/rancher/rke2"; - NODENAME="$(cat "$OUTPUTDIR/etc/hostname")" \ - envsubst \ - '$LOCAL_REGISTRY_HOST,$NODENAME' \ - < "$BUILDROOT/files/configs/etc/rancher/rke2/config.yaml.tmpl" \ - > "$OUTPUTDIR/etc/rancher/rke2/config.yaml"; - envsubst \ - '$SUPER_REGISTRY_HOST,$LOCAL_REGISTRY_HOST' \ - < "$BUILDROOT/files/configs/etc/rancher/rke2/registries.yaml.tmpl" \ - > "$OUTPUTDIR/etc/rancher/rke2/registries.yaml"; -} - -check_args; -template_rke2_configs_preinstall; diff --git a/src/services/apps/mongodb/main.py b/src/services/apps/mongodb/main.py deleted file mode 100755 index b7f5d73c..00000000 --- a/src/services/apps/mongodb/main.py +++ /dev/null @@ -1,423 +0,0 @@ -#!/usr/bin/env python3 - -import sys -import os -import shutil -import subprocess -import json -import time -from pathlib import Path -from typing import Optional, Tuple, List, Dict - -from provision_plugin_sdk import ProvisionPlugin, PluginInput, PluginOutput - -# Configuration -MONGO_PORT = int(os.environ.get("MONGO_PORT", "27017")) -MONGO_CONFIG_FILE = Path("/etc/mongod.conf") -MONGO_DATA_DIR = Path("/var/lib/mongodb") -MONGO_LOG_DIR = Path("/var/log/mongodb") -REPLICA_SET_NAME = os.environ.get("MONGO_RS", "rs0") - -plugin = ProvisionPlugin() - -# Helpers -def get_node_tunnel_ip(node_id: str, wg_props: List[dict]) -> Optional[str]: - for prop in wg_props: - if prop.get("node_id") == node_id and prop.get("name") == "tunnel_ip": - return prop.get("value") - return None - -def check_all_nodes_have_wg(cluster_nodes: List[dict], wg_props: List[dict]) -> bool: - for node in cluster_nodes: - if not get_node_tunnel_ip(node.get("node_id"), wg_props): - return False - return True - -def is_rs_initialized(mongo_props: List[dict]) -> bool: - for prop in mongo_props: - if prop.get("name") == "mongodb_rs_initialized" and prop.get("value") == "true": - return True - return False - -def get_mongo_service_name() -> str: - # Prefer "mongod", fallback to "mongodb" - try: - res = subprocess.run(["systemctl", "status", "mongod"], capture_output=True, text=True) - if res.returncode in (0, 3): # active or inactive - return "mongod" - except Exception: - pass - try: - res = subprocess.run(["systemctl", "status", "mongodb"], capture_output=True, text=True) - if res.returncode in (0, 3): - return "mongodb" - except Exception: - pass - return "mongod" - -def is_mongo_available() -> bool: - return shutil.which("mongod") is not None - -def install_mongodb(): - # Try installing via apt (best effort, Ubuntu expected) - if not os.path.exists("/etc/os-release"): - raise Exception("Cannot detect OS: /etc/os-release not found") - with open("/etc/os-release", "r") as f: - os_release = f.read().lower() - if "ubuntu" not in os_release: - raise Exception("Unsupported OS for MongoDB installation") - - # Update and try packages commonly available - res = subprocess.run(["apt-get", "update"], capture_output=True, text=True) - if res.returncode != 0: - raise Exception(f"apt-get update failed: {res.stderr}") - - # Prefer 'mongodb' first (may exist in Ubuntu repos), fallback to 'mongodb-org' (requires repo) - for pkg in (["mongodb"], ["mongodb-org"]): - res = subprocess.run(["apt-get", "install", "-y", *pkg], capture_output=True, text=True) - if res.returncode == 0: - return - raise Exception("Failed to install MongoDB via apt (mongodb, mongodb-org)") - -def write_mongod_config(bind_ip: str): - MONGO_DATA_DIR.mkdir(parents=True, exist_ok=True) - MONGO_LOG_DIR.mkdir(parents=True, exist_ok=True) - # Minimal YAML config - cfg = f"""# managed by provision plugin -storage: - dbPath: {str(MONGO_DATA_DIR)} -systemLog: - destination: file - logAppend: true - path: {str(MONGO_LOG_DIR)}/mongod.log -net: - bindIp: 127.0.0.1,{bind_ip} - port: {MONGO_PORT} -replication: - replSetName: {REPLICA_SET_NAME} -processManagement: - timeZoneInfo: /usr/share/zoneinfo -""" - MONGO_CONFIG_FILE.write_text(cfg) - -def ensure_runtime_dirs(): - try: - # Ensure data, log and runtime dirs exist and owned by mongodb - MONGO_DATA_DIR.mkdir(parents=True, exist_ok=True) - MONGO_LOG_DIR.mkdir(parents=True, exist_ok=True) - run_dir = Path("/run/mongodb") - run_dir.mkdir(parents=True, exist_ok=True) - try: - shutil.chown(str(MONGO_DATA_DIR), user="mongodb", group="mongodb") - shutil.chown(str(MONGO_LOG_DIR), user="mongodb", group="mongodb") - shutil.chown(str(run_dir), user="mongodb", group="mongodb") - except Exception: - # If user/group not present or chown fails, ignore; systemd tmpfiles may fix it - pass - except Exception: - pass - -def capture_mongo_diagnostics(svc: str) -> str: - parts: List[str] = [] - try: - res = subprocess.run(["systemctl", "status", svc, "--no-pager"], capture_output=True, text=True, timeout=10) - parts.append(f"systemctl status {svc}:\n{(res.stdout or '')}\n{(res.stderr or '')}") - except Exception as e: - parts.append(f"systemctl status {svc} error: {e}") - try: - res = subprocess.run(["journalctl", "-u", svc, "-n", "200", "--no-pager"], capture_output=True, text=True, timeout=10) - parts.append(f"journalctl -u {svc} -n 200:\n{(res.stdout or '')}\n{(res.stderr or '')}") - except Exception as e: - parts.append(f"journalctl fetch error: {e}") - try: - log_path = MONGO_LOG_DIR / "mongod.log" - if log_path.exists(): - with open(log_path, "r") as f: - lines = f.readlines()[-200:] - parts.append("tail -n 200 /var/log/mongodb/mongod.log:\n" + "".join(lines)) - else: - parts.append("mongod.log not found at /var/log/mongodb/mongod.log") - except Exception as e: - parts.append(f"read mongod.log error: {e}") - return "\n\n".join(parts) - -def mongo_shell_binary() -> Optional[str]: - for b in ("mongosh", "mongo"): - if shutil.which(b): - return b - return None - -def mongo_eval_json(host: str, js: str, timeout: int = 10) -> Tuple[bool, Optional[dict], Optional[str]]: - """ - Execute JS and try to parse JSON result. We wrap the expression to JSON.stringify(). - """ - bin_ = mongo_shell_binary() - if not bin_: - return False, None, "No mongo shell (mongosh or mongo) found" - cmd = [ - bin_, - f"mongodb://{host}:{MONGO_PORT}/admin", - "--quiet", - "--eval", - f"try {{ let r=({js}); r = (r===undefined)? {{ok:1}} : r; print(JSON.stringify(r)); }} catch(e) {{ print(JSON.stringify({{ok:0, error:''+e}})); }}" - ] - try: - res = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout) - out = (res.stdout or "").strip().splitlines() - line = out[-1] if out else "" - try: - obj = json.loads(line) if line else None - except Exception: - obj = None - ok = res.returncode == 0 and isinstance(obj, dict) - return ok, obj, res.stderr - except Exception as e: - return False, None, str(e) - -def wait_for_mongo_ready(host: str, timeout_sec: int = 60) -> bool: - start = time.time() - # If mongo shell not available, fallback to checking TCP port open - if not mongo_shell_binary(): - import socket - while time.time() - start < timeout_sec: - try: - with socket.create_connection((host, MONGO_PORT), timeout=2): - return True - except Exception: - time.sleep(2) - return False - while time.time() - start < timeout_sec: - ok, obj, _ = mongo_eval_json(host, "db.runCommand({ping:1})", timeout=5) - if ok and obj and obj.get("ok") == 1: - return True - time.sleep(2) - return False - -def is_mongo_running() -> Tuple[bool, Optional[str]]: - try: - svc = get_mongo_service_name() - res = subprocess.run(["systemctl", "is-active", svc], capture_output=True, text=True) - active = res.stdout.strip() == "active" - return active, None if active else f"Service status: {res.stdout.strip()}" - except Exception as e: - return False, f"Failed to check service status: {str(e)}" - -def rs_status(host: str) -> Tuple[Optional[dict], Optional[str]]: - ok, obj, err = mongo_eval_json(host, "rs.status()", timeout=10) - if ok and obj: - return obj, None - return None, err - -def rs_initiate(host: str, members_hosts: List[str]) -> bool: - members = [{"_id": i, "host": h} for i, h in enumerate(members_hosts)] - js = f'rs.initiate({{ _id: "{REPLICA_SET_NAME}", members: {json.dumps(members)} }})' - ok, obj, _ = mongo_eval_json(host, js, timeout=20) - return bool(ok and obj and obj.get("ok") == 1) - -def rs_add_missing(host: str, desired_hosts: List[str]) -> None: - ok, current, _ = mongo_eval_json(host, "rs.conf()", timeout=10) - if not ok or not isinstance(current, dict): - return - cfg = current - existing_hosts = set() - for m in (cfg.get("members") or []): - h = m.get("host") - if h: - existing_hosts.add(h) - for h in desired_hosts: - if h not in existing_hosts: - mongo_eval_json(host, f'rs.add("{h}")', timeout=15) - -# Commands -@plugin.command("init") -def handle_init(input_data: PluginInput) -> PluginOutput: - try: - if not is_mongo_available(): - install_mongodb() - MONGO_LOG_DIR.mkdir(parents=True, exist_ok=True) - return PluginOutput(status="completed", local_state=input_data.local_state) - except Exception as e: - return PluginOutput(status="error", error_message=str(e), local_state=input_data.local_state) - -@plugin.command("apply") -def handle_apply(input_data: PluginInput) -> PluginOutput: - local_node_id = input_data.local_node_id - state_json = input_data.state or {} - local_state = input_data.local_state or {} - - if not isinstance(state_json, dict): - return PluginOutput(status="error", error_message="Invalid state format", local_state=local_state) - - cluster_nodes = state_json.get("clusterNodes", []) - mongo_props = state_json.get("mongodbNodeProperties", []) - wg_props = state_json.get("wgNodeProperties", []) - - if not check_all_nodes_have_wg(cluster_nodes, wg_props): - return PluginOutput(status="postponed", error_message="Waiting for WireGuard to be configured on all nodes", local_state=local_state) - - # Determine leader - cluster = state_json.get("cluster", {}) - leader_node_id = cluster.get("leader_node") - is_leader = leader_node_id == local_node_id - initialized = is_rs_initialized(mongo_props) - - local_tunnel_ip = get_node_tunnel_ip(local_node_id, wg_props) - if not local_tunnel_ip: - return PluginOutput(status="error", error_message="Local node has no WireGuard tunnel IP", local_state=local_state) - - # Write config bound to WG IP with replication enabled - try: - write_mongod_config(local_tunnel_ip) - except Exception as e: - return PluginOutput(status="error", error_message=f"Failed to write mongod config: {e}", local_state=local_state) - - # Ensure service is running on correct IP - ensure_runtime_dirs() - needs_restart = False - running, _ = is_mongo_running() - if not running: - needs_restart = True - else: - # best-effort ping on WG IP - if not wait_for_mongo_ready(local_tunnel_ip, timeout_sec=5): - needs_restart = True - - if needs_restart: - try: - svc = get_mongo_service_name() - subprocess.run(["systemctl", "daemon-reload"], capture_output=True, text=True) - subprocess.run(["systemctl", "enable", svc], capture_output=True, text=True) - res = subprocess.run(["systemctl", "restart", svc], capture_output=True, text=True, timeout=30) - if res.returncode != 0: - diag = capture_mongo_diagnostics(svc) - return PluginOutput(status="error", error_message=f"Failed to start {svc}: {res.stderr}\n\n{diag}", local_state=local_state) - except Exception as e: - svc = "mongod" - diag = capture_mongo_diagnostics(svc) - return PluginOutput(status="error", error_message=f"Failed to start mongod: {e}\n\n{diag}", local_state=local_state) - - if not wait_for_mongo_ready(local_tunnel_ip, timeout_sec=60): - node_props = {"mongodb_node_ready": "false"} - svc = get_mongo_service_name() - diag = capture_mongo_diagnostics(svc) - return PluginOutput(status="postponed", error_message=f"mongod not ready yet\n\n{diag}", node_properties=node_props, local_state=local_state) - - # At this point local mongod is up - node_ready_props = {"mongodb_node_ready": "true"} - - # Leader initializes or updates the replica set - # Always configure a replica set even with a single node - if is_leader and not initialized: - # Build desired members from all cluster nodes (their WG IPs) - desired_hosts = [] - for n in cluster_nodes: - ip = get_node_tunnel_ip(n.get("node_id"), wg_props) - if ip: - desired_hosts.append(f"{ip}:{MONGO_PORT}") - - # If multiple nodes, wait until all have mongod ready before initiating - if len(cluster_nodes) > 1: - not_ready = [] - for n in cluster_nodes: - nid = n.get("node_id") - ready = False - for p in mongo_props: - if p.get("node_id") == nid and p.get("name") == "mongodb_node_ready" and p.get("value") == "true": - ready = True - break - if not ready: - not_ready.append(nid) - if not_ready: - return PluginOutput( - status="postponed", - error_message=f"Waiting for nodes to be ready: {', '.join(not_ready)}", - node_properties=node_ready_props, - local_state=local_state - ) - - # Initiate replica set (single or multi-node) - if rs_initiate(local_tunnel_ip, desired_hosts): - # Give it a moment to elect primary - time.sleep(3) - done_props = {"mongodb_rs_initialized": "true", **node_ready_props} - return PluginOutput(status="completed", node_properties=done_props, local_state=local_state) - else: - return PluginOutput(status="postponed", error_message="Failed to initiate replica set", node_properties=node_ready_props, local_state=local_state) - - # If already initialized, leader may add missing members - if is_leader and initialized: - desired_hosts = [] - for n in cluster_nodes: - ip = get_node_tunnel_ip(n.get("node_id"), wg_props) - if ip: - desired_hosts.append(f"{ip}:{MONGO_PORT}") - try: - rs_add_missing(local_tunnel_ip, desired_hosts) - except Exception: - pass - - # Non-leader or after init: ensure local node reports ready - return PluginOutput(status="completed" if initialized else "postponed", - error_message=None if initialized else f"Waiting for leader node {leader_node_id} to initialize replica set", - node_properties=node_ready_props, - local_state=local_state) - -@plugin.command("health") -def handle_health(input_data: PluginInput) -> PluginOutput: - state_json = input_data.state or {} - local_state = input_data.local_state or {} - local_node_id = input_data.local_node_id - - running, err = is_mongo_running() - if not running: - if err and "Failed to" in err: - return PluginOutput(status="error", error_message=err, local_state=local_state) - return PluginOutput(status="postponed", error_message=err or "mongod not running", local_state=local_state) - - wg_props = state_json.get("wgNodeProperties", []) if isinstance(state_json, dict) else [] - ip = get_node_tunnel_ip(local_node_id, wg_props) - if not ip: - return PluginOutput(status="postponed", error_message="No tunnel IP available", local_state=local_state) - - if not wait_for_mongo_ready(ip, timeout_sec=5): - return PluginOutput(status="postponed", error_message="MongoDB ping failed", local_state=local_state) - - # Check rs.status() ok if initialized - st, _ = rs_status(ip) - if st and st.get("ok") == 1: - return PluginOutput(status="completed", local_state=local_state) - # If not initialized yet, still healthy if process is running - return PluginOutput(status="postponed", error_message="Replica set not healthy/initialized yet", local_state=local_state) - -@plugin.command("finalize") -def handle_finalize(input_data: PluginInput) -> PluginOutput: - # No-op for now; graceful removal could be implemented (step down, remove member, etc.) - return PluginOutput(status="completed", local_state=input_data.local_state or {}) - -@plugin.command("destroy") -def handle_destroy(input_data: PluginInput) -> PluginOutput: - try: - svc = get_mongo_service_name() - subprocess.run(["systemctl", "stop", svc], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - subprocess.run(["systemctl", "disable", svc], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - - if MONGO_CONFIG_FILE.exists(): - try: - MONGO_CONFIG_FILE.unlink() - except Exception: - pass - if MONGO_DATA_DIR.exists(): - shutil.rmtree(MONGO_DATA_DIR, ignore_errors=True) - if MONGO_LOG_DIR.exists(): - shutil.rmtree(MONGO_LOG_DIR, ignore_errors=True) - - node_properties = { - "mongodb_node_ready": None, - "mongodb_rs_initialized": None, - } - return PluginOutput(status="completed", node_properties=node_properties, local_state={}) - except Exception as e: - return PluginOutput(status="error", error_message=f"Failed to destroy MongoDB: {e}", local_state={}) - -if __name__ == "__main__": - plugin.run() diff --git a/src/services/apps/mongodb/manifest.yaml b/src/services/apps/mongodb/manifest.yaml deleted file mode 100644 index 5923c799..00000000 --- a/src/services/apps/mongodb/manifest.yaml +++ /dev/null @@ -1,73 +0,0 @@ -name: mongodb -version: 1.0.0 -commands: - - init - - apply - - health - - finalize - - destroy -healthcheckIntervalSecs: 60 -entrypoint: main.py -stateExpr: - engine: jq - query: | - ($swarmdb.clusters[] | select(.id == "{{ clusterId }}" and .deleted_ts == null)) as $cluster | - - ([$swarmdb.clusternodes[] | select(.cluster == "{{ clusterId }}" and .deleted_ts == null)]) as $mongoClusterNodes | - - ($mongoClusterNodes | map(.node)) as $mongoNodeIds | - - ( - $swarmdb.clusters[] | - select(.cluster_policy == "wireguard" and .deleted_ts == null) | - select( - ( - [$swarmdb.clusternodes[] | select(.deleted_ts == null and (.node | IN($mongoNodeIds[])))] | - length > 0 - ) - ) - ) as $wgCluster | - - { - cluster: { - id: $cluster.id, - cluster_policy: $cluster.cluster_policy, - leader_node: $cluster.leader_node - }, - - clusterNodes: [ - $mongoClusterNodes[] | - {id, node_id: .node, cluster} - ] | sort_by(.id, .node_id, .cluster), - - mongodbNodeProperties: [ - $swarmdb.clusternodeproperties[] | - select( - (.cluster_node | startswith("{{ clusterId }}:")) and - .deleted_ts == null and - (.name | startswith("mongodb_")) - ) | - {cluster_node, name, value, node_id: ( .cluster_node as $cn | $swarmdb.clusternodes[] | select(.id == $cn)) | .node} - ] | sort_by(.cluster_node, .name, .value, .node_id), - - wgCluster: { - id: $wgCluster.id - }, - - wgNodeProperties: [ - $swarmdb.clusternodeproperties[] | - select( - (.cluster_node | startswith($wgCluster.id)) and - .deleted_ts == null and - .name == "tunnel_ip" - ) | - {cluster_node, name, value, node_id: ( .cluster_node as $cn | $swarmdb.clusternodes[] | select(.id == $cn)) | .node} - ] | sort_by(.cluster_node, .name, .value, .node_id), - - nodeAddrs: [ - $swarmdb.nodes[] | - select(.node_id | IN($mongoNodeIds[])) | - {node_id: .node_id, addr: .addr, port: .port} - ] | sort_by(.node_id, .addr, .port) - } - diff --git a/src/services/apps/nats/main.py b/src/services/apps/nats/main.py deleted file mode 100644 index ef2bb628..00000000 --- a/src/services/apps/nats/main.py +++ /dev/null @@ -1,327 +0,0 @@ -#!/usr/bin/env python3 - -import sys -import os -import shutil -import subprocess -import socket -import time -from pathlib import Path -from typing import Optional - -from provision_plugin_sdk import ProvisionPlugin, PluginInput, PluginOutput -import pwd -import grp - -# Configuration -NATS_VERSION = os.environ.get("NATS_VERSION", "2") # informational -NATS_CLIENT_PORT = 4222 -NATS_CLUSTER_PORT = 6222 -NATS_MONITOR_PORT = 8222 -NATS_CONFIG_DIR = Path("/etc/nats") -NATS_CONFIG_FILE = NATS_CONFIG_DIR / "nats-server.conf" -NATS_DATA_DIR = Path("/var/lib/nats") -NATS_SERVICE_NAME = "nats-server" -NATS_BIN = "nats-server" -CLUSTER_NAME = os.environ.get("NATS_CLUSTER_NAME", "swarm-nats") - -# Plugin setup -plugin = ProvisionPlugin() - - -# Helpers -def _get_uid_gid(user: str, group: str) -> tuple[int, int]: - """Return uid,gid for user/group, fallback to 0 if not found.""" - try: - uid = pwd.getpwnam(user).pw_uid - except KeyError: - uid = 0 - try: - gid = grp.getgrnam(group).gr_gid - except KeyError: - gid = 0 - return uid, gid - - -def _ensure_dir_owned(path: Path, user: str, group: str, mode: int = 0o750) -> None: - """Ensure directory exists with given owner and mode.""" - path.mkdir(parents=True, exist_ok=True) - uid, gid = _get_uid_gid(user, group) - try: - os.chown(path.as_posix(), uid, gid) - except PermissionError: - # Not fatal; service may still have access if perms allow - pass - try: - os.chmod(path.as_posix(), mode) - except PermissionError: - pass - - -def get_node_tunnel_ip(node_id: str, wg_props: list) -> Optional[str]: - for prop in wg_props: - if prop.get("node_id") == node_id and prop.get("name") == "tunnel_ip": - return prop.get("value") - return None - - -def check_all_nodes_have_wg(cluster_nodes: list, wg_props: list) -> bool: - for node in cluster_nodes: - node_id = node.get("node_id") - if not get_node_tunnel_ip(node_id, wg_props): - return False - return True - - -def get_leader_node(state_json: dict) -> Optional[str]: - cluster = state_json.get("cluster", {}) - return cluster.get("leader_node") - - -def is_nats_available() -> bool: - return shutil.which(NATS_BIN) is not None - - -def install_nats(): - try: - if not os.path.exists("/etc/os-release"): - raise Exception("Cannot detect OS: /etc/os-release not found") - with open("/etc/os-release", "r") as f: - os_release = f.read() - if "ubuntu" in os_release.lower(): - r = subprocess.run(["apt-get", "update"], capture_output=True, text=True) - if r.returncode != 0: - raise Exception(f"apt-get update failed: {r.stderr}") - # Use distro package if available - r = subprocess.run(["apt-get", "install", "-y", "nats-server"], capture_output=True, text=True) - if r.returncode != 0: - raise Exception(f"nats-server installation failed: {r.stderr}") - return - raise Exception("Unsupported OS for NATS installation") - except Exception as e: - print(f"[!] Failed to install NATS: {e}", file=sys.stderr) - raise - - -def write_nats_config(local_node_id: str, local_tunnel_ip: str, cluster_nodes: list, wg_props: list): - NATS_CONFIG_DIR.mkdir(parents=True, exist_ok=True) - # Ensure data dir is owned by nats so JetStream can create subdirs - _ensure_dir_owned(NATS_DATA_DIR, user="nats", group="nats", mode=0o750) - - # Build routes for all peers except self - routes = [] - for node in cluster_nodes: - nid = node.get("node_id") - t_ip = get_node_tunnel_ip(nid, wg_props) - if not t_ip: - continue - if nid == local_node_id: - continue - routes.append(f"nats://{t_ip}:{NATS_CLUSTER_PORT}") - - cfg_lines = [ - f"port: {NATS_CLIENT_PORT}", - f"http: {NATS_MONITOR_PORT}", - f"host: {local_tunnel_ip}", - "", - "jetstream: {", - f" store_dir: \"{(NATS_DATA_DIR / 'jetstream').as_posix()}\"", - "}", - f"server_name: {local_node_id}", - ] - # Only enable clustering when there are peers to route to. - if routes: - cfg_lines += [ - "", - "cluster: {", - f" name: {CLUSTER_NAME},", - f" host: {local_tunnel_ip},", - f" port: {NATS_CLUSTER_PORT},", - " routes: [", - ] - for r in routes: - cfg_lines.append(f' "{r}",') - cfg_lines += [ - " ]", - "}", - ] - cfg_lines += [ - "", - "resolver: memory", - "no_auth_user: ''", - ] - - NATS_CONFIG_FILE.write_text("\n".join(cfg_lines) + "\n") - - -def wait_for_tcp(ip: str, port: int, timeout_sec: int = 60) -> bool: - start = time.time() - last_err = None - while time.time() - start < timeout_sec: - try: - with socket.create_connection((ip, port), timeout=3): - return True - except Exception as e: - last_err = str(e) - time.sleep(2) - print(f"[!] Port {ip}:{port} not reachable within {timeout_sec}s. Last error: {last_err}", file=sys.stderr) - return False - - -def is_service_active(service: str) -> tuple[bool, Optional[str]]: - try: - result = subprocess.run(["systemctl", "is-active", service], capture_output=True, text=True) - active = result.stdout.strip() == "active" - return active, None if active else f"Service status: {result.stdout.strip()}" - except Exception as e: - return False, f"Failed to check service status: {str(e)}" - - -def is_cluster_initialized(nats_props: list) -> bool: - for prop in nats_props: - if prop.get("name") == "nats_cluster_initialized" and prop.get("value") == "true": - return True - return False - - -def mark_node_ready() -> dict: - return {"nats_node_ready": "true"} - - -# Commands -@plugin.command("init") -def handle_init(input_data: PluginInput) -> PluginOutput: - try: - if not is_nats_available(): - install_nats() - # Ensure runtime dirs exist and owned by nats - _ensure_dir_owned(Path("/var/log/nats"), user="nats", group="nats", mode=0o750) - _ensure_dir_owned(NATS_DATA_DIR, user="nats", group="nats", mode=0o750) - return PluginOutput(status="completed", local_state=input_data.local_state) - except Exception as e: - return PluginOutput(status="error", error_message=str(e), local_state=input_data.local_state) - - -@plugin.command("apply") -def handle_apply(input_data: PluginInput) -> PluginOutput: - local_node_id = input_data.local_node_id - state_json = input_data.state or {} - local_state = input_data.local_state or {} - - if not isinstance(state_json, dict): - return PluginOutput(status="error", error_message="Invalid state format", local_state=local_state) - - cluster_nodes = state_json.get("clusterNodes", []) - wg_props = state_json.get("wgNodeProperties", []) - nats_props = state_json.get("natsNodeProperties", []) - - if not check_all_nodes_have_wg(cluster_nodes, wg_props): - return PluginOutput( - status="postponed", - error_message="Waiting for WireGuard to be configured on all nodes", - local_state=local_state, - ) - - leader_node_id = get_leader_node(state_json) - is_leader = (leader_node_id == local_node_id) - cluster_initialized = is_cluster_initialized(nats_props) - - local_tunnel_ip = get_node_tunnel_ip(local_node_id, wg_props) - if not local_tunnel_ip: - return PluginOutput(status="error", error_message="Local node has no WireGuard tunnel IP", local_state=local_state) - - # Write NATS config based on current cluster view - try: - write_nats_config(local_node_id, local_tunnel_ip, cluster_nodes, wg_props) - except Exception as e: - return PluginOutput(status="error", error_message=f"Failed to write NATS config: {e}", local_state=local_state) - - # Enable and (re)start service if needed - active, _ = is_service_active(NATS_SERVICE_NAME) - needs_restart = not active - - try: - subprocess.run(["systemctl", "enable", NATS_SERVICE_NAME], capture_output=True, text=True) - result = subprocess.run(["systemctl", "restart", NATS_SERVICE_NAME], capture_output=True, text=True) - if result.returncode != 0: - return PluginOutput(status="error", error_message=f"Failed to start NATS: {result.stderr}", local_state=local_state) - except Exception as e: - return PluginOutput(status="error", error_message=f"Failed to start NATS: {e}", local_state=local_state) - - # Wait for client port to be ready - if not wait_for_tcp(local_tunnel_ip, NATS_CLIENT_PORT, timeout_sec=60): - return PluginOutput( - status="postponed", - error_message="NATS did not become ready within timeout", - node_properties=mark_node_ready(), - local_state=local_state, - ) - - # Leader marks cluster initialized (NATS clustering forms via routes automatically) - if is_leader and not cluster_initialized: - node_properties = {"nats_cluster_initialized": "true", "nats_node_ready": "true"} - return PluginOutput(status="completed", node_properties=node_properties, local_state=local_state) - - if cluster_initialized: - # Already initialized — ensure this node is marked ready - return PluginOutput(status="completed", node_properties=mark_node_ready(), local_state=local_state) - - # Non-leader: mark ready and wait for leader - return PluginOutput( - status="postponed", - error_message=f"Waiting for leader node {leader_node_id} to mark cluster initialized", - node_properties=mark_node_ready(), - local_state=local_state, - ) - - -@plugin.command("health") -def handle_health(input_data: PluginInput) -> PluginOutput: - local_node_id = input_data.local_node_id - state_json = input_data.state or {} - local_state = input_data.local_state or {} - - active, err = is_service_active(NATS_SERVICE_NAME) - if not active: - if err and "Failed to" in err: - return PluginOutput(status="error", error_message=err, local_state=local_state) - return PluginOutput(status="postponed", error_message=err or "NATS service is not running", local_state=local_state) - - wg_props = state_json.get("wgNodeProperties", []) if isinstance(state_json, dict) else [] - local_tunnel_ip = get_node_tunnel_ip(local_node_id, wg_props) - if not local_tunnel_ip: - return PluginOutput(status="postponed", error_message="No tunnel IP available", local_state=local_state) - - # Check TCP connectivity to client port - if not wait_for_tcp(local_tunnel_ip, NATS_CLIENT_PORT, timeout_sec=5): - return PluginOutput(status="postponed", error_message="NATS not accepting connections yet", local_state=local_state) - - return PluginOutput(status="completed", local_state=local_state) - - -@plugin.command("finalize") -def handle_finalize(input_data: PluginInput) -> PluginOutput: - # No-op for now; could implement graceful cluster changes if needed - return PluginOutput(status="completed", local_state=input_data.local_state or {}) - - -@plugin.command("destroy") -def handle_destroy(input_data: PluginInput) -> PluginOutput: - try: - subprocess.run(["systemctl", "stop", NATS_SERVICE_NAME], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - subprocess.run(["systemctl", "disable", NATS_SERVICE_NAME], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - if NATS_CONFIG_DIR.exists(): - shutil.rmtree(NATS_CONFIG_DIR, ignore_errors=True) - if NATS_DATA_DIR.exists(): - shutil.rmtree(NATS_DATA_DIR, ignore_errors=True) - node_properties = { - "nats_node_ready": None, - "nats_cluster_initialized": None, - } - return PluginOutput(status="completed", node_properties=node_properties, local_state={}) - except Exception as e: - return PluginOutput(status="error", error_message=f"Failed to destroy NATS: {e}", local_state={}) - - -if __name__ == "__main__": - plugin.run() diff --git a/src/services/apps/nats/manifest.yaml b/src/services/apps/nats/manifest.yaml deleted file mode 100644 index cd12ea2d..00000000 --- a/src/services/apps/nats/manifest.yaml +++ /dev/null @@ -1,74 +0,0 @@ -name: nats -version: 1.0.0 -commands: - - init - - apply - - health - - finalize - - destroy -healthcheckIntervalSecs: 60 -entrypoint: main.py -stateExpr: - engine: jq - query: | - ($swarmdb.clusters[] | select(.id == "{{ clusterId }}" and .deleted_ts == null)) as $cluster | - - ([$swarmdb.clusternodes[] | select(.cluster == "{{ clusterId }}" and .deleted_ts == null)]) as $natsClusterNodes | - - ($natsClusterNodes | map(.node)) as $natsNodeIds | - - ( - $swarmdb.clusters[] | - select(.cluster_policy == "wireguard" and .deleted_ts == null) | - select( - ( - [$swarmdb.clusternodes[] | select(.deleted_ts == null and (.node | IN($natsNodeIds[])))] | - length > 0 - ) - ) - ) as $wgCluster | - - { - cluster: { - id: $cluster.id, - cluster_policy: $cluster.cluster_policy, - leader_node: $cluster.leader_node - }, - - clusterNodes: [ - $natsClusterNodes[] | - {id, node_id: .node, cluster} - ] | sort_by(.id, .node_id, .cluster), - - natsNodeProperties: [ - $swarmdb.clusternodeproperties[] | - select( - (.cluster_node | startswith("{{ clusterId }}:")) and - .deleted_ts == null and - (.name | startswith("nats_")) - ) | - {cluster_node, name, value, node_id: ( .cluster_node as $cn | $swarmdb.clusternodes[] | select(.id == $cn)) | .node} - ] | sort_by(.cluster_node, .name, .value, .node_id), - - wgCluster: { - id: $wgCluster.id - }, - - wgNodeProperties: [ - $swarmdb.clusternodeproperties[] | - select( - (.cluster_node | startswith($wgCluster.id)) and - .deleted_ts == null and - .name == "tunnel_ip" - ) | - {cluster_node, name, value, node_id: ( .cluster_node as $cn | $swarmdb.clusternodes[] | select(.id == $cn)) | .node} - ] | sort_by(.cluster_node, .name, .value, .node_id), - - nodeAddrs: [ - $swarmdb.nodes[] | - select(.node_id | IN($natsNodeIds[])) | - {node_id: .node_id, addr: .addr, port: .port} - ] | sort_by(.node_id, .addr, .port) - } - - diff --git a/src/services/apps/test-app-route/main.py b/src/services/apps/test-app-route/main.py deleted file mode 100644 index f7c2bddb..00000000 --- a/src/services/apps/test-app-route/main.py +++ /dev/null @@ -1,409 +0,0 @@ -#!/usr/bin/env python3 - -import json -import sys -import time -from typing import List, Tuple, Optional - -from provision_plugin_sdk import ProvisionPlugin, PluginInput, PluginOutput - - -ROUTE_DOMAIN = "test.test.oresty.superprotocol.io" -ROUTE_KEY = f"manual-routes:{ROUTE_DOMAIN}" -APP_PORT = 34567 - - -plugin = ProvisionPlugin() - - -def get_node_tunnel_ip(node_id: str, wg_props: list) -> Optional[str]: - """Get WireGuard tunnel IP for a node.""" - for prop in wg_props: - if prop.get("node_id") == node_id and prop.get("name") == "tunnel_ip": - return prop.get("value") - return None - - -def get_leader_node(state_json: dict) -> Optional[str]: - """Get leader node ID from cluster info.""" - cluster = state_json.get("cluster", {}) - return cluster.get("leader_node") - - -def is_local_node_leader(local_node_id: str, state_json: dict) -> bool: - """Check if the local node is the cluster leader.""" - return get_leader_node(state_json) == local_node_id - - -def get_sentinel_connection_info(state_json: dict) -> List[Tuple[str, int]]: - """Get Redis Sentinel connection endpoints.""" - sentinel_props = state_json.get("sentinelNodeProperties", []) - wg_props = state_json.get("sentinelWgNodeProperties", []) or state_json.get("wgNodeProperties", []) - - endpoints: List[Tuple[str, int]] = [] - for prop in sentinel_props: - if prop.get("name") != "redis_sentinel_node_ready" or prop.get("value") != "true": - continue - node_id = prop.get("node_id") - if not node_id: - continue - tunnel_ip = get_node_tunnel_ip(node_id, wg_props) - if tunnel_ip: - endpoints.append((tunnel_ip, 26379)) - - return sorted(set(endpoints)) - - -def get_redis_master_endpoint(state_json: dict) -> Tuple[Tuple[str, int] | None, str | None]: - """Resolve Redis master via Sentinel.""" - sentinel_endpoints = get_sentinel_connection_info(state_json) - if not sentinel_endpoints: - return None, "No Redis Sentinel endpoints available" - - try: - import redis - except ImportError: - return None, "redis-py library not installed" - - last_error: str | None = None - for host, port in sentinel_endpoints: - try: - r = redis.Redis( - host=host, - port=port, - decode_responses=True, - socket_connect_timeout=2, - ) - res = r.execute_command("SENTINEL", "get-master-addr-by-name", "redis-master") - if isinstance(res, (list, tuple)) and len(res) >= 2: - return (res[0], int(res[1])), None - except Exception as e: - last_error = f"Sentinel {host}:{port} error: {e}" - - return None, last_error or "Failed to resolve Redis master via Sentinel" - - -def ensure_route_in_redis(state_json: dict) -> Tuple[bool, str | None]: - """Create or update the OpenResty route in Redis Cluster. - - Targets are derived from Swarm state: - - For each node in the test-app cluster (clusterNodes), - we find its WireGuard tunnel IP via wgNodeProperties. - - Each such IP becomes a backend URL http://:APP_PORT. - """ - master_endpoint, err = get_redis_master_endpoint(state_json) - if not master_endpoint: - msg = err or "No Redis master endpoint available" - print(f"[!] {msg}", file=sys.stderr) - return False, msg - - cluster_nodes = state_json.get("clusterNodes", []) - wg_props = state_json.get("wgNodeProperties", []) - - # Collect tunnel IPs of all nodes that run test-app - tunnel_ips: List[str] = [] - for node in cluster_nodes: - node_id = node.get("node_id") - ip = get_node_tunnel_ip(node_id, wg_props) - if ip: - tunnel_ips.append(ip) - - if not tunnel_ips: - msg = "No WireGuard tunnel IPs available for test-app nodes" - print(f"[!] {msg}", file=sys.stderr) - return False, msg - - try: - import redis - except ImportError: - return False, "redis-py library not installed" - - # A few retries in case Sentinel/Redis is still converging or there are - # short-lived connectivity issues. - max_retries = 20 - retry_delay_sec = 5 - last_error: str | None = None - - for attempt in range(1, max_retries + 1): - try: - print( - f"[*] Attempt {attempt}/{max_retries} to set route {ROUTE_KEY} " - f"in Redis via master={master_endpoint}", - file=sys.stderr, - ) - host, port = master_endpoint - r = redis.Redis( - host=host, - port=port, - decode_responses=True, - socket_connect_timeout=5, - ) - r.ping() - - route_config = { - "targets": [ - {"url": f"http://{ip}:{APP_PORT}", "weight": 1} - for ip in tunnel_ips - ], - "policy": "rr", - "preserve_host": False, - } - - r.set(ROUTE_KEY, json.dumps(route_config)) - print( - f"[*] Successfully set route {ROUTE_KEY} -> {json.dumps(route_config)} " - f"in Redis on attempt {attempt}", - file=sys.stderr, - ) - return True, None - except Exception as e: - last_error = f"Failed to write route to Redis on attempt {attempt}: {e}" - print(f"[!] {last_error}", file=sys.stderr) - - if attempt < max_retries: - time.sleep(retry_delay_sec) - - # All attempts failed - return False, last_error or "Failed to write route to Redis after retries" - - -def delete_route_from_redis(state_json: dict) -> Tuple[bool, str | None]: - """Delete the OpenResty route from Redis Cluster.""" - master_endpoint, err = get_redis_master_endpoint(state_json) - if not master_endpoint: - return False, err or "No Redis master endpoint available" - - try: - import redis - except ImportError: - return False, "redis-py library not installed" - - try: - host, port = master_endpoint - r = redis.Redis( - host=host, - port=port, - decode_responses=True, - socket_connect_timeout=5, - ) - r.delete(ROUTE_KEY) - print(f"[*] Deleted route {ROUTE_KEY} from Redis", file=sys.stderr) - return True, None - except Exception as e: - error_msg = f"Failed to delete route from Redis: {str(e)}" - print(f"[!] {error_msg}", file=sys.stderr) - return False, error_msg - - -@plugin.command("init") -def handle_init(input_data: PluginInput) -> PluginOutput: - """Wait until Redis is reachable (for leader); non-leaders are no-op.""" - local_state = input_data.local_state or {} - state_json = input_data.state or {} - - if not isinstance(state_json, dict): - return PluginOutput( - status="error", - error_message="Invalid state format in init", - local_state=local_state, - ) - - local_node_id = input_data.local_node_id - - # Only leader needs to wait for Redis; other nodes can treat init as no-op. - if not is_local_node_leader(local_node_id, state_json): - return PluginOutput(status="completed", local_state=local_state) - - master_endpoint, err = get_redis_master_endpoint(state_json) - if not master_endpoint: - msg = err or "No Redis master endpoint available (init)" - print(f"[!] {msg}", file=sys.stderr) - return PluginOutput( - status="postponed", - error_message=msg, - local_state=local_state, - ) - - # Optionally verify that Redis cluster is reachable from at least one endpoint. - try: - import redis - except ImportError: - msg = "redis-py library not installed (init)" - print(f"[!] {msg}", file=sys.stderr) - return PluginOutput( - status="postponed", - error_message=msg, - local_state=local_state, - ) - - max_retries = 3 - retry_delay_sec = 5 - last_error: str | None = None - - for attempt in range(1, max_retries + 1): - try: - print( - f"[*] init: attempt {attempt}/{max_retries} to ping Redis master " - f"via {master_endpoint}", - file=sys.stderr, - ) - host, port = master_endpoint - r = redis.Redis( - host=host, - port=port, - decode_responses=True, - socket_connect_timeout=5, - ) - r.ping() - - print( - f"[*] init: Redis master is reachable on attempt {attempt}", - file=sys.stderr, - ) - return PluginOutput(status="completed", local_state=local_state) - except Exception as e: - last_error = f"init: failed to ping Redis master on attempt {attempt}: {e}" - print(f"[!] {last_error}", file=sys.stderr) - if attempt < max_retries: - time.sleep(retry_delay_sec) - - # If we reach here, Redis is still not reachable — postpone init. - return PluginOutput( - status="postponed", - error_message=last_error or "init: Redis master is not reachable", - local_state=local_state, - ) - - -@plugin.command("apply") -def handle_apply(input_data: PluginInput) -> PluginOutput: - """Create the Redis route for test-app on the leader node only.""" - local_node_id = input_data.local_node_id - state_json = input_data.state or {} - local_state = input_data.local_state or {} - - if not isinstance(state_json, dict): - return PluginOutput( - status="error", - error_message="Invalid state format", - local_state=local_state, - ) - - # Only leader node should write the route - if not is_local_node_leader(local_node_id, state_json): - return PluginOutput( - status="completed", - local_state=local_state, - ) - - success, error = ensure_route_in_redis(state_json) - if not success: - # Also log the error locally so it shows up in node logs even if - # the executor does not print error_message from PluginOutput. - if error: - print(f"[!] test-app-route apply: {error}", file=sys.stderr) - return PluginOutput( - status="postponed", - error_message=error or "Failed to configure route in Redis", - local_state=local_state, - ) - - node_properties = {"test_app_route_configured": "true"} - return PluginOutput( - status="completed", - node_properties=node_properties, - local_state=local_state, - ) - - -@plugin.command("health") -def handle_health(input_data: PluginInput) -> PluginOutput: - """Leader can optionally verify that the route exists; others are no-op.""" - local_node_id = input_data.local_node_id - state_json = input_data.state or {} - local_state = input_data.local_state or {} - - if not isinstance(state_json, dict): - return PluginOutput( - status="error", - error_message="Invalid state format", - local_state=local_state, - ) - - if not is_local_node_leader(local_node_id, state_json): - # Non-leader nodes do not manage this route - return PluginOutput(status="completed", local_state=local_state) - - try: - import redis - except ImportError: - # If library is missing, health is postponed rather than fatal - return PluginOutput( - status="postponed", - error_message="redis-py library not installed", - local_state=local_state, - ) - - master_endpoint, err = get_redis_master_endpoint(state_json) - if not master_endpoint: - return PluginOutput( - status="postponed", - error_message=err or "No Redis master endpoint available", - local_state=local_state, - ) - - try: - host, port = master_endpoint - r = redis.Redis( - host=host, - port=port, - decode_responses=True, - socket_connect_timeout=5, - ) - value = r.get(ROUTE_KEY) - if not value: - return PluginOutput( - status="postponed", - error_message=f"Route {ROUTE_KEY} not found in Redis", - local_state=local_state, - ) - except Exception as e: - return PluginOutput( - status="postponed", - error_message=f"Failed to verify route in Redis: {e}", - local_state=local_state, - ) - - return PluginOutput(status="completed", local_state=local_state) - - -@plugin.command("finalize") -def handle_finalize(input_data: PluginInput) -> PluginOutput: - """No special finalize logic required.""" - local_state = input_data.local_state or {} - return PluginOutput(status="completed", local_state=local_state) - - -@plugin.command("destroy") -def handle_destroy(input_data: PluginInput) -> PluginOutput: - """On leader node, remove the route from Redis.""" - local_node_id = input_data.local_node_id - state_json = input_data.state or {} - local_state = input_data.local_state or {} - - # Only leader attempts to clean up the route - if isinstance(state_json, dict) and is_local_node_leader(local_node_id, state_json): - delete_route_from_redis(state_json) - - node_properties = { - "test_app_route_configured": None, - } - return PluginOutput( - status="completed", - node_properties=node_properties, - local_state=local_state, - ) - - -if __name__ == "__main__": - plugin.run() diff --git a/src/services/apps/test-app-route/manifest.yaml b/src/services/apps/test-app-route/manifest.yaml deleted file mode 100644 index d1817f66..00000000 --- a/src/services/apps/test-app-route/manifest.yaml +++ /dev/null @@ -1,142 +0,0 @@ -name: test-app-route -version: 1.0.0 -commands: - - init - - apply - - health - - finalize - - destroy -healthcheckIntervalSecs: 60 -entrypoint: main.py -stateExpr: - engine: jq - query: | - ($swarmdb.clusters[] | select(.id == "{{ clusterId }}" and .deleted_ts == null)) as $cluster | - - ([$swarmdb.clusternodes[] | select(.cluster == "{{ clusterId }}" and .deleted_ts == null)]) as $appClusterNodes | - - ($appClusterNodes | map(.node)) as $appNodeIds | - - # Find Redis cluster with at least one ready node (not bound to app nodes) - ( - [ - $swarmdb.clusters[] | - select(.cluster_policy == "redis" and .deleted_ts == null) | - . as $c | - select( - ( - [$swarmdb.clusternodeproperties[] | - select( - (.cluster_node | startswith($c.id)) and - .deleted_ts == null and - .name == "redis_node_ready" and - .value == "true" - ) - ] | length > 0 - ) - ) - ] | .[0] - ) as $redisCluster | - - # Redis cluster nodes - ([$swarmdb.clusternodes[] | select(.cluster == $redisCluster.id and .deleted_ts == null)]) as $redisClusterNodes | - ($redisClusterNodes | map(.node)) as $redisNodeIds | - - # Find Redis Sentinel cluster - ( - $swarmdb.clusters[] | - select(.cluster_policy == "redis-sentinel" and .deleted_ts == null) - ) as $sentinelCluster | - - # Redis Sentinel cluster nodes - ([$swarmdb.clusternodes[] | select(.cluster == $sentinelCluster.id and .deleted_ts == null)]) as $sentinelClusterNodes | - ($sentinelClusterNodes | map(.node)) as $sentinelNodeIds | - - # Find WireGuard cluster that contains any Redis nodes - ( - $swarmdb.clusters[] | - select(.cluster_policy == "wireguard" and .deleted_ts == null) | - select( - ( - [$swarmdb.clusternodes[] | select(.deleted_ts == null and (.node | IN($redisNodeIds[])))] | - length > 0 - ) - ) - ) as $wgCluster | - - # Find WireGuard cluster that contains any Redis Sentinel nodes - ( - $swarmdb.clusters[] | - select(.cluster_policy == "wireguard" and .deleted_ts == null) | - select( - ( - [$swarmdb.clusternodes[] | select(.deleted_ts == null and (.node | IN($sentinelNodeIds[])))] | - length > 0 - ) - ) - ) as $sentinelWgCluster | - - { - cluster: { - id: $cluster.id, - cluster_policy: $cluster.cluster_policy, - leader_node: $cluster.leader_node - }, - - clusterNodes: [ - $appClusterNodes[] | - {id, node_id: .node, cluster} - ] | sort_by(.id, .node_id, .cluster), - - redisCluster: { - id: $redisCluster.id - }, - - redisNodeProperties: [ - $swarmdb.clusternodeproperties[] | - select( - (.cluster_node | startswith($redisCluster.id)) and - .deleted_ts == null and - (.name | startswith("redis_")) - ) | - {cluster_node, name, value, node_id: ( .cluster_node as $cn | $swarmdb.clusternodes[] | select(.id == $cn)) | .node} - ] | sort_by(.cluster_node, .name, .value, .node_id), - - sentinelCluster: { - id: $sentinelCluster.id - }, - - sentinelNodeProperties: [ - $swarmdb.clusternodeproperties[] | - select( - (.cluster_node | startswith($sentinelCluster.id)) and - .deleted_ts == null and - (.name | startswith("redis_sentinel_")) - ) | - {cluster_node, name, value, node_id: ( .cluster_node as $cn | $swarmdb.clusternodes[] | select(.id == $cn)) | .node} - ] | sort_by(.cluster_node, .name, .value, .node_id), - - wgCluster: { - id: $wgCluster.id - }, - - wgNodeProperties: [ - $swarmdb.clusternodeproperties[] | - select( - (.cluster_node | startswith($wgCluster.id)) and - .deleted_ts == null and - .name == "tunnel_ip" - ) | - {cluster_node, name, value, node_id: ( .cluster_node as $cn | $swarmdb.clusternodes[] | select(.id == $cn)) | .node} - ] | sort_by(.cluster_node, .name, .value, .node_id), - - sentinelWgNodeProperties: [ - $swarmdb.clusternodeproperties[] | - select( - (.cluster_node | startswith($sentinelWgCluster.id)) and - .deleted_ts == null and - .name == "tunnel_ip" - ) | - {cluster_node, name, value, node_id: ( .cluster_node as $cn | $swarmdb.clusternodes[] | select(.id == $cn)) | .node} - ] | sort_by(.cluster_node, .name, .value, .node_id) - } diff --git a/src/services/apps/test-app/main.py b/src/services/apps/test-app/main.py deleted file mode 100644 index 46f6433d..00000000 --- a/src/services/apps/test-app/main.py +++ /dev/null @@ -1,338 +0,0 @@ -#!/usr/bin/env python3 - -import os -import socket -import subprocess -import sys -import time -from http.server import BaseHTTPRequestHandler, HTTPServer -from pathlib import Path - -from provision_plugin_sdk import ProvisionPlugin, PluginInput, PluginOutput - - -APP_PORT = int(os.environ.get("TEST_APP_PORT", "34567")) -APP_SCRIPT_PATH = Path("/usr/local/bin/test-app-server.py") -SYSTEMD_SERVICE_NAME = "test-app" -SYSTEMD_SERVICE_PATH = Path(f"/etc/systemd/system/{SYSTEMD_SERVICE_NAME}.service") - - -plugin = ProvisionPlugin() - - -class HelloWorldHandler(BaseHTTPRequestHandler): - """Simple HTTP handler that responds with 'Hello World' to all methods.""" - - def _send_hello(self): - body = b"Hello World" - self.send_response(200) - self.send_header("Content-Type", "text/plain; charset=utf-8") - self.send_header("Content-Length", str(len(body))) - self.end_headers() - self.wfile.write(body) - - def log_message(self, format: str, *args): - # Log to stderr with a simple prefix to avoid polluting stdout used by the plugin - sys.stderr.write(f"[test-app] {self.address_string()} - {format % args}\n") - - def do_GET(self): - self._send_hello() - - def do_POST(self): - self._send_hello() - - def do_PUT(self): - self._send_hello() - - def do_DELETE(self): - self._send_hello() - - def do_PATCH(self): - self._send_hello() - - def do_HEAD(self): - # HEAD should not include body, but we still reuse status/headers - self.send_response(200) - self.send_header("Content-Type", "text/plain; charset=utf-8") - self.send_header("Content-Length", "0") - self.end_headers() - - def do_OPTIONS(self): - self.send_response(200) - self.send_header("Allow", "GET,POST,PUT,DELETE,PATCH,HEAD,OPTIONS") - self.end_headers() - - -def write_app_script(): - """Write the test-app HTTP server script to disk if it does not exist.""" - APP_SCRIPT_PATH.parent.mkdir(parents=True, exist_ok=True) - - script_content = f"""#!/usr/bin/env python3 -from http.server import BaseHTTPRequestHandler, HTTPServer -import sys - - -class HelloWorldHandler(BaseHTTPRequestHandler): - def _send_hello(self): - body = b"Hello World" - self.send_response(200) - self.send_header("Content-Type", "text/plain; charset=utf-8") - self.send_header("Content-Length", str(len(body))) - self.end_headers() - self.wfile.write(body) - - def log_message(self, format, *args): - sys.stderr.write(f"[test-app] {{self.address_string()}} - {{format % args}}\\n") - - def do_GET(self): - self._send_hello() - - def do_POST(self): - self._send_hello() - - def do_PUT(self): - self._send_hello() - - def do_DELETE(self): - self._send_hello() - - def do_PATCH(self): - self._send_hello() - - def do_HEAD(self): - self.send_response(200) - self.send_header("Content-Type", "text/plain; charset=utf-8") - self.send_header("Content-Length", "0") - self.end_headers() - - def do_OPTIONS(self): - self.send_response(200) - self.send_header("Allow", "GET,POST,PUT,DELETE,PATCH,HEAD,OPTIONS") - self.end_headers() - - -def run(): - server = HTTPServer(("0.0.0.0", {APP_PORT}), HelloWorldHandler) - sys.stderr.write(f"[test-app] Listening on 0.0.0.0:{APP_PORT}\\n") - server.serve_forever() - - -if __name__ == "__main__": - run() -""" - - APP_SCRIPT_PATH.write_text(script_content) - APP_SCRIPT_PATH.chmod(0o755) - - -def write_systemd_service(): - """Create or update systemd service unit for test-app.""" - service_content = f"""[Unit] -Description=Test App HTTP Server -After=network.target - -[Service] -Type=simple -ExecStart=/usr/bin/python3 {APP_SCRIPT_PATH} -Restart=always -RestartSec=5 -User=root - -[Install] -WantedBy=multi-user.target -""" - - SYSTEMD_SERVICE_PATH.parent.mkdir(parents=True, exist_ok=True) - SYSTEMD_SERVICE_PATH.write_text(service_content) - - # Reload systemd units - subprocess.run(["systemctl", "daemon-reload"], check=False) - - -def is_service_running() -> tuple[bool, str | None]: - """Check if test-app systemd service is running.""" - try: - result = subprocess.run( - ["systemctl", "is-active", SYSTEMD_SERVICE_NAME], - capture_output=True, - text=True, - ) - is_active = result.stdout.strip() == "active" - return is_active, None if is_active else f"Service status: {result.stdout.strip()}" - except Exception as e: - return False, f"Failed to check service status: {str(e)}" - - -def wait_for_port_ready(timeout_sec: int = 30) -> bool: - """Wait until APP_PORT is listening on localhost.""" - deadline = time.time() + timeout_sec - last_error = None - - while time.time() < deadline: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(2) - try: - sock.connect(("127.0.0.1", APP_PORT)) - sock.close() - return True - except Exception as e: - last_error = str(e) - time.sleep(1) - finally: - sock.close() - - print(f"[!] test-app did not open port {APP_PORT} within {timeout_sec}s: {last_error}", file=sys.stderr) - return False - - -@plugin.command("init") -def handle_init(input_data: PluginInput) -> PluginOutput: - """Init is a no-op for test-app (no packages to install).""" - local_state = input_data.local_state or {} - return PluginOutput(status="completed", local_state=local_state) - - -@plugin.command("apply") -def handle_apply(input_data: PluginInput) -> PluginOutput: - """Deploy and start the test-app HTTP server.""" - local_state = input_data.local_state or {} - - try: - write_app_script() - write_systemd_service() - except Exception as e: - return PluginOutput( - status="error", - error_message=f"Failed to write test-app files: {e}", - local_state=local_state, - ) - - # Enable and restart systemd service - try: - result = subprocess.run( - ["systemctl", "enable", SYSTEMD_SERVICE_NAME], - capture_output=True, - text=True, - ) - if result.returncode != 0: - return PluginOutput( - status="error", - error_message=f"Failed to enable {SYSTEMD_SERVICE_NAME}: {result.stderr}", - local_state=local_state, - ) - - result = subprocess.run( - ["systemctl", "restart", SYSTEMD_SERVICE_NAME], - capture_output=True, - text=True, - ) - if result.returncode != 0: - return PluginOutput( - status="error", - error_message=f"Failed to start {SYSTEMD_SERVICE_NAME}: {result.stderr}", - local_state=local_state, - ) - except Exception as e: - return PluginOutput( - status="error", - error_message=f"Failed to start {SYSTEMD_SERVICE_NAME}: {e}", - local_state=local_state, - ) - - # Wait for the port to become ready - if not wait_for_port_ready(timeout_sec=30): - return PluginOutput( - status="postponed", - error_message=f"{SYSTEMD_SERVICE_NAME} did not become ready on port {APP_PORT}", - local_state=local_state, - ) - - node_properties = {"test_app_ready": "true"} - return PluginOutput( - status="completed", - node_properties=node_properties, - local_state=local_state, - ) - - -@plugin.command("health") -def handle_health(input_data: PluginInput) -> PluginOutput: - """Check that test-app service is running.""" - local_state = input_data.local_state or {} - - running, error = is_service_running() - if not running: - if error and "Failed to" in error: - return PluginOutput(status="error", error_message=error, local_state=local_state) - return PluginOutput(status="postponed", error_message=error or "test-app service is not running", local_state=local_state) - - # Optionally verify port is still open - if not wait_for_port_ready(timeout_sec=5): - return PluginOutput( - status="postponed", - error_message=f"{SYSTEMD_SERVICE_NAME} port {APP_PORT} is not reachable", - local_state=local_state, - ) - - return PluginOutput(status="completed", local_state=local_state) - - -@plugin.command("finalize") -def handle_finalize(input_data: PluginInput) -> PluginOutput: - """Gracefully stop test-app before node removal.""" - local_state = input_data.local_state or {} - try: - subprocess.run( - ["systemctl", "stop", SYSTEMD_SERVICE_NAME], - check=False, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - except Exception as e: - print(f"[!] Failed to stop {SYSTEMD_SERVICE_NAME}: {e}", file=sys.stderr) - - return PluginOutput(status="completed", local_state=local_state) - - -@plugin.command("destroy") -def handle_destroy(input_data: PluginInput) -> PluginOutput: - """Completely remove test-app service and script.""" - try: - subprocess.run( - ["systemctl", "stop", SYSTEMD_SERVICE_NAME], - check=False, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - subprocess.run( - ["systemctl", "disable", SYSTEMD_SERVICE_NAME], - check=False, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - - if SYSTEMD_SERVICE_PATH.exists(): - SYSTEMD_SERVICE_PATH.unlink() - - if APP_SCRIPT_PATH.exists(): - APP_SCRIPT_PATH.unlink() - - node_properties = { - "test_app_ready": None, - } - - return PluginOutput( - status="completed", - node_properties=node_properties, - local_state={}, - ) - except Exception as e: - return PluginOutput( - status="error", - error_message=f"Failed to destroy {SYSTEMD_SERVICE_NAME}: {e}", - local_state={}, - ) - - -if __name__ == "__main__": - plugin.run() diff --git a/src/services/apps/test-app/manifest.yaml b/src/services/apps/test-app/manifest.yaml deleted file mode 100644 index b197e773..00000000 --- a/src/services/apps/test-app/manifest.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: test-app -version: 1.0.0 -commands: - - init - - apply - - health - - finalize - - destroy -healthcheckIntervalSecs: 60 -entrypoint: main.py -stateExpr: - engine: jq - query: | - ($swarmdb.clusters[] | select(.id == "{{ clusterId }}" and .deleted_ts == null)) as $cluster | - - ([$swarmdb.clusternodes[] | select(.cluster == "{{ clusterId }}" and .deleted_ts == null)]) as $appClusterNodes | - - { - cluster: { - id: $cluster.id, - cluster_policy: $cluster.cluster_policy, - leader_node: $cluster.leader_node - }, - - clusterNodes: [ - $appClusterNodes[] | - {id, node_id: .node, cluster} - ] | sort_by(.id, .node_id, .cluster) - } diff --git a/src/swarm-scripts/00-initialize-secrets.sh b/src/swarm-scripts/00-initialize-secrets.sh new file mode 100644 index 00000000..a5bcb1c2 --- /dev/null +++ b/src/swarm-scripts/00-initialize-secrets.sh @@ -0,0 +1,44 @@ +#!/bin/bash +set -euo pipefail + +CONFIG="/sp/swarm/config.yaml" + +cfg() { + python3 -c " +import yaml +c = yaml.safe_load(open('$CONFIG')) or {} +v = c +for k in '$1'.split('.'): + v = v.get(k) if isinstance(v, dict) else None +print('' if v is None else v)" +} + +DB_HOST=${DB_HOST:-127.0.0.1} +DB_PORT=${DB_PORT:-3306} +DB_USER=${DB_USER:-root} +DB_NAME=${DB_NAME:-swarmdb} + +POWERDNS_API_URL=$(cfg "powerdns_api_url") +POWERDNS_API_KEY=$(cfg "powerdns_api_key") +BASE_DOMAIN=$(cfg "base_domain") + +AUTH_SERVICE_YAML="" +AUTH_SERVICE_YAML_PATH="/sp/swarm/auth-service.yaml" +[ -f "$AUTH_SERVICE_YAML_PATH" ] && AUTH_SERVICE_YAML=$(cat "$AUTH_SERVICE_YAML_PATH") + +EVIDENCE_SIGN_KEY=$(openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 2>/dev/null) + +ensure_secret() { + local key="$1" + local value="$2" + [ -n "$value" ] || return 0 + + DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ + python3 "$(dirname "$0")/swarm-cli.py" create SwarmSecrets "$key" --value "$value" >/dev/null +} + +ensure_secret "powerdns_api_url" "$POWERDNS_API_URL" +ensure_secret "powerdns_api_key" "$POWERDNS_API_KEY" +ensure_secret "base_domain" "$BASE_DOMAIN" +ensure_secret "auth_service_yaml" "$AUTH_SERVICE_YAML" +ensure_secret "evidence_sign_key" "$EVIDENCE_SIGN_KEY" diff --git a/src/swarm-scripts/10.setup-wireguard.sh b/src/swarm-scripts/10.setup-wireguard.sh index 990608d7..5c2fab3d 100644 --- a/src/swarm-scripts/10.setup-wireguard.sh +++ b/src/swarm-scripts/10.setup-wireguard.sh @@ -16,8 +16,8 @@ CLUSTER_POLICY=${CLUSTER_POLICY:-wireguard} CLUSTER_ID=${CLUSTER_ID:-wireguard} # Path to manifest file INSIDE the container (configs are mounted to /configs) -MANIFEST_PATH=${MANIFEST_PATH:-/etc/swarm-cloud/services/${SERVICE_NAME}/manifest.yaml} -LOCATION_PATH=${LOCATION_PATH:-/etc/swarm-cloud/services/${SERVICE_NAME}} +MANIFEST_PATH=${MANIFEST_PATH:-/etc/swarm-services/${SERVICE_NAME}/manifest.yaml} +LOCATION_PATH=${LOCATION_PATH:-/etc/swarm-services/${SERVICE_NAME}} if [ ! -f "$MANIFEST_PATH" ]; then echo "Manifest not found at: $MANIFEST_PATH" >&2 diff --git a/src/swarm-scripts/20.setup-hw-measurement.sh b/src/swarm-scripts/20.setup-hw-measurement.sh index 00ebc94f..49cdbf08 100644 --- a/src/swarm-scripts/20.setup-hw-measurement.sh +++ b/src/swarm-scripts/20.setup-hw-measurement.sh @@ -6,7 +6,7 @@ set -euo pipefail # # Note: # - The hw-measurement manifest and main.py should be available inside the container at: -# /etc/swarm-cloud/services/hw-measurement/manifest.yaml and /etc/swarm-cloud/services/hw-measurement/main.py +# /etc/swarm-services/hw-measurement/manifest.yaml and /etc/swarm-services/hw-measurement/main.py # (mount or copy them similarly to the wireguard service) # # - hw-measurement depends on a WireGuard cluster existing and sharing nodes with it. @@ -24,8 +24,8 @@ CLUSTER_POLICY=${CLUSTER_POLICY:-hw-measurement} CLUSTER_ID=${CLUSTER_ID:-hw-measurement} # Path to manifest file INSIDE the container (configs are mounted to /configs) -MANIFEST_PATH=${MANIFEST_PATH:-/etc/swarm-cloud/services/${SERVICE_NAME}/manifest.yaml} -LOCATION_PATH=${LOCATION_PATH:-/etc/swarm-cloud/services/${SERVICE_NAME}} +MANIFEST_PATH=${MANIFEST_PATH:-/etc/swarm-services/${SERVICE_NAME}/manifest.yaml} +LOCATION_PATH=${LOCATION_PATH:-/etc/swarm-services/${SERVICE_NAME}} SERVICE_PK="${CLUSTER_POLICY}:${SERVICE_NAME}" if [ ! -f "$MANIFEST_PATH" ]; then diff --git a/src/swarm-scripts/30.setup-latency-measurement.sh b/src/swarm-scripts/30.setup-latency-measurement.sh index f7c0d93e..f0b1ff87 100644 --- a/src/swarm-scripts/30.setup-latency-measurement.sh +++ b/src/swarm-scripts/30.setup-latency-measurement.sh @@ -6,7 +6,7 @@ set -euo pipefail # # Note: # - The latency-measurement manifest and main.py should be available inside the container at: -# /etc/swarm-cloud/services/latency-measurement/manifest.yaml and /etc/swarm-cloud/services/latency-measurement/main.py +# /etc/swarm-services/latency-measurement/manifest.yaml and /etc/swarm-services/latency-measurement/main.py # (mount or copy them similarly to the wireguard service) # # - latency-measurement depends on a WireGuard cluster existing and sharing nodes with it. @@ -24,8 +24,8 @@ CLUSTER_POLICY=${CLUSTER_POLICY:-latency-measurement} CLUSTER_ID=${CLUSTER_ID:-latency-measurement} # Path to manifest file INSIDE the container (configs are mounted to /configs) -MANIFEST_PATH=${MANIFEST_PATH:-/etc/swarm-cloud/services/${SERVICE_NAME}/manifest.yaml} -LOCATION_PATH=${LOCATION_PATH:-/etc/swarm-cloud/services/${SERVICE_NAME}} +MANIFEST_PATH=${MANIFEST_PATH:-/etc/swarm-services/${SERVICE_NAME}/manifest.yaml} +LOCATION_PATH=${LOCATION_PATH:-/etc/swarm-services/${SERVICE_NAME}} SERVICE_PK="${CLUSTER_POLICY}:${SERVICE_NAME}" if [ ! -f "$MANIFEST_PATH" ]; then diff --git a/src/swarm-scripts/40.setup-geo-ip-measurement.sh b/src/swarm-scripts/40.setup-geo-ip-measurement.sh index 6dfe6cf5..b1741db0 100644 --- a/src/swarm-scripts/40.setup-geo-ip-measurement.sh +++ b/src/swarm-scripts/40.setup-geo-ip-measurement.sh @@ -6,7 +6,7 @@ set -euo pipefail # # Note: # - The geo-ip-measurement manifest and main.py should be available inside the container at: -# /etc/swarm-cloud/services/geo-ip-measurement/manifest.yaml and /etc/swarm-cloud/services/geo-ip-measurement/main.py +# /etc/swarm-services/geo-ip-measurement/manifest.yaml and /etc/swarm-services/geo-ip-measurement/main.py # (mount or copy them similarly to the wireguard service) # # - geo-ip-measurement depends on a WireGuard cluster existing and sharing nodes with it. @@ -24,8 +24,8 @@ CLUSTER_POLICY=${CLUSTER_POLICY:-geo-ip-measurement} CLUSTER_ID=${CLUSTER_ID:-geo-ip-measurement} # Path to manifest file INSIDE the container (configs are mounted to /configs) -MANIFEST_PATH=${MANIFEST_PATH:-/etc/swarm-cloud/services/${SERVICE_NAME}/manifest.yaml} -LOCATION_PATH=${LOCATION_PATH:-/etc/swarm-cloud/services/${SERVICE_NAME}} +MANIFEST_PATH=${MANIFEST_PATH:-/etc/swarm-services/${SERVICE_NAME}/manifest.yaml} +LOCATION_PATH=${LOCATION_PATH:-/etc/swarm-services/${SERVICE_NAME}} SERVICE_PK="${CLUSTER_POLICY}:${SERVICE_NAME}" if [ ! -f "$MANIFEST_PATH" ]; then diff --git a/src/swarm-scripts/50.setup-rke2.sh b/src/swarm-scripts/50.setup-rke2.sh index 477a45e3..e692e21d 100644 --- a/src/swarm-scripts/50.setup-rke2.sh +++ b/src/swarm-scripts/50.setup-rke2.sh @@ -6,7 +6,7 @@ set -euo pipefail # # Note: # - The rke2 manifest and main.py should be available inside the container at: -# /etc/swarm-cloud/services/rke2/manifest.yaml and /etc/swarm-cloud/services/rke2/main.py +# /etc/swarm-services/rke2/manifest.yaml and /etc/swarm-services/rke2/main.py # (mount or copy them similarly to the wireguard service) # # - rke2 depends on a WireGuard cluster existing and sharing nodes with it. @@ -24,8 +24,8 @@ CLUSTER_POLICY=${CLUSTER_POLICY:-rke2} CLUSTER_ID=${CLUSTER_ID:-rke2} # Path to manifest file INSIDE the container (configs are mounted to /configs) -MANIFEST_PATH=${MANIFEST_PATH:-/etc/swarm-cloud/services/${SERVICE_NAME}/manifest.yaml} -LOCATION_PATH=${LOCATION_PATH:-/etc/swarm-cloud/services/${SERVICE_NAME}} +MANIFEST_PATH=${MANIFEST_PATH:-/etc/swarm-services/${SERVICE_NAME}/manifest.yaml} +LOCATION_PATH=${LOCATION_PATH:-/etc/swarm-services/${SERVICE_NAME}} SERVICE_PK="${CLUSTER_POLICY}:${SERVICE_NAME}" if [ ! -f "$MANIFEST_PATH" ]; then @@ -50,7 +50,7 @@ if DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ else echo "Creating ClusterService '$SERVICE_PK'..." DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ - python3 "$(dirname "$0")/swarm-cli.py" create ClusterServices "$SERVICE_PK" --name="$SERVICE_NAME" --cluster_policy="$CLUSTER_POLICY" --version="$SERVICE_VERSION" --location="$LOCATION_PATH" --omit-command-init + python3 "$(dirname "$0")/swarm-cli.py" create ClusterServices "$SERVICE_PK" --name="$SERVICE_NAME" --cluster_policy="$CLUSTER_POLICY" --version="$SERVICE_VERSION" --location="$LOCATION_PATH" fi echo "Done. The provision worker will reconcile '$SERVICE_NAME' shortly." diff --git a/src/swarm-scripts/60.setup-redis.sh b/src/swarm-scripts/60.setup-redis.sh index 352fe0c5..1ab3dca7 100644 --- a/src/swarm-scripts/60.setup-redis.sh +++ b/src/swarm-scripts/60.setup-redis.sh @@ -6,7 +6,7 @@ set -euo pipefail # # Note: # - The redis manifest and main.py are provided by the image at: -# /etc/swarm-cloud/services/redis/{manifest.yaml, main.py} +# /etc/swarm-services/redis/{manifest.yaml, main.py} # This script only registers service records in SwarmDB. # - redis depends on a WireGuard cluster existing and sharing nodes with it. # When bootstrapping WireGuard, prefer ClusterPolicy id 'wireguard' to match redis's stateExpr. @@ -28,9 +28,9 @@ SENTINEL_CLUSTER_POLICY=${SENTINEL_CLUSTER_POLICY:-redis-sentinel} SENTINEL_MAX_SIZE=${SENTINEL_MAX_SIZE:-3} # Location stored in ClusterServices; must exist on all nodes (baked into image) -REDIS_LOCATION_PATH=${REDIS_LOCATION_PATH:-/etc/swarm-cloud/services/${REDIS_SERVICE_NAME}} +REDIS_LOCATION_PATH=${REDIS_LOCATION_PATH:-/etc/swarm-services/${REDIS_SERVICE_NAME}} REDIS_MANIFEST_PATH=${REDIS_MANIFEST_PATH:-${REDIS_LOCATION_PATH}/manifest.yaml} -SENTINEL_LOCATION_PATH=${SENTINEL_LOCATION_PATH:-/etc/swarm-cloud/services/${SENTINEL_SERVICE_NAME}} +SENTINEL_LOCATION_PATH=${SENTINEL_LOCATION_PATH:-/etc/swarm-services/${SENTINEL_SERVICE_NAME}} SENTINEL_MANIFEST_PATH=${SENTINEL_MANIFEST_PATH:-${SENTINEL_LOCATION_PATH}/manifest.yaml} REDIS_SERVICE_PK="${REDIS_CLUSTER_POLICY}:${REDIS_SERVICE_NAME}" SENTINEL_SERVICE_PK="${SENTINEL_CLUSTER_POLICY}:${SENTINEL_SERVICE_NAME}" @@ -72,7 +72,7 @@ if DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ else echo "Creating ClusterService '$REDIS_SERVICE_PK'..." DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ - python3 "$(dirname "$0")/swarm-cli.py" create ClusterServices "$REDIS_SERVICE_PK" --name="$REDIS_SERVICE_NAME" --cluster_policy="$REDIS_CLUSTER_POLICY" --version="$REDIS_SERVICE_VERSION" --location="$REDIS_LOCATION_PATH" --omit-command-init + python3 "$(dirname "$0")/swarm-cli.py" create ClusterServices "$REDIS_SERVICE_PK" --name="$REDIS_SERVICE_NAME" --cluster_policy="$REDIS_CLUSTER_POLICY" --version="$REDIS_SERVICE_VERSION" --location="$REDIS_LOCATION_PATH" fi echo "Ensuring ClusterService '$SENTINEL_SERVICE_PK'..." @@ -82,7 +82,7 @@ if DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ else echo "Creating ClusterService '$SENTINEL_SERVICE_PK'..." DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ - python3 "$(dirname "$0")/swarm-cli.py" create ClusterServices "$SENTINEL_SERVICE_PK" --name="$SENTINEL_SERVICE_NAME" --cluster_policy="$SENTINEL_CLUSTER_POLICY" --version="$SENTINEL_SERVICE_VERSION" --location="$SENTINEL_LOCATION_PATH" --omit-command-init + python3 "$(dirname "$0")/swarm-cli.py" create ClusterServices "$SENTINEL_SERVICE_PK" --name="$SENTINEL_SERVICE_NAME" --cluster_policy="$SENTINEL_CLUSTER_POLICY" --version="$SENTINEL_SERVICE_VERSION" --location="$SENTINEL_LOCATION_PATH" fi echo "Done. The provision worker will reconcile '$REDIS_SERVICE_NAME' shortly." diff --git a/src/swarm-scripts/61.setup-cockroachdb.sh b/src/swarm-scripts/61.setup-cockroachdb.sh index 37cf2d3d..b0f3f384 100644 --- a/src/swarm-scripts/61.setup-cockroachdb.sh +++ b/src/swarm-scripts/61.setup-cockroachdb.sh @@ -6,7 +6,7 @@ set -euo pipefail # # Notes: # - The cockroachdb manifest and main.py are provided by the image at: -# /etc/swarm-cloud/services/cockroachdb/{manifest.yaml, main.py} +# /etc/swarm-services/cockroachdb/{manifest.yaml, main.py} # We do not reimplement any logic here, only register ClusterPolicy and ClusterService. # - cockroachdb depends on WireGuard as expressed in its own manifest and provision plugin. # @@ -24,8 +24,8 @@ CLUSTER_ID=${CLUSTER_ID:-cockroachdb} # Location and manifest inside the container. # IMPORTANT: This script runs only on one node. All nodes must have the same location available already -# (baked into the image), so we point to /etc/swarm-cloud/services/${SERVICE_NAME}. -LOCATION_PATH=${LOCATION_PATH:-/etc/swarm-cloud/services/${SERVICE_NAME}} +# (baked into the image), so we point to /etc/swarm-services/${SERVICE_NAME}. +LOCATION_PATH=${LOCATION_PATH:-/etc/swarm-services/${SERVICE_NAME}} MANIFEST_PATH=${MANIFEST_PATH:-${LOCATION_PATH}/manifest.yaml} SERVICE_PK="${CLUSTER_POLICY}:${SERVICE_NAME}" @@ -51,7 +51,7 @@ if DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ else echo "Creating ClusterService '$SERVICE_PK'..." DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ - python3 "$(dirname "$0")/swarm-cli.py" create ClusterServices "$SERVICE_PK" --name="$SERVICE_NAME" --cluster_policy="$CLUSTER_POLICY" --version="$SERVICE_VERSION" --location="$LOCATION_PATH" --omit-command-init + python3 "$(dirname "$0")/swarm-cli.py" create ClusterServices "$SERVICE_PK" --name="$SERVICE_NAME" --cluster_policy="$CLUSTER_POLICY" --version="$SERVICE_VERSION" --location="$LOCATION_PATH" fi echo "Done. The provision worker will reconcile '$SERVICE_NAME' shortly." diff --git a/src/swarm-scripts/62.setup-knot.sh b/src/swarm-scripts/62.setup-knot.sh index b370b743..30e4fcf6 100644 --- a/src/swarm-scripts/62.setup-knot.sh +++ b/src/swarm-scripts/62.setup-knot.sh @@ -6,7 +6,7 @@ set -euo pipefail # # Notes: # - The knot manifest and main.py are provided by the image at: -# /etc/swarm-cloud/services/knot/{manifest.yaml, main.py} +# /etc/swarm-services/knot/{manifest.yaml, main.py} # We do not reimplement any logic here, only register ClusterPolicy and ClusterService. # - knot depends on WireGuard as expressed in its own manifest and provision plugin. # @@ -24,8 +24,8 @@ CLUSTER_ID=${CLUSTER_ID:-knot} # Location and manifest inside the container. # IMPORTANT: This script runs only on one node. All nodes must have the same location available already -# (baked into the image), so we point to /etc/swarm-cloud/services/${SERVICE_NAME}. -LOCATION_PATH=${LOCATION_PATH:-/etc/swarm-cloud/services/${SERVICE_NAME}} +# (baked into the image), so we point to /etc/swarm-services/${SERVICE_NAME}. +LOCATION_PATH=${LOCATION_PATH:-/etc/swarm-services/${SERVICE_NAME}} MANIFEST_PATH=${MANIFEST_PATH:-${LOCATION_PATH}/manifest.yaml} SERVICE_PK="${CLUSTER_POLICY}:${SERVICE_NAME}" @@ -51,11 +51,7 @@ if DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ else echo "Creating ClusterService '$SERVICE_PK'..." DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ - python3 "$(dirname "$0")/swarm-cli.py" create ClusterServices "$SERVICE_PK" --name="$SERVICE_NAME" --cluster_policy="$CLUSTER_POLICY" --version="$SERVICE_VERSION" --location="$LOCATION_PATH" --omit-command-init + python3 "$(dirname "$0")/swarm-cli.py" create ClusterServices "$SERVICE_PK" --name="$SERVICE_NAME" --cluster_policy="$CLUSTER_POLICY" --version="$SERVICE_VERSION" --location="$LOCATION_PATH" fi -echo "Ensuring SwarmSecret 'base_domain' is present via swarm-cli..." -DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" DB_PASSWORD="${DB_PASSWORD-}" \ - python3 "$(dirname "$0")/swarm-cli.py" create SwarmSecrets base_domain --value="test.oresty.superprotocol.io" - echo "Done. The provision worker will reconcile '$SERVICE_NAME' shortly." diff --git a/src/swarm-scripts/63.setup-openresty.sh b/src/swarm-scripts/63.setup-openresty.sh index 47bdcb8b..a2bc0d87 100644 --- a/src/swarm-scripts/63.setup-openresty.sh +++ b/src/swarm-scripts/63.setup-openresty.sh @@ -6,7 +6,7 @@ set -euo pipefail # # Note: # - The openresty manifest and main.py are provided by the image at: -# /etc/swarm-cloud/services/openresty/{manifest.yaml, main.py} +# /etc/swarm-services/openresty/{manifest.yaml, main.py} # This script only registers service records in SwarmDB. # - openresty depends on Redis + WireGuard clusters (see its stateExpr). # @@ -24,8 +24,8 @@ CLUSTER_ID=${CLUSTER_ID:-openresty} # Location and manifest inside the container. # IMPORTANT: This script runs only on one node. All nodes must have the same location available already -# (baked into the image), so we point to /etc/swarm-cloud/services/${SERVICE_NAME}. -LOCATION_PATH=${LOCATION_PATH:-/etc/swarm-cloud/services/${SERVICE_NAME}} +# (baked into the image), so we point to /etc/swarm-services/${SERVICE_NAME}. +LOCATION_PATH=${LOCATION_PATH:-/etc/swarm-services/${SERVICE_NAME}} MANIFEST_PATH=${MANIFEST_PATH:-${LOCATION_PATH}/manifest.yaml} SERVICE_PK="${CLUSTER_POLICY}:${SERVICE_NAME}" diff --git a/src/swarm-scripts/64.setup-swarm-cloud-api.sh b/src/swarm-scripts/64.setup-swarm-cloud-api.sh index c4a354c4..4ad8aa72 100644 --- a/src/swarm-scripts/64.setup-swarm-cloud-api.sh +++ b/src/swarm-scripts/64.setup-swarm-cloud-api.sh @@ -7,7 +7,7 @@ set -euo pipefail # # Notes: # - The swarm-cloud-api manifest and main.py are provided by the image at: -# /etc/swarm-cloud/services/swarm-cloud-api/{manifest.yaml, main.py} +# /etc/swarm-services/swarm-cloud-api/{manifest.yaml, main.py} # We do not reimplement any logic here, only register ClusterPolicy and ClusterService. # - swarm-cloud-api depends on CockroachDB, Redis, WireGuard and Knot as expressed # in its own manifest and provision plugin. @@ -26,18 +26,33 @@ SERVICE_VERSION=${SERVICE_VERSION:-1.0.0} CLUSTER_POLICY=${CLUSTER_POLICY:-swarm-cloud-api} CLUSTER_ID=${CLUSTER_ID:-swarm-cloud-api} +# swarm-cloud-ui descriptors +UI_SERVICE_NAME=${UI_SERVICE_NAME:-swarm-cloud-ui} +UI_SERVICE_VERSION=${UI_SERVICE_VERSION:-1.0.0} +UI_CLUSTER_POLICY=${UI_CLUSTER_POLICY:-swarm-cloud-ui} +UI_CLUSTER_ID=${UI_CLUSTER_ID:-swarm-cloud-ui} + # Location and manifest inside the container. # IMPORTANT: This script runs only on one node. All nodes must have the same location available already -# (baked into the image), so we point to /etc/swarm-cloud/services/${SERVICE_NAME}. -LOCATION_PATH=${LOCATION_PATH:-/etc/swarm-cloud/services/${SERVICE_NAME}} +# (baked into the image), so we point to /etc/swarm-services/${SERVICE_NAME}. +LOCATION_PATH=${LOCATION_PATH:-/etc/swarm-services/${SERVICE_NAME}} MANIFEST_PATH=${MANIFEST_PATH:-${LOCATION_PATH}/manifest.yaml} SERVICE_PK="${CLUSTER_POLICY}:${SERVICE_NAME}" +UI_LOCATION_PATH=${UI_LOCATION_PATH:-/etc/swarm-services/${UI_SERVICE_NAME}} +UI_MANIFEST_PATH=${UI_MANIFEST_PATH:-${UI_LOCATION_PATH}/manifest.yaml} +UI_SERVICE_PK="${UI_CLUSTER_POLICY}:${UI_SERVICE_NAME}" + if [ ! -f "$MANIFEST_PATH" ]; then echo "Manifest not found at: $MANIFEST_PATH" >&2 exit 1 fi +if [ ! -f "$UI_MANIFEST_PATH" ]; then + echo "Manifest not found at: $UI_MANIFEST_PATH" >&2 + exit 1 +fi + echo "Ensuring ClusterPolicy '$CLUSTER_POLICY'..." if DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ python3 "$(dirname "$0")/swarm-cli.py" get ClusterPolicies "$CLUSTER_POLICY" >/dev/null 2>&1; then @@ -58,4 +73,24 @@ else python3 "$(dirname "$0")/swarm-cli.py" create ClusterServices "$SERVICE_PK" --name="$SERVICE_NAME" --cluster_policy="$CLUSTER_POLICY" --version="$SERVICE_VERSION" --location="$LOCATION_PATH" fi +echo "Ensuring ClusterPolicy '$UI_CLUSTER_POLICY'..." +if DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ + python3 "$(dirname "$0")/swarm-cli.py" get ClusterPolicies "$UI_CLUSTER_POLICY" >/dev/null 2>&1; then + echo "ClusterPolicy '$UI_CLUSTER_POLICY' already exists, skipping creation." +else + echo "Creating ClusterPolicy '$UI_CLUSTER_POLICY'..." + DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ + python3 "$(dirname "$0")/swarm-cli.py" create ClusterPolicies "$UI_CLUSTER_POLICY" --minSize=1 --maxSize=1 --maxClusters=1 +fi + +echo "Ensuring ClusterService '$UI_SERVICE_PK'..." +if DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ + python3 "$(dirname "$0")/swarm-cli.py" get ClusterServices "$UI_SERVICE_PK" >/dev/null 2>&1; then + echo "ClusterService '$UI_SERVICE_PK' already exists, skipping creation." +else + echo "Creating ClusterService '$UI_SERVICE_PK'..." + DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ + python3 "$(dirname "$0")/swarm-cli.py" create ClusterServices "$UI_SERVICE_PK" --name="$UI_SERVICE_NAME" --cluster_policy="$UI_CLUSTER_POLICY" --version="$UI_SERVICE_VERSION" --location="$UI_LOCATION_PATH" +fi + echo "Done. The provision worker will reconcile '$SERVICE_NAME' shortly." diff --git a/src/swarm-scripts/70.setup-mongodb.sh b/src/swarm-scripts/65.setup-mongo.sh similarity index 69% rename from src/swarm-scripts/70.setup-mongodb.sh rename to src/swarm-scripts/65.setup-mongo.sh index 12a173c6..da5137fa 100644 --- a/src/swarm-scripts/70.setup-mongodb.sh +++ b/src/swarm-scripts/65.setup-mongo.sh @@ -1,16 +1,8 @@ #!/bin/bash set -euo pipefail -# This script bootstraps the mongodb service into SwarmDB via mysql client. -# Run it INSIDE the container. Assumes mysql client is available. -# -# Note: -# - The mongodb manifest and main.py are provided by the image at: -# /etc/swarm-services/mongodb/{manifest.yaml, main.py} -# This script only registers service records in SwarmDB. -# - mongodb depends on a WireGuard cluster existing and sharing nodes with it. -# When bootstrapping WireGuard, prefer ClusterPolicy id 'wireguard' to match mongodb's stateExpr. -# +# This script bootstraps the mongodb service into SwarmDB via swarm-cli. +# Run it INSIDE the container. Assumes mysql client and swarm-cli.py are available. DB_HOST=${DB_HOST:-127.0.0.1} DB_PORT=${DB_PORT:-3306} @@ -21,11 +13,8 @@ DB_NAME=${DB_NAME:-swarmdb} SERVICE_NAME=${SERVICE_NAME:-mongodb} SERVICE_VERSION=${SERVICE_VERSION:-1.0.0} CLUSTER_POLICY=${CLUSTER_POLICY:-mongodb} -CLUSTER_ID=${CLUSTER_ID:-mongodb} # Location and manifest inside the container. -# IMPORTANT: This script runs only on one node. All nodes must have the same location available already -# (baked into the image), so we point to /etc/swarm-services/${SERVICE_NAME}. LOCATION_PATH=${LOCATION_PATH:-/etc/swarm-services/${SERVICE_NAME}} MANIFEST_PATH=${MANIFEST_PATH:-${LOCATION_PATH}/manifest.yaml} SERVICE_PK="${CLUSTER_POLICY}:${SERVICE_NAME}" @@ -42,7 +31,7 @@ if DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ else echo "Creating ClusterPolicy '$CLUSTER_POLICY'..." DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ - python3 "$(dirname "$0")/swarm-cli.py" create ClusterPolicies "$CLUSTER_POLICY" --minSize=1 --maxSize=5 --maxClusters=1 + python3 "$(dirname "$0")/swarm-cli.py" create ClusterPolicies "$CLUSTER_POLICY" --minSize=1 --maxSize=3 --maxClusters=1 fi echo "Ensuring ClusterService '$SERVICE_PK'..." @@ -52,7 +41,7 @@ if DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ else echo "Creating ClusterService '$SERVICE_PK'..." DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ - python3 "$(dirname "$0")/swarm-cli.py" create ClusterServices "$SERVICE_PK" --name="$SERVICE_NAME" --cluster_policy="$CLUSTER_POLICY" --version="$SERVICE_VERSION" --location="$LOCATION_PATH" --omit-command-init + python3 "$(dirname "$0")/swarm-cli.py" create ClusterServices "$SERVICE_PK" --name="$SERVICE_NAME" --cluster_policy="$CLUSTER_POLICY" --version="$SERVICE_VERSION" --location="$LOCATION_PATH" fi echo "Done. The provision worker will reconcile '$SERVICE_NAME' shortly." diff --git a/src/swarm-scripts/65.setup-nats.sh b/src/swarm-scripts/65.setup-nats.sh deleted file mode 100644 index 9d304044..00000000 --- a/src/swarm-scripts/65.setup-nats.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# This script bootstraps the nats service into SwarmDB via mysql client. -# Run it INSIDE the container. Assumes mysql client is available. -# -# Note: -# - The nats manifest and main.py are provided by the image at: -# /etc/swarm-services/nats/{manifest.yaml, main.py} -# This script only registers service records in SwarmDB. -# - nats depends on a WireGuard cluster existing and sharing nodes with it. -# When bootstrapping WireGuard, prefer ClusterPolicy id 'wireguard' to match nats's stateExpr. -# - -DB_HOST=${DB_HOST:-127.0.0.1} -DB_PORT=${DB_PORT:-3306} -DB_USER=${DB_USER:-root} -DB_NAME=${DB_NAME:-swarmdb} - -# Service descriptors -SERVICE_NAME=${SERVICE_NAME:-nats} -SERVICE_VERSION=${SERVICE_VERSION:-1.0.0} -CLUSTER_POLICY=${CLUSTER_POLICY:-nats} -CLUSTER_ID=${CLUSTER_ID:-nats} - -# Location and manifest inside the container. -# IMPORTANT: This script runs only on one node. All nodes must have the same location available already -# (baked into the image), so we point to /etc/swarm-services/${SERVICE_NAME}. -LOCATION_PATH=${LOCATION_PATH:-/etc/swarm-services/${SERVICE_NAME}} -MANIFEST_PATH=${MANIFEST_PATH:-${LOCATION_PATH}/manifest.yaml} -SERVICE_PK="${CLUSTER_POLICY}:${SERVICE_NAME}" - -if [ ! -f "$MANIFEST_PATH" ]; then - echo "Manifest not found at: $MANIFEST_PATH" >&2 - exit 1 -fi - -echo "Ensuring ClusterPolicy '$CLUSTER_POLICY'..." -if DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ - python3 "$(dirname "$0")/swarm-cli.py" get ClusterPolicies "$CLUSTER_POLICY" >/dev/null 2>&1; then - echo "ClusterPolicy '$CLUSTER_POLICY' already exists, skipping creation." -else - echo "Creating ClusterPolicy '$CLUSTER_POLICY'..." - DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ - python3 "$(dirname "$0")/swarm-cli.py" create ClusterPolicies "$CLUSTER_POLICY" --minSize=1 --maxSize=3 --maxClusters=1 -fi - -echo "Ensuring ClusterService '$SERVICE_PK'..." -if DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ - python3 "$(dirname "$0")/swarm-cli.py" get ClusterServices "$SERVICE_PK" >/dev/null 2>&1; then - echo "ClusterService '$SERVICE_PK' already exists, skipping creation." -else - echo "Creating ClusterService '$SERVICE_PK'..." - DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ - python3 "$(dirname "$0")/swarm-cli.py" create ClusterServices "$SERVICE_PK" --name="$SERVICE_NAME" --cluster_policy="$CLUSTER_POLICY" --version="$SERVICE_VERSION" --location="$LOCATION_PATH" --omit-command-init -fi - -echo "Done. The provision worker will reconcile '$SERVICE_NAME' shortly." diff --git a/src/swarm-scripts/66.setup-test-app.sh b/src/swarm-scripts/66.setup-swarm-cloud-ui.sh similarity index 76% rename from src/swarm-scripts/66.setup-test-app.sh rename to src/swarm-scripts/66.setup-swarm-cloud-ui.sh index 18ba36d2..8a91e218 100644 --- a/src/swarm-scripts/66.setup-test-app.sh +++ b/src/swarm-scripts/66.setup-swarm-cloud-ui.sh @@ -1,14 +1,8 @@ #!/bin/bash set -euo pipefail -# This script bootstraps the test-app service into SwarmDB via swarm-cli. +# This script bootstraps the swarm-cloud-ui service into SwarmDB via swarm-cli. # Run it INSIDE the container. Assumes mysql client and swarm-cli.py are available. -# -# Notes: -# - The test-app manifest and main.py are expected to be available at: -# /etc/swarm-services/test-app/{manifest.yaml, main.py} -# This script only registers ClusterPolicy and ClusterService. -# DB_HOST=${DB_HOST:-127.0.0.1} DB_PORT=${DB_PORT:-3306} @@ -16,14 +10,11 @@ DB_USER=${DB_USER:-root} DB_NAME=${DB_NAME:-swarmdb} # Service descriptors -SERVICE_NAME=${SERVICE_NAME:-test-app} +SERVICE_NAME=${SERVICE_NAME:-swarm-cloud-ui} SERVICE_VERSION=${SERVICE_VERSION:-1.0.0} -CLUSTER_POLICY=${CLUSTER_POLICY:-test-app} -CLUSTER_ID=${CLUSTER_ID:-test-app} +CLUSTER_POLICY=${CLUSTER_POLICY:-swarm-cloud-ui} # Location and manifest inside the container. -# IMPORTANT: This script runs only on one node. All nodes must have the same location available already -# (baked into the image), so we point to /etc/swarm-services/${SERVICE_NAME}. LOCATION_PATH=${LOCATION_PATH:-/etc/swarm-services/${SERVICE_NAME}} MANIFEST_PATH=${MANIFEST_PATH:-${LOCATION_PATH}/manifest.yaml} SERVICE_PK="${CLUSTER_POLICY}:${SERVICE_NAME}" diff --git a/src/swarm-scripts/67.setup-test-app-route.sh b/src/swarm-scripts/67.setup-test-app-route.sh deleted file mode 100644 index fc65886b..00000000 --- a/src/swarm-scripts/67.setup-test-app-route.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# This script bootstraps the test-app-route service into SwarmDB via swarm-cli. -# Run it INSIDE the container. Assumes mysql client and swarm-cli.py are available. -# -# Notes: -# - The test-app-route manifest and main.py are expected to be available at: -# /etc/swarm-services/test-app-route/{manifest.yaml, main.py} -# This script only registers ClusterPolicy and ClusterService. -# - The logic to ensure that the Redis route is written only on the leader node -# is implemented inside the service's provision plugin (main.py), not here. -# - -DB_HOST=${DB_HOST:-127.0.0.1} -DB_PORT=${DB_PORT:-3306} -DB_USER=${DB_USER:-root} -DB_NAME=${DB_NAME:-swarmdb} - -# Service descriptors -SERVICE_NAME=${SERVICE_NAME:-test-app-route} -SERVICE_VERSION=${SERVICE_VERSION:-1.0.0} -CLUSTER_POLICY=${CLUSTER_POLICY:-test-app-route} -CLUSTER_ID=${CLUSTER_ID:-test-app-route} - -# Location and manifest inside the container. -# IMPORTANT: This script runs only on one node. All nodes must have the same location available already -# (baked into the image), so we point to /etc/swarm-services/${SERVICE_NAME}. -LOCATION_PATH=${LOCATION_PATH:-/etc/swarm-services/${SERVICE_NAME}} -MANIFEST_PATH=${MANIFEST_PATH:-${LOCATION_PATH}/manifest.yaml} -SERVICE_PK="${CLUSTER_POLICY}:${SERVICE_NAME}" - -if [ ! -f "$MANIFEST_PATH" ]; then - echo "Manifest not found at: $MANIFEST_PATH" >&2 - exit 1 -fi - -echo "Ensuring ClusterPolicy '$CLUSTER_POLICY'..." -if DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ - python3 "$(dirname "$0")/swarm-cli.py" get ClusterPolicies "$CLUSTER_POLICY" >/dev/null 2>&1; then - echo "ClusterPolicy '$CLUSTER_POLICY' already exists, skipping creation." -else - echo "Creating ClusterPolicy '$CLUSTER_POLICY'..." - DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ - python3 "$(dirname "$0")/swarm-cli.py" create ClusterPolicies "$CLUSTER_POLICY" --minSize=1 --maxSize=1 --maxClusters=1 -fi - -echo "Ensuring ClusterService '$SERVICE_PK'..." -if DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ - python3 "$(dirname "$0")/swarm-cli.py" get ClusterServices "$SERVICE_PK" >/dev/null 2>&1; then - echo "ClusterService '$SERVICE_PK' already exists, skipping creation." -else - echo "Creating ClusterService '$SERVICE_PK'..." - DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ - python3 "$(dirname "$0")/swarm-cli.py" create ClusterServices "$SERVICE_PK" --name="$SERVICE_NAME" --cluster_policy="$CLUSTER_POLICY" --version="$SERVICE_VERSION" --location="$LOCATION_PATH" -fi - -echo "Done. The provision worker will reconcile '$SERVICE_NAME' shortly." diff --git a/src/swarm-scripts/71.setup-auth-service.sh b/src/swarm-scripts/71.setup-auth-service.sh new file mode 100644 index 00000000..816bedc2 --- /dev/null +++ b/src/swarm-scripts/71.setup-auth-service.sh @@ -0,0 +1,72 @@ + +#!/bin/bash +set -euo pipefail + +# This script bootstraps the auth-service into SwarmDB via swarm-cli. +# Run it INSIDE the container. Assumes python3 and swarm-cli.py are available. +# +# Notes: +# - The service manifest is expected to be available on all nodes at: +# ${LOCATION_PATH}/manifest.yaml +# If you don't have a manifest yet, set ALLOW_MISSING_MANIFEST=1 to still +# register the ClusterService (manifest will be stored as NULL). +# - auth-service dependencies (MongoDB/NATS/etc.) are expected to be expressed +# in the manifest (stateExpr/commands) and handled by provision workers. + +DB_HOST=${DB_HOST:-127.0.0.1} +DB_PORT=${DB_PORT:-3306} +DB_USER=${DB_USER:-root} +DB_NAME=${DB_NAME:-swarmdb} + +# Service descriptors +SERVICE_NAME=${SERVICE_NAME:-auth-service} +SERVICE_VERSION=${SERVICE_VERSION:-1.0.0} +CLUSTER_POLICY=${CLUSTER_POLICY:-auth-service} +CLUSTER_ID=${CLUSTER_ID:-auth-service} + +# Location stored in ClusterServices; must exist on all nodes. +# The service provisioner (manifest.yaml + main.py) is baked into the image under +# /etc/swarm-services/${SERVICE_NAME}. The application payload lives under +# /etc/auth-service and is referenced by the provisioner. +LOCATION_PATH=${LOCATION_PATH:-/etc/swarm-services/${SERVICE_NAME}} +MANIFEST_PATH=${MANIFEST_PATH:-${LOCATION_PATH}/manifest.yaml} +SERVICE_PK="${CLUSTER_POLICY}:${SERVICE_NAME}" + +ALLOW_MISSING_MANIFEST=${ALLOW_MISSING_MANIFEST:-0} + +if [ ! -f "$MANIFEST_PATH" ]; then + if [ "$ALLOW_MISSING_MANIFEST" = "1" ] || [ "$ALLOW_MISSING_MANIFEST" = "true" ]; then + echo "Warning: manifest not found at: $MANIFEST_PATH (continuing due to ALLOW_MISSING_MANIFEST=1)" >&2 + else + echo "Manifest not found at: $MANIFEST_PATH" >&2 + echo "If you want to register the service without a manifest, set ALLOW_MISSING_MANIFEST=1" >&2 + exit 1 + fi +fi + +echo "Ensuring ClusterPolicy '$CLUSTER_POLICY'..." +if DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ + python3 "$(dirname "$0")/swarm-cli.py" get ClusterPolicies "$CLUSTER_POLICY" >/dev/null 2>&1; then + echo "ClusterPolicy '$CLUSTER_POLICY' already exists, skipping creation." +else + echo "Creating ClusterPolicy '$CLUSTER_POLICY'..." + DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ + python3 "$(dirname "$0")/swarm-cli.py" create ClusterPolicies "$CLUSTER_POLICY" --minSize=1 --maxSize=1 --maxClusters=1 +fi + +echo "Ensuring ClusterService '$SERVICE_PK'..." +if DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ + python3 "$(dirname "$0")/swarm-cli.py" get ClusterServices "$SERVICE_PK" >/dev/null 2>&1; then + echo "ClusterService '$SERVICE_PK' already exists, skipping creation." +else + echo "Creating ClusterService '$SERVICE_PK'..." + DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ + python3 "$(dirname "$0")/swarm-cli.py" create ClusterServices "$SERVICE_PK" \ + --name="$SERVICE_NAME" \ + --cluster_policy="$CLUSTER_POLICY" \ + --version="$SERVICE_VERSION" \ + --location="$LOCATION_PATH" +fi + +echo "Done. The provision worker will reconcile '$SERVICE_NAME' shortly." + diff --git a/src/swarm-scripts/72.setup-domain-initializer.sh b/src/swarm-scripts/72.setup-domain-initializer.sh new file mode 100644 index 00000000..b7bbe465 --- /dev/null +++ b/src/swarm-scripts/72.setup-domain-initializer.sh @@ -0,0 +1,69 @@ +#!/bin/bash +set -euo pipefail + +# This script bootstraps the domain-initializer service into SwarmDB via swarm-cli. +# Run it INSIDE the container. Assumes python3 and swarm-cli.py are available. +# +# Notes: +# - The service manifest is expected to be available on all nodes at: +# ${LOCATION_PATH}/manifest.yaml +# If you don't have a manifest yet, set ALLOW_MISSING_MANIFEST=1 to still +# register the ClusterService (manifest will be stored as NULL). +# - domain-initializer dependencies are expected to be expressed in the manifest +# (stateExpr/commands) and handled by provision workers. + +DB_HOST=${DB_HOST:-127.0.0.1} +DB_PORT=${DB_PORT:-3306} +DB_USER=${DB_USER:-root} +DB_NAME=${DB_NAME:-swarmdb} + +# Service descriptors +SERVICE_NAME=${SERVICE_NAME:-domain-initializer} +SERVICE_VERSION=${SERVICE_VERSION:-1.0.0} +CLUSTER_POLICY=${CLUSTER_POLICY:-domain-initializer} +CLUSTER_ID=${CLUSTER_ID:-domain-initializer} + +# Location stored in ClusterServices; must exist on all nodes. +# The service provisioner (manifest.yaml + main.py) is baked into the image under +# /etc/swarm-services/${SERVICE_NAME}. +LOCATION_PATH=${LOCATION_PATH:-/etc/swarm-services/${SERVICE_NAME}} +MANIFEST_PATH=${MANIFEST_PATH:-${LOCATION_PATH}/manifest.yaml} +SERVICE_PK="${CLUSTER_POLICY}:${SERVICE_NAME}" + +ALLOW_MISSING_MANIFEST=${ALLOW_MISSING_MANIFEST:-0} + +if [ ! -f "$MANIFEST_PATH" ]; then + if [ "$ALLOW_MISSING_MANIFEST" = "1" ] || [ "$ALLOW_MISSING_MANIFEST" = "true" ]; then + echo "Warning: manifest not found at: $MANIFEST_PATH (continuing due to ALLOW_MISSING_MANIFEST=1)" >&2 + else + echo "Manifest not found at: $MANIFEST_PATH" >&2 + echo "If you want to register the service without a manifest, set ALLOW_MISSING_MANIFEST=1" >&2 + exit 1 + fi +fi + +echo "Ensuring ClusterPolicy '$CLUSTER_POLICY'..." +if DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ + python3 "$(dirname "$0")/swarm-cli.py" get ClusterPolicies "$CLUSTER_POLICY" >/dev/null 2>&1; then + echo "ClusterPolicy '$CLUSTER_POLICY' already exists, skipping creation." +else + echo "Creating ClusterPolicy '$CLUSTER_POLICY'..." + DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ + python3 "$(dirname "$0")/swarm-cli.py" create ClusterPolicies "$CLUSTER_POLICY" --minSize=1 --maxSize=1 --maxClusters=1 +fi + +echo "Ensuring ClusterService '$SERVICE_PK'..." +if DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ + python3 "$(dirname "$0")/swarm-cli.py" get ClusterServices "$SERVICE_PK" >/dev/null 2>&1; then + echo "ClusterService '$SERVICE_PK' already exists, skipping creation." +else + echo "Creating ClusterService '$SERVICE_PK'..." + DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ + python3 "$(dirname "$0")/swarm-cli.py" create ClusterServices "$SERVICE_PK" \ + --name="$SERVICE_NAME" \ + --cluster_policy="$CLUSTER_POLICY" \ + --version="$SERVICE_VERSION" \ + --location="$LOCATION_PATH" +fi + +echo "Done. The provision worker will reconcile '$SERVICE_NAME' shortly." diff --git a/src/swarm-scripts/73.setup-route-manager.sh b/src/swarm-scripts/73.setup-route-manager.sh new file mode 100644 index 00000000..696be333 --- /dev/null +++ b/src/swarm-scripts/73.setup-route-manager.sh @@ -0,0 +1,69 @@ +#!/bin/bash +set -euo pipefail + +# This script bootstraps the route-manager service into SwarmDB via swarm-cli. +# Run it INSIDE the container. Assumes python3 and swarm-cli.py are available. +# +# Notes: +# - The service manifest is expected to be available on all nodes at: +# ${LOCATION_PATH}/manifest.yaml +# If you don't have a manifest yet, set ALLOW_MISSING_MANIFEST=1 to still +# register the ClusterService (manifest will be stored as NULL). +# - route-manager dependencies are expected to be expressed in the manifest +# (stateExpr/commands) and handled by provision workers. + +DB_HOST=${DB_HOST:-127.0.0.1} +DB_PORT=${DB_PORT:-3306} +DB_USER=${DB_USER:-root} +DB_NAME=${DB_NAME:-swarmdb} + +# Service descriptors +SERVICE_NAME=${SERVICE_NAME:-route-manager} +SERVICE_VERSION=${SERVICE_VERSION:-1.0.0} +CLUSTER_POLICY=${CLUSTER_POLICY:-route-manager} +CLUSTER_ID=${CLUSTER_ID:-route-manager} + +# Location stored in ClusterServices; must exist on all nodes. +# The service provisioner (manifest.yaml + main.py) is baked into the image under +# /etc/swarm-services/${SERVICE_NAME}. +LOCATION_PATH=${LOCATION_PATH:-/etc/swarm-services/${SERVICE_NAME}} +MANIFEST_PATH=${MANIFEST_PATH:-${LOCATION_PATH}/manifest.yaml} +SERVICE_PK="${CLUSTER_POLICY}:${SERVICE_NAME}" + +ALLOW_MISSING_MANIFEST=${ALLOW_MISSING_MANIFEST:-0} + +if [ ! -f "$MANIFEST_PATH" ]; then + if [ "$ALLOW_MISSING_MANIFEST" = "1" ] || [ "$ALLOW_MISSING_MANIFEST" = "true" ]; then + echo "Warning: manifest not found at: $MANIFEST_PATH (continuing due to ALLOW_MISSING_MANIFEST=1)" >&2 + else + echo "Manifest not found at: $MANIFEST_PATH" >&2 + echo "If you want to register the service without a manifest, set ALLOW_MISSING_MANIFEST=1" >&2 + exit 1 + fi +fi + +echo "Ensuring ClusterPolicy '$CLUSTER_POLICY'..." +if DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ + python3 "$(dirname "$0")/swarm-cli.py" get ClusterPolicies "$CLUSTER_POLICY" >/dev/null 2>&1; then + echo "ClusterPolicy '$CLUSTER_POLICY' already exists, skipping creation." +else + echo "Creating ClusterPolicy '$CLUSTER_POLICY'..." + DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ + python3 "$(dirname "$0")/swarm-cli.py" create ClusterPolicies "$CLUSTER_POLICY" --minSize=1 --maxSize=1 --maxClusters=1 +fi + +echo "Ensuring ClusterService '$SERVICE_PK'..." +if DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ + python3 "$(dirname "$0")/swarm-cli.py" get ClusterServices "$SERVICE_PK" >/dev/null 2>&1; then + echo "ClusterService '$SERVICE_PK' already exists, skipping creation." +else + echo "Creating ClusterService '$SERVICE_PK'..." + DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_NAME="$DB_NAME" \ + python3 "$(dirname "$0")/swarm-cli.py" create ClusterServices "$SERVICE_PK" \ + --name="$SERVICE_NAME" \ + --cluster_policy="$CLUSTER_POLICY" \ + --version="$SERVICE_VERSION" \ + --location="$LOCATION_PATH" +fi + +echo "Done. The provision worker will reconcile '$SERVICE_NAME' shortly."