openship 0.4.5 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -24
- package/dist/bare-2HYDXQEH.js +23 -0
- package/dist/{bowser-L6PVH37V.js → bowser-O3AEA6AN.js} +3 -1
- package/dist/{chunk-YFLQGDN2.js → chunk-4TJC4JZQ.js} +18 -9
- package/dist/{chunk-JXQCFTDV.js → chunk-4Z5QO7N6.js} +4 -2
- package/dist/{chunk-JJVSOE4I.js → chunk-5MZ624VX.js} +2 -0
- package/dist/{chunk-TDFK6ISF.js → chunk-6APIPIJY.js} +6 -4
- package/dist/{chunk-3LCRNQ2C.js → chunk-6APULWSU.js} +151 -133
- package/dist/{chunk-FTBXJ2VA.js → chunk-6B5YCFFV.js} +6 -4
- package/dist/{chunk-MDFYN2QH.js → chunk-AVIB7CT7.js} +3 -1
- package/dist/{chunk-W7KGHYLV.js → chunk-DYPH4HCI.js} +2 -0
- package/dist/{chunk-5WI7ZXOC.js → chunk-EI575GHN.js} +4 -2
- package/dist/{chunk-Z6EKX6CX.js → chunk-FPRHYBY2.js} +2 -0
- package/dist/{chunk-FZJX6XP7.js → chunk-FRNAR33O.js} +4 -2
- package/dist/{chunk-XRWV5ZNR.js → chunk-FXTBHTWY.js} +360 -42
- package/dist/chunk-ILFETZOM.js +131 -0
- package/dist/{chunk-E3ZPCNPU.js → chunk-IMBD5PZ5.js} +251 -105
- package/dist/{chunk-VJTXRCTT.js → chunk-IPYPMI3S.js} +10 -6
- package/dist/{chunk-PARRPUXN.js → chunk-IXCHURVK.js} +5 -3
- package/dist/{chunk-YJAS5RQX.js → chunk-JYGD5WSN.js} +2 -0
- package/dist/{chunk-JWBXYO24.js → chunk-KI2EY3WB.js} +2 -0
- package/dist/{chunk-YVXF6GGR.js → chunk-L2VOGB2X.js} +5 -104
- package/dist/{chunk-K5OH5U7N.js → chunk-MZ2WEHHY.js} +5 -3
- package/dist/{chunk-SJBR527P.js → chunk-O6ZZF52V.js} +3 -1
- package/dist/{chunk-VLPJLXXB.js → chunk-OWED2GRM.js} +3 -1
- package/dist/{chunk-C4FOQJ63.js → chunk-OXWDY22G.js} +4 -2
- package/dist/{chunk-NPVJYR7M.js → chunk-Q2CIRAOK.js} +2 -0
- package/dist/chunk-R5BHFFAV.js +282 -0
- package/dist/{chunk-JYAGTMSJ.js → chunk-RPHRPFEH.js} +2 -0
- package/dist/{chunk-R74UB4X4.js → chunk-SJIFLLX4.js} +397 -85
- package/dist/{chunk-HYL4QYZM.js → chunk-TGIINPVM.js} +13 -2
- package/dist/{chunk-SYCZE6FQ.js → chunk-TYVME3XD.js} +7 -1
- package/dist/{chunk-UMYQALBH.js → chunk-UXWAVM4C.js} +1788 -1484
- package/dist/{chunk-HDV4NSA3.js → chunk-V6AN6XLZ.js} +16 -6
- package/dist/{chunk-3YCMJRDZ.js → chunk-VDMQJHPI.js} +36 -15
- package/dist/{chunk-RZPDJC2J.js → chunk-VFINNTUL.js} +20 -1
- package/dist/{chunk-65KKUJBR.js → chunk-VWPEKOH6.js} +4 -2
- package/dist/{chunk-GHXGCANU.js → chunk-W73X43R7.js} +4 -2
- package/dist/{chunk-7EYFCKZX.js → chunk-XFCDYZUQ.js} +8 -6
- package/dist/cloud-2KXREXMO.js +30 -0
- package/dist/{cloud-7IGG45CF.js → cloud-7QYOGF7F.js} +4 -2
- package/dist/detect-2XSCFFTF.js +44 -0
- package/dist/{dist-NQB6QGX3.js → dist-IQJ6H6UZ.js} +4 -2
- package/dist/{dist-es-DCR6UJMP.js → dist-es-5DY7K32L.js} +7 -5
- package/dist/{dist-es-TYYZ5KEF.js → dist-es-JSTDUS7Q.js} +7 -5
- package/dist/{dist-es-7EZIM4JU.js → dist-es-OSNJBLXB.js} +6 -4
- package/dist/{dist-es-6RSPR56Q.js → dist-es-RYHIPOTN.js} +15 -13
- package/dist/{dist-es-33Z3Y6VE.js → dist-es-VNKHQVIB.js} +5 -3
- package/dist/{dist-es-D6QQVWRZ.js → dist-es-WV4AMOII.js} +9 -7
- package/dist/{dist-es-GMEQINIE.js → dist-es-X3B5QYKB.js} +7 -5
- package/dist/docker-LODK5Y2S.js +16 -0
- package/dist/docker-edge-executor-VHW3JPJS.js +13 -0
- package/dist/edge-import-ZQ4Y5HEA.js +45 -0
- package/dist/ensure-container-edge-ED25XL3Z.js +31 -0
- package/dist/{event-streams-G7F6Q7QY.js → event-streams-EHRAKL75.js} +5 -3
- package/dist/{executor-O5NR7LMP.js → executor-YPT6RH6O.js} +9 -7
- package/dist/index.d.ts +2 -1
- package/dist/index.js +279 -245
- package/dist/{loadSso-LH753OSJ.js → loadSso-46FQRIZS.js} +11 -9
- package/dist/local-executor-V7U5VOZW.js +12 -0
- package/dist/nginx-XWJAOGS4.js +24 -0
- package/dist/{noop-6KC6HRDB.js → noop-FMMFG2EZ.js} +4 -2
- package/dist/{openresty-lua-KFCOJAG4.js → openresty-lua-4WLGDSER.js} +11 -3
- package/dist/server/engine/ChangeLog +2289 -0
- package/dist/server/engine/Documentations +5 -0
- package/dist/server/engine/LICENSE +674 -0
- package/dist/server/engine/README.md +34 -0
- package/dist/server/engine/conf/amavisd +150 -0
- package/dist/server/engine/conf/clamav +71 -0
- package/dist/server/engine/conf/core +632 -0
- package/dist/server/engine/conf/dovecot +151 -0
- package/dist/server/engine/conf/fail2ban +72 -0
- package/dist/server/engine/conf/global +698 -0
- package/dist/server/engine/conf/iredapd +52 -0
- package/dist/server/engine/conf/logwatch +28 -0
- package/dist/server/engine/conf/postfix +97 -0
- package/dist/server/engine/conf/postgresql +93 -0
- package/dist/server/engine/conf/spamassassin +39 -0
- package/dist/server/engine/dialog/config_via_dialog.sh +258 -0
- package/dist/server/engine/dialog/optional_components.sh +122 -0
- package/dist/server/engine/dialog/pgsql_config.sh +52 -0
- package/dist/server/engine/dialog/virtual_domain_config.sh +94 -0
- package/dist/server/engine/functions/amavisd.sh +267 -0
- package/dist/server/engine/functions/backend.sh +85 -0
- package/dist/server/engine/functions/clamav.sh +100 -0
- package/dist/server/engine/functions/cleanup.sh +393 -0
- package/dist/server/engine/functions/dovecot.sh +583 -0
- package/dist/server/engine/functions/fail2ban.sh +209 -0
- package/dist/server/engine/functions/iredapd.sh +356 -0
- package/dist/server/engine/functions/optional_components.sh +24 -0
- package/dist/server/engine/functions/packages.sh +522 -0
- package/dist/server/engine/functions/packages_freebsd.sh +179 -0
- package/dist/server/engine/functions/postfix.sh +457 -0
- package/dist/server/engine/functions/postgresql.sh +239 -0
- package/dist/server/engine/functions/spamassassin.sh +49 -0
- package/dist/server/engine/functions/system_accounts.sh +128 -0
- package/dist/server/engine/iRedMail.sh +144 -0
- package/dist/server/engine/pkgs/get_all.sh +338 -0
- package/dist/server/engine/pkgs/pkgs.freebsd.sha256 +3 -0
- package/dist/server/engine/pkgs/pkgs.openbsd.sha256 +6 -0
- package/dist/server/engine/pkgs/pkgs.sha256 +1 -0
- package/dist/server/engine/samples/amavisd/amavisd.conf +677 -0
- package/dist/server/engine/samples/amavisd/amavisd.mysql +278 -0
- package/dist/server/engine/samples/amavisd/amavisd.pgsql +253 -0
- package/dist/server/engine/samples/amavisd/amavisd.schema +942 -0
- package/dist/server/engine/samples/amavisd/default_spam_policy.sql +46 -0
- package/dist/server/engine/samples/dovecot/dovecot-2.4-mariadb.conf +758 -0
- package/dist/server/engine/samples/dovecot/dovecot-2.4-openldap.conf +767 -0
- package/dist/server/engine/samples/dovecot/dovecot-2.4-pgsql.conf +762 -0
- package/dist/server/engine/samples/dovecot/dovecot-last-login.conf +50 -0
- package/dist/server/engine/samples/dovecot/dovecot-ldap.conf +21 -0
- package/dist/server/engine/samples/dovecot/dovecot-share-folder.conf +22 -0
- package/dist/server/engine/samples/dovecot/dovecot-sql.conf +28 -0
- package/dist/server/engine/samples/dovecot/dovecot-used-quota.conf +13 -0
- package/dist/server/engine/samples/dovecot/dovecot.conf +484 -0
- package/dist/server/engine/samples/dovecot/dovecot.sieve +7 -0
- package/dist/server/engine/samples/dovecot/dovecot2-quota-warning.sh +38 -0
- package/dist/server/engine/samples/dovecot/sql/imap_share_folder.mysql +16 -0
- package/dist/server/engine/samples/dovecot/sql/last_login.mysql +12 -0
- package/dist/server/engine/samples/dovecot/sql/used_quota.mysql +26 -0
- package/dist/server/engine/samples/dovecot/systemd/override.conf +3 -0
- package/dist/server/engine/samples/fail2ban/action.d/banned_db.conf +9 -0
- package/dist/server/engine/samples/fail2ban/bin/fail2ban_banned_db +268 -0
- package/dist/server/engine/samples/fail2ban/fail2ban.local +17 -0
- package/dist/server/engine/samples/fail2ban/filter.d/dovecot.iredmail.conf +5 -0
- package/dist/server/engine/samples/fail2ban/filter.d/postfix-pregreet.iredmail.conf +16 -0
- package/dist/server/engine/samples/fail2ban/filter.d/postfix.iredmail.conf +18 -0
- package/dist/server/engine/samples/fail2ban/filter.d/roundcube.iredmail.conf +9 -0
- package/dist/server/engine/samples/fail2ban/jail.d/dovecot.local +8 -0
- package/dist/server/engine/samples/fail2ban/jail.d/nginx-http-auth.local +8 -0
- package/dist/server/engine/samples/fail2ban/jail.d/postfix-pregreet.local +9 -0
- package/dist/server/engine/samples/fail2ban/jail.d/postfix.local +8 -0
- package/dist/server/engine/samples/fail2ban/jail.d/roundcube.local +9 -0
- package/dist/server/engine/samples/fail2ban/jail.d/sogo.local +8 -0
- package/dist/server/engine/samples/fail2ban/jail.d/sshd.local +8 -0
- package/dist/server/engine/samples/fail2ban/jail.local +6 -0
- package/dist/server/engine/samples/fail2ban/openbsd/rc +27 -0
- package/dist/server/engine/samples/fail2ban/sql/fail2ban.mysql +49 -0
- package/dist/server/engine/samples/fail2ban/sql/fail2ban.pgsql +48 -0
- package/dist/server/engine/samples/firewall/firewalld/services/ssh.xml +6 -0
- package/dist/server/engine/samples/firewall/firewalld/zones/iredmail.xml +15 -0
- package/dist/server/engine/samples/firewall/iptables/ip6tables.init.debian +362 -0
- package/dist/server/engine/samples/firewall/iptables/ip6tables.rules +53 -0
- package/dist/server/engine/samples/firewall/iptables/iptables.init.debian +363 -0
- package/dist/server/engine/samples/firewall/iptables/iptables.rules +54 -0
- package/dist/server/engine/samples/firewall/nftables.conf +58 -0
- package/dist/server/engine/samples/freebsd/newsyslog.conf.d/dovecot.conf +3 -0
- package/dist/server/engine/samples/freebsd/newsyslog.conf.d/iredapd.conf +1 -0
- package/dist/server/engine/samples/freebsd/newsyslog.conf.d/mlmmjadmin.conf +1 -0
- package/dist/server/engine/samples/freebsd/newsyslog.conf.d/php-fpm.conf +2 -0
- package/dist/server/engine/samples/freebsd/newsyslog.conf.d/slapd.conf +1 -0
- package/dist/server/engine/samples/freebsd/syslog.d/dovecot.conf +2 -0
- package/dist/server/engine/samples/freebsd/syslog.d/fail2ban.conf +2 -0
- package/dist/server/engine/samples/freebsd/syslog.d/iredapd.conf +2 -0
- package/dist/server/engine/samples/freebsd/syslog.d/mlmmjadmin.conf +2 -0
- package/dist/server/engine/samples/freebsd/syslog.d/php-fpm.conf +2 -0
- package/dist/server/engine/samples/freebsd/syslog.d/slapd.conf +2 -0
- package/dist/server/engine/samples/iredmail/iredmail.mysql +491 -0
- package/dist/server/engine/samples/iredmail/iredmail.pgsql +465 -0
- package/dist/server/engine/samples/iredmail/iredmail.schema +456 -0
- package/dist/server/engine/samples/iredmail/used_quota_triggers_dovecot_2.3.pgsql +40 -0
- package/dist/server/engine/samples/iredmail/used_quota_triggers_dovecot_2.4.pgsql +18 -0
- package/dist/server/engine/samples/logrotate/dovecot +13 -0
- package/dist/server/engine/samples/logrotate/iredapd +13 -0
- package/dist/server/engine/samples/logrotate/mlmmjadmin +13 -0
- package/dist/server/engine/samples/logrotate/openldap +19 -0
- package/dist/server/engine/samples/logrotate/php-fpm +13 -0
- package/dist/server/engine/samples/openbsd/ldapd.conf +58 -0
- package/dist/server/engine/samples/openbsd/pf.conf +28 -0
- package/dist/server/engine/samples/postfix/command_filter.pcre +3 -0
- package/dist/server/engine/samples/postfix/freebsd/mailer.conf +9 -0
- package/dist/server/engine/samples/postfix/helo_access.pcre +180 -0
- package/dist/server/engine/samples/postfix/ldap/catchall_maps.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/recipient_bcc_maps_domain.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/recipient_bcc_maps_user.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/relay_domains.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/sender_bcc_maps_domain.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/sender_bcc_maps_user.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/sender_dependent_relayhost_maps_domain.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/sender_dependent_relayhost_maps_user.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/sender_login_maps.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/transport_maps_domain.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/transport_maps_user.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/virtual_alias_maps.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/virtual_group_maps.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/virtual_group_members_maps.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/virtual_mailbox_domains.cf +11 -0
- package/dist/server/engine/samples/postfix/ldap/virtual_mailbox_maps.cf +12 -0
- package/dist/server/engine/samples/postfix/main.cf +282 -0
- package/dist/server/engine/samples/postfix/main.cf.amavisd +7 -0
- package/dist/server/engine/samples/postfix/main.cf.dovecot +8 -0
- package/dist/server/engine/samples/postfix/main.cf.ldap +39 -0
- package/dist/server/engine/samples/postfix/main.cf.mlmmj +5 -0
- package/dist/server/engine/samples/postfix/main.cf.mysql +39 -0
- package/dist/server/engine/samples/postfix/main.cf.pgsql +39 -0
- package/dist/server/engine/samples/postfix/main.cf.postscreen +30 -0
- package/dist/server/engine/samples/postfix/master.cf +87 -0
- package/dist/server/engine/samples/postfix/mysql/catchall_maps.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/domain_alias_catchall_maps.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/domain_alias_maps.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/recipient_bcc_maps_domain.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/recipient_bcc_maps_user.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/relay_domains.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/sender_bcc_maps_domain.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/sender_bcc_maps_user.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/sender_dependent_relayhost_maps.cf +6 -0
- package/dist/server/engine/samples/postfix/mysql/sender_login_maps.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/transport_maps_domain.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/transport_maps_maillist.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/transport_maps_user.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/virtual_alias_maps.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/virtual_mailbox_domains.cf +5 -0
- package/dist/server/engine/samples/postfix/mysql/virtual_mailbox_maps.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/catchall_maps.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/domain_alias_catchall_maps.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/domain_alias_maps.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/recipient_bcc_maps_domain.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/recipient_bcc_maps_user.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/relay_domains.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/sender_bcc_maps_domain.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/sender_bcc_maps_user.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/sender_dependent_relayhost_maps.cf +6 -0
- package/dist/server/engine/samples/postfix/pgsql/sender_login_maps.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/transport_maps_domain.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/transport_maps_maillist.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/transport_maps_user.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/virtual_alias_maps.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/virtual_mailbox_domains.cf +5 -0
- package/dist/server/engine/samples/postfix/pgsql/virtual_mailbox_maps.cf +5 -0
- package/dist/server/engine/samples/postfix/postscreen_access.cidr +6 -0
- package/dist/server/engine/samples/postgresql/sql/add_first_domain_and_user.sql +34 -0
- package/dist/server/engine/samples/postgresql/sql/grant_permissions.sql +14 -0
- package/dist/server/engine/samples/postgresql/sql/init_vmail_db.sql +15 -0
- package/dist/server/engine/samples/rsyslog.d/1-iredmail-dovecot.conf +23 -0
- package/dist/server/engine/samples/rsyslog.d/1-iredmail-fail2ban.conf +2 -0
- package/dist/server/engine/samples/rsyslog.d/1-iredmail-iredapd.conf +12 -0
- package/dist/server/engine/samples/rsyslog.d/1-iredmail-mlmmjadmin.conf +12 -0
- package/dist/server/engine/samples/rsyslog.d/1-iredmail-openldap.conf +2 -0
- package/dist/server/engine/samples/rsyslog.d/1-iredmail-phpfpm.conf +3 -0
- package/dist/server/engine/samples/spamassassin/local.cf +186 -0
- package/dist/server/engine/samples/spamassassin/razor.conf +1 -0
- package/dist/server/engine/samples/systemd/clamd.service.d/override.conf +2 -0
- package/dist/server/engine/samples/systemd/mariadb.service.d/override.conf +3 -0
- package/dist/server/engine/samples/systemd/slapd.service.d/override.conf +6 -0
- package/dist/server/engine/samples/yum/CentOS-Linux-AppStream.repo +6 -0
- package/dist/server/engine/samples/yum/CentOS-Linux-PowerTools.repo +6 -0
- package/dist/server/engine/samples/yum/CentOS-Stream-AppStream.repo +7 -0
- package/dist/server/engine/samples/yum/CentOS-Stream-PowerTools.repo +7 -0
- package/dist/server/engine/samples/yum/symas-openldap.repo +6 -0
- package/dist/server/engine/tools/add_enabled_service.py +60 -0
- package/dist/server/engine/tools/backup_mysql.sh +238 -0
- package/dist/server/engine/tools/backup_openldap.sh +230 -0
- package/dist/server/engine/tools/backup_pgsql.sh +217 -0
- package/dist/server/engine/tools/backup_sogo.sh +156 -0
- package/dist/server/engine/tools/create_mail_user_OpenLDAP.py +335 -0
- package/dist/server/engine/tools/create_mail_user_OpenLDAP.sh +287 -0
- package/dist/server/engine/tools/create_mail_user_SQL.sh +127 -0
- package/dist/server/engine/tools/enable_postscreen.sh +144 -0
- package/dist/server/engine/tools/fail2ban_unban_ip.sh +39 -0
- package/dist/server/engine/tools/find_sasl_login_ip.sh +20 -0
- package/dist/server/engine/tools/find_top_sasl_usernames.sh +28 -0
- package/dist/server/engine/tools/generate_ssl_keys.sh +85 -0
- package/dist/server/engine/tools/kill_high_cpu_sogo_process.sh +32 -0
- package/dist/server/engine/tools/ldap_assign_user_to_groups.py +151 -0
- package/dist/server/engine/tools/ldap_move_members_to_another_group.py +129 -0
- package/dist/server/engine/tools/migrate_sql_alias_table.py +177 -0
- package/dist/server/engine/update/0.9.8/amavisd.mysql +29 -0
- package/dist/server/engine/update/0.9.8/amavisd.pgsql +39 -0
- package/dist/server/engine/update/0.9.8/iredmail.mysql +53 -0
- package/dist/server/engine/update/0.9.8/iredmail.pgsql +61 -0
- package/dist/server/engine/update/0.9.9/iredmail.mysql +2 -0
- package/dist/server/engine/update/0.9.9/iredmail.pgsql +2 -0
- package/dist/server/engine/update/1.0/iredmail.mysql +2 -0
- package/dist/server/engine/update/1.0/iredmail.pgsql +2 -0
- package/dist/server/engine/update/1.2/last_login.mysql +16 -0
- package/dist/server/engine/update/1.4.0/iredmail.mysql +22 -0
- package/dist/server/engine/update/1.4.0/iredmail.pgsql +20 -0
- package/dist/server/engine/update/1.4.1/iredmail.mysql +3 -0
- package/dist/server/engine/update/1.4.1/iredmail.pgsql +3 -0
- package/dist/server/engine/update/1.4.1/sogo.mysql +11 -0
- package/dist/server/engine/update/1.4.1/sogo.pgsql +30 -0
- package/dist/server/engine/update/1.4.1/update-ldap.py +66 -0
- package/dist/server/engine/update/1.4.2/iredmail.mysql +9 -0
- package/dist/server/engine/update/1.4.2/iredmail.pgsql +9 -0
- package/dist/server/engine/update/1.4.2/sogo.pgsql +30 -0
- package/dist/server/engine/update/1.6.0/sogo_view.pgsql +27 -0
- package/dist/server/engine/update/1.6.3/iredmail.mysql +1 -0
- package/dist/server/engine/update/1.7.0/fail2ban.mysql +11 -0
- package/dist/server/engine/update/1.7.0/fail2ban.pgsql +12 -0
- package/dist/server/engine/update/1.7.1/amavisd.mysql +1 -0
- package/dist/server/engine/update/1.7.2/vmail.mysql +30 -0
- package/dist/server/engine/update/1.7.3/deleted_mailboxes.mysql +43 -0
- package/dist/server/engine/update/1.7.3/iredadmin.mysql +26 -0
- package/dist/server/engine/update/1.7.3/vmail.mysql +79 -0
- package/dist/server/engine/update/1.7.3/vmail.pgsql +9 -0
- package/dist/server/engine/update/README.md +1 -0
- package/dist/server/engine/update/ldap/README.md +40 -0
- package/dist/server/engine/update/ldap/update-ldap-dovecot-2.3.py +69 -0
- package/dist/server/index.js +55793 -53701
- package/dist/server/migrations/0071_gh_device_token.sql +3 -0
- package/dist/server/migrations/0072_gh_device_token_method.sql +1 -0
- package/dist/server/migrations/meta/_journal.json +14 -0
- package/dist/setup-2DMFIA74.js +20 -0
- package/dist/{signin-PVC7MBZJ.js → signin-ITOPKKUB.js} +11 -9
- package/dist/{sso-oidc-ZXY77BSL.js → sso-oidc-NON2PD7Y.js} +11 -9
- package/dist/{sts-LX5L4UU3.js → sts-HHFFSAY2.js} +10 -8
- package/package.json +1 -1
- package/dist/bare-CNSFDNSZ.js +0 -20
- package/dist/cloud-655YYCEW.js +0 -26
- package/dist/docker-UWQPDEGW.js +0 -14
- package/dist/docker-edge-executor-ZLAYE5FE.js +0 -11
- package/dist/edge-import-AFID2EN3.js +0 -41
- package/dist/nginx-M36GDYL7.js +0 -18
- package/dist/setup-ZXXB5S22.js +0 -16
|
@@ -1,1472 +1,1746 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
":" //# ; exec "$(command -v bun || command -v node)" "$0" "$@"
|
|
1
3
|
import { createRequire as __ospCreateRequire } from "node:module";
|
|
2
4
|
const require = __ospCreateRequire(import.meta.url);
|
|
3
5
|
import {
|
|
4
|
-
describeProcess,
|
|
5
|
-
probeListeningPort,
|
|
6
6
|
resolveEnvironment
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-L2VOGB2X.js";
|
|
8
8
|
import {
|
|
9
9
|
formatDuration,
|
|
10
10
|
isRemoteConnectionError,
|
|
11
11
|
systemDebug
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-RPHRPFEH.js";
|
|
13
|
+
import {
|
|
14
|
+
containerCommand,
|
|
15
|
+
edgeContainerExecutor,
|
|
16
|
+
isSafeCertPath,
|
|
17
|
+
readDeclaredPair,
|
|
18
|
+
readMaybeInContainer,
|
|
19
|
+
validateCertFor
|
|
20
|
+
} from "./chunk-FXTBHTWY.js";
|
|
13
21
|
import {
|
|
14
22
|
sq
|
|
15
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-FPRHYBY2.js";
|
|
24
|
+
import {
|
|
25
|
+
EDGE_CONTAINER_NAME,
|
|
26
|
+
EdgeConflictError,
|
|
27
|
+
EdgeMigrateRequested,
|
|
28
|
+
edgeFailureReason,
|
|
29
|
+
freeEdgeTargets,
|
|
30
|
+
invalidateEdgeContainer,
|
|
31
|
+
ourLuaOnHost,
|
|
32
|
+
probeEdge,
|
|
33
|
+
resolveOurEdgeContainer,
|
|
34
|
+
sq as sq2,
|
|
35
|
+
stopTargetsForStatus
|
|
36
|
+
} from "./chunk-R5BHFFAV.js";
|
|
16
37
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
deployLuaScripts,
|
|
20
|
-
detectOpenRestyPaths,
|
|
21
|
-
ensureOpenRestyConfig
|
|
22
|
-
} from "./chunk-RZPDJC2J.js";
|
|
38
|
+
waitForPortListening
|
|
39
|
+
} from "./chunk-ILFETZOM.js";
|
|
23
40
|
import {
|
|
24
|
-
|
|
41
|
+
EDGE_CONTAINER_MOUNTS,
|
|
42
|
+
EDGE_HOST_PATHS,
|
|
43
|
+
OPENRESTY_DEFAULT_PATHS,
|
|
44
|
+
detectOpenRestyPaths
|
|
45
|
+
} from "./chunk-VFINNTUL.js";
|
|
46
|
+
import {
|
|
47
|
+
buildEdgeImageRef,
|
|
25
48
|
compareSemver,
|
|
26
49
|
safeErrorMessage
|
|
27
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-6APULWSU.js";
|
|
28
51
|
|
|
29
|
-
// ../../packages/adapters/src/system/
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
return
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
};
|
|
52
|
+
// ../../packages/adapters/src/system/proxy/import/parse-utils.ts
|
|
53
|
+
async function tryExec(executor, command) {
|
|
54
|
+
try {
|
|
55
|
+
return await executor.exec(command);
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
36
58
|
}
|
|
37
|
-
return {
|
|
38
|
-
supported: true,
|
|
39
|
-
installCommand: "curl -fsSL https://get.docker.com | sh",
|
|
40
|
-
startCommand: profile.serviceManager === "systemd" ? "systemctl enable docker && systemctl start docker" : void 0,
|
|
41
|
-
verifyCommand: "docker --version"
|
|
42
|
-
};
|
|
43
59
|
}
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
apt: "apt-get update -qq && apt-get install -y -qq git",
|
|
47
|
-
dnf: "dnf install -y git",
|
|
48
|
-
yum: "yum install -y git",
|
|
49
|
-
apk: "apk add --no-cache git",
|
|
50
|
-
brew: "brew install git"
|
|
51
|
-
};
|
|
52
|
-
const installCommand = commands[profile.packageManager];
|
|
53
|
-
if (!installCommand) {
|
|
54
|
-
return {
|
|
55
|
-
supported: false,
|
|
56
|
-
unsupportedReason: "No supported package manager found for Git installation"
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
return {
|
|
60
|
-
supported: true,
|
|
61
|
-
installCommand,
|
|
62
|
-
verifyCommand: "git --version"
|
|
63
|
-
};
|
|
60
|
+
function stripComments(text) {
|
|
61
|
+
return text.replace(/#.*$/gm, "");
|
|
64
62
|
}
|
|
65
|
-
function
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
63
|
+
function extractBlocks(text, keyword) {
|
|
64
|
+
const blocks = [];
|
|
65
|
+
const re = new RegExp(`(?:^|[\\s;}])${keyword}\\b[^{;]*\\{`, "g");
|
|
66
|
+
let m;
|
|
67
|
+
while ((m = re.exec(text)) !== null) {
|
|
68
|
+
const openIdx = m.index + m[0].length - 1;
|
|
69
|
+
let depth = 1;
|
|
70
|
+
let i = openIdx + 1;
|
|
71
|
+
for (; i < text.length && depth > 0; i++) {
|
|
72
|
+
if (text[i] === "{") depth++;
|
|
73
|
+
else if (text[i] === "}") depth--;
|
|
74
|
+
}
|
|
75
|
+
if (depth === 0) {
|
|
76
|
+
blocks.push(text.slice(openIdx + 1, i - 1));
|
|
77
|
+
re.lastIndex = i;
|
|
78
|
+
} else {
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
79
81
|
}
|
|
80
|
-
return
|
|
81
|
-
supported: true,
|
|
82
|
-
installCommand,
|
|
83
|
-
verifyCommand: "rsync --version | head -n 1"
|
|
84
|
-
};
|
|
82
|
+
return blocks;
|
|
85
83
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
unsupportedReason: "OpenResty installation is only supported on Linux servers"
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
let installCommand;
|
|
116
|
-
if (profile.packageManager === "apt") {
|
|
117
|
-
const distro = profile.distro === "debian" ? "debian" : "ubuntu";
|
|
118
|
-
installCommand = [
|
|
119
|
-
"set -e",
|
|
120
|
-
// Heal a stale/bad openresty.list left by a prior run (e.g. the #86
|
|
121
|
-
// 'resolute' pin) BEFORE the first apt-get update reads it — otherwise
|
|
122
|
-
// set -e aborts here and the repo never gets rewritten.
|
|
123
|
-
"rm -f /etc/apt/sources.list.d/openresty.list",
|
|
124
|
-
"apt-get update -qq && apt-get install -y -qq wget gnupg2 lsb-release",
|
|
125
|
-
"wget -qO /tmp/openresty-pubkey.gpg https://openresty.org/package/pubkey.gpg",
|
|
126
|
-
"gpg --yes --dearmor -o /usr/share/keyrings/openresty.gpg /tmp/openresty-pubkey.gpg",
|
|
127
|
-
`REPO=${distro}`,
|
|
128
|
-
...OPENRESTY_APT_SOURCES,
|
|
129
|
-
"apt-get update -qq && apt-get install -y -qq openresty"
|
|
130
|
-
].join("\n");
|
|
131
|
-
} else if (profile.packageManager === "dnf") {
|
|
132
|
-
const distro = profile.distro === "fedora" ? "fedora" : "centos";
|
|
133
|
-
installCommand = `wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/${distro}/openresty.repo && dnf install -y openresty`;
|
|
134
|
-
} else if (profile.packageManager === "yum") {
|
|
135
|
-
installCommand = "wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/centos/openresty.repo && yum install -y openresty";
|
|
136
|
-
} else if (profile.packageManager === "apk") {
|
|
137
|
-
installCommand = [
|
|
138
|
-
"apk add --no-cache wget",
|
|
139
|
-
"wget -qO /etc/apk/keys/admin@openresty.com-5ea678a6.rsa.pub https://openresty.org/package/alpine/admin@openresty.com-5ea678a6.rsa.pub",
|
|
140
|
-
`. /etc/os-release && echo "https://openresty.org/package/alpine/v$( echo $VERSION_ID | cut -d. -f1,2 )/main" >> /etc/apk/repositories`,
|
|
141
|
-
"apk update && apk add openresty"
|
|
142
|
-
].join(" && ");
|
|
143
|
-
} else {
|
|
144
|
-
installCommand = [
|
|
145
|
-
"set -e",
|
|
146
|
-
"if command -v apt-get >/dev/null 2>&1; then",
|
|
147
|
-
" rm -f /etc/apt/sources.list.d/openresty.list",
|
|
148
|
-
" apt-get update -qq && apt-get install -y -qq wget gnupg2 lsb-release",
|
|
149
|
-
" wget -qO /tmp/openresty-pubkey.gpg https://openresty.org/package/pubkey.gpg",
|
|
150
|
-
" gpg --yes --dearmor -o /usr/share/keyrings/openresty.gpg /tmp/openresty-pubkey.gpg",
|
|
151
|
-
' DISTRO=$(. /etc/os-release 2>/dev/null && echo "$ID" || echo "ubuntu")',
|
|
152
|
-
' case "$DISTRO" in debian) REPO=debian ;; *) REPO=ubuntu ;; esac',
|
|
153
|
-
...OPENRESTY_APT_SOURCES.map((l) => " " + l),
|
|
154
|
-
" apt-get update -qq && apt-get install -y -qq openresty",
|
|
155
|
-
"elif command -v dnf >/dev/null 2>&1; then",
|
|
156
|
-
" wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/centos/openresty.repo && dnf install -y openresty",
|
|
157
|
-
"elif command -v yum >/dev/null 2>&1; then",
|
|
158
|
-
" wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/centos/openresty.repo && yum install -y openresty",
|
|
159
|
-
"elif command -v apk >/dev/null 2>&1; then",
|
|
160
|
-
" apk add --no-cache wget \\",
|
|
161
|
-
" && wget -qO /etc/apk/keys/admin@openresty.com-5ea678a6.rsa.pub https://openresty.org/package/alpine/admin@openresty.com-5ea678a6.rsa.pub \\",
|
|
162
|
-
' && . /etc/os-release && echo "https://openresty.org/package/alpine/v$(echo $VERSION_ID | cut -d. -f1,2)/main" >> /etc/apk/repositories \\',
|
|
163
|
-
" && apk update && apk add openresty",
|
|
164
|
-
"else",
|
|
165
|
-
' echo "No supported package manager found (tried apt-get, dnf, yum, apk)" >&2 && exit 1',
|
|
166
|
-
"fi"
|
|
167
|
-
].join("\n");
|
|
84
|
+
function sq3(value) {
|
|
85
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ../../packages/adapters/src/system/proxy/import/nginx.ts
|
|
89
|
+
async function loadNginxConfig(executor) {
|
|
90
|
+
const dumped = await tryExec(executor, "nginx -T 2>/dev/null");
|
|
91
|
+
if (dumped && /server\s*\{/.test(dumped)) return dumped;
|
|
92
|
+
const cat = await tryExec(
|
|
93
|
+
executor,
|
|
94
|
+
"cat /etc/nginx/nginx.conf /etc/nginx/sites-enabled/* /etc/nginx/conf.d/*.conf 2>/dev/null"
|
|
95
|
+
);
|
|
96
|
+
return cat ?? "";
|
|
97
|
+
}
|
|
98
|
+
function firstDirective(body, name) {
|
|
99
|
+
const m = body.match(new RegExp(`(?:^|[;{\\s])${name}\\s+([^;]+);`));
|
|
100
|
+
return m?.[1]?.trim();
|
|
101
|
+
}
|
|
102
|
+
function parseUpstreams(config) {
|
|
103
|
+
const map = /* @__PURE__ */ new Map();
|
|
104
|
+
const re = /(?:^|[\s;}])upstream\s+(\S+)\s*\{([^}]*)\}/g;
|
|
105
|
+
let m;
|
|
106
|
+
while ((m = re.exec(config)) !== null) {
|
|
107
|
+
const name = m[1];
|
|
108
|
+
const server = m[2].match(/(?:^|[\s;{])server\s+([^;\s]+)/);
|
|
109
|
+
if (name && server?.[1]) map.set(name, server[1].trim());
|
|
168
110
|
}
|
|
169
|
-
return
|
|
170
|
-
supported: true,
|
|
171
|
-
installCommand,
|
|
172
|
-
startCommand: profile.serviceManager === "systemd" ? "systemctl enable openresty && systemctl start openresty" : void 0,
|
|
173
|
-
verifyCommand: "openresty -v 2>&1"
|
|
174
|
-
};
|
|
111
|
+
return map;
|
|
175
112
|
}
|
|
176
|
-
function
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
113
|
+
function resolveProxyTarget(proxyPass, upstreams) {
|
|
114
|
+
const raw = proxyPass.replace(/;$/, "").trim();
|
|
115
|
+
if (raw.includes("$")) return { reason: `proxy_pass "${raw}" uses an nginx variable` };
|
|
116
|
+
if (/\/\/unix:/i.test(raw)) return { reason: `proxy_pass "${raw}" targets a unix socket` };
|
|
117
|
+
const m = raw.match(/^(https?:\/\/)([^/]+)(\/.*)?$/i);
|
|
118
|
+
if (!m) return { reason: `unrecognized proxy_pass "${raw}"` };
|
|
119
|
+
const scheme = m[1];
|
|
120
|
+
const authority = m[2];
|
|
121
|
+
const host = authority.replace(/:\d+$/, "");
|
|
122
|
+
if (upstreams.has(host)) return { url: `${scheme}${upstreams.get(host)}` };
|
|
123
|
+
const isIp = /^\d{1,3}(\.\d{1,3}){3}$/.test(host);
|
|
124
|
+
if (isIp || host === "localhost" || host.includes(".")) return { url: raw };
|
|
125
|
+
return { reason: `proxy_pass host "${host}" is an undeclared upstream \u2014 not migratable` };
|
|
126
|
+
}
|
|
127
|
+
function extractLocationProxies(serverBody) {
|
|
128
|
+
const out = [];
|
|
129
|
+
const re = /(?:^|[\s;}])location\s+([^{]+?)\s*\{/g;
|
|
130
|
+
let m;
|
|
131
|
+
while ((m = re.exec(serverBody)) !== null) {
|
|
132
|
+
const path = m[1].trim();
|
|
133
|
+
const openIdx = m.index + m[0].length - 1;
|
|
134
|
+
let depth = 1;
|
|
135
|
+
let i = openIdx + 1;
|
|
136
|
+
for (; i < serverBody.length && depth > 0; i++) {
|
|
137
|
+
if (serverBody[i] === "{") depth++;
|
|
138
|
+
else if (serverBody[i] === "}") depth--;
|
|
139
|
+
}
|
|
140
|
+
if (depth !== 0) break;
|
|
141
|
+
const locBody = serverBody.slice(openIdx + 1, i - 1);
|
|
142
|
+
re.lastIndex = i;
|
|
143
|
+
const pp = firstDirective(locBody, "proxy_pass");
|
|
144
|
+
if (pp) out.push({ path, proxyPass: pp });
|
|
189
145
|
}
|
|
190
|
-
return
|
|
191
|
-
supported: true,
|
|
192
|
-
installCommand,
|
|
193
|
-
verifyCommand: "certbot --version 2>/dev/null"
|
|
194
|
-
};
|
|
146
|
+
return out;
|
|
195
147
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
],
|
|
210
|
-
parseVersion: (output) => output.match(/openresty\/(\S+)/)?.[1] ?? output.match(/nginx\/(\S+)/)?.[1] ?? output,
|
|
211
|
-
missingMessage: "OpenResty is not installed",
|
|
212
|
-
notRunningMessage: "OpenResty is installed but not running"
|
|
213
|
-
},
|
|
214
|
-
certbot: {
|
|
215
|
-
versionCommand: "certbot --version 2>/dev/null",
|
|
216
|
-
parseVersion: (output) => output.match(/certbot\s+(\S+)/)?.[1] ?? output,
|
|
217
|
-
missingMessage: "Certbot is not installed"
|
|
218
|
-
},
|
|
219
|
-
git: {
|
|
220
|
-
versionCommand: "git --version",
|
|
221
|
-
parseVersion: (output) => output.match(/git version (\S+)/)?.[1] ?? output,
|
|
222
|
-
missingMessage: "Git is not installed"
|
|
223
|
-
},
|
|
224
|
-
rsync: {
|
|
225
|
-
versionCommand: "rsync --version | head -n 1",
|
|
226
|
-
parseVersion: (output) => output.match(/rsync\s+version\s+(\S+)/i)?.[1] ?? output,
|
|
227
|
-
missingMessage: "rsync is not installed"
|
|
228
|
-
}
|
|
229
|
-
},
|
|
230
|
-
installs: {
|
|
231
|
-
docker: dockerInstallPlan,
|
|
232
|
-
git: gitInstallPlan,
|
|
233
|
-
rsync: rsyncInstallPlan,
|
|
234
|
-
openresty: openrestyInstallPlan,
|
|
235
|
-
certbot: certbotInstallPlan
|
|
236
|
-
}
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
// ../../packages/adapters/src/system/available-version.ts
|
|
240
|
-
var PACKAGE_NAMES = {
|
|
241
|
-
git: "git",
|
|
242
|
-
rsync: "rsync",
|
|
243
|
-
openresty: "openresty",
|
|
244
|
-
certbot: "certbot",
|
|
245
|
-
docker: null
|
|
246
|
-
};
|
|
247
|
-
function normalizePkgVersion(raw) {
|
|
248
|
-
let v = raw.trim();
|
|
249
|
-
const colon = v.lastIndexOf(":");
|
|
250
|
-
if (colon !== -1) v = v.slice(colon + 1);
|
|
251
|
-
const dash = v.indexOf("-");
|
|
252
|
-
if (dash !== -1) v = v.slice(0, dash);
|
|
253
|
-
return v.trim();
|
|
148
|
+
function isHttpsUpgradeForSelf(body, names) {
|
|
149
|
+
const targets = [
|
|
150
|
+
...body.matchAll(/(?:^|[\s;{])return\s+30[1-8]\s+([^;]+);/g),
|
|
151
|
+
...body.matchAll(/(?:^|[\s;{])rewrite\s+\S+\s+([^;]+?)\s+permanent\s*;/g)
|
|
152
|
+
].map((m) => m[1].trim());
|
|
153
|
+
if (targets.length === 0) return false;
|
|
154
|
+
return targets.every((raw) => {
|
|
155
|
+
if (!/^https:\/\//i.test(raw)) return false;
|
|
156
|
+
const authority = raw.replace(/^https:\/\//i, "").split(/[/\s]/)[0] ?? "";
|
|
157
|
+
const host = (authority.match(/^(\$[a-z_]+|[^$:]+)/i)?.[1] ?? "").replace(/:\d+$/, "");
|
|
158
|
+
if (host === "$host" || host === "$server_name" || host === "$http_host") return true;
|
|
159
|
+
return names.includes(host);
|
|
160
|
+
});
|
|
254
161
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
162
|
+
function parseServer(body, source, upstreams) {
|
|
163
|
+
const warnings = [];
|
|
164
|
+
const names = firstDirective(body, "server_name")?.split(/\s+/).filter((n) => n && n !== "_" && !n.startsWith("~")) ?? [];
|
|
165
|
+
const listens = [...body.matchAll(/(?:^|[;{\s])listen\s+([^;]+);/g)].map((m) => m[1]);
|
|
166
|
+
const ssl = listens.some((l) => /\bssl\b/.test(l) || /\b443\b/.test(l));
|
|
167
|
+
const root = firstDirective(body, "root");
|
|
168
|
+
const certPath = firstDirective(body, "ssl_certificate");
|
|
169
|
+
const keyPath = firstDirective(body, "ssl_certificate_key");
|
|
170
|
+
if (names.length === 0) return { warnings: [] };
|
|
171
|
+
const rawTargets = extractLocationProxies(body);
|
|
172
|
+
if (rawTargets.length === 0) {
|
|
173
|
+
const serverLevel = firstDirective(body, "proxy_pass");
|
|
174
|
+
if (serverLevel) rawTargets.push({ path: "/", proxyPass: serverLevel });
|
|
175
|
+
}
|
|
176
|
+
const resolved = [];
|
|
177
|
+
for (const t of rawTargets) {
|
|
178
|
+
const r = resolveProxyTarget(t.proxyPass, upstreams);
|
|
179
|
+
if ("reason" in r) warnings.push(`nginx: ${names[0]} ${t.path} \u2014 ${r.reason} (skipped)`);
|
|
180
|
+
else resolved.push({ path: t.path, url: r.url });
|
|
181
|
+
}
|
|
182
|
+
let target;
|
|
183
|
+
let routes;
|
|
184
|
+
if (resolved.length > 0) {
|
|
185
|
+
const primary = resolved.find((r) => r.path === "/") ?? resolved[0];
|
|
186
|
+
target = { kind: "proxy", url: primary.url };
|
|
187
|
+
routes = resolved;
|
|
188
|
+
} else if (root) {
|
|
189
|
+
target = { kind: "static", root: root.replace(/;$/, "") };
|
|
190
|
+
} else if (isHttpsUpgradeForSelf(body, names)) {
|
|
191
|
+
return { warnings: [] };
|
|
192
|
+
} else {
|
|
193
|
+
warnings.push(`nginx: ${names[0]} has neither proxy_pass nor root \u2014 skipped (${source})`);
|
|
194
|
+
return { warnings };
|
|
260
195
|
}
|
|
196
|
+
const site = { serverNames: names, ssl, target, source };
|
|
197
|
+
if (routes) site.routes = routes;
|
|
198
|
+
if (certPath && keyPath) site.tls = { certPath, keyPath };
|
|
199
|
+
return { site, warnings };
|
|
261
200
|
}
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
return
|
|
201
|
+
function parseNginxConfig(raw) {
|
|
202
|
+
const warnings = [];
|
|
203
|
+
const sites = [];
|
|
204
|
+
if (!raw.trim()) {
|
|
205
|
+
return { proxy: "nginx", sites, warnings: ["nginx: no readable configuration found"] };
|
|
267
206
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
);
|
|
273
|
-
return out?.trim().split(/\s+/)[1]?.trim() ?? null;
|
|
207
|
+
const config = stripComments(raw);
|
|
208
|
+
const upstreams = parseUpstreams(config);
|
|
209
|
+
const blocks = extractBlocks(config, "server");
|
|
210
|
+
if (blocks.length === 0) {
|
|
211
|
+
warnings.push("nginx: no server blocks found");
|
|
274
212
|
}
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
213
|
+
for (const body of blocks) {
|
|
214
|
+
const { site, warnings: blockWarnings } = parseServer(body, "nginx", upstreams);
|
|
215
|
+
warnings.push(...blockWarnings);
|
|
216
|
+
if (site) sites.push(site);
|
|
279
217
|
}
|
|
280
|
-
return
|
|
218
|
+
return { proxy: "nginx", sites, warnings };
|
|
281
219
|
}
|
|
282
|
-
async function
|
|
283
|
-
|
|
284
|
-
(s) => s.installed && s.version && PACKAGE_NAMES[s.name]
|
|
285
|
-
);
|
|
286
|
-
await Promise.all(
|
|
287
|
-
targets.map(async (s) => {
|
|
288
|
-
try {
|
|
289
|
-
const pkg = PACKAGE_NAMES[s.name];
|
|
290
|
-
const candidate = await probeCandidate(executor, profile.packageManager, pkg);
|
|
291
|
-
if (!candidate) return;
|
|
292
|
-
const avail = normalizePkgVersion(candidate);
|
|
293
|
-
const installed = normalizePkgVersion(s.version);
|
|
294
|
-
if (avail && compareSemver(avail, installed) > 0) {
|
|
295
|
-
s.availableVersion = avail;
|
|
296
|
-
s.updateAvailable = true;
|
|
297
|
-
}
|
|
298
|
-
} catch {
|
|
299
|
-
}
|
|
300
|
-
})
|
|
301
|
-
);
|
|
220
|
+
async function scanNginx(executor) {
|
|
221
|
+
return parseNginxConfig(await loadNginxConfig(executor));
|
|
302
222
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
name: "docker",
|
|
308
|
-
label: "Docker",
|
|
309
|
-
description: "Container runtime for deployments",
|
|
310
|
-
installable: true,
|
|
311
|
-
category: "core"
|
|
312
|
-
},
|
|
313
|
-
{
|
|
314
|
-
name: "git",
|
|
315
|
-
label: "Git",
|
|
316
|
-
description: "Version control for source code",
|
|
317
|
-
installable: true,
|
|
318
|
-
category: "core"
|
|
319
|
-
},
|
|
320
|
-
{
|
|
321
|
-
name: "openresty",
|
|
322
|
-
label: "OpenResty",
|
|
323
|
-
description: "Reverse proxy with Lua scripting for routing and traffic control",
|
|
324
|
-
installable: true,
|
|
325
|
-
category: "infrastructure"
|
|
326
|
-
},
|
|
327
|
-
{
|
|
328
|
-
name: "certbot",
|
|
329
|
-
label: "Certbot",
|
|
330
|
-
description: "Let's Encrypt certificate provisioning",
|
|
331
|
-
installable: true,
|
|
332
|
-
category: "infrastructure"
|
|
333
|
-
},
|
|
334
|
-
{
|
|
335
|
-
name: "rsync",
|
|
336
|
-
label: "rsync",
|
|
337
|
-
description: "Fast directory sync for remote local-build transfers",
|
|
338
|
-
installable: true,
|
|
339
|
-
category: "infrastructure"
|
|
340
|
-
}
|
|
223
|
+
var OUR_EDGE_SITE_GLOBS = [
|
|
224
|
+
`${EDGE_HOST_PATHS.sitesDir}/*.conf`,
|
|
225
|
+
`${OPENRESTY_DEFAULT_PATHS.sitesDir}/*.conf`,
|
|
226
|
+
"/etc/openresty/sites-enabled/*.conf"
|
|
341
227
|
];
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
);
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
installable: false,
|
|
351
|
-
category: "infrastructure"
|
|
352
|
-
};
|
|
228
|
+
async function scanOpenshipEdge(executor) {
|
|
229
|
+
const cat = `cat ${OUR_EDGE_SITE_GLOBS.join(" ")} 2>/dev/null`;
|
|
230
|
+
const raw = await tryExec(executor, cat);
|
|
231
|
+
if (raw?.trim()) return parseNginxConfig(raw);
|
|
232
|
+
const container = await resolveOurEdgeContainer(executor);
|
|
233
|
+
if (!container) return parseNginxConfig("");
|
|
234
|
+
const fromContainer = await tryExec(executor, containerCommand(container, cat));
|
|
235
|
+
return parseNginxConfig(fromContainer ?? "");
|
|
353
236
|
}
|
|
354
237
|
|
|
355
|
-
// ../../packages/adapters/src/system/
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
`exec:abort ${command} (${formatDuration(startedAt)}) ${safeErrorMessage(err)}`
|
|
371
|
-
);
|
|
372
|
-
throw err;
|
|
238
|
+
// ../../packages/adapters/src/system/proxy/import/caddy.ts
|
|
239
|
+
var CADDYFILE_PATHS = ["/etc/caddy/Caddyfile", "/etc/Caddyfile"];
|
|
240
|
+
async function loadCaddyJson(executor) {
|
|
241
|
+
for (const p of CADDYFILE_PATHS) {
|
|
242
|
+
const out = await tryExec(executor, `caddy adapt --adapter caddyfile --config ${p} 2>/dev/null`);
|
|
243
|
+
if (out && out.trim().startsWith("{")) return out;
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
function flattenHandlers(handlers, out = []) {
|
|
248
|
+
for (const h of handlers ?? []) {
|
|
249
|
+
if (h.handler === "subroute") {
|
|
250
|
+
for (const r of h.routes ?? []) flattenHandlers(r.handle, out);
|
|
251
|
+
} else {
|
|
252
|
+
out.push(h);
|
|
373
253
|
}
|
|
374
|
-
const msg = safeErrorMessage(err);
|
|
375
|
-
systemDebug(
|
|
376
|
-
"checks",
|
|
377
|
-
`exec:fail ${command} (${formatDuration(startedAt)}) ${msg}`
|
|
378
|
-
);
|
|
379
|
-
return null;
|
|
380
254
|
}
|
|
255
|
+
return out;
|
|
381
256
|
}
|
|
382
|
-
function
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
installed: true,
|
|
390
|
-
version,
|
|
391
|
-
running,
|
|
392
|
-
healthy: running !== void 0 ? running : true,
|
|
393
|
-
message: running ? `${name} ${version} - running` : `${name} ${version} - installed`
|
|
394
|
-
};
|
|
257
|
+
function targetFromRoute(route) {
|
|
258
|
+
const flat = flattenHandlers(route.handle);
|
|
259
|
+
const dial = flat.find((h) => h.handler === "reverse_proxy")?.upstreams?.find((u) => u.dial)?.dial;
|
|
260
|
+
if (dial) return { kind: "proxy", url: /^https?:\/\//.test(dial) ? dial : `http://${dial}` };
|
|
261
|
+
const root = flat.find((h) => h.handler === "vars" && typeof h.root === "string")?.root;
|
|
262
|
+
if (root && flat.some((h) => h.handler === "file_server")) return { kind: "static", root };
|
|
263
|
+
return null;
|
|
395
264
|
}
|
|
396
|
-
function
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
installable: component.installable,
|
|
403
|
-
installed: !!opts?.version,
|
|
404
|
-
version: opts?.version,
|
|
405
|
-
running: opts?.running,
|
|
406
|
-
healthy: false,
|
|
407
|
-
message
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
async function checkDocker(executor) {
|
|
411
|
-
const startedAt = Date.now();
|
|
412
|
-
const recipe = systemCatalog.checks.docker;
|
|
413
|
-
const version = await tryExec2(executor, recipe.versionCommand);
|
|
414
|
-
if (!version) {
|
|
415
|
-
systemDebug("checks", `docker:missing (${formatDuration(startedAt)})`);
|
|
416
|
-
return unhealthy("docker", recipe.missingMessage);
|
|
265
|
+
function parseCaddyJson(raw) {
|
|
266
|
+
let cfg;
|
|
267
|
+
try {
|
|
268
|
+
cfg = JSON.parse(raw);
|
|
269
|
+
} catch {
|
|
270
|
+
return null;
|
|
417
271
|
}
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
272
|
+
const servers = cfg.apps?.http?.servers;
|
|
273
|
+
if (!servers) return null;
|
|
274
|
+
const sites = [];
|
|
275
|
+
for (const srv of Object.values(servers)) {
|
|
276
|
+
const ssl = (srv.listen ?? []).some((l) => l.endsWith(":443"));
|
|
277
|
+
for (const route of srv.routes ?? []) {
|
|
278
|
+
const hosts = (route.match ?? []).flatMap((m) => m.host ?? []).filter((h) => h && h !== "*");
|
|
279
|
+
if (hosts.length === 0) continue;
|
|
280
|
+
const target = targetFromRoute(route);
|
|
281
|
+
if (!target) continue;
|
|
282
|
+
sites.push({ serverNames: hosts, ssl, target, source: "caddy (adapt)" });
|
|
283
|
+
}
|
|
426
284
|
}
|
|
427
|
-
|
|
428
|
-
return healthy("docker", parsed, true);
|
|
285
|
+
return { sites, warnings: [] };
|
|
429
286
|
}
|
|
430
|
-
async function
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
if (!version) {
|
|
435
|
-
systemDebug("checks", `git:missing (${formatDuration(startedAt)})`);
|
|
436
|
-
return unhealthy("git", recipe.missingMessage);
|
|
287
|
+
async function loadCaddyfile(executor) {
|
|
288
|
+
for (const p of CADDYFILE_PATHS) {
|
|
289
|
+
const out = await tryExec(executor, `cat ${p} 2>/dev/null`);
|
|
290
|
+
if (out && out.trim()) return out;
|
|
437
291
|
}
|
|
438
|
-
|
|
439
|
-
systemDebug("checks", `git:healthy (${formatDuration(startedAt)})`);
|
|
440
|
-
return healthy("git", parsed);
|
|
292
|
+
return "";
|
|
441
293
|
}
|
|
442
|
-
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
294
|
+
function caddyBlocks(text) {
|
|
295
|
+
const out = [];
|
|
296
|
+
let i = 0;
|
|
297
|
+
let headerStart = 0;
|
|
298
|
+
while (i < text.length) {
|
|
299
|
+
if (text[i] === "{") {
|
|
300
|
+
const header = text.slice(headerStart, i).trim();
|
|
301
|
+
let depth = 1;
|
|
302
|
+
let j = i + 1;
|
|
303
|
+
for (; j < text.length && depth > 0; j++) {
|
|
304
|
+
if (text[j] === "{") depth++;
|
|
305
|
+
else if (text[j] === "}") depth--;
|
|
306
|
+
}
|
|
307
|
+
if (depth !== 0) break;
|
|
308
|
+
out.push({ header, body: text.slice(i + 1, j - 1) });
|
|
309
|
+
i = j;
|
|
310
|
+
headerStart = j;
|
|
311
|
+
} else {
|
|
312
|
+
i++;
|
|
313
|
+
}
|
|
449
314
|
}
|
|
450
|
-
|
|
451
|
-
systemDebug("checks", `rsync:healthy (${formatDuration(startedAt)})`);
|
|
452
|
-
return healthy("rsync", parsed);
|
|
315
|
+
return out;
|
|
453
316
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
317
|
+
function parseAddresses(header) {
|
|
318
|
+
return header.split(/[\s,]+/).filter(Boolean).map((addr) => {
|
|
319
|
+
const httpScheme = /^http:\/\//i.test(addr);
|
|
320
|
+
const host = addr.replace(/^https?:\/\//i, "").replace(/:\d+$/, "");
|
|
321
|
+
const isPort80 = /:80$/.test(addr);
|
|
322
|
+
return { host, ssl: !httpScheme && !isPort80 };
|
|
323
|
+
}).filter((a) => a.host && !a.host.startsWith(":") && a.host !== "*");
|
|
324
|
+
}
|
|
325
|
+
async function scanCaddy(executor) {
|
|
326
|
+
const json = await loadCaddyJson(executor);
|
|
327
|
+
if (json) {
|
|
328
|
+
const parsed = parseCaddyJson(json);
|
|
329
|
+
if (parsed && parsed.sites.length > 0) {
|
|
330
|
+
return { proxy: "caddy", sites: parsed.sites, warnings: parsed.warnings };
|
|
331
|
+
}
|
|
461
332
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
);
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
return
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
333
|
+
return scanCaddyText(executor);
|
|
334
|
+
}
|
|
335
|
+
async function scanCaddyText(executor) {
|
|
336
|
+
const raw = await loadCaddyfile(executor);
|
|
337
|
+
const warnings = [];
|
|
338
|
+
const sites = [];
|
|
339
|
+
if (!raw.trim()) {
|
|
340
|
+
return { proxy: "caddy", sites, warnings: ["caddy: no readable Caddyfile found"] };
|
|
341
|
+
}
|
|
342
|
+
const text = stripComments(raw);
|
|
343
|
+
let blocks = caddyBlocks(text);
|
|
344
|
+
if (blocks.length === 0) {
|
|
345
|
+
const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
346
|
+
if (lines.length > 0) {
|
|
347
|
+
blocks = [{ header: lines[0], body: lines.slice(1).join("\n") }];
|
|
348
|
+
} else {
|
|
349
|
+
warnings.push("caddy: config present but no parseable site blocks");
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
for (const { header, body } of blocks) {
|
|
353
|
+
if (!header) continue;
|
|
354
|
+
const addrs = parseAddresses(header);
|
|
355
|
+
if (addrs.length === 0) {
|
|
356
|
+
warnings.push(`caddy: skipped a block with no usable address (${header.slice(0, 40)})`);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const proxyMatch = body.match(/(?:^|\s)reverse_proxy\s+([^\n{]+)/);
|
|
360
|
+
const rootMatch = body.match(/(?:^|\s)root\s+\*?\s*([^\n\s]+)/);
|
|
361
|
+
const tlsMatch = body.match(/(?:^|\s)tls\s+(\S+)\s+(\S+)/);
|
|
362
|
+
let target;
|
|
363
|
+
if (proxyMatch) {
|
|
364
|
+
const upstream = proxyMatch[1].trim().split(/\s+/)[0];
|
|
365
|
+
target = { kind: "proxy", url: /^https?:\/\//.test(upstream) ? upstream : `http://${upstream}` };
|
|
366
|
+
} else if (rootMatch) {
|
|
367
|
+
target = { kind: "static", root: rootMatch[1] };
|
|
368
|
+
} else {
|
|
369
|
+
warnings.push(`caddy: ${addrs[0].host} has no reverse_proxy or root \u2014 skipped`);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const site = {
|
|
373
|
+
serverNames: addrs.map((a) => a.host),
|
|
374
|
+
ssl: addrs.some((a) => a.ssl),
|
|
375
|
+
target,
|
|
376
|
+
source: "caddy"
|
|
377
|
+
};
|
|
378
|
+
if (tlsMatch && tlsMatch[1].includes("/")) {
|
|
379
|
+
site.tls = { certPath: tlsMatch[1], keyPath: tlsMatch[2] };
|
|
380
|
+
}
|
|
381
|
+
sites.push(site);
|
|
473
382
|
}
|
|
474
|
-
|
|
383
|
+
return { proxy: "caddy", sites, warnings };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ../../packages/adapters/src/system/proxy/import/apache.ts
|
|
387
|
+
async function vhostFilesFromDump(executor) {
|
|
388
|
+
const dump = await tryExec(
|
|
475
389
|
executor,
|
|
476
|
-
|
|
390
|
+
"apachectl -S 2>/dev/null || apache2ctl -S 2>/dev/null || httpd -S 2>/dev/null"
|
|
477
391
|
);
|
|
478
|
-
if (!
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
{ version: parsed, running: true }
|
|
484
|
-
);
|
|
392
|
+
if (!dump) return [];
|
|
393
|
+
const files = /* @__PURE__ */ new Set();
|
|
394
|
+
for (const m of dump.matchAll(/\(([^()]+?):\d+\)/g)) {
|
|
395
|
+
const f = m[1].trim();
|
|
396
|
+
if (f.startsWith("/")) files.add(f);
|
|
485
397
|
}
|
|
486
|
-
|
|
487
|
-
return healthy("openresty", parsed, true);
|
|
398
|
+
return [...files];
|
|
488
399
|
}
|
|
489
|
-
async function
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
systemDebug("checks", `certbot:missing (${formatDuration(startedAt)})`);
|
|
495
|
-
return unhealthy("certbot", recipe.missingMessage);
|
|
400
|
+
async function loadApacheConfig(executor) {
|
|
401
|
+
const files = await vhostFilesFromDump(executor);
|
|
402
|
+
if (files.length > 0) {
|
|
403
|
+
const cat2 = await tryExec(executor, `cat ${files.map(sq3).join(" ")} 2>/dev/null`);
|
|
404
|
+
if (cat2 && cat2.trim()) return cat2;
|
|
496
405
|
}
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
var COMPONENT_CHECKS = {
|
|
502
|
-
docker: checkDocker,
|
|
503
|
-
openresty: checkOpenResty,
|
|
504
|
-
certbot: checkCertbot,
|
|
505
|
-
git: checkGit,
|
|
506
|
-
rsync: checkRsync
|
|
507
|
-
};
|
|
508
|
-
async function mapWithConcurrency(items, limit, fn) {
|
|
509
|
-
const results = new Array(items.length);
|
|
510
|
-
let cursor = 0;
|
|
511
|
-
const worker = async () => {
|
|
512
|
-
while (cursor < items.length) {
|
|
513
|
-
const index = cursor++;
|
|
514
|
-
results[index] = await fn(items[index]);
|
|
515
|
-
}
|
|
516
|
-
};
|
|
517
|
-
await Promise.all(
|
|
518
|
-
Array.from({ length: Math.min(limit, items.length) }, worker)
|
|
406
|
+
const cat = await tryExec(
|
|
407
|
+
executor,
|
|
408
|
+
"cat /etc/apache2/sites-enabled/*.conf /etc/apache2/apache2.conf /etc/httpd/conf.d/*.conf /etc/httpd/conf/httpd.conf 2>/dev/null"
|
|
519
409
|
);
|
|
520
|
-
return
|
|
410
|
+
return cat ?? "";
|
|
521
411
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
);
|
|
530
|
-
const results = await mapWithConcurrency(
|
|
531
|
-
entries,
|
|
532
|
-
CHECK_CONCURRENCY,
|
|
533
|
-
([, fn]) => fn(executor)
|
|
412
|
+
function directive(body, name) {
|
|
413
|
+
const m = body.match(new RegExp(`(?:^|\\n)\\s*${name}\\s+([^\\n]+)`, "i"));
|
|
414
|
+
return m?.[1]?.trim();
|
|
415
|
+
}
|
|
416
|
+
function directiveAll(body, name) {
|
|
417
|
+
return [...body.matchAll(new RegExp(`(?:^|\\n)\\s*${name}\\s+([^\\n]+)`, "gi"))].map(
|
|
418
|
+
(m) => m[1].trim()
|
|
534
419
|
);
|
|
535
|
-
await enrichAvailable(executor, results);
|
|
536
|
-
systemDebug("checks", `checkAll:done (${formatDuration(startedAt)})`);
|
|
537
|
-
return results;
|
|
538
420
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
421
|
+
function parseVhost(body, portHint) {
|
|
422
|
+
const serverName = directive(body, "ServerName")?.split(/\s+/)[0];
|
|
423
|
+
const aliases = directiveAll(body, "ServerAlias").flatMap((line) => line.split(/\s+/));
|
|
424
|
+
const names = [serverName, ...aliases].filter((n) => Boolean(n));
|
|
425
|
+
if (names.length === 0) {
|
|
426
|
+
return { warning: "apache: skipped a VirtualHost with no ServerName" };
|
|
427
|
+
}
|
|
428
|
+
const routes = [...body.matchAll(/(?:^|\n)\s*ProxyPass\s+(\S+)\s+(\S+)/gi)].map((m) => ({ path: m[1], url: m[2] })).filter((p) => p.url && p.url !== "!");
|
|
429
|
+
const docRoot = directive(body, "DocumentRoot")?.replace(/^["']|["']$/g, "");
|
|
430
|
+
const certPath = directive(body, "SSLCertificateFile");
|
|
431
|
+
const keyPath = directive(body, "SSLCertificateKeyFile");
|
|
432
|
+
const ssl = /:443/.test(portHint) || /SSLEngine\s+on/i.test(body) || Boolean(certPath);
|
|
433
|
+
let target;
|
|
434
|
+
if (routes.length > 0) {
|
|
435
|
+
const primary = routes.find((r) => r.path === "/") ?? routes[0];
|
|
436
|
+
target = { kind: "proxy", url: primary.url };
|
|
437
|
+
} else if (docRoot) {
|
|
438
|
+
target = { kind: "static", root: docRoot };
|
|
439
|
+
} else {
|
|
440
|
+
return { warning: `apache: ${names[0]} has neither ProxyPass nor DocumentRoot \u2014 skipped` };
|
|
544
441
|
}
|
|
442
|
+
const site = { serverNames: names, ssl, target, source: "apache" };
|
|
443
|
+
if (routes.length > 0) site.routes = routes;
|
|
444
|
+
if (certPath && keyPath) site.tls = { certPath, keyPath };
|
|
445
|
+
return site;
|
|
545
446
|
}
|
|
546
|
-
|
|
547
|
-
const
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
)
|
|
560
|
-
|
|
447
|
+
function vhostBlocks(text) {
|
|
448
|
+
const out = [];
|
|
449
|
+
const re = /<VirtualHost([^>]*)>([\s\S]*?)<\/VirtualHost>/gi;
|
|
450
|
+
let m;
|
|
451
|
+
while ((m = re.exec(text)) !== null) {
|
|
452
|
+
out.push({ args: m[1].trim(), body: m[2] });
|
|
453
|
+
}
|
|
454
|
+
return out;
|
|
455
|
+
}
|
|
456
|
+
async function scanApache(executor) {
|
|
457
|
+
const raw = await loadApacheConfig(executor);
|
|
458
|
+
const warnings = [];
|
|
459
|
+
const sites = [];
|
|
460
|
+
if (!raw.trim()) {
|
|
461
|
+
return { proxy: "apache", sites, warnings: ["apache: no readable configuration found"] };
|
|
462
|
+
}
|
|
463
|
+
const text = stripComments(raw);
|
|
464
|
+
const blocks = vhostBlocks(text);
|
|
465
|
+
if (blocks.length === 0) warnings.push("apache: no <VirtualHost> blocks found");
|
|
466
|
+
for (const { args, body } of blocks) {
|
|
467
|
+
const parsed = parseVhost(body, args);
|
|
468
|
+
if ("warning" in parsed) warnings.push(parsed.warning);
|
|
469
|
+
else sites.push(parsed);
|
|
470
|
+
}
|
|
471
|
+
return { proxy: "apache", sites, warnings };
|
|
561
472
|
}
|
|
562
473
|
|
|
563
|
-
// ../../packages/adapters/src/system/
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
474
|
+
// ../../packages/adapters/src/system/proxy/import/traefik.ts
|
|
475
|
+
function extractHosts(rule) {
|
|
476
|
+
const hosts = [];
|
|
477
|
+
for (const call of rule.matchAll(/\bHost\(([^)]*)\)/gi)) {
|
|
478
|
+
for (const m of call[1].matchAll(/[`'"]([^`'"]+)[`'"]/g)) {
|
|
479
|
+
const h = m[1].trim();
|
|
480
|
+
if (h) hosts.push(h);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return hosts;
|
|
567
484
|
}
|
|
568
|
-
function
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
485
|
+
function extraMatchers(rule) {
|
|
486
|
+
const found = /* @__PURE__ */ new Set();
|
|
487
|
+
for (const m of rule.matchAll(
|
|
488
|
+
/\b(PathPrefix|PathRegexp|Path|HeadersRegexp|Headers|Method|Query|ClientIP|HostRegexp)\b/gi
|
|
489
|
+
)) {
|
|
490
|
+
found.add(m[1]);
|
|
491
|
+
}
|
|
492
|
+
return [...found];
|
|
572
493
|
}
|
|
573
|
-
function
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
);
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
494
|
+
function parseTraefikLabels(containers) {
|
|
495
|
+
const sites = [];
|
|
496
|
+
const warnings = [];
|
|
497
|
+
for (const c of containers) {
|
|
498
|
+
if (c.labels["traefik.enable"] === "false") continue;
|
|
499
|
+
const routers = /* @__PURE__ */ new Map();
|
|
500
|
+
const servicePorts = /* @__PURE__ */ new Map();
|
|
501
|
+
for (const [key, val] of Object.entries(c.labels)) {
|
|
502
|
+
const r = key.match(/^traefik\.http\.routers\.([^.]+)\.(.+)$/);
|
|
503
|
+
if (r) {
|
|
504
|
+
const bag = routers.get(r[1]) ?? {};
|
|
505
|
+
bag[r[2]] = val;
|
|
506
|
+
routers.set(r[1], bag);
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
const s = key.match(/^traefik\.http\.services\.([^.]+)\.loadbalancer\.server\.port$/);
|
|
510
|
+
if (s) servicePorts.set(s[1], val);
|
|
590
511
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
512
|
+
for (const [rname, props] of routers) {
|
|
513
|
+
const rule = props.rule;
|
|
514
|
+
if (!rule) continue;
|
|
515
|
+
const hosts = extractHosts(rule);
|
|
516
|
+
if (hosts.length === 0) {
|
|
517
|
+
warnings.push(
|
|
518
|
+
/HostRegexp/i.test(rule) ? `traefik: router "${rname}" uses HostRegexp (${rule}) \u2014 regex hosts aren't migratable` : `traefik: router "${rname}" has no Host() rule (${rule}) \u2014 skipped`
|
|
519
|
+
);
|
|
520
|
+
continue;
|
|
596
521
|
}
|
|
597
|
-
|
|
598
|
-
|
|
522
|
+
if (!c.ip) {
|
|
523
|
+
warnings.push(
|
|
524
|
+
`traefik: ${hosts.join(", ")} \u2014 couldn't resolve container ${c.name}'s IP; re-add the upstream manually`
|
|
525
|
+
);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
const svc = props.service;
|
|
529
|
+
const rawPort = (svc && servicePorts.get(svc)) ?? (servicePorts.size === 1 ? [...servicePorts.values()][0] : void 0) ?? "80";
|
|
530
|
+
const port = /^\d{1,5}$/.test(String(rawPort)) ? String(rawPort) : "80";
|
|
531
|
+
const url = `http://${c.ip}:${port}`;
|
|
532
|
+
const extras = extraMatchers(rule);
|
|
533
|
+
if (extras.length > 0) {
|
|
534
|
+
warnings.push(
|
|
535
|
+
`traefik: ${hosts.join(", ")} also matches ${extras.join(", ")} \u2014 only the Host() part is migrated; re-add path/header rules manually`
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
if (props.middlewares) {
|
|
539
|
+
warnings.push(`traefik: ${hosts.join(", ")} uses middleware(s) "${props.middlewares}" \u2014 not migrated`);
|
|
540
|
+
}
|
|
541
|
+
const ssl = Object.keys(props).some((p) => p === "tls" || p.startsWith("tls."));
|
|
542
|
+
sites.push({
|
|
543
|
+
serverNames: hosts,
|
|
544
|
+
ssl,
|
|
545
|
+
target: { kind: "proxy", url },
|
|
546
|
+
source: `traefik container ${c.name}`
|
|
547
|
+
});
|
|
599
548
|
}
|
|
600
|
-
}
|
|
549
|
+
}
|
|
550
|
+
return { proxy: "traefik", sites, warnings };
|
|
601
551
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
409,
|
|
610
|
-
"EDGE_CONFLICT"
|
|
611
|
-
);
|
|
612
|
-
this.status = status;
|
|
613
|
-
this.name = "EdgeConflictError";
|
|
552
|
+
async function scanTraefik(executor) {
|
|
553
|
+
const out = await tryExec(
|
|
554
|
+
executor,
|
|
555
|
+
"docker ps -q 2>/dev/null | xargs -r docker inspect --format '{{.Name}} {{json .Config.Labels}} {{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' 2>/dev/null"
|
|
556
|
+
);
|
|
557
|
+
if (!out) {
|
|
558
|
+
return { proxy: "traefik", sites: [], warnings: ["traefik: couldn't read container labels via docker inspect"] };
|
|
614
559
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
560
|
+
const containers = [];
|
|
561
|
+
for (const line of out.split("\n")) {
|
|
562
|
+
const [rawName, rawLabels, rawIps] = line.split(" ");
|
|
563
|
+
if (!rawName || !rawLabels) continue;
|
|
564
|
+
let labels;
|
|
565
|
+
try {
|
|
566
|
+
labels = JSON.parse(rawLabels) ?? {};
|
|
567
|
+
} catch {
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
if (!Object.keys(labels).some((k) => k.startsWith("traefik."))) continue;
|
|
571
|
+
const ip = (rawIps ?? "").trim().split(/\s+/)[0] || void 0;
|
|
572
|
+
containers.push({ name: rawName.replace(/^\//, ""), labels, ip });
|
|
623
573
|
}
|
|
624
|
-
|
|
625
|
-
|
|
574
|
+
return parseTraefikLabels(containers);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ../../packages/adapters/src/system/proxy/import/index.ts
|
|
578
|
+
var INSTALLED_MARKERS = [
|
|
579
|
+
{ proxy: "nginx", paths: ["/etc/nginx/nginx.conf"] },
|
|
580
|
+
{ proxy: "caddy", paths: ["/etc/caddy/Caddyfile"] },
|
|
581
|
+
{ proxy: "apache", paths: ["/etc/apache2/apache2.conf", "/etc/httpd/conf/httpd.conf"] },
|
|
582
|
+
{
|
|
583
|
+
proxy: "openresty",
|
|
584
|
+
paths: ["/usr/local/openresty/nginx/conf/nginx.conf", "/etc/openresty/nginx.conf"]
|
|
585
|
+
}
|
|
586
|
+
];
|
|
587
|
+
async function detectInstalledProxy(executor) {
|
|
588
|
+
const probe = INSTALLED_MARKERS.map(({ proxy, paths }) => {
|
|
589
|
+
const test = paths.map((p) => `[ -f '${p}' ]`).join(" || ");
|
|
590
|
+
return `{ ${test}; } && echo ${proxy}`;
|
|
591
|
+
}).join("; ");
|
|
592
|
+
let out = "";
|
|
626
593
|
try {
|
|
627
|
-
|
|
594
|
+
out = await executor.exec(`{ ${probe}; } 2>/dev/null; true`, { timeout: 1e4 });
|
|
628
595
|
} catch {
|
|
629
596
|
return null;
|
|
630
597
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
if (!text) return void 0;
|
|
634
|
-
const t = text.toLowerCase();
|
|
635
|
-
if (t.includes("openresty")) return "openresty";
|
|
636
|
-
if (/(^|[\s/:])nginx/.test(t)) return "nginx";
|
|
637
|
-
if (/(^|[\s/:])caddy/.test(t)) return "caddy";
|
|
638
|
-
if (/(apache2|httpd)/.test(t)) return "apache";
|
|
639
|
-
if (/(^|[\s/:])traefik/.test(t)) return "traefik";
|
|
640
|
-
if (/(^|[\s/:])haproxy/.test(t)) return "haproxy";
|
|
641
|
-
return void 0;
|
|
642
|
-
}
|
|
643
|
-
function sq2(value) {
|
|
644
|
-
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
645
|
-
}
|
|
646
|
-
function isOurEdgeContainer(name, image) {
|
|
647
|
-
return /openship-edge/i.test(`${name ?? ""} ${image ?? ""}`);
|
|
648
|
-
}
|
|
649
|
-
async function ourEdgeContainerRunning(executor) {
|
|
650
|
-
const byName = await tryExec3(
|
|
651
|
-
executor,
|
|
652
|
-
`docker ps --filter name=openship-edge --format '{{.Names}}' 2>/dev/null | head -1`
|
|
653
|
-
);
|
|
654
|
-
if (byName && byName.trim()) return true;
|
|
655
|
-
const all = await tryExec3(executor, `docker ps --format '{{.Names}} {{.Image}}' 2>/dev/null`);
|
|
656
|
-
if (!all) return false;
|
|
657
|
-
return all.split("\n").map((line) => line.trim()).filter(Boolean).some((line) => {
|
|
658
|
-
const [name, image] = line.split(" ");
|
|
659
|
-
return isOurEdgeContainer(name, image);
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
async function ourLuaOnHost(executor) {
|
|
663
|
-
const lua = await tryExec3(
|
|
664
|
-
executor,
|
|
665
|
-
`test -f ${OPENRESTY_LUA_DIR}/site_logger.lua && echo ok`
|
|
598
|
+
const found = new Set(
|
|
599
|
+
out.split("\n").map((l) => l.trim()).filter(Boolean)
|
|
666
600
|
);
|
|
667
|
-
return
|
|
601
|
+
return INSTALLED_MARKERS.find((m) => found.has(m.proxy))?.proxy ?? null;
|
|
668
602
|
}
|
|
669
|
-
async function
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
}
|
|
689
|
-
var WORKER_RE = /\b(?:nginx|openresty)\s*:\s*worker process/i;
|
|
690
|
-
var MASTER_RE = /\b(?:nginx|openresty)\s*:\s*master process/i;
|
|
691
|
-
async function resolveProxyMaster(executor, listener) {
|
|
692
|
-
if (!listener?.pid) return listener;
|
|
693
|
-
if (!WORKER_RE.test(`${listener.rawCommand ?? ""} ${listener.command ?? ""}`)) return listener;
|
|
694
|
-
const ppidRaw = await tryExec3(
|
|
695
|
-
executor,
|
|
696
|
-
`awk '/^PPid:/{print $2}' /proc/${listener.pid}/status 2>/dev/null || true`
|
|
697
|
-
);
|
|
698
|
-
const ppid = Number.parseInt((ppidRaw ?? "").trim(), 10);
|
|
699
|
-
if (!Number.isInteger(ppid) || ppid <= 1) return listener;
|
|
700
|
-
const master = await describeProcess(executor, ppid);
|
|
701
|
-
if (!master || !MASTER_RE.test(`${master.rawCommand ?? ""} ${master.command ?? ""}`)) {
|
|
702
|
-
return listener;
|
|
703
|
-
}
|
|
704
|
-
return master;
|
|
705
|
-
}
|
|
706
|
-
async function probeEdgePort(executor, port, ours) {
|
|
707
|
-
const listener = await resolveProxyMaster(executor, await probeListeningPort(executor, port));
|
|
708
|
-
const docker = await detectDockerOnPort(executor, port);
|
|
709
|
-
if (!listener && !docker) return null;
|
|
710
|
-
const proxy = classifyProxy(
|
|
711
|
-
[
|
|
712
|
-
docker?.image,
|
|
713
|
-
docker?.name,
|
|
714
|
-
listener?.rawCommand,
|
|
715
|
-
listener?.command,
|
|
716
|
-
listener?.systemdUnit
|
|
717
|
-
].filter(Boolean).join(" ")
|
|
718
|
-
);
|
|
719
|
-
const foreignDockerProxy = Boolean(docker) && !isOurEdgeContainer(docker?.name, docker?.image);
|
|
720
|
-
const managedByOpenship = isOurEdgeContainer(docker?.name, docker?.image) || ours.containerRunning && !foreignDockerProxy || !docker && ours.luaOnHost && await listenerIsOurOpenResty(executor, listener);
|
|
721
|
-
return {
|
|
722
|
-
port,
|
|
723
|
-
pid: listener?.pid ?? void 0,
|
|
724
|
-
command: docker ? `docker container ${docker.name} (${docker.image})` : listener?.command,
|
|
725
|
-
rawCommand: listener?.rawCommand,
|
|
726
|
-
systemdUnit: listener?.systemdUnit,
|
|
727
|
-
systemdDescription: listener?.systemdDescription,
|
|
728
|
-
isDocker: Boolean(docker),
|
|
729
|
-
containerName: docker?.name,
|
|
730
|
-
proxy,
|
|
731
|
-
managedByOpenship
|
|
732
|
-
};
|
|
603
|
+
async function scanImportableSites(executor, proxy) {
|
|
604
|
+
switch (proxy) {
|
|
605
|
+
case "nginx":
|
|
606
|
+
return scanNginx(executor);
|
|
607
|
+
case "caddy":
|
|
608
|
+
return scanCaddy(executor);
|
|
609
|
+
case "apache":
|
|
610
|
+
return scanApache(executor);
|
|
611
|
+
case "traefik":
|
|
612
|
+
return scanTraefik(executor);
|
|
613
|
+
case "openresty":
|
|
614
|
+
return scanOpenshipEdge(executor);
|
|
615
|
+
default:
|
|
616
|
+
return {
|
|
617
|
+
proxy,
|
|
618
|
+
sites: [],
|
|
619
|
+
warnings: [`${proxy}: config import not supported \u2014 takeover only`]
|
|
620
|
+
};
|
|
621
|
+
}
|
|
733
622
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
const luaOnHost = await ourLuaOnHost(executor);
|
|
737
|
-
const all = [];
|
|
738
|
-
for (const port of EDGE_PORTS) {
|
|
739
|
-
const occ = await probeEdgePort(executor, port, { containerRunning, luaOnHost });
|
|
740
|
-
if (occ) all.push(occ);
|
|
741
|
-
}
|
|
742
|
-
const foreign = all.filter((o) => !o.managedByOpenship);
|
|
743
|
-
let classification;
|
|
744
|
-
if (all.length === 0) classification = "free";
|
|
745
|
-
else if (foreign.length === 0) classification = "ours";
|
|
746
|
-
else if (foreign.every((o) => o.proxy && o.proxy !== "openresty")) classification = "known";
|
|
747
|
-
else classification = "unknown";
|
|
748
|
-
return {
|
|
749
|
-
classification,
|
|
750
|
-
occupants: foreign,
|
|
751
|
-
canProceedClean: classification === "free" || classification === "ours"
|
|
752
|
-
};
|
|
623
|
+
function canImportProxy(proxy) {
|
|
624
|
+
return proxy === "nginx" || proxy === "caddy" || proxy === "apache" || proxy === "traefik" || proxy === "openresty";
|
|
753
625
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
626
|
+
|
|
627
|
+
// ../../packages/adapters/src/system/proxy/import/caddy-certs.ts
|
|
628
|
+
var CADDY_DATA_ROOTS = [
|
|
629
|
+
"/var/lib/caddy/.local/share/caddy",
|
|
630
|
+
"/root/.local/share/caddy",
|
|
631
|
+
"/home/caddy/.local/share/caddy",
|
|
632
|
+
"/data/caddy"
|
|
633
|
+
];
|
|
634
|
+
function candidateNames(host) {
|
|
635
|
+
const names = [host];
|
|
636
|
+
const dot = host.indexOf(".");
|
|
637
|
+
if (dot > 0) names.push(`wildcard_${host.slice(dot)}`);
|
|
638
|
+
return names;
|
|
639
|
+
}
|
|
640
|
+
async function findPairIn(exec, root, host, container) {
|
|
641
|
+
for (const name of candidateNames(host)) {
|
|
642
|
+
const glob = `${root}/certificates/*/${name}/${name}.crt`;
|
|
643
|
+
let certPath = firstCrt(await tryExec(exec, `ls -1 ${glob} 2>/dev/null`));
|
|
644
|
+
if (!certPath && container) {
|
|
645
|
+
certPath = firstCrt(
|
|
646
|
+
await tryExec(exec, containerCommand(container, `ls -1 ${glob} 2>/dev/null`))
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
if (certPath) return { certPath, keyPath: certPath.replace(/\.crt$/, ".key") };
|
|
770
650
|
}
|
|
771
|
-
return
|
|
651
|
+
return null;
|
|
772
652
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
`systemctl disable --now ${sq2(t.unit)} 2>/dev/null || systemctl stop ${sq2(t.unit)} 2>/dev/null || true; systemctl reset-failed ${sq2(t.unit)} 2>/dev/null || true`
|
|
785
|
-
);
|
|
786
|
-
} else if (t.pid) {
|
|
787
|
-
onLog(`Stopping ${t.label ?? `process ${t.pid}`}${where}...`, "warn");
|
|
788
|
-
await tryExec3(executor, `kill ${t.pid} 2>/dev/null || true`);
|
|
789
|
-
await new Promise((r) => setTimeout(r, 800));
|
|
790
|
-
await tryExec3(executor, `kill -9 ${t.pid} 2>/dev/null || true`);
|
|
653
|
+
function firstCrt(listing) {
|
|
654
|
+
return (listing ?? "").split("\n").map((l) => l.trim()).find((l) => l.endsWith(".crt"));
|
|
655
|
+
}
|
|
656
|
+
async function caddyStoreCert(exec, host, container) {
|
|
657
|
+
for (const root of CADDY_DATA_ROOTS) {
|
|
658
|
+
const pair = await findPairIn(exec, root, host, container);
|
|
659
|
+
if (!pair) continue;
|
|
660
|
+
const certPem = await readMaybeInContainer(exec, pair.certPath, container);
|
|
661
|
+
const keyPem = await readMaybeInContainer(exec, pair.keyPath, container);
|
|
662
|
+
if (certPem.trim() && keyPem.trim()) {
|
|
663
|
+
return { certPem, keyPem, source: `caddy store ${pair.certPath}` };
|
|
791
664
|
}
|
|
792
665
|
}
|
|
793
|
-
|
|
666
|
+
return null;
|
|
794
667
|
}
|
|
795
668
|
|
|
796
|
-
// ../../packages/adapters/src/system/proxy/import/
|
|
797
|
-
|
|
669
|
+
// ../../packages/adapters/src/system/proxy/import/traefik-certs.ts
|
|
670
|
+
var DEFAULT_ACME_PATHS = ["/acme.json", "/etc/traefik/acme.json", "/data/acme.json", "/letsencrypt/acme.json"];
|
|
671
|
+
var TRAEFIK_STATIC_CONFIGS = [
|
|
672
|
+
"/etc/traefik/traefik.yml",
|
|
673
|
+
"/etc/traefik/traefik.yaml",
|
|
674
|
+
"/etc/traefik/traefik.toml"
|
|
675
|
+
];
|
|
676
|
+
function prop(obj, name) {
|
|
677
|
+
const hit = Object.keys(obj).find((k) => k.toLowerCase() === name.toLowerCase());
|
|
678
|
+
return hit === void 0 ? void 0 : obj[hit];
|
|
679
|
+
}
|
|
680
|
+
async function candidateStoragePaths(exec, container) {
|
|
681
|
+
const found = [];
|
|
682
|
+
const collect = (text) => {
|
|
683
|
+
for (const m of (text ?? "").matchAll(/acme\.storage[=:\s]+["']?(\/[^\s"',}]+)/gi)) {
|
|
684
|
+
if (isSafeCertPath(m[1])) found.push(m[1]);
|
|
685
|
+
}
|
|
686
|
+
for (const m of (text ?? "").matchAll(/(?:^|\s)storage:\s*["']?(\/[^\s"',}]+\.json)/gi)) {
|
|
687
|
+
if (isSafeCertPath(m[1])) found.push(m[1]);
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
if (container) {
|
|
691
|
+
collect(
|
|
692
|
+
await tryExec(
|
|
693
|
+
exec,
|
|
694
|
+
`docker inspect ${sq2(container)} --format '{{json .Config.Cmd}} {{json .Args}} {{json .Config.Env}}' 2>/dev/null`
|
|
695
|
+
)
|
|
696
|
+
);
|
|
697
|
+
for (const cfg of TRAEFIK_STATIC_CONFIGS) {
|
|
698
|
+
collect(await readMaybeInContainer(exec, cfg, container));
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return [.../* @__PURE__ */ new Set([...found, ...DEFAULT_ACME_PATHS])];
|
|
702
|
+
}
|
|
703
|
+
async function loadAcmeJson(exec, container) {
|
|
704
|
+
for (const path of await candidateStoragePaths(exec, container)) {
|
|
705
|
+
const raw = await readMaybeInContainer(exec, path, container);
|
|
706
|
+
if (raw.trim().startsWith("{")) return { raw, path };
|
|
707
|
+
}
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
function certFromAcmeJson(raw, host) {
|
|
711
|
+
let parsed;
|
|
798
712
|
try {
|
|
799
|
-
|
|
713
|
+
parsed = JSON.parse(raw);
|
|
800
714
|
} catch {
|
|
801
715
|
return null;
|
|
802
716
|
}
|
|
717
|
+
const target = host.toLowerCase();
|
|
718
|
+
const matches = (name) => {
|
|
719
|
+
const n = (name ?? "").toLowerCase();
|
|
720
|
+
if (!n) return false;
|
|
721
|
+
if (n === target) return true;
|
|
722
|
+
if (!n.startsWith("*.")) return false;
|
|
723
|
+
const suffix = n.slice(1);
|
|
724
|
+
return target.endsWith(suffix) && !target.slice(0, -suffix.length).includes(".");
|
|
725
|
+
};
|
|
726
|
+
for (const [resolver, value] of Object.entries(parsed)) {
|
|
727
|
+
if (!value || typeof value !== "object") continue;
|
|
728
|
+
const certs = prop(value, "certificates");
|
|
729
|
+
if (!Array.isArray(certs)) continue;
|
|
730
|
+
for (const entry of certs) {
|
|
731
|
+
const domain = entry && prop(entry, "domain");
|
|
732
|
+
const names = [domain?.main, ...domain?.sans ?? []];
|
|
733
|
+
if (!names.some(matches)) continue;
|
|
734
|
+
const certB64 = prop(entry, "certificate");
|
|
735
|
+
const keyB64 = prop(entry, "key");
|
|
736
|
+
if (typeof certB64 !== "string" || typeof keyB64 !== "string") continue;
|
|
737
|
+
const certPem = Buffer.from(certB64, "base64").toString("utf-8");
|
|
738
|
+
const keyPem = Buffer.from(keyB64, "base64").toString("utf-8");
|
|
739
|
+
if (!certPem.includes("-----BEGIN") || !keyPem.includes("-----BEGIN")) continue;
|
|
740
|
+
return { certPem, keyPem, resolver };
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return null;
|
|
803
744
|
}
|
|
804
|
-
function
|
|
805
|
-
|
|
745
|
+
async function traefikAcmeCert(exec, host, container) {
|
|
746
|
+
const loaded = await loadAcmeJson(exec, container);
|
|
747
|
+
if (!loaded) return null;
|
|
748
|
+
const hit = certFromAcmeJson(loaded.raw, host);
|
|
749
|
+
if (!hit) return null;
|
|
750
|
+
return {
|
|
751
|
+
certPem: hit.certPem,
|
|
752
|
+
keyPem: hit.keyPem,
|
|
753
|
+
source: `traefik ${loaded.path} (resolver ${hit.resolver})`
|
|
754
|
+
};
|
|
806
755
|
}
|
|
807
|
-
function
|
|
808
|
-
const
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
let i =
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
}
|
|
819
|
-
if (depth === 0) {
|
|
820
|
-
blocks.push(text.slice(openIdx + 1, i - 1));
|
|
821
|
-
re.lastIndex = i;
|
|
822
|
-
} else {
|
|
823
|
-
break;
|
|
756
|
+
async function traefikDeclaredCertPaths(exec, container) {
|
|
757
|
+
const pairs = [];
|
|
758
|
+
for (const cfg of TRAEFIK_STATIC_CONFIGS) {
|
|
759
|
+
const text = await readMaybeInContainer(exec, cfg, container);
|
|
760
|
+
if (!text.trim()) continue;
|
|
761
|
+
const certFiles = [...text.matchAll(/certFile:\s*["']?(\/[^\s"',}]+)/gi)].map((m) => m[1]);
|
|
762
|
+
const keyFiles = [...text.matchAll(/keyFile:\s*["']?(\/[^\s"',}]+)/gi)].map((m) => m[1]);
|
|
763
|
+
for (let i = 0; i < Math.min(certFiles.length, keyFiles.length); i++) {
|
|
764
|
+
if (isSafeCertPath(certFiles[i]) && isSafeCertPath(keyFiles[i])) {
|
|
765
|
+
pairs.push({ certPath: certFiles[i], keyPath: keyFiles[i] });
|
|
766
|
+
}
|
|
824
767
|
}
|
|
825
768
|
}
|
|
826
|
-
return
|
|
827
|
-
}
|
|
828
|
-
function sq3(value) {
|
|
829
|
-
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
769
|
+
return pairs;
|
|
830
770
|
}
|
|
831
771
|
|
|
832
|
-
// ../../packages/adapters/src/system/proxy/
|
|
833
|
-
|
|
834
|
-
const
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
if (upstreams.has(host)) return { url: `${scheme}${upstreams.get(host)}` };
|
|
867
|
-
const isIp = /^\d{1,3}(\.\d{1,3}){3}$/.test(host);
|
|
868
|
-
if (isIp || host === "localhost" || host.includes(".")) return { url: raw };
|
|
869
|
-
return { reason: `proxy_pass host "${host}" is an undeclared upstream \u2014 not migratable` };
|
|
870
|
-
}
|
|
871
|
-
function extractLocationProxies(serverBody) {
|
|
872
|
-
const out = [];
|
|
873
|
-
const re = /(?:^|[\s;}])location\s+([^{]+?)\s*\{/g;
|
|
874
|
-
let m;
|
|
875
|
-
while ((m = re.exec(serverBody)) !== null) {
|
|
876
|
-
const path = m[1].trim();
|
|
877
|
-
const openIdx = m.index + m[0].length - 1;
|
|
878
|
-
let depth = 1;
|
|
879
|
-
let i = openIdx + 1;
|
|
880
|
-
for (; i < serverBody.length && depth > 0; i++) {
|
|
881
|
-
if (serverBody[i] === "{") depth++;
|
|
882
|
-
else if (serverBody[i] === "}") depth--;
|
|
772
|
+
// ../../packages/adapters/src/system/proxy/api.ts
|
|
773
|
+
function buildProxyRouteIndex(sites) {
|
|
774
|
+
const byPort = /* @__PURE__ */ new Map();
|
|
775
|
+
for (const site of sites) {
|
|
776
|
+
const upstreams = site.routes ?? (site.target.kind === "proxy" ? [{ path: "/", url: site.target.url }] : []);
|
|
777
|
+
for (const up of upstreams) {
|
|
778
|
+
let port;
|
|
779
|
+
try {
|
|
780
|
+
port = Number(new URL(up.url).port);
|
|
781
|
+
} catch {
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
if (!Number.isFinite(port) || port <= 0) continue;
|
|
785
|
+
const list = byPort.get(port) ?? [];
|
|
786
|
+
const existing = list.find((r) => r.path === up.path);
|
|
787
|
+
if (existing) {
|
|
788
|
+
existing.domains = [.../* @__PURE__ */ new Set([...existing.domains, ...site.serverNames])];
|
|
789
|
+
if (site.ssl) {
|
|
790
|
+
existing.ssl.enabled = true;
|
|
791
|
+
if (!existing.ssl.certPath && site.tls) {
|
|
792
|
+
existing.ssl.certPath = site.tls.certPath;
|
|
793
|
+
existing.ssl.keyPath = site.tls.keyPath;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
} else {
|
|
797
|
+
list.push({
|
|
798
|
+
port,
|
|
799
|
+
path: up.path,
|
|
800
|
+
domains: [...site.serverNames],
|
|
801
|
+
ssl: site.ssl ? { enabled: true, certPath: site.tls?.certPath, keyPath: site.tls?.keyPath } : { enabled: false },
|
|
802
|
+
source: site.source
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
byPort.set(port, list);
|
|
883
806
|
}
|
|
884
|
-
if (depth !== 0) break;
|
|
885
|
-
const locBody = serverBody.slice(openIdx + 1, i - 1);
|
|
886
|
-
re.lastIndex = i;
|
|
887
|
-
const pp = firstDirective(locBody, "proxy_pass");
|
|
888
|
-
if (pp) out.push({ path, proxyPass: pp });
|
|
889
|
-
}
|
|
890
|
-
return out;
|
|
891
|
-
}
|
|
892
|
-
function isHttpsUpgradeForSelf(body, names) {
|
|
893
|
-
const targets = [
|
|
894
|
-
...body.matchAll(/(?:^|[\s;{])return\s+30[1-8]\s+([^;]+);/g),
|
|
895
|
-
...body.matchAll(/(?:^|[\s;{])rewrite\s+\S+\s+([^;]+?)\s+permanent\s*;/g)
|
|
896
|
-
].map((m) => m[1].trim());
|
|
897
|
-
if (targets.length === 0) return false;
|
|
898
|
-
return targets.every((raw) => {
|
|
899
|
-
if (!/^https:\/\//i.test(raw)) return false;
|
|
900
|
-
const authority = raw.replace(/^https:\/\//i, "").split(/[/\s]/)[0] ?? "";
|
|
901
|
-
const host = (authority.match(/^(\$[a-z_]+|[^$:]+)/i)?.[1] ?? "").replace(/:\d+$/, "");
|
|
902
|
-
if (host === "$host" || host === "$server_name" || host === "$http_host") return true;
|
|
903
|
-
return names.includes(host);
|
|
904
|
-
});
|
|
905
|
-
}
|
|
906
|
-
function parseServer(body, source, upstreams) {
|
|
907
|
-
const warnings = [];
|
|
908
|
-
const names = firstDirective(body, "server_name")?.split(/\s+/).filter((n) => n && n !== "_" && !n.startsWith("~")) ?? [];
|
|
909
|
-
const listens = [...body.matchAll(/(?:^|[;{\s])listen\s+([^;]+);/g)].map((m) => m[1]);
|
|
910
|
-
const ssl = listens.some((l) => /\bssl\b/.test(l) || /\b443\b/.test(l));
|
|
911
|
-
const root = firstDirective(body, "root");
|
|
912
|
-
const certPath = firstDirective(body, "ssl_certificate");
|
|
913
|
-
const keyPath = firstDirective(body, "ssl_certificate_key");
|
|
914
|
-
if (names.length === 0) return { warnings: [] };
|
|
915
|
-
const rawTargets = extractLocationProxies(body);
|
|
916
|
-
if (rawTargets.length === 0) {
|
|
917
|
-
const serverLevel = firstDirective(body, "proxy_pass");
|
|
918
|
-
if (serverLevel) rawTargets.push({ path: "/", proxyPass: serverLevel });
|
|
919
|
-
}
|
|
920
|
-
const resolved = [];
|
|
921
|
-
for (const t of rawTargets) {
|
|
922
|
-
const r = resolveProxyTarget(t.proxyPass, upstreams);
|
|
923
|
-
if ("reason" in r) warnings.push(`nginx: ${names[0]} ${t.path} \u2014 ${r.reason} (skipped)`);
|
|
924
|
-
else resolved.push({ path: t.path, url: r.url });
|
|
925
|
-
}
|
|
926
|
-
let target;
|
|
927
|
-
let routes;
|
|
928
|
-
if (resolved.length > 0) {
|
|
929
|
-
const primary = resolved.find((r) => r.path === "/") ?? resolved[0];
|
|
930
|
-
target = { kind: "proxy", url: primary.url };
|
|
931
|
-
routes = resolved;
|
|
932
|
-
} else if (root) {
|
|
933
|
-
target = { kind: "static", root: root.replace(/;$/, "") };
|
|
934
|
-
} else if (isHttpsUpgradeForSelf(body, names)) {
|
|
935
|
-
return { warnings: [] };
|
|
936
|
-
} else {
|
|
937
|
-
warnings.push(`nginx: ${names[0]} has neither proxy_pass nor root \u2014 skipped (${source})`);
|
|
938
|
-
return { warnings };
|
|
939
807
|
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
808
|
+
return byPort;
|
|
809
|
+
}
|
|
810
|
+
async function declaredCert(ctx) {
|
|
811
|
+
const tls = ctx.site?.tls;
|
|
812
|
+
if (!tls) return null;
|
|
813
|
+
const pems = await readDeclaredPair(ctx.exec, tls.certPath, tls.keyPath, ctx.container);
|
|
814
|
+
return pems ? { ...pems, source: tls.certPath } : null;
|
|
815
|
+
}
|
|
816
|
+
var CERT_SOURCES = {
|
|
817
|
+
nginx: [declaredCert],
|
|
818
|
+
openresty: [declaredCert],
|
|
819
|
+
apache: [declaredCert],
|
|
820
|
+
caddy: [declaredCert, (c) => caddyStoreCert(c.exec, c.host, c.container)],
|
|
821
|
+
traefik: [
|
|
822
|
+
(c) => traefikAcmeCert(c.exec, c.host, c.container),
|
|
823
|
+
// File provider: paths aren't bound to a hostname (traefik picks by SNI), so
|
|
824
|
+
// hand every declared pair to the validator and let coverage decide.
|
|
825
|
+
async (c) => {
|
|
826
|
+
for (const pair of await traefikDeclaredCertPaths(c.exec, c.container)) {
|
|
827
|
+
const pems = await readDeclaredPair(c.exec, pair.certPath, pair.keyPath, c.container);
|
|
828
|
+
if (!pems) continue;
|
|
829
|
+
if (validateCertFor(c.host, pems, pair.certPath).cert) {
|
|
830
|
+
return { ...pems, source: pair.certPath };
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
],
|
|
836
|
+
// Takeover-only: no importable config, so no cert to find either.
|
|
837
|
+
haproxy: []
|
|
838
|
+
};
|
|
839
|
+
async function edgeProxy(exec, opts = {}) {
|
|
840
|
+
const status = opts.status ?? await probeEdge(exec).catch(() => null);
|
|
841
|
+
if (!status) return null;
|
|
842
|
+
let kind = null;
|
|
843
|
+
let ours = false;
|
|
844
|
+
if (status.classification === "ours") {
|
|
845
|
+
kind = "openresty";
|
|
846
|
+
ours = true;
|
|
847
|
+
} else if (status.classification === "known") {
|
|
848
|
+
const found = status.occupants.find((o) => o.proxy)?.proxy;
|
|
849
|
+
kind = found && canImportProxy(found) ? found : null;
|
|
850
|
+
}
|
|
851
|
+
if (!kind) kind = await detectInstalledProxy(exec).catch(() => null);
|
|
852
|
+
if (!kind) return null;
|
|
853
|
+
const container = status.occupants.find((o) => o.proxy === kind)?.containerName ?? null;
|
|
854
|
+
return makeApi(exec, kind, ours, container);
|
|
855
|
+
}
|
|
856
|
+
function edgeProxyFor(exec, kind, opts = {}) {
|
|
857
|
+
return makeApi(exec, kind, opts.ours ?? kind === "openresty", opts.container ?? null);
|
|
858
|
+
}
|
|
859
|
+
async function collectProxyCerts(api, sites) {
|
|
860
|
+
const certPems = {};
|
|
946
861
|
const warnings = [];
|
|
947
|
-
const
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
warnings.push("nginx: no server blocks found");
|
|
956
|
-
}
|
|
957
|
-
for (const body of blocks) {
|
|
958
|
-
const { site, warnings: blockWarnings } = parseServer(body, "nginx", upstreams);
|
|
959
|
-
warnings.push(...blockWarnings);
|
|
960
|
-
if (site) sites.push(site);
|
|
862
|
+
const hosts = [...new Set(sites.filter((s) => s.ssl).flatMap((s) => s.serverNames))];
|
|
863
|
+
for (const host of hosts) {
|
|
864
|
+
const candidate = await api.certCandidateFor(host).catch(() => null);
|
|
865
|
+
if (candidate?.cert) {
|
|
866
|
+
certPems[host] = { certPem: candidate.cert.certPem, keyPem: candidate.cert.keyPem };
|
|
867
|
+
} else if (candidate?.reason) {
|
|
868
|
+
warnings.push(`${host}: no reusable certificate \u2014 ${candidate.reason}`);
|
|
869
|
+
}
|
|
961
870
|
}
|
|
962
|
-
return {
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
|
|
871
|
+
return { certPems, warnings };
|
|
872
|
+
}
|
|
873
|
+
function makeApi(exec, kind, ours, container) {
|
|
874
|
+
let scan = null;
|
|
875
|
+
const listSites = () => {
|
|
876
|
+
scan ??= (ours ? scanOpenshipEdge(exec) : scanImportableSites(exec, kind)).catch(() => ({
|
|
877
|
+
proxy: kind,
|
|
878
|
+
sites: [],
|
|
879
|
+
warnings: [`${kind}: config scan failed`]
|
|
880
|
+
}));
|
|
881
|
+
return scan;
|
|
882
|
+
};
|
|
883
|
+
const siteFor = async (host) => {
|
|
884
|
+
const target = host.toLowerCase();
|
|
885
|
+
const { sites } = await listSites();
|
|
886
|
+
return sites.find((s) => s.serverNames.some((n) => n.toLowerCase() === target)) ?? // A wildcard vhost (`*.example.com`) serves the host without naming it.
|
|
887
|
+
sites.find(
|
|
888
|
+
(s) => s.serverNames.some((n) => {
|
|
889
|
+
const name = n.toLowerCase();
|
|
890
|
+
if (!name.startsWith("*.")) return false;
|
|
891
|
+
const suffix = name.slice(1);
|
|
892
|
+
return target.endsWith(suffix) && !target.slice(0, -suffix.length).includes(".");
|
|
893
|
+
})
|
|
894
|
+
) ?? null;
|
|
895
|
+
};
|
|
896
|
+
const certCandidateFor = async (host) => {
|
|
897
|
+
const site = await siteFor(host);
|
|
898
|
+
const ctx = { exec, host, site, container };
|
|
899
|
+
let lastReason = null;
|
|
900
|
+
for (const source of CERT_SOURCES[kind] ?? []) {
|
|
901
|
+
const raw = await source(ctx).catch(() => null);
|
|
902
|
+
if (!raw) continue;
|
|
903
|
+
const candidate = validateCertFor(host, raw, raw.source);
|
|
904
|
+
if (candidate.cert) return candidate;
|
|
905
|
+
lastReason = candidate.reason;
|
|
906
|
+
}
|
|
907
|
+
return { cert: null, reason: lastReason ?? `${kind}: no certificate found for ${host}` };
|
|
908
|
+
};
|
|
909
|
+
return {
|
|
910
|
+
kind,
|
|
911
|
+
ours,
|
|
912
|
+
container,
|
|
913
|
+
listSites,
|
|
914
|
+
sitesByPort: async () => buildProxyRouteIndex((await listSites()).sites),
|
|
915
|
+
siteFor,
|
|
916
|
+
certFor: async (host) => (await certCandidateFor(host)).cert,
|
|
917
|
+
certCandidateFor
|
|
918
|
+
};
|
|
966
919
|
}
|
|
967
920
|
|
|
968
|
-
// ../../packages/adapters/src/system/proxy/
|
|
969
|
-
var
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
921
|
+
// ../../packages/adapters/src/system/proxy/takeover-journal.ts
|
|
922
|
+
var JOURNAL_DIR = "/var/lib/openship";
|
|
923
|
+
var JOURNAL_PATH = `${JOURNAL_DIR}/edge-takeover.json`;
|
|
924
|
+
function log(message, level = "info") {
|
|
925
|
+
return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
|
|
926
|
+
}
|
|
927
|
+
async function tryExec2(executor, cmd) {
|
|
928
|
+
try {
|
|
929
|
+
return await executor.exec(cmd);
|
|
930
|
+
} catch {
|
|
931
|
+
return null;
|
|
974
932
|
}
|
|
975
|
-
return null;
|
|
976
933
|
}
|
|
977
|
-
function
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
934
|
+
async function buildJournal(executor, status) {
|
|
935
|
+
const units = /* @__PURE__ */ new Map();
|
|
936
|
+
const containers = /* @__PURE__ */ new Map();
|
|
937
|
+
const processes = /* @__PURE__ */ new Map();
|
|
938
|
+
for (const o of status.occupants) {
|
|
939
|
+
if (o.containerName && !containers.has(o.containerName)) {
|
|
940
|
+
const r = await tryExec2(
|
|
941
|
+
executor,
|
|
942
|
+
`docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' ${sq2(o.containerName)} 2>/dev/null`
|
|
943
|
+
);
|
|
944
|
+
containers.set(o.containerName, { name: o.containerName, restart: r?.trim() || "no" });
|
|
945
|
+
} else if (o.systemdUnit && !units.has(o.systemdUnit)) {
|
|
946
|
+
const en = await tryExec2(executor, `systemctl is-enabled ${sq2(o.systemdUnit)} 2>/dev/null`);
|
|
947
|
+
units.set(o.systemdUnit, { unit: o.systemdUnit, wasEnabled: en?.trim() === "enabled" });
|
|
948
|
+
} else if (!o.containerName && !o.systemdUnit && o.pid && !processes.has(o.pid)) {
|
|
949
|
+
processes.set(o.pid, { pid: o.pid, command: o.rawCommand });
|
|
983
950
|
}
|
|
984
951
|
}
|
|
985
|
-
return
|
|
952
|
+
return {
|
|
953
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
954
|
+
units: [...units.values()],
|
|
955
|
+
containers: [...containers.values()],
|
|
956
|
+
processes: [...processes.values()]
|
|
957
|
+
};
|
|
986
958
|
}
|
|
987
|
-
function
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
return null;
|
|
959
|
+
async function writeJournal(executor, journal) {
|
|
960
|
+
try {
|
|
961
|
+
await executor.mkdir(JOURNAL_DIR);
|
|
962
|
+
await executor.writeFile(JOURNAL_PATH, JSON.stringify(journal, null, 2));
|
|
963
|
+
} catch {
|
|
964
|
+
}
|
|
994
965
|
}
|
|
995
|
-
function
|
|
996
|
-
|
|
966
|
+
async function clearJournal(executor) {
|
|
967
|
+
await tryExec2(executor, `rm -f ${JOURNAL_PATH}`);
|
|
968
|
+
}
|
|
969
|
+
async function readJournal(executor) {
|
|
970
|
+
const raw = await tryExec2(executor, `cat ${JOURNAL_PATH} 2>/dev/null`);
|
|
971
|
+
if (!raw?.trim()) return null;
|
|
997
972
|
try {
|
|
998
|
-
|
|
973
|
+
return JSON.parse(raw);
|
|
999
974
|
} catch {
|
|
975
|
+
await clearJournal(executor);
|
|
1000
976
|
return null;
|
|
1001
977
|
}
|
|
1002
|
-
const servers = cfg.apps?.http?.servers;
|
|
1003
|
-
if (!servers) return null;
|
|
1004
|
-
const sites = [];
|
|
1005
|
-
for (const srv of Object.values(servers)) {
|
|
1006
|
-
const ssl = (srv.listen ?? []).some((l) => l.endsWith(":443"));
|
|
1007
|
-
for (const route of srv.routes ?? []) {
|
|
1008
|
-
const hosts = (route.match ?? []).flatMap((m) => m.host ?? []).filter((h) => h && h !== "*");
|
|
1009
|
-
if (hosts.length === 0) continue;
|
|
1010
|
-
const target = targetFromRoute(route);
|
|
1011
|
-
if (!target) continue;
|
|
1012
|
-
sites.push({ serverNames: hosts, ssl, target, source: "caddy (adapt)" });
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
return { sites, warnings: [] };
|
|
1016
978
|
}
|
|
1017
|
-
async function
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
979
|
+
async function rollback(executor, journal, onLog) {
|
|
980
|
+
onLog(log("Rolling back \u2014 restoring the previous proxy...", "warn"));
|
|
981
|
+
await tryExec2(executor, `docker update --restart=no ${sq2(EDGE_CONTAINER_NAME)} 2>/dev/null || true`);
|
|
982
|
+
await tryExec2(executor, `docker stop ${sq2(EDGE_CONTAINER_NAME)} 2>/dev/null || true`);
|
|
983
|
+
await tryExec2(
|
|
984
|
+
executor,
|
|
985
|
+
"systemctl disable --now openresty 2>/dev/null || systemctl stop openresty 2>/dev/null || true; systemctl reset-failed openresty 2>/dev/null || true"
|
|
986
|
+
);
|
|
987
|
+
for (const u of journal.units) {
|
|
988
|
+
await tryExec2(
|
|
989
|
+
executor,
|
|
990
|
+
u.wasEnabled ? `systemctl enable --now ${sq2(u.unit)} 2>/dev/null || true` : `systemctl start ${sq2(u.unit)} 2>/dev/null || true`
|
|
991
|
+
);
|
|
1021
992
|
}
|
|
1022
|
-
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
const out = [];
|
|
1026
|
-
let i = 0;
|
|
1027
|
-
let headerStart = 0;
|
|
1028
|
-
while (i < text.length) {
|
|
1029
|
-
if (text[i] === "{") {
|
|
1030
|
-
const header = text.slice(headerStart, i).trim();
|
|
1031
|
-
let depth = 1;
|
|
1032
|
-
let j = i + 1;
|
|
1033
|
-
for (; j < text.length && depth > 0; j++) {
|
|
1034
|
-
if (text[j] === "{") depth++;
|
|
1035
|
-
else if (text[j] === "}") depth--;
|
|
1036
|
-
}
|
|
1037
|
-
if (depth !== 0) break;
|
|
1038
|
-
out.push({ header, body: text.slice(i + 1, j - 1) });
|
|
1039
|
-
i = j;
|
|
1040
|
-
headerStart = j;
|
|
1041
|
-
} else {
|
|
1042
|
-
i++;
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
return out;
|
|
1046
|
-
}
|
|
1047
|
-
function parseAddresses(header) {
|
|
1048
|
-
return header.split(/[\s,]+/).filter(Boolean).map((addr) => {
|
|
1049
|
-
const httpScheme = /^http:\/\//i.test(addr);
|
|
1050
|
-
const host = addr.replace(/^https?:\/\//i, "").replace(/:\d+$/, "");
|
|
1051
|
-
const isPort80 = /:80$/.test(addr);
|
|
1052
|
-
return { host, ssl: !httpScheme && !isPort80 };
|
|
1053
|
-
}).filter((a) => a.host && !a.host.startsWith(":") && a.host !== "*");
|
|
1054
|
-
}
|
|
1055
|
-
async function scanCaddy(executor) {
|
|
1056
|
-
const json = await loadCaddyJson(executor);
|
|
1057
|
-
if (json) {
|
|
1058
|
-
const parsed = parseCaddyJson(json);
|
|
1059
|
-
if (parsed && parsed.sites.length > 0) {
|
|
1060
|
-
return { proxy: "caddy", sites: parsed.sites, warnings: parsed.warnings };
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
return scanCaddyText(executor);
|
|
1064
|
-
}
|
|
1065
|
-
async function scanCaddyText(executor) {
|
|
1066
|
-
const raw = await loadCaddyfile(executor);
|
|
1067
|
-
const warnings = [];
|
|
1068
|
-
const sites = [];
|
|
1069
|
-
if (!raw.trim()) {
|
|
1070
|
-
return { proxy: "caddy", sites, warnings: ["caddy: no readable Caddyfile found"] };
|
|
1071
|
-
}
|
|
1072
|
-
const text = stripComments(raw);
|
|
1073
|
-
let blocks = caddyBlocks(text);
|
|
1074
|
-
if (blocks.length === 0) {
|
|
1075
|
-
const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1076
|
-
if (lines.length > 0) {
|
|
1077
|
-
blocks = [{ header: lines[0], body: lines.slice(1).join("\n") }];
|
|
1078
|
-
} else {
|
|
1079
|
-
warnings.push("caddy: config present but no parseable site blocks");
|
|
1080
|
-
}
|
|
993
|
+
for (const c of journal.containers) {
|
|
994
|
+
await tryExec2(executor, `docker update --restart=${sq2(c.restart)} ${sq2(c.name)} 2>/dev/null || true`);
|
|
995
|
+
await tryExec2(executor, `docker start ${sq2(c.name)} 2>/dev/null || true`);
|
|
1081
996
|
}
|
|
1082
|
-
for (const
|
|
1083
|
-
if (
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
}
|
|
1089
|
-
const proxyMatch = body.match(/(?:^|\s)reverse_proxy\s+([^\n{]+)/);
|
|
1090
|
-
const rootMatch = body.match(/(?:^|\s)root\s+\*?\s*([^\n\s]+)/);
|
|
1091
|
-
const tlsMatch = body.match(/(?:^|\s)tls\s+(\S+)\s+(\S+)/);
|
|
1092
|
-
let target;
|
|
1093
|
-
if (proxyMatch) {
|
|
1094
|
-
const upstream = proxyMatch[1].trim().split(/\s+/)[0];
|
|
1095
|
-
target = { kind: "proxy", url: /^https?:\/\//.test(upstream) ? upstream : `http://${upstream}` };
|
|
1096
|
-
} else if (rootMatch) {
|
|
1097
|
-
target = { kind: "static", root: rootMatch[1] };
|
|
997
|
+
for (const p of journal.processes ?? []) {
|
|
998
|
+
if (p.command) {
|
|
999
|
+
await tryExec2(
|
|
1000
|
+
executor,
|
|
1001
|
+
`setsid -f sh -c ${sq2(p.command)} 2>/dev/null || (nohup sh -c ${sq2(p.command)} >/dev/null 2>&1 &) || true`
|
|
1002
|
+
);
|
|
1098
1003
|
} else {
|
|
1099
|
-
|
|
1100
|
-
continue;
|
|
1101
|
-
}
|
|
1102
|
-
const site = {
|
|
1103
|
-
serverNames: addrs.map((a) => a.host),
|
|
1104
|
-
ssl: addrs.some((a) => a.ssl),
|
|
1105
|
-
target,
|
|
1106
|
-
source: "caddy"
|
|
1107
|
-
};
|
|
1108
|
-
if (tlsMatch && tlsMatch[1].includes("/")) {
|
|
1109
|
-
site.tls = { certPath: tlsMatch[1], keyPath: tlsMatch[2] };
|
|
1004
|
+
onLog(log(`Could not restore process ${p.pid} \u2014 no command captured.`, "warn"));
|
|
1110
1005
|
}
|
|
1111
|
-
sites.push(site);
|
|
1112
1006
|
}
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
);
|
|
1122
|
-
if (!dump) return [];
|
|
1123
|
-
const files = /* @__PURE__ */ new Set();
|
|
1124
|
-
for (const m of dump.matchAll(/\(([^()]+?):\d+\)/g)) {
|
|
1125
|
-
const f = m[1].trim();
|
|
1126
|
-
if (f.startsWith("/")) files.add(f);
|
|
1007
|
+
const restored = await portIsServed(executor, 80);
|
|
1008
|
+
if (!restored) {
|
|
1009
|
+
onLog(
|
|
1010
|
+
log(
|
|
1011
|
+
"Nothing is listening on :80 after the restore \u2014 the box is NOT serving. Start your proxy by hand (e.g. `systemctl start nginx`, or `docker start <name>`).",
|
|
1012
|
+
"error"
|
|
1013
|
+
)
|
|
1014
|
+
);
|
|
1127
1015
|
}
|
|
1128
|
-
return
|
|
1016
|
+
return restored;
|
|
1129
1017
|
}
|
|
1130
|
-
async function
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1133
|
-
const cat2 = await tryExec4(executor, `cat ${files.map(sq3).join(" ")} 2>/dev/null`);
|
|
1134
|
-
if (cat2 && cat2.trim()) return cat2;
|
|
1135
|
-
}
|
|
1136
|
-
const cat = await tryExec4(
|
|
1018
|
+
async function portIsServed(executor, port) {
|
|
1019
|
+
const hex = port.toString(16).toUpperCase().padStart(4, "0");
|
|
1020
|
+
const out = await tryExec2(
|
|
1137
1021
|
executor,
|
|
1138
|
-
|
|
1022
|
+
`(command -v ss >/dev/null 2>&1 && ss -ltn 2>/dev/null | grep -qE ':${port}[[:space:]]' && echo yes) || (grep -qiE '^[[:space:]]*[0-9]+:[[:space:]]*[0-9A-F]{8}:${hex}[[:space:]]+[0-9A-F]{8}:0000[[:space:]]+0A' /proc/net/tcp 2>/dev/null && echo yes) || true`
|
|
1139
1023
|
);
|
|
1140
|
-
return
|
|
1141
|
-
}
|
|
1142
|
-
function directive(body, name) {
|
|
1143
|
-
const m = body.match(new RegExp(`(?:^|\\n)\\s*${name}\\s+([^\\n]+)`, "i"));
|
|
1144
|
-
return m?.[1]?.trim();
|
|
1024
|
+
return (out ?? "").includes("yes");
|
|
1145
1025
|
}
|
|
1146
|
-
function
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
);
|
|
1026
|
+
async function beginEdgeTakeover(executor, status, onLog) {
|
|
1027
|
+
await writeJournal(executor, await buildJournal(executor, status));
|
|
1028
|
+
await freeEdgeTargets(executor, stopTargetsForStatus(status), (m, l) => onLog(log(m, l)));
|
|
1150
1029
|
}
|
|
1151
|
-
function
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1154
|
-
const
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
}
|
|
1158
|
-
const routes = [...body.matchAll(/(?:^|\n)\s*ProxyPass\s+(\S+)\s+(\S+)/gi)].map((m) => ({ path: m[1], url: m[2] })).filter((p) => p.url && p.url !== "!");
|
|
1159
|
-
const docRoot = directive(body, "DocumentRoot")?.replace(/^["']|["']$/g, "");
|
|
1160
|
-
const certPath = directive(body, "SSLCertificateFile");
|
|
1161
|
-
const keyPath = directive(body, "SSLCertificateKeyFile");
|
|
1162
|
-
const ssl = /:443/.test(portHint) || /SSLEngine\s+on/i.test(body) || Boolean(certPath);
|
|
1163
|
-
let target;
|
|
1164
|
-
if (routes.length > 0) {
|
|
1165
|
-
const primary = routes.find((r) => r.path === "/") ?? routes[0];
|
|
1166
|
-
target = { kind: "proxy", url: primary.url };
|
|
1167
|
-
} else if (docRoot) {
|
|
1168
|
-
target = { kind: "static", root: docRoot };
|
|
1169
|
-
} else {
|
|
1170
|
-
return { warning: `apache: ${names[0]} has neither ProxyPass nor DocumentRoot \u2014 skipped` };
|
|
1171
|
-
}
|
|
1172
|
-
const site = { serverNames: names, ssl, target, source: "apache" };
|
|
1173
|
-
if (routes.length > 0) site.routes = routes;
|
|
1174
|
-
if (certPath && keyPath) site.tls = { certPath, keyPath };
|
|
1175
|
-
return site;
|
|
1030
|
+
async function rollbackEdgeTakeover(executor, onLog) {
|
|
1031
|
+
const journal = await readJournal(executor);
|
|
1032
|
+
if (!journal || journal.completed) return false;
|
|
1033
|
+
const restored = await rollback(executor, journal, onLog);
|
|
1034
|
+
await clearJournal(executor);
|
|
1035
|
+
return restored;
|
|
1176
1036
|
}
|
|
1177
|
-
function
|
|
1178
|
-
const
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
out.push({ args: m[1].trim(), body: m[2] });
|
|
1037
|
+
async function completeEdgeTakeover(executor) {
|
|
1038
|
+
const journal = await readJournal(executor);
|
|
1039
|
+
if (journal) {
|
|
1040
|
+
journal.completed = true;
|
|
1041
|
+
await writeJournal(executor, journal);
|
|
1183
1042
|
}
|
|
1184
|
-
|
|
1043
|
+
await clearJournal(executor);
|
|
1185
1044
|
}
|
|
1186
|
-
async function
|
|
1187
|
-
const
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
return
|
|
1045
|
+
async function recoverInterruptedTakeover(executor, onLog, isEdgeHealthy) {
|
|
1046
|
+
const journal = await readJournal(executor);
|
|
1047
|
+
if (!journal) return;
|
|
1048
|
+
if (journal.completed) {
|
|
1049
|
+
await clearJournal(executor);
|
|
1050
|
+
return;
|
|
1192
1051
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
const parsed = parseVhost(body, args);
|
|
1198
|
-
if ("warning" in parsed) warnings.push(parsed.warning);
|
|
1199
|
-
else sites.push(parsed);
|
|
1052
|
+
if (isEdgeHealthy && await isEdgeHealthy()) {
|
|
1053
|
+
onLog(log("Found an unfinished edge takeover, but our edge is serving \u2014 keeping it.", "warn"));
|
|
1054
|
+
await clearJournal(executor);
|
|
1055
|
+
return;
|
|
1200
1056
|
}
|
|
1201
|
-
|
|
1057
|
+
onLog(log("Found an interrupted edge takeover \u2014 restoring the previous proxy.", "warn"));
|
|
1058
|
+
await rollback(executor, journal, onLog);
|
|
1059
|
+
await clearJournal(executor);
|
|
1202
1060
|
}
|
|
1203
1061
|
|
|
1204
|
-
// ../../packages/adapters/src/system/
|
|
1205
|
-
function
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
}
|
|
1062
|
+
// ../../packages/adapters/src/system/catalog.ts
|
|
1063
|
+
function dockerInstallPlan(profile) {
|
|
1064
|
+
if (profile.os !== "linux") {
|
|
1065
|
+
return {
|
|
1066
|
+
supported: false,
|
|
1067
|
+
unsupportedReason: "Docker installation is only supported on Linux servers"
|
|
1068
|
+
};
|
|
1212
1069
|
}
|
|
1213
|
-
return
|
|
1070
|
+
return {
|
|
1071
|
+
supported: true,
|
|
1072
|
+
installCommand: "curl -fsSL https://get.docker.com | sh",
|
|
1073
|
+
startCommand: profile.serviceManager === "systemd" ? "systemctl enable docker && systemctl start docker" : void 0,
|
|
1074
|
+
verifyCommand: "docker --version"
|
|
1075
|
+
};
|
|
1214
1076
|
}
|
|
1215
|
-
function
|
|
1216
|
-
const
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1077
|
+
function gitInstallPlan(profile) {
|
|
1078
|
+
const commands = {
|
|
1079
|
+
apt: "apt-get update -qq && apt-get install -y -qq git",
|
|
1080
|
+
dnf: "dnf install -y git",
|
|
1081
|
+
yum: "yum install -y git",
|
|
1082
|
+
apk: "apk add --no-cache git",
|
|
1083
|
+
brew: "brew install git"
|
|
1084
|
+
};
|
|
1085
|
+
const installCommand = commands[profile.packageManager];
|
|
1086
|
+
if (!installCommand) {
|
|
1087
|
+
return {
|
|
1088
|
+
supported: false,
|
|
1089
|
+
unsupportedReason: "No supported package manager found for Git installation"
|
|
1090
|
+
};
|
|
1221
1091
|
}
|
|
1222
|
-
return
|
|
1092
|
+
return {
|
|
1093
|
+
supported: true,
|
|
1094
|
+
installCommand,
|
|
1095
|
+
verifyCommand: "git --version"
|
|
1096
|
+
};
|
|
1223
1097
|
}
|
|
1224
|
-
function
|
|
1225
|
-
const
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1098
|
+
function rsyncInstallPlan(profile) {
|
|
1099
|
+
const commands = {
|
|
1100
|
+
apt: "apt-get update -qq && apt-get install -y -qq rsync",
|
|
1101
|
+
dnf: "dnf install -y rsync",
|
|
1102
|
+
yum: "yum install -y rsync",
|
|
1103
|
+
apk: "apk add --no-cache rsync",
|
|
1104
|
+
brew: "brew install rsync"
|
|
1105
|
+
};
|
|
1106
|
+
const installCommand = commands[profile.packageManager];
|
|
1107
|
+
if (!installCommand) {
|
|
1108
|
+
return {
|
|
1109
|
+
supported: false,
|
|
1110
|
+
unsupportedReason: "No supported package manager found for rsync installation"
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
return {
|
|
1114
|
+
supported: true,
|
|
1115
|
+
installCommand,
|
|
1116
|
+
verifyCommand: "rsync --version | head -n 1"
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
var OPENRESTY_APT_SOURCES = [
|
|
1120
|
+
// ARM64: openresty.org publishes aarch64 debs under a SEPARATE path
|
|
1121
|
+
// (openresty.org/package/arm64/<repo>); the default path is amd64-only, so on
|
|
1122
|
+
// aarch64 `apt-get install openresty` finds no candidate and `set -e` aborts
|
|
1123
|
+
// the whole setup (issue #109). Switch the base URL by arch and tag the deb
|
|
1124
|
+
// line with `arch=` so apt fetches the right index. `dpkg --print-architecture`
|
|
1125
|
+
// returns exactly amd64/arm64 (matches OpenResty's path + arch qualifier).
|
|
1126
|
+
'OR_ARCH="$(dpkg --print-architecture 2>/dev/null || echo amd64)"',
|
|
1127
|
+
'if [ "$OR_ARCH" = arm64 ]; then OR_BASE="http://openresty.org/package/arm64/$REPO"; else OR_BASE="http://openresty.org/package/$REPO"; fi',
|
|
1128
|
+
// `|| REPO_CODENAME=""` so a failed substitution (no lsb_release AND no
|
|
1129
|
+
// /etc/os-release, e.g. a stripped container) doesn't trip `set -e` before
|
|
1130
|
+
// the empty-var fallback below can pick a default.
|
|
1131
|
+
'REPO_CODENAME="$(lsb_release -sc 2>/dev/null || { . /etc/os-release 2>/dev/null && echo "$VERSION_CODENAME"; })" || REPO_CODENAME=""',
|
|
1132
|
+
'if [ "$REPO" = debian ]; then OR_FALLBACKS="bookworm bullseye"; else OR_FALLBACKS="noble jammy focal"; fi',
|
|
1133
|
+
'OR_CODENAME=""',
|
|
1134
|
+
"for c in $REPO_CODENAME $OR_FALLBACKS; do",
|
|
1135
|
+
' if wget -q --spider --tries=2 --timeout=15 "$OR_BASE/dists/$c/Release"; then OR_CODENAME="$c"; break; fi',
|
|
1136
|
+
"done",
|
|
1137
|
+
'if [ -z "$OR_CODENAME" ]; then case "$REPO" in debian) OR_CODENAME=bookworm ;; *) OR_CODENAME=noble ;; esac; fi',
|
|
1138
|
+
'[ "$OR_CODENAME" = "$REPO_CODENAME" ] || echo "[openresty] apt repo has no codename $REPO_CODENAME; using nearest supported LTS $OR_CODENAME" >&2',
|
|
1139
|
+
'echo "deb [arch=$OR_ARCH signed-by=/usr/share/keyrings/openresty.gpg] $OR_BASE $OR_CODENAME main" > /etc/apt/sources.list.d/openresty.list'
|
|
1140
|
+
];
|
|
1141
|
+
function openrestyInstallPlan(profile) {
|
|
1142
|
+
if (profile.os !== "linux") {
|
|
1143
|
+
return {
|
|
1144
|
+
supported: false,
|
|
1145
|
+
unsupportedReason: "OpenResty installation is only supported on Linux servers"
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
let installCommand;
|
|
1149
|
+
if (profile.packageManager === "apt") {
|
|
1150
|
+
const distro = profile.distro === "debian" ? "debian" : "ubuntu";
|
|
1151
|
+
installCommand = [
|
|
1152
|
+
"set -e",
|
|
1153
|
+
// Heal a stale/bad openresty.list left by a prior run (e.g. the #86
|
|
1154
|
+
// 'resolute' pin) BEFORE the first apt-get update reads it — otherwise
|
|
1155
|
+
// set -e aborts here and the repo never gets rewritten.
|
|
1156
|
+
"rm -f /etc/apt/sources.list.d/openresty.list",
|
|
1157
|
+
"apt-get update -qq && apt-get install -y -qq wget gnupg2 lsb-release",
|
|
1158
|
+
"wget -qO /tmp/openresty-pubkey.gpg https://openresty.org/package/pubkey.gpg",
|
|
1159
|
+
"gpg --yes --dearmor -o /usr/share/keyrings/openresty.gpg /tmp/openresty-pubkey.gpg",
|
|
1160
|
+
`REPO=${distro}`,
|
|
1161
|
+
...OPENRESTY_APT_SOURCES,
|
|
1162
|
+
"apt-get update -qq && apt-get install -y -qq openresty"
|
|
1163
|
+
].join("\n");
|
|
1164
|
+
} else if (profile.packageManager === "dnf") {
|
|
1165
|
+
const distro = profile.distro === "fedora" ? "fedora" : "centos";
|
|
1166
|
+
installCommand = `wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/${distro}/openresty.repo && dnf install -y openresty`;
|
|
1167
|
+
} else if (profile.packageManager === "yum") {
|
|
1168
|
+
installCommand = "wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/centos/openresty.repo && yum install -y openresty";
|
|
1169
|
+
} else if (profile.packageManager === "apk") {
|
|
1170
|
+
installCommand = [
|
|
1171
|
+
"apk add --no-cache wget",
|
|
1172
|
+
"wget -qO /etc/apk/keys/admin@openresty.com-5ea678a6.rsa.pub https://openresty.org/package/alpine/admin@openresty.com-5ea678a6.rsa.pub",
|
|
1173
|
+
`. /etc/os-release && echo "https://openresty.org/package/alpine/v$( echo $VERSION_ID | cut -d. -f1,2 )/main" >> /etc/apk/repositories`,
|
|
1174
|
+
"apk update && apk add openresty"
|
|
1175
|
+
].join(" && ");
|
|
1176
|
+
} else {
|
|
1177
|
+
installCommand = [
|
|
1178
|
+
"set -e",
|
|
1179
|
+
"if command -v apt-get >/dev/null 2>&1; then",
|
|
1180
|
+
" rm -f /etc/apt/sources.list.d/openresty.list",
|
|
1181
|
+
" apt-get update -qq && apt-get install -y -qq wget gnupg2 lsb-release",
|
|
1182
|
+
" wget -qO /tmp/openresty-pubkey.gpg https://openresty.org/package/pubkey.gpg",
|
|
1183
|
+
" gpg --yes --dearmor -o /usr/share/keyrings/openresty.gpg /tmp/openresty-pubkey.gpg",
|
|
1184
|
+
' DISTRO=$(. /etc/os-release 2>/dev/null && echo "$ID" || echo "ubuntu")',
|
|
1185
|
+
' case "$DISTRO" in debian) REPO=debian ;; *) REPO=ubuntu ;; esac',
|
|
1186
|
+
...OPENRESTY_APT_SOURCES.map((l) => " " + l),
|
|
1187
|
+
" apt-get update -qq && apt-get install -y -qq openresty",
|
|
1188
|
+
"elif command -v dnf >/dev/null 2>&1; then",
|
|
1189
|
+
" wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/centos/openresty.repo && dnf install -y openresty",
|
|
1190
|
+
"elif command -v yum >/dev/null 2>&1; then",
|
|
1191
|
+
" wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/centos/openresty.repo && yum install -y openresty",
|
|
1192
|
+
"elif command -v apk >/dev/null 2>&1; then",
|
|
1193
|
+
" apk add --no-cache wget \\",
|
|
1194
|
+
" && wget -qO /etc/apk/keys/admin@openresty.com-5ea678a6.rsa.pub https://openresty.org/package/alpine/admin@openresty.com-5ea678a6.rsa.pub \\",
|
|
1195
|
+
' && . /etc/os-release && echo "https://openresty.org/package/alpine/v$(echo $VERSION_ID | cut -d. -f1,2)/main" >> /etc/apk/repositories \\',
|
|
1196
|
+
" && apk update && apk add openresty",
|
|
1197
|
+
"else",
|
|
1198
|
+
' echo "No supported package manager found (tried apt-get, dnf, yum, apk)" >&2 && exit 1',
|
|
1199
|
+
"fi"
|
|
1200
|
+
].join("\n");
|
|
1201
|
+
}
|
|
1202
|
+
return {
|
|
1203
|
+
supported: true,
|
|
1204
|
+
installCommand,
|
|
1205
|
+
startCommand: profile.serviceManager === "systemd" ? "systemctl enable openresty && systemctl start openresty" : void 0,
|
|
1206
|
+
verifyCommand: "openresty -v 2>&1"
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
function certbotInstallPlan(profile) {
|
|
1210
|
+
const commands = {
|
|
1211
|
+
apt: "apt-get update -qq && apt-get install -y -qq certbot",
|
|
1212
|
+
dnf: "dnf install -y certbot",
|
|
1213
|
+
yum: "yum install -y certbot",
|
|
1214
|
+
apk: "apk add --no-cache certbot"
|
|
1215
|
+
};
|
|
1216
|
+
const installCommand = commands[profile.packageManager];
|
|
1217
|
+
if (!installCommand) {
|
|
1218
|
+
return {
|
|
1219
|
+
supported: false,
|
|
1220
|
+
unsupportedReason: "Certbot installation is only supported on Linux with apt, dnf, or yum"
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
return {
|
|
1224
|
+
supported: true,
|
|
1225
|
+
installCommand,
|
|
1226
|
+
verifyCommand: "certbot --version 2>/dev/null"
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
var systemCatalog = {
|
|
1230
|
+
checks: {
|
|
1231
|
+
docker: {
|
|
1232
|
+
versionCommand: "docker --version",
|
|
1233
|
+
daemonCommand: "docker info --format '{{.ServerVersion}}'",
|
|
1234
|
+
parseVersion: (output) => output.match(/Docker version ([^\s,]+)/)?.[1] ?? output,
|
|
1235
|
+
missingMessage: "Docker is not installed",
|
|
1236
|
+
notRunningMessage: "Docker is installed but the daemon is not running"
|
|
1237
|
+
},
|
|
1238
|
+
openresty: {
|
|
1239
|
+
versionCommand: "openresty -v 2>&1 || /usr/local/openresty/bin/openresty -v 2>&1",
|
|
1240
|
+
runningCommands: [
|
|
1241
|
+
"pgrep -f 'nginx.*openresty' || pgrep -f '/usr/local/openresty'"
|
|
1242
|
+
],
|
|
1243
|
+
parseVersion: (output) => output.match(/openresty\/(\S+)/)?.[1] ?? output.match(/nginx\/(\S+)/)?.[1] ?? output,
|
|
1244
|
+
missingMessage: "OpenResty is not installed",
|
|
1245
|
+
notRunningMessage: "OpenResty is installed but not running"
|
|
1246
|
+
},
|
|
1247
|
+
certbot: {
|
|
1248
|
+
versionCommand: "certbot --version 2>/dev/null",
|
|
1249
|
+
parseVersion: (output) => output.match(/certbot\s+(\S+)/)?.[1] ?? output,
|
|
1250
|
+
missingMessage: "Certbot is not installed"
|
|
1251
|
+
},
|
|
1252
|
+
git: {
|
|
1253
|
+
versionCommand: "git --version",
|
|
1254
|
+
parseVersion: (output) => output.match(/git version (\S+)/)?.[1] ?? output,
|
|
1255
|
+
missingMessage: "Git is not installed"
|
|
1256
|
+
},
|
|
1257
|
+
rsync: {
|
|
1258
|
+
versionCommand: "rsync --version | head -n 1",
|
|
1259
|
+
parseVersion: (output) => output.match(/rsync\s+version\s+(\S+)/i)?.[1] ?? output,
|
|
1260
|
+
missingMessage: "rsync is not installed"
|
|
1241
1261
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1262
|
+
},
|
|
1263
|
+
installs: {
|
|
1264
|
+
docker: dockerInstallPlan,
|
|
1265
|
+
git: gitInstallPlan,
|
|
1266
|
+
rsync: rsyncInstallPlan,
|
|
1267
|
+
openresty: openrestyInstallPlan,
|
|
1268
|
+
certbot: certbotInstallPlan
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
|
|
1272
|
+
// ../../packages/adapters/src/system/elevated-executor.ts
|
|
1273
|
+
var INSTALL_ENV = "DEBIAN_FRONTEND=noninteractive DPKG_FORCE=confnew";
|
|
1274
|
+
function elevateCommand(command) {
|
|
1275
|
+
return `sudo -n sh -c ${sq(`export ${INSTALL_ENV}; ${command}`)}`;
|
|
1276
|
+
}
|
|
1277
|
+
function dirOf(path) {
|
|
1278
|
+
const idx = path.lastIndexOf("/");
|
|
1279
|
+
if (idx < 0) return ".";
|
|
1280
|
+
return idx === 0 ? "/" : path.slice(0, idx);
|
|
1281
|
+
}
|
|
1282
|
+
function elevatedExecutor(inner) {
|
|
1283
|
+
const writeFileElevated = async (path, content) => {
|
|
1284
|
+
const tmp = `/tmp/.openship-elev-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
1285
|
+
await inner.writeFile(tmp, content);
|
|
1286
|
+
await inner.exec(
|
|
1287
|
+
elevateCommand(`mkdir -p ${sq(dirOf(path))} && mv -f ${sq(tmp)} ${sq(path)}`)
|
|
1288
|
+
);
|
|
1289
|
+
};
|
|
1290
|
+
const overrides = {
|
|
1291
|
+
exec: (command, opts) => inner.exec(elevateCommand(command), opts),
|
|
1292
|
+
streamExec: (command, onLog) => inner.streamExec(elevateCommand(command), onLog),
|
|
1293
|
+
writeFile: writeFileElevated,
|
|
1294
|
+
// Elevated for the same reason as writeFile: the vhost/conf dirs are root-owned,
|
|
1295
|
+
// and the Proxy would otherwise forward this to the inner executor unelevated —
|
|
1296
|
+
// EACCES immediately after an elevated write succeeded.
|
|
1297
|
+
rename: async (from, to) => {
|
|
1298
|
+
await inner.exec(elevateCommand(`mv -f ${sq(from)} ${sq(to)}`));
|
|
1299
|
+
},
|
|
1300
|
+
mkdir: async (path) => {
|
|
1301
|
+
await inner.exec(elevateCommand(`mkdir -p ${sq(path)}`));
|
|
1302
|
+
},
|
|
1303
|
+
rm: async (path) => {
|
|
1304
|
+
await inner.exec(elevateCommand(`rm -rf ${sq(path)}`));
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
return new Proxy(inner, {
|
|
1308
|
+
get(target, prop2) {
|
|
1309
|
+
if (Object.prototype.hasOwnProperty.call(overrides, prop2)) {
|
|
1310
|
+
return overrides[prop2];
|
|
1270
1311
|
}
|
|
1271
|
-
const
|
|
1272
|
-
|
|
1273
|
-
serverNames: hosts,
|
|
1274
|
-
ssl,
|
|
1275
|
-
target: { kind: "proxy", url },
|
|
1276
|
-
source: `traefik container ${c.name}`
|
|
1277
|
-
});
|
|
1312
|
+
const value = Reflect.get(target, prop2, target);
|
|
1313
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
1278
1314
|
}
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// ../../packages/adapters/src/system/installer.ts
|
|
1319
|
+
function log2(message, level = "info") {
|
|
1320
|
+
return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
|
|
1321
|
+
}
|
|
1322
|
+
async function prepareExecutor(executor, component) {
|
|
1323
|
+
const profile = await resolveEnvironment(executor);
|
|
1324
|
+
if (profile.isRoot) return { ok: true, executor, profile };
|
|
1325
|
+
if (profile.canSudo) return { ok: true, executor: elevatedExecutor(executor), profile };
|
|
1326
|
+
return {
|
|
1327
|
+
ok: false,
|
|
1328
|
+
result: {
|
|
1329
|
+
component,
|
|
1330
|
+
success: false,
|
|
1331
|
+
error: `Installing ${component} needs root. Connect this server as root, or as a user with passwordless sudo.`
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
async function installDocker(executor, onLog) {
|
|
1336
|
+
const prep = await prepareExecutor(executor, "docker");
|
|
1337
|
+
if (!prep.ok) return prep.result;
|
|
1338
|
+
executor = prep.executor;
|
|
1339
|
+
const profile = prep.profile;
|
|
1340
|
+
const plan = systemCatalog.installs.docker(profile);
|
|
1341
|
+
if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
|
|
1342
|
+
return { component: "docker", success: false, error: plan.unsupportedReason ?? "Docker install not supported" };
|
|
1343
|
+
}
|
|
1344
|
+
onLog(log2("Installing Docker Engine..."));
|
|
1345
|
+
try {
|
|
1346
|
+
const { code } = await executor.streamExec(plan.installCommand, onLog);
|
|
1347
|
+
if (code !== 0) return { component: "docker", success: false, error: "Docker install failed" };
|
|
1348
|
+
if (plan.startCommand) {
|
|
1349
|
+
onLog(log2("Starting Docker service..."));
|
|
1350
|
+
await executor.streamExec(plan.startCommand, onLog);
|
|
1351
|
+
}
|
|
1352
|
+
const version = await executor.exec(plan.verifyCommand);
|
|
1353
|
+
const parsed = systemCatalog.checks.docker.parseVersion(version);
|
|
1354
|
+
onLog(log2(`Docker ${parsed} installed`));
|
|
1355
|
+
return { component: "docker", success: true, version: parsed };
|
|
1356
|
+
} catch (err) {
|
|
1357
|
+
const msg = safeErrorMessage(err);
|
|
1358
|
+
onLog(log2(`Docker installation failed: ${msg}`, "error"));
|
|
1359
|
+
return { component: "docker", success: false, error: msg };
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
async function installGit(executor, onLog, opts) {
|
|
1363
|
+
const prep = await prepareExecutor(executor, "git");
|
|
1364
|
+
if (!prep.ok) return prep.result;
|
|
1365
|
+
executor = prep.executor;
|
|
1366
|
+
const profile = prep.profile;
|
|
1367
|
+
const plan = systemCatalog.installs.git(profile);
|
|
1368
|
+
if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
|
|
1369
|
+
return { component: "git", success: false, error: plan.unsupportedReason ?? "Git install not supported" };
|
|
1370
|
+
}
|
|
1371
|
+
onLog(log2(opts?.label ? `Installing Git in ${opts.label}...` : "Installing Git..."));
|
|
1372
|
+
try {
|
|
1373
|
+
const { code } = await executor.streamExec(plan.installCommand, onLog);
|
|
1374
|
+
if (code !== 0) return { component: "git", success: false, error: "Git installation failed" };
|
|
1375
|
+
const version = await executor.exec(plan.verifyCommand);
|
|
1376
|
+
const parsed = systemCatalog.checks.git.parseVersion(version);
|
|
1377
|
+
onLog(log2(opts?.label ? `Git ${parsed} installed in ${opts.label}` : `Git ${parsed} installed`));
|
|
1378
|
+
return { component: "git", success: true, version: parsed };
|
|
1379
|
+
} catch (err) {
|
|
1380
|
+
const msg = safeErrorMessage(err);
|
|
1381
|
+
onLog(log2(`Git installation failed: ${msg}`, "error"));
|
|
1382
|
+
return { component: "git", success: false, error: msg };
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
async function installRsync(executor, onLog) {
|
|
1386
|
+
const prep = await prepareExecutor(executor, "rsync");
|
|
1387
|
+
if (!prep.ok) return prep.result;
|
|
1388
|
+
executor = prep.executor;
|
|
1389
|
+
const profile = prep.profile;
|
|
1390
|
+
const plan = systemCatalog.installs.rsync(profile);
|
|
1391
|
+
if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
|
|
1392
|
+
return { component: "rsync", success: false, error: plan.unsupportedReason ?? "rsync install not supported" };
|
|
1393
|
+
}
|
|
1394
|
+
onLog(log2("Installing rsync..."));
|
|
1395
|
+
try {
|
|
1396
|
+
const { code } = await executor.streamExec(plan.installCommand, onLog);
|
|
1397
|
+
if (code !== 0) return { component: "rsync", success: false, error: "rsync installation failed" };
|
|
1398
|
+
const version = await executor.exec(plan.verifyCommand);
|
|
1399
|
+
const parsed = systemCatalog.checks.rsync.parseVersion(version);
|
|
1400
|
+
onLog(log2(`rsync ${parsed} installed`));
|
|
1401
|
+
return { component: "rsync", success: true, version: parsed };
|
|
1402
|
+
} catch (err) {
|
|
1403
|
+
const msg = safeErrorMessage(err);
|
|
1404
|
+
onLog(log2(`rsync installation failed: ${msg}`, "error"));
|
|
1405
|
+
return { component: "rsync", success: false, error: msg };
|
|
1279
1406
|
}
|
|
1280
|
-
return { proxy: "traefik", sites, warnings };
|
|
1281
1407
|
}
|
|
1282
|
-
async function
|
|
1283
|
-
const
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
)
|
|
1287
|
-
|
|
1288
|
-
|
|
1408
|
+
async function installContainerEdge(executor, onLog, config) {
|
|
1409
|
+
const prep = await prepareExecutor(executor, "edge");
|
|
1410
|
+
if (!prep.ok) return prep.result;
|
|
1411
|
+
executor = prep.executor;
|
|
1412
|
+
if (!await dockerAvailable(executor)) {
|
|
1413
|
+
const error = "The edge is a container image and needs Docker on this server. Install the Docker component first, then install the edge.";
|
|
1414
|
+
onLog(log2(error, "error"));
|
|
1415
|
+
return { component: "edge", success: false, error };
|
|
1289
1416
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
continue;
|
|
1417
|
+
try {
|
|
1418
|
+
const result = await ensureContainerEdge(executor, {
|
|
1419
|
+
onLog,
|
|
1420
|
+
config,
|
|
1421
|
+
image: config?.edgeImage
|
|
1422
|
+
});
|
|
1423
|
+
if (result.converted) {
|
|
1424
|
+
onLog(log2("Edge moved from host OpenResty to the container edge", "warn"));
|
|
1299
1425
|
}
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1426
|
+
const version = await executor.exec(containerCommand(result.container, "openresty -v 2>&1")).catch(() => "");
|
|
1427
|
+
return {
|
|
1428
|
+
component: "edge",
|
|
1429
|
+
success: true,
|
|
1430
|
+
version: version ? systemCatalog.checks.openresty.parseVersion(version) : void 0
|
|
1431
|
+
};
|
|
1432
|
+
} catch (err) {
|
|
1433
|
+
if (err instanceof EdgeMigrateRequested) throw err;
|
|
1434
|
+
const msg = safeErrorMessage(err);
|
|
1435
|
+
onLog(log2(`Edge setup failed: ${msg}`, "error"));
|
|
1436
|
+
return { component: "edge", success: false, error: msg };
|
|
1303
1437
|
}
|
|
1304
|
-
return parseTraefikLabels(containers);
|
|
1305
1438
|
}
|
|
1439
|
+
var COMPONENT_INSTALLERS = {
|
|
1440
|
+
docker: (exec, log5) => installDocker(exec, log5),
|
|
1441
|
+
// The edge is ONE component and it is a container image. `installOpenResty` /
|
|
1442
|
+
// `installCertbot` are NOT reachable from here — they remain only for the mail
|
|
1443
|
+
// server's own proxy (mail.service.ts) and for converting a legacy host edge.
|
|
1444
|
+
edge: (exec, log5, config) => installContainerEdge(exec, log5, config),
|
|
1445
|
+
git: (exec, log5) => installGit(exec, log5),
|
|
1446
|
+
rsync: (exec, log5) => installRsync(exec, log5)
|
|
1447
|
+
};
|
|
1306
1448
|
|
|
1307
|
-
// ../../packages/adapters/src/system/
|
|
1308
|
-
var
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1449
|
+
// ../../packages/adapters/src/system/available-version.ts
|
|
1450
|
+
var PACKAGE_NAMES = {
|
|
1451
|
+
git: "git",
|
|
1452
|
+
rsync: "rsync",
|
|
1453
|
+
// The edge is an IMAGE, not a repo package — its "newer version available" comes
|
|
1454
|
+
// from the registry (see the update scanner's imageDigest), never from apt.
|
|
1455
|
+
edge: null,
|
|
1456
|
+
docker: null
|
|
1457
|
+
};
|
|
1458
|
+
function normalizePkgVersion(raw) {
|
|
1459
|
+
let v = raw.trim();
|
|
1460
|
+
const colon = v.lastIndexOf(":");
|
|
1461
|
+
if (colon !== -1) v = v.slice(colon + 1);
|
|
1462
|
+
const dash = v.indexOf("-");
|
|
1463
|
+
if (dash !== -1) v = v.slice(0, dash);
|
|
1464
|
+
return v.trim();
|
|
1465
|
+
}
|
|
1466
|
+
async function tryExec3(executor, cmd) {
|
|
1319
1467
|
try {
|
|
1320
|
-
|
|
1468
|
+
return await executor.exec(cmd);
|
|
1321
1469
|
} catch {
|
|
1322
1470
|
return null;
|
|
1323
1471
|
}
|
|
1324
|
-
const found = new Set(
|
|
1325
|
-
out.split("\n").map((l) => l.trim()).filter(Boolean)
|
|
1326
|
-
);
|
|
1327
|
-
return INSTALLED_MARKERS.find((m) => found.has(m.proxy))?.proxy ?? null;
|
|
1328
1472
|
}
|
|
1329
|
-
async function
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
proxy,
|
|
1342
|
-
sites: [],
|
|
1343
|
-
warnings: [`${proxy}: config import not supported \u2014 takeover only`]
|
|
1344
|
-
};
|
|
1473
|
+
async function probeCandidate(executor, pm, pkg) {
|
|
1474
|
+
const q = `'${pkg.replace(/'/g, "'\\''")}'`;
|
|
1475
|
+
if (pm === "apt") {
|
|
1476
|
+
const out = await tryExec3(executor, `apt-cache policy ${q} 2>/dev/null`);
|
|
1477
|
+
return out?.match(/Candidate:\s*(\S+)/)?.[1]?.trim() ?? null;
|
|
1478
|
+
}
|
|
1479
|
+
if (pm === "dnf" || pm === "yum") {
|
|
1480
|
+
const out = await tryExec3(
|
|
1481
|
+
executor,
|
|
1482
|
+
`${pm} -q --cacheonly list available ${q} 2>/dev/null | tail -n1`
|
|
1483
|
+
);
|
|
1484
|
+
return out?.trim().split(/\s+/)[1]?.trim() ?? null;
|
|
1345
1485
|
}
|
|
1486
|
+
if (pm === "apk") {
|
|
1487
|
+
const out = await tryExec3(executor, `apk policy ${q} 2>/dev/null`);
|
|
1488
|
+
const versions = [...out?.matchAll(/^\s+([\w.+~-]+):\s*$/gm) ?? []].map((m) => m[1]);
|
|
1489
|
+
return versions.length ? versions[versions.length - 1] : null;
|
|
1490
|
+
}
|
|
1491
|
+
return null;
|
|
1346
1492
|
}
|
|
1347
|
-
function
|
|
1348
|
-
|
|
1493
|
+
async function enrichAvailableVersions(executor, profile, statuses) {
|
|
1494
|
+
const targets = statuses.filter(
|
|
1495
|
+
(s) => s.installed && s.version && PACKAGE_NAMES[s.name]
|
|
1496
|
+
);
|
|
1497
|
+
await Promise.all(
|
|
1498
|
+
targets.map(async (s) => {
|
|
1499
|
+
try {
|
|
1500
|
+
const pkg = PACKAGE_NAMES[s.name];
|
|
1501
|
+
const candidate = await probeCandidate(executor, profile.packageManager, pkg);
|
|
1502
|
+
if (!candidate) return;
|
|
1503
|
+
const avail = normalizePkgVersion(candidate);
|
|
1504
|
+
const installed = normalizePkgVersion(s.version);
|
|
1505
|
+
if (avail && compareSemver(avail, installed) > 0) {
|
|
1506
|
+
s.availableVersion = avail;
|
|
1507
|
+
s.updateAvailable = true;
|
|
1508
|
+
}
|
|
1509
|
+
} catch {
|
|
1510
|
+
}
|
|
1511
|
+
})
|
|
1512
|
+
);
|
|
1349
1513
|
}
|
|
1350
1514
|
|
|
1351
|
-
// ../../packages/adapters/src/system/
|
|
1352
|
-
var
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1515
|
+
// ../../packages/adapters/src/system/components.ts
|
|
1516
|
+
var SYSTEM_COMPONENTS = [
|
|
1517
|
+
{
|
|
1518
|
+
name: "docker",
|
|
1519
|
+
label: "Docker",
|
|
1520
|
+
description: "Container runtime for deployments",
|
|
1521
|
+
installable: true,
|
|
1522
|
+
category: "core"
|
|
1523
|
+
},
|
|
1524
|
+
{
|
|
1525
|
+
name: "git",
|
|
1526
|
+
label: "Git",
|
|
1527
|
+
description: "Version control for source code",
|
|
1528
|
+
installable: true,
|
|
1529
|
+
category: "core"
|
|
1530
|
+
},
|
|
1531
|
+
{
|
|
1532
|
+
// ONE component for the edge, because it is ONE artifact: the openship-edge
|
|
1533
|
+
// image. OpenResty, its Lua, and certbot all ship inside it — they were three
|
|
1534
|
+
// rows in this list for a thing that is installed, checked, and removed as a
|
|
1535
|
+
// unit, and neither "openresty" nor "certbot" was independently installable
|
|
1536
|
+
// on a converted box.
|
|
1537
|
+
name: "edge",
|
|
1538
|
+
label: "Edge",
|
|
1539
|
+
description: "Routing + TLS \u2014 the openship-edge container (OpenResty, Lua, certbot)",
|
|
1540
|
+
installable: true,
|
|
1541
|
+
category: "infrastructure"
|
|
1542
|
+
},
|
|
1543
|
+
{
|
|
1544
|
+
name: "rsync",
|
|
1545
|
+
label: "rsync",
|
|
1546
|
+
description: "Fast directory sync for remote local-build transfers",
|
|
1547
|
+
installable: true,
|
|
1548
|
+
category: "infrastructure"
|
|
1362
1549
|
}
|
|
1550
|
+
];
|
|
1551
|
+
var SYSTEM_COMPONENTS_BY_NAME = new Map(
|
|
1552
|
+
SYSTEM_COMPONENTS.map((component) => [component.name, component])
|
|
1553
|
+
);
|
|
1554
|
+
function getSystemComponentDefinition(name) {
|
|
1555
|
+
return SYSTEM_COMPONENTS_BY_NAME.get(name) ?? {
|
|
1556
|
+
name,
|
|
1557
|
+
label: name,
|
|
1558
|
+
description: `${name} component`,
|
|
1559
|
+
installable: false,
|
|
1560
|
+
category: "infrastructure"
|
|
1561
|
+
};
|
|
1363
1562
|
}
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
const
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1563
|
+
|
|
1564
|
+
// ../../packages/adapters/src/system/checks.ts
|
|
1565
|
+
async function tryExec4(executor, command) {
|
|
1566
|
+
const startedAt = Date.now();
|
|
1567
|
+
systemDebug("checks", `exec:start ${command}`);
|
|
1568
|
+
try {
|
|
1569
|
+
const result = await executor.exec(command, { timeout: 1e4 });
|
|
1570
|
+
systemDebug(
|
|
1571
|
+
"checks",
|
|
1572
|
+
`exec:ok ${command} (${formatDuration(startedAt)})`
|
|
1573
|
+
);
|
|
1574
|
+
return result;
|
|
1575
|
+
} catch (err) {
|
|
1576
|
+
if (isRemoteConnectionError(err)) {
|
|
1577
|
+
systemDebug(
|
|
1578
|
+
"checks",
|
|
1579
|
+
`exec:abort ${command} (${formatDuration(startedAt)}) ${safeErrorMessage(err)}`
|
|
1373
1580
|
);
|
|
1374
|
-
|
|
1375
|
-
} else if (o.systemdUnit && !units.has(o.systemdUnit)) {
|
|
1376
|
-
const en = await tryExec5(executor, `systemctl is-enabled ${sq2(o.systemdUnit)} 2>/dev/null`);
|
|
1377
|
-
units.set(o.systemdUnit, { unit: o.systemdUnit, wasEnabled: en?.trim() === "enabled" });
|
|
1378
|
-
} else if (!o.containerName && !o.systemdUnit && o.pid && !processes.has(o.pid)) {
|
|
1379
|
-
processes.set(o.pid, { pid: o.pid, command: o.rawCommand });
|
|
1581
|
+
throw err;
|
|
1380
1582
|
}
|
|
1583
|
+
const msg = safeErrorMessage(err);
|
|
1584
|
+
systemDebug(
|
|
1585
|
+
"checks",
|
|
1586
|
+
`exec:fail ${command} (${formatDuration(startedAt)}) ${msg}`
|
|
1587
|
+
);
|
|
1588
|
+
return null;
|
|
1381
1589
|
}
|
|
1590
|
+
}
|
|
1591
|
+
function healthy(name, version, running) {
|
|
1592
|
+
const component = getSystemComponentDefinition(name);
|
|
1382
1593
|
return {
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1594
|
+
name,
|
|
1595
|
+
label: component.label,
|
|
1596
|
+
description: component.description,
|
|
1597
|
+
installable: component.installable,
|
|
1598
|
+
installed: true,
|
|
1599
|
+
version,
|
|
1600
|
+
running,
|
|
1601
|
+
healthy: running !== void 0 ? running : true,
|
|
1602
|
+
message: running ? `${name} ${version} - running` : `${name} ${version} - installed`
|
|
1387
1603
|
};
|
|
1388
1604
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1605
|
+
function unhealthy(name, message, opts) {
|
|
1606
|
+
const component = getSystemComponentDefinition(name);
|
|
1607
|
+
return {
|
|
1608
|
+
name,
|
|
1609
|
+
label: component.label,
|
|
1610
|
+
description: component.description,
|
|
1611
|
+
installable: component.installable,
|
|
1612
|
+
installed: !!opts?.version,
|
|
1613
|
+
version: opts?.version,
|
|
1614
|
+
running: opts?.running,
|
|
1615
|
+
healthy: false,
|
|
1616
|
+
message
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
async function checkDocker(executor) {
|
|
1620
|
+
const startedAt = Date.now();
|
|
1621
|
+
const recipe = systemCatalog.checks.docker;
|
|
1622
|
+
const version = await tryExec4(executor, recipe.versionCommand);
|
|
1623
|
+
if (!version) {
|
|
1624
|
+
systemDebug("checks", `docker:missing (${formatDuration(startedAt)})`);
|
|
1625
|
+
return unhealthy("docker", recipe.missingMessage);
|
|
1626
|
+
}
|
|
1627
|
+
const parsed = recipe.parseVersion(version);
|
|
1628
|
+
const info = await tryExec4(executor, recipe.daemonCommand);
|
|
1629
|
+
if (!info) {
|
|
1630
|
+
systemDebug("checks", `docker:not-running (${formatDuration(startedAt)})`);
|
|
1631
|
+
return unhealthy("docker", recipe.notRunningMessage, {
|
|
1632
|
+
version: parsed,
|
|
1633
|
+
running: false
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
systemDebug("checks", `docker:healthy (${formatDuration(startedAt)})`);
|
|
1637
|
+
return healthy("docker", parsed, true);
|
|
1638
|
+
}
|
|
1639
|
+
async function checkGit(executor) {
|
|
1640
|
+
const startedAt = Date.now();
|
|
1641
|
+
const recipe = systemCatalog.checks.git;
|
|
1642
|
+
const version = await tryExec4(executor, recipe.versionCommand);
|
|
1643
|
+
if (!version) {
|
|
1644
|
+
systemDebug("checks", `git:missing (${formatDuration(startedAt)})`);
|
|
1645
|
+
return unhealthy("git", recipe.missingMessage);
|
|
1394
1646
|
}
|
|
1647
|
+
const parsed = recipe.parseVersion(version);
|
|
1648
|
+
systemDebug("checks", `git:healthy (${formatDuration(startedAt)})`);
|
|
1649
|
+
return healthy("git", parsed);
|
|
1395
1650
|
}
|
|
1396
|
-
async function
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
return JSON.parse(raw);
|
|
1404
|
-
} catch {
|
|
1405
|
-
await clearJournal(executor);
|
|
1406
|
-
return null;
|
|
1651
|
+
async function checkRsync(executor) {
|
|
1652
|
+
const startedAt = Date.now();
|
|
1653
|
+
const recipe = systemCatalog.checks.rsync;
|
|
1654
|
+
const version = await tryExec4(executor, recipe.versionCommand);
|
|
1655
|
+
if (!version) {
|
|
1656
|
+
systemDebug("checks", `rsync:missing (${formatDuration(startedAt)})`);
|
|
1657
|
+
return unhealthy("rsync", recipe.missingMessage);
|
|
1407
1658
|
}
|
|
1659
|
+
const parsed = recipe.parseVersion(version);
|
|
1660
|
+
systemDebug("checks", `rsync:healthy (${formatDuration(startedAt)})`);
|
|
1661
|
+
return healthy("rsync", parsed);
|
|
1408
1662
|
}
|
|
1409
|
-
async function
|
|
1410
|
-
|
|
1411
|
-
await
|
|
1412
|
-
|
|
1413
|
-
"
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
executor,
|
|
1418
|
-
u.wasEnabled ? `systemctl enable --now ${sq2(u.unit)} 2>/dev/null || true` : `systemctl start ${sq2(u.unit)} 2>/dev/null || true`
|
|
1663
|
+
async function checkEdge(executor) {
|
|
1664
|
+
const startedAt = Date.now();
|
|
1665
|
+
const container = await resolveOurEdgeContainer(executor);
|
|
1666
|
+
if (!container) {
|
|
1667
|
+
systemDebug("checks", `edge:missing (${formatDuration(startedAt)})`);
|
|
1668
|
+
return unhealthy(
|
|
1669
|
+
"edge",
|
|
1670
|
+
"No edge on this server. The edge is the openship-edge container \u2014 install it (requires Docker)."
|
|
1419
1671
|
);
|
|
1420
1672
|
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
`setsid -f sh -c ${sq2(p.command)} 2>/dev/null || (nohup sh -c ${sq2(p.command)} >/dev/null 2>&1 &) || true`
|
|
1430
|
-
);
|
|
1431
|
-
} else {
|
|
1432
|
-
onLog(log(`Could not restore process ${p.pid} \u2014 no command captured.`, "warn"));
|
|
1433
|
-
}
|
|
1673
|
+
const version = await tryExec4(executor, containerCommand(container, "openresty -v 2>&1"));
|
|
1674
|
+
if (!version) {
|
|
1675
|
+
systemDebug("checks", `edge:container-unresponsive (${formatDuration(startedAt)})`);
|
|
1676
|
+
return unhealthy(
|
|
1677
|
+
"edge",
|
|
1678
|
+
`The edge container ${container} is running but not responding \u2014 check \`docker logs ${container}\``,
|
|
1679
|
+
{ running: false }
|
|
1680
|
+
);
|
|
1434
1681
|
}
|
|
1682
|
+
systemDebug("checks", `edge:healthy (${formatDuration(startedAt)})`);
|
|
1683
|
+
return healthy("edge", systemCatalog.checks.openresty.parseVersion(version), true);
|
|
1435
1684
|
}
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1685
|
+
var COMPONENT_CHECKS = {
|
|
1686
|
+
docker: checkDocker,
|
|
1687
|
+
edge: checkEdge,
|
|
1688
|
+
git: checkGit,
|
|
1689
|
+
rsync: checkRsync
|
|
1690
|
+
};
|
|
1691
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
1692
|
+
const results = new Array(items.length);
|
|
1693
|
+
let cursor = 0;
|
|
1694
|
+
const worker = async () => {
|
|
1695
|
+
while (cursor < items.length) {
|
|
1696
|
+
const index = cursor++;
|
|
1697
|
+
results[index] = await fn(items[index]);
|
|
1698
|
+
}
|
|
1699
|
+
};
|
|
1700
|
+
await Promise.all(
|
|
1701
|
+
Array.from({ length: Math.min(limit, items.length) }, worker)
|
|
1702
|
+
);
|
|
1703
|
+
return results;
|
|
1439
1704
|
}
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1705
|
+
var CHECK_CONCURRENCY = 4;
|
|
1706
|
+
async function checkAll(executor) {
|
|
1707
|
+
const startedAt = Date.now();
|
|
1708
|
+
const entries = SYSTEM_COMPONENTS.map((component) => [component.name, COMPONENT_CHECKS[component.name]]).filter((entry) => Boolean(entry[1]));
|
|
1709
|
+
systemDebug(
|
|
1710
|
+
"checks",
|
|
1711
|
+
`checkAll:start [${entries.map(([name]) => name).join(", ")}]`
|
|
1712
|
+
);
|
|
1713
|
+
const results = await mapWithConcurrency(
|
|
1714
|
+
entries,
|
|
1715
|
+
CHECK_CONCURRENCY,
|
|
1716
|
+
([, fn]) => fn(executor)
|
|
1717
|
+
);
|
|
1718
|
+
await enrichAvailable(executor, results);
|
|
1719
|
+
systemDebug("checks", `checkAll:done (${formatDuration(startedAt)})`);
|
|
1720
|
+
return results;
|
|
1446
1721
|
}
|
|
1447
|
-
async function
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1722
|
+
async function enrichAvailable(executor, results) {
|
|
1723
|
+
try {
|
|
1724
|
+
const profile = await resolveEnvironment(executor);
|
|
1725
|
+
await enrichAvailableVersions(executor, profile, results);
|
|
1726
|
+
} catch {
|
|
1452
1727
|
}
|
|
1453
|
-
await clearJournal(executor);
|
|
1454
1728
|
}
|
|
1455
|
-
async function
|
|
1456
|
-
const
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1729
|
+
async function checkComponents(executor, names) {
|
|
1730
|
+
const startedAt = Date.now();
|
|
1731
|
+
const fns = names.map((name) => COMPONENT_CHECKS[name]).filter((fn) => Boolean(fn));
|
|
1732
|
+
systemDebug("checks", `checkComponents:start [${names.join(", ")}]`);
|
|
1733
|
+
const results = await mapWithConcurrency(
|
|
1734
|
+
fns,
|
|
1735
|
+
CHECK_CONCURRENCY,
|
|
1736
|
+
(fn) => fn(executor)
|
|
1737
|
+
);
|
|
1738
|
+
await enrichAvailable(executor, results);
|
|
1739
|
+
systemDebug(
|
|
1740
|
+
"checks",
|
|
1741
|
+
`checkComponents:done [${names.join(", ")}] (${formatDuration(startedAt)})`
|
|
1742
|
+
);
|
|
1743
|
+
return results;
|
|
1470
1744
|
}
|
|
1471
1745
|
|
|
1472
1746
|
// ../../packages/adapters/src/system/proxy/index.ts
|
|
@@ -1496,20 +1770,20 @@ function describeEdgeOwner(occupants) {
|
|
|
1496
1770
|
}
|
|
1497
1771
|
|
|
1498
1772
|
// ../../packages/adapters/src/system/proxy/consent.ts
|
|
1499
|
-
function
|
|
1773
|
+
function log3(message, level = "info") {
|
|
1500
1774
|
return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
|
|
1501
1775
|
}
|
|
1502
1776
|
async function ensureEdgeClear(executor, config, onLog) {
|
|
1503
1777
|
const status = await probeEdge(executor);
|
|
1504
1778
|
if (status.canProceedClean) return { tookOver: false };
|
|
1505
1779
|
const takeover = async () => {
|
|
1506
|
-
onLog(
|
|
1780
|
+
onLog(log3(
|
|
1507
1781
|
`Taking over ports from ${status.occupants.map((o) => o.command ?? o.port).join(", ")}...`,
|
|
1508
1782
|
"warn"
|
|
1509
1783
|
));
|
|
1510
1784
|
const configured = config?.edgePolicy?.stopTargets ?? [];
|
|
1511
1785
|
const targets = configured.length ? configured : stopTargetsForStatus(status);
|
|
1512
|
-
await freeEdgeTargets(executor, targets, (m, l) => onLog(
|
|
1786
|
+
await freeEdgeTargets(executor, targets, (m, l) => onLog(log3(m, l)));
|
|
1513
1787
|
};
|
|
1514
1788
|
if (config?.edgePolicy?.mode === "takeover") {
|
|
1515
1789
|
await takeover();
|
|
@@ -1549,223 +1823,253 @@ async function ensureEdgeClear(executor, config, onLog) {
|
|
|
1549
1823
|
throw new EdgeConflictError(status);
|
|
1550
1824
|
}
|
|
1551
1825
|
|
|
1552
|
-
// ../../packages/adapters/src/system/
|
|
1553
|
-
function
|
|
1826
|
+
// ../../packages/adapters/src/system/proxy/ensure-container-edge.ts
|
|
1827
|
+
function log4(message, level = "info") {
|
|
1554
1828
|
return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
|
|
1555
1829
|
}
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1830
|
+
var injectedDefaultImage;
|
|
1831
|
+
function setDefaultEdgeImage(image) {
|
|
1832
|
+
injectedDefaultImage = image?.trim() || void 0;
|
|
1833
|
+
}
|
|
1834
|
+
function resolveEdgeImage(explicit) {
|
|
1835
|
+
return buildEdgeImageRef({ explicit, injectedDefault: injectedDefaultImage });
|
|
1836
|
+
}
|
|
1837
|
+
async function dockerAvailable(executor) {
|
|
1838
|
+
return executor.exec("docker version --format '{{.Server.Version}}' 2>/dev/null").then((v) => Boolean(v.trim())).catch(() => false);
|
|
1839
|
+
}
|
|
1840
|
+
async function runningImage(executor, container) {
|
|
1841
|
+
const out = await executor.exec(`docker inspect -f '{{.Config.Image}}' ${sq(container)} 2>/dev/null`).catch(() => "");
|
|
1842
|
+
return out.trim() || null;
|
|
1843
|
+
}
|
|
1844
|
+
async function containerEdgeProvider(executor, container, opts) {
|
|
1845
|
+
const { NginxProvider: Provider } = await import("./nginx-XWJAOGS4.js");
|
|
1846
|
+
return new Provider({
|
|
1847
|
+
...opts,
|
|
1848
|
+
// Vhosts go to the BIND-MOUNTED HOST dir (so every host-side reader — the
|
|
1849
|
+
// migrate proxy scan, cert reuse, the mail cert symlinks — keeps working),
|
|
1850
|
+
// while reload/certbot run inside the container.
|
|
1851
|
+
paths: EDGE_HOST_PATHS,
|
|
1852
|
+
executor: edgeContainerExecutor(executor, container),
|
|
1853
|
+
// MUST be pinned: re-detection answers from inside the container and would
|
|
1854
|
+
// repoint sitesDir at a host dir the edge never reads.
|
|
1855
|
+
pinPaths: true
|
|
1856
|
+
});
|
|
1564
1857
|
}
|
|
1565
|
-
async function
|
|
1566
|
-
const
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1858
|
+
async function localContainerEdgeProvider(container, opts) {
|
|
1859
|
+
const { DockerEdgeExecutor } = await import("./docker-edge-executor-VHW3JPJS.js");
|
|
1860
|
+
const { NginxProvider: Provider } = await import("./nginx-XWJAOGS4.js");
|
|
1861
|
+
return new Provider({
|
|
1862
|
+
...opts,
|
|
1863
|
+
paths: OPENRESTY_DEFAULT_PATHS,
|
|
1864
|
+
executor: new DockerEdgeExecutor({ containerName: container }),
|
|
1865
|
+
// nginx.conf + the Lua are BAKED into the image — nothing to detect, install
|
|
1866
|
+
// or patch, and detection would answer from inside the container anyway.
|
|
1867
|
+
pinPaths: true
|
|
1868
|
+
});
|
|
1572
1869
|
}
|
|
1573
|
-
|
|
1574
|
-
const
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1870
|
+
function buildEdgeRunCommand(container, image) {
|
|
1871
|
+
const mounts = EDGE_CONTAINER_MOUNTS.map(
|
|
1872
|
+
// `:z` relabels for SELinux-enforcing hosts; a no-op elsewhere.
|
|
1873
|
+
(m) => `-v ${sq(`${m.host}:${m.container}:z`)}`
|
|
1874
|
+
).join(" ");
|
|
1875
|
+
return [
|
|
1876
|
+
"docker run -d",
|
|
1877
|
+
`--name ${sq(container)}`,
|
|
1878
|
+
"--network host",
|
|
1879
|
+
"--restart unless-stopped",
|
|
1880
|
+
mounts,
|
|
1881
|
+
sq(image)
|
|
1882
|
+
].join(" ");
|
|
1883
|
+
}
|
|
1884
|
+
async function swapEdgeImage(executor, container, from, to, opts) {
|
|
1885
|
+
const { onLog } = opts;
|
|
1886
|
+
onLog(log4(`Updating the edge: ${from} \u2192 ${to}`));
|
|
1887
|
+
const pull = await executor.streamExec(`docker pull ${sq(to)}`, onLog);
|
|
1888
|
+
if (pull.code !== 0) {
|
|
1889
|
+
onLog(log4(`Could not pull ${to} \u2014 the edge stays on ${from}.`, "warn"));
|
|
1890
|
+
return { swapped: false, edgeDown: false };
|
|
1891
|
+
}
|
|
1892
|
+
const start = async (image) => {
|
|
1893
|
+
await executor.exec(`docker rm -f ${sq(container)} 2>/dev/null || true`).catch(() => {
|
|
1894
|
+
});
|
|
1895
|
+
const run = await executor.streamExec(
|
|
1896
|
+
buildEdgeRunCommand(container, image),
|
|
1897
|
+
onLog
|
|
1898
|
+
);
|
|
1899
|
+
invalidateEdgeContainer(executor);
|
|
1900
|
+
if (run.code !== 0) return false;
|
|
1901
|
+
const listening = await waitForPortListening(executor, 80, {
|
|
1902
|
+
timeoutMs: opts.verifyTimeoutMs ?? 3e4
|
|
1903
|
+
});
|
|
1904
|
+
return !(listening.checked && !listening.listening);
|
|
1584
1905
|
};
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
if (!prep.ok) return prep.result;
|
|
1589
|
-
executor = prep.executor;
|
|
1590
|
-
const profile = prep.profile;
|
|
1591
|
-
const plan = systemCatalog.installs.docker(profile);
|
|
1592
|
-
if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
|
|
1593
|
-
return { component: "docker", success: false, error: plan.unsupportedReason ?? "Docker install not supported" };
|
|
1906
|
+
if (await start(to)) {
|
|
1907
|
+
onLog(log4(`Edge updated to ${to}`));
|
|
1908
|
+
return { swapped: true, edgeDown: false };
|
|
1594
1909
|
}
|
|
1595
|
-
onLog(
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
if (code !== 0) return { component: "docker", success: false, error: "Docker install failed" };
|
|
1599
|
-
if (plan.startCommand) {
|
|
1600
|
-
onLog(log3("Starting Docker service..."));
|
|
1601
|
-
await executor.streamExec(plan.startCommand, onLog);
|
|
1602
|
-
}
|
|
1603
|
-
const version = await executor.exec(plan.verifyCommand);
|
|
1604
|
-
const parsed = systemCatalog.checks.docker.parseVersion(version);
|
|
1605
|
-
onLog(log3(`Docker ${parsed} installed`));
|
|
1606
|
-
return { component: "docker", success: true, version: parsed };
|
|
1607
|
-
} catch (err) {
|
|
1608
|
-
const msg = safeErrorMessage(err);
|
|
1609
|
-
onLog(log3(`Docker installation failed: ${msg}`, "error"));
|
|
1610
|
-
return { component: "docker", success: false, error: msg };
|
|
1910
|
+
onLog(log4(`Edge ${to} failed to come up \u2014 rolling back to ${from}...`, "error"));
|
|
1911
|
+
if (await start(from)) {
|
|
1912
|
+
return { swapped: false, edgeDown: false };
|
|
1611
1913
|
}
|
|
1914
|
+
onLog(log4(`Rollback to ${from} ALSO failed \u2014 the edge is down on this server.`, "error"));
|
|
1915
|
+
return { swapped: false, edgeDown: true };
|
|
1612
1916
|
}
|
|
1613
|
-
async function
|
|
1614
|
-
const
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
const plan = systemCatalog.installs.git(profile);
|
|
1619
|
-
if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
|
|
1620
|
-
return { component: "git", success: false, error: plan.unsupportedReason ?? "Git install not supported" };
|
|
1621
|
-
}
|
|
1622
|
-
onLog(log3(opts?.label ? `Installing Git in ${opts.label}...` : "Installing Git..."));
|
|
1623
|
-
try {
|
|
1624
|
-
const { code } = await executor.streamExec(plan.installCommand, onLog);
|
|
1625
|
-
if (code !== 0) return { component: "git", success: false, error: "Git installation failed" };
|
|
1626
|
-
const version = await executor.exec(plan.verifyCommand);
|
|
1627
|
-
const parsed = systemCatalog.checks.git.parseVersion(version);
|
|
1628
|
-
onLog(log3(opts?.label ? `Git ${parsed} installed in ${opts.label}` : `Git ${parsed} installed`));
|
|
1629
|
-
return { component: "git", success: true, version: parsed };
|
|
1630
|
-
} catch (err) {
|
|
1631
|
-
const msg = safeErrorMessage(err);
|
|
1632
|
-
onLog(log3(`Git installation failed: ${msg}`, "error"));
|
|
1633
|
-
return { component: "git", success: false, error: msg };
|
|
1634
|
-
}
|
|
1917
|
+
async function listDir(executor, dir) {
|
|
1918
|
+
const out = await executor.exec(`ls -1 ${sq(dir)} 2>/dev/null || true`).catch(() => "");
|
|
1919
|
+
return new Set(
|
|
1920
|
+
out.split("\n").map((l) => l.trim()).filter(Boolean)
|
|
1921
|
+
);
|
|
1635
1922
|
}
|
|
1636
|
-
async function
|
|
1637
|
-
const
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1923
|
+
async function sanitizeCarriedVhosts(executor, sitesDir, onLog) {
|
|
1924
|
+
const script = [
|
|
1925
|
+
`for f in ${sq(sitesDir)}/*.conf; do`,
|
|
1926
|
+
` [ -f "$f" ] || continue;`,
|
|
1927
|
+
// A real server_name is anything that isn't the `_` wildcard.
|
|
1928
|
+
` if ! grep -qE '^[[:space:]]*server_name[[:space:]]+[^_;[:space:]]' "$f"; then`,
|
|
1929
|
+
` echo "dropped-catchall $f"; rm -f "$f"; continue;`,
|
|
1930
|
+
` fi;`,
|
|
1931
|
+
` if grep -qE '[[:space:]]default_server' "$f"; then`,
|
|
1932
|
+
` sed -i -E 's/([[:space:]]listen[^;]*)[[:space:]]+default_server/\\1/g' "$f" && echo "unset-default $f";`,
|
|
1933
|
+
` fi;`,
|
|
1934
|
+
`done`
|
|
1935
|
+
].join(" ");
|
|
1936
|
+
const out = await executor.exec(script).catch(() => "");
|
|
1937
|
+
for (const line of out.split("\n").map((l) => l.trim()).filter(Boolean)) {
|
|
1938
|
+
const [action, file] = [line.slice(0, line.indexOf(" ")), line.slice(line.indexOf(" ") + 1)];
|
|
1939
|
+
if (action === "dropped-catchall") {
|
|
1940
|
+
onLog(log4(`Dropped the bare edge's catch-all ${file} \u2014 the edge image provides it.`, "warn"));
|
|
1941
|
+
} else if (action === "unset-default") {
|
|
1942
|
+
onLog(log4(`Removed default_server from ${file} \u2014 the edge image owns it.`, "warn"));
|
|
1943
|
+
}
|
|
1657
1944
|
}
|
|
1658
1945
|
}
|
|
1659
|
-
async function
|
|
1660
|
-
const
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
const
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1946
|
+
async function ensureContainerEdge(executor, opts) {
|
|
1947
|
+
const { onLog } = opts;
|
|
1948
|
+
const container = opts.container?.trim() || EDGE_CONTAINER_NAME;
|
|
1949
|
+
const image = resolveEdgeImage(opts.image);
|
|
1950
|
+
const existing = await resolveOurEdgeContainer(executor, { fresh: true });
|
|
1951
|
+
if (existing) {
|
|
1952
|
+
const current = await runningImage(executor, existing);
|
|
1953
|
+
if (current && current !== image) {
|
|
1954
|
+
const swap = await swapEdgeImage(executor, existing, current, image, opts);
|
|
1955
|
+
return {
|
|
1956
|
+
container: existing,
|
|
1957
|
+
image,
|
|
1958
|
+
converted: false,
|
|
1959
|
+
updated: swap.swapped,
|
|
1960
|
+
edgeDown: swap.edgeDown
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
return { container: existing, image, converted: false };
|
|
1667
1964
|
}
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
const parsed = systemCatalog.checks.certbot.parseVersion(version);
|
|
1674
|
-
onLog(log3(`Certbot ${parsed} installed`));
|
|
1675
|
-
return { component: "certbot", success: true, version: parsed };
|
|
1676
|
-
} catch (err) {
|
|
1677
|
-
const msg = safeErrorMessage(err);
|
|
1678
|
-
onLog(log3(`Certbot installation failed: ${msg}`, "error"));
|
|
1679
|
-
return { component: "certbot", success: false, error: msg };
|
|
1965
|
+
await ensureEdgeClear(executor, opts.config, onLog);
|
|
1966
|
+
if (!await dockerAvailable(executor)) {
|
|
1967
|
+
throw new Error(
|
|
1968
|
+
"Docker isn't available on this server, and the edge now runs as a container. Install the Docker component first, then set up the edge."
|
|
1969
|
+
);
|
|
1680
1970
|
}
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
const
|
|
1684
|
-
if (
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
const plan = systemCatalog.installs.openresty(profile);
|
|
1689
|
-
if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
|
|
1690
|
-
return { component: "openresty", success: false, error: plan.unsupportedReason ?? "OpenResty install not supported" };
|
|
1971
|
+
const bareWasOurs = await ourLuaOnHost(executor);
|
|
1972
|
+
onLog(log4(`Pulling edge image ${image}...`));
|
|
1973
|
+
const pull = await executor.streamExec(`docker pull ${sq(image)}`, onLog);
|
|
1974
|
+
if (pull.code !== 0) {
|
|
1975
|
+
throw new Error(
|
|
1976
|
+
`Could not pull the edge image ${image} \u2014 the box is unchanged and still serving. Check the server's network access to the registry, then retry.`
|
|
1977
|
+
);
|
|
1691
1978
|
}
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1979
|
+
for (const mount of EDGE_CONTAINER_MOUNTS) {
|
|
1980
|
+
await executor.exec(`mkdir -p ${sq(mount.host)}`).catch(() => {
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
const sitesTarget = EDGE_HOST_PATHS.sitesDir;
|
|
1984
|
+
let beforeCarry = null;
|
|
1985
|
+
if (bareWasOurs) {
|
|
1986
|
+
const barePaths = await detectOpenRestyPaths(executor).catch(() => OPENRESTY_DEFAULT_PATHS);
|
|
1987
|
+
if (barePaths.sitesDir !== sitesTarget && await executor.exists(barePaths.sitesDir)) {
|
|
1988
|
+
onLog(log4(`Carrying vhosts from ${barePaths.sitesDir}...`));
|
|
1989
|
+
beforeCarry = await listDir(executor, sitesTarget);
|
|
1990
|
+
await executor.exec(`cp -a ${sq(`${barePaths.sitesDir}/.`)} ${sq(`${sitesTarget}/`)} 2>/dev/null || true`).catch(() => {
|
|
1991
|
+
});
|
|
1992
|
+
await sanitizeCarriedVhosts(executor, sitesTarget, onLog);
|
|
1700
1993
|
}
|
|
1701
|
-
|
|
1702
|
-
|
|
1994
|
+
}
|
|
1995
|
+
const restoreBare = async () => {
|
|
1996
|
+
if (!bareWasOurs) return;
|
|
1997
|
+
if (beforeCarry) {
|
|
1998
|
+
const now = await listDir(executor, sitesTarget);
|
|
1999
|
+
const added = [...now].filter((name) => !beforeCarry.has(name));
|
|
2000
|
+
if (added.length > 0) {
|
|
2001
|
+
await executor.exec(`rm -f ${added.map((n) => sq(`${sitesTarget}/${n}`)).join(" ")}`).catch(() => {
|
|
2002
|
+
});
|
|
2003
|
+
onLog(log4(`Removed ${added.length} carried vhost(s) so the next edge start is clean.`, "warn"));
|
|
2004
|
+
}
|
|
1703
2005
|
}
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
2006
|
+
onLog(log4("Restoring the host OpenResty edge...", "warn"));
|
|
2007
|
+
await executor.exec("systemctl enable --now openresty 2>/dev/null || true").catch(() => {
|
|
2008
|
+
});
|
|
2009
|
+
};
|
|
2010
|
+
try {
|
|
2011
|
+
if (bareWasOurs) {
|
|
2012
|
+
onLog(log4("Stopping the host OpenResty edge..."));
|
|
2013
|
+
await executor.exec("systemctl disable --now openresty 2>/dev/null || true").catch(() => {
|
|
2014
|
+
});
|
|
2015
|
+
await executor.exec("systemctl reset-failed openresty 2>/dev/null || true").catch(() => {
|
|
2016
|
+
});
|
|
1715
2017
|
}
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
${journal}`, "error"));
|
|
1732
|
-
return { component: "openresty", success: false, error: "OpenResty installed but failed to start" };
|
|
1733
|
-
}
|
|
2018
|
+
await executor.exec(`docker rm -f ${sq(container)} 2>/dev/null || true`).catch(() => {
|
|
2019
|
+
});
|
|
2020
|
+
onLog(log4("Starting the edge container..."));
|
|
2021
|
+
const run = await executor.streamExec(
|
|
2022
|
+
buildEdgeRunCommand(container, image),
|
|
2023
|
+
onLog
|
|
2024
|
+
);
|
|
2025
|
+
invalidateEdgeContainer(executor);
|
|
2026
|
+
if (run.code !== 0) throw new Error("the edge container failed to start");
|
|
2027
|
+
await executor.exec(containerCommand(container, "openresty -t"));
|
|
2028
|
+
const listening = await waitForPortListening(executor, 80, {
|
|
2029
|
+
timeoutMs: opts.verifyTimeoutMs ?? 3e4
|
|
2030
|
+
});
|
|
2031
|
+
if (listening.checked && !listening.listening) {
|
|
2032
|
+
throw new Error("the edge container started but nothing is listening on :80");
|
|
1734
2033
|
}
|
|
1735
|
-
await executor.exec(
|
|
1736
|
-
onLog(
|
|
1737
|
-
|
|
1738
|
-
const version = await executor.exec(plan.verifyCommand);
|
|
1739
|
-
const parsed = systemCatalog.checks.openresty.parseVersion(version);
|
|
1740
|
-
onLog(log3(`OpenResty ${parsed} installed`));
|
|
1741
|
-
return { component: "openresty", success: true, version: parsed };
|
|
2034
|
+
const version = await executor.exec(containerCommand(container, "openresty -v 2>&1")).catch(() => "");
|
|
2035
|
+
onLog(log4(`Edge container running${version.trim() ? ` (${version.trim()})` : ""}`));
|
|
2036
|
+
return { container, image, converted: bareWasOurs };
|
|
1742
2037
|
} catch (err) {
|
|
1743
|
-
if (err instanceof EdgeMigrateRequested) throw err;
|
|
1744
2038
|
const msg = safeErrorMessage(err);
|
|
1745
|
-
onLog(
|
|
1746
|
-
|
|
2039
|
+
onLog(log4(`Edge container setup failed: ${msg}`, "error"));
|
|
2040
|
+
const logs = await executor.exec(`docker logs --tail 40 ${sq(container)} 2>&1 || true`).catch(() => "");
|
|
2041
|
+
if (logs.trim()) onLog(log4(`Edge container logs:
|
|
2042
|
+
${logs}`, "error"));
|
|
2043
|
+
await executor.exec(`docker rm -f ${sq(container)} 2>/dev/null || true`).catch(() => {
|
|
2044
|
+
});
|
|
2045
|
+
invalidateEdgeContainer(executor);
|
|
2046
|
+
await restoreBare();
|
|
2047
|
+
const reason = edgeFailureReason(logs);
|
|
2048
|
+
throw new Error(`Edge container setup failed: ${reason ? `${reason} (${msg})` : msg}`);
|
|
1747
2049
|
}
|
|
1748
2050
|
}
|
|
1749
|
-
var COMPONENT_INSTALLERS = {
|
|
1750
|
-
docker: (exec, log4) => installDocker(exec, log4),
|
|
1751
|
-
openresty: (exec, log4, config) => installOpenResty(exec, log4, config),
|
|
1752
|
-
certbot: (exec, log4) => installCertbot(exec, log4),
|
|
1753
|
-
git: (exec, log4) => installGit(exec, log4),
|
|
1754
|
-
rsync: (exec, log4) => installRsync(exec, log4)
|
|
1755
|
-
};
|
|
1756
2051
|
|
|
1757
2052
|
export {
|
|
1758
2053
|
systemCatalog,
|
|
1759
2054
|
checkGit,
|
|
1760
2055
|
checkAll,
|
|
1761
2056
|
checkComponents,
|
|
1762
|
-
ourEdgeContainerRunning,
|
|
1763
2057
|
detectInstalledProxy,
|
|
1764
2058
|
scanImportableSites,
|
|
2059
|
+
edgeProxy,
|
|
2060
|
+
edgeProxyFor,
|
|
2061
|
+
collectProxyCerts,
|
|
1765
2062
|
beginEdgeTakeover,
|
|
1766
2063
|
rollbackEdgeTakeover,
|
|
1767
2064
|
completeEdgeTakeover,
|
|
1768
2065
|
recoverInterruptedTakeover,
|
|
2066
|
+
setDefaultEdgeImage,
|
|
2067
|
+
resolveEdgeImage,
|
|
2068
|
+
dockerAvailable,
|
|
2069
|
+
containerEdgeProvider,
|
|
2070
|
+
localContainerEdgeProvider,
|
|
2071
|
+
buildEdgeRunCommand,
|
|
2072
|
+
ensureContainerEdge,
|
|
1769
2073
|
importSites,
|
|
1770
2074
|
foreignProxyOnEdge,
|
|
1771
2075
|
installGit,
|