@stacksjs/ts-cloud 0.16.53 → 0.16.54
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/dist/bin/{chunk-mpawwybt.js → chunk-9de905c6.js} +1 -1
- package/dist/bin/{chunk-rmxexkvr.js → chunk-gqyfa360.js} +1 -1
- package/dist/bin/{chunk-gee6a9zv.js → chunk-z3f0k8jt.js} +10 -10
- package/dist/bin/cli.js +4 -4
- package/dist/bin/dashboard-server.js +1 -1
- package/dist/{chunk-9fvtnx6h.js → chunk-07z9bps2.js} +1 -1
- package/dist/{chunk-hwm83kj0.js → chunk-b11w48de.js} +29 -29
- package/dist/deploy/index.js +1 -1
- package/dist/drivers/index.js +1 -1
- package/dist/index.js +1 -1
- package/dist/ui/404.html +1 -1
- package/dist/ui/accept-invitation.html +1 -1
- package/dist/ui/access-denied.html +1 -1
- package/dist/ui/account/automation.html +6 -6
- package/dist/ui/account/security.html +2 -2
- package/dist/ui/applications/compose.html +6 -6
- package/dist/ui/applications/new.html +2 -2
- package/dist/ui/data/backups.html +6 -6
- package/dist/ui/data/services.html +6 -6
- package/dist/ui/data/volumes.html +5 -5
- package/dist/ui/index.html +6 -6
- package/dist/ui/infrastructure/topology.html +5 -5
- package/dist/ui/integrations.html +2 -2
- package/dist/ui/operations/alerts.html +6 -6
- package/dist/ui/operations/configuration.html +6 -6
- package/dist/ui/operations/jobs.html +5 -5
- package/dist/ui/operations/maintenance.html +5 -5
- package/dist/ui/operations/observability.html +6 -6
- package/dist/ui/operations/previews.html +6 -6
- package/dist/ui/operations/queue.html +5 -5
- package/dist/ui/operations/regions.html +5 -5
- package/dist/ui/operations/releases.html +6 -6
- package/dist/ui/operations/spend.html +6 -6
- package/dist/ui/operations/workloads.html +6 -6
- package/dist/ui/security.html +1 -1
- package/dist/ui/server/actions.html +6 -6
- package/dist/ui/server/activity.html +2 -2
- package/dist/ui/server/capacity.html +6 -6
- package/dist/ui/server/database.html +6 -6
- package/dist/ui/server/deployments.html +6 -6
- package/dist/ui/server/diagnostics.html +2 -2
- package/dist/ui/server/firewall.html +6 -6
- package/dist/ui/server/fleet.html +6 -6
- package/dist/ui/server/logs.html +6 -6
- package/dist/ui/server/metrics.html +6 -6
- package/dist/ui/server/security.html +2 -2
- package/dist/ui/server/services.html +1 -1
- package/dist/ui/server/sites.html +6 -6
- package/dist/ui/server/ssh-keys.html +6 -6
- package/dist/ui/server/team.html +6 -6
- package/dist/ui/server/terminal.html +2 -2
- package/dist/ui/serverless/alarms.html +6 -6
- package/dist/ui/serverless/assets.html +2 -2
- package/dist/ui/serverless/cost.html +1 -1
- package/dist/ui/serverless/data.html +6 -6
- package/dist/ui/serverless/deployments.html +2 -2
- package/dist/ui/serverless/firewall.html +1 -1
- package/dist/ui/serverless/functions.html +6 -6
- package/dist/ui/serverless/logs.html +6 -6
- package/dist/ui/serverless/metrics.html +1 -1
- package/dist/ui/serverless/queues.html +6 -6
- package/dist/ui/serverless/secrets.html +6 -6
- package/dist/ui/serverless/traces.html +6 -6
- package/dist/ui/serverless.html +6 -6
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
import{hi}from"./chunk-
|
|
1
|
+
import{hi}from"./chunk-z3f0k8jt.js";import"./chunk-9t78j764.js";import"./chunk-1q6wqzh1.js";import"./chunk-rbyrd9we.js";import"./chunk-k1kk78qg.js";import"./chunk-7cactd6c.js";import"./chunk-0xfy78nc.js";import"./chunk-rqvrjp8g.js";import"./chunk-z6xnegpe.js";export{hi as resolveServerDashboardData};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{aa,je,A,H,w,B,Oe,Qt}from"./chunk-rqvrjp8g.js";import{ce,Sn,re,vn,h,Mr,oa}from"./chunk-dg8ceycc.js";import{Ue,S,ys,Sr,Si,vi,cn,Tc,Mt,ln,ft,Ye,Fe,Ze,Ec,yt,vr,Be,et,Ci,Ti,lo,uo,tt,dn,Er,bt,m,So,Ns,xs}from"./chunk-jxm62h8r.js";import{qt,p}from"./chunk-0xfy78nc.js";import{v}from"./chunk-rbyrd9we.js";import{g}from"./chunk-7cactd6c.js";import{J,I,U,kr,Mo,Nr,fn}from"./chunk-k1kk78qg.js";import{We,gr,no,_,sn,fc,hc,$t,fi,hi}from"./chunk-
|
|
1
|
+
import{aa,je,A,H,w,B,Oe,Qt}from"./chunk-rqvrjp8g.js";import{ce,Sn,re,vn,h,Mr,oa}from"./chunk-dg8ceycc.js";import{Ue,S,ys,Sr,Si,vi,cn,Tc,Mt,ln,ft,Ye,Fe,Ze,Ec,yt,vr,Be,et,Ci,Ti,lo,uo,tt,dn,Er,bt,m,So,Ns,xs}from"./chunk-jxm62h8r.js";import{qt,p}from"./chunk-0xfy78nc.js";import{v}from"./chunk-rbyrd9we.js";import{g}from"./chunk-7cactd6c.js";import{J,I,U,kr,Mo,Nr,fn}from"./chunk-k1kk78qg.js";import{We,gr,no,_,sn,fc,hc,$t,fi,hi}from"./chunk-z3f0k8jt.js";import{Do}from"./chunk-1q6wqzh1.js";import{fe}from"./chunk-95djv2ja.js";import{Hs,qs}from"./chunk-cdseg15k.js";import{xr,wt,_r,Or,$r,Lr,bn}from"./chunk-hkx5j0bn.js";import{se,G,di}from"./chunk-28124w0n.js";import{E}from"./chunk-n2jh5p5x.js";import{V}from"./chunk-p23wfy3r.js";import{P}from"./chunk-sy4zws71.js";import{K,Ms,Us,_o,Vt,z,Fs}from"./chunk-jyvr7eh5.js";import{hs}from"./chunk-gr1ra9nj.js";import{an}from"./chunk-p6qa21fr.js";import{O,N,na}from"./chunk-kqd186ky.js";import{createCipheriv as $b,createDecipheriv as Lb,createHash as $p,randomBytes as Ui}from"node:crypto";import{createHmac as Nb,timingSafeEqual as Mb}from"node:crypto";var $d="ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";function Ha(e){let t=0,r=0,n="";for(let i of e){r=r<<8|i,t+=8;while(t>=5)n+=$d[r>>>t-5&31],t-=5}if(t>0)n+=$d[r<<5-t&31];return n}function xp(e){let t=e.toUpperCase().replace(/[^A-Z2-7]/g,""),r=0,n=0,i=[];for(let o of t){let s=$d.indexOf(o);if(s<0)throw Error("Invalid base32 secret");if(n=n<<5|s,r+=5,r>=8)i.push(n>>>r-8&255),r-=8}return Buffer.from(i)}function Dp(e,t,r=6){let n=Buffer.alloc(8);n.writeBigUInt64BE(BigInt(t));let i=Nb("sha1",xp(e)).update(n).digest(),o=i[i.length-1]&15,s=(i[o]&127)<<24|(i[o+1]&255)<<16|(i[o+2]&255)<<8|i[o+3]&255;return String(s%10**r).padStart(r,"0")}function Wa(e,t,r=Date.now(),n=1){let i=t.replace(/[\s-]/g,"");if(!/^\d{6}$/.test(i))return;let o=Buffer.from(i),s=Math.floor(r/1000/30);for(let c=-n;c<=n;c++){if(s+c<0)continue;let u=Buffer.from(Dp(e,s+c));if(o.length===u.length&&Mb(o,u))return s+c}return}function Ld(e){let t=e.issuer?.trim()||"ts-cloud",r=`${t}:${e.account}`;return`otpauth://totp/${encodeURIComponent(r)}?secret=${encodeURIComponent(e.secret)}&issuer=${encodeURIComponent(t)}&algorithm=SHA1&digits=6&period=30`}var Lp=1800000,bo=43200000,Bp=3600000,Fp=300000,jp=600000;function hn(e){return typeof e==="string"?e:void 0}function Up(e){if(typeof e!=="string")return{};try{return JSON.parse(e)}catch{return{}}}function Bb(e){let t=e.trim();if(!/^[a-z0-9][a-z0-9._-]{1,31}$/i.test(t))throw Error("Username must be 2-32 characters: letters, numbers, dot, dash or underscore");return t}function Bd(e){if(e===void 0||!e.trim())return;let t=e.trim().toLowerCase();if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t)||t.length>254)throw Error("A valid email address is required");return t}function Fb(e){let t=e.trim().toLowerCase();if(!/^[a-z0-9][a-z0-9-]{1,47}$/.test(t))throw Error("OIDC provider slug must be 2-48 lowercase letters, numbers or dashes");return t}function jb(e){let t=new URL(e.trim());if(t.search||t.hash||t.username||t.password)throw Error("OIDC issuer cannot contain credentials, a query, or a fragment");if(t.protocol!=="https:"&&!(t.protocol==="http:"&&["127.0.0.1","::1","localhost"].includes(t.hostname)))throw Error("OIDC issuer must use HTTPS");return t.href.replace(/\/$/,"")}function Ub(e){let t=[...new Set(e.map((r)=>r.trim().toLowerCase()).filter(Boolean))];if(t.length===0)throw Error("At least one explicit OIDC email domain is required");if(t.some((r)=>!/^[a-z0-9](?:[a-z0-9.-]{0,251}[a-z0-9])?$/.test(r)||!r.includes(".")||r.includes("..")))throw Error("OIDC email domains must be explicit DNS names without wildcards");return t}function zb(e){let t=[...new Set(["openid",...e??["email","profile"]].map((r)=>r.trim()).filter(Boolean))];if(t.some((r)=>!/^[\x21\x23-\x5B\x5D-\x7E]+$/.test(r)))throw Error("OIDC scopes contain unsupported characters");return t}function ui(e){return $p("sha256").update(e).digest("hex")}function pa(e){return{id:String(e.id),actorId:String(e.actor_id),username:String(e.username),email:hn(e.email),emailVerifiedAt:hn(e.email_verified_at),passwordHash:String(e.password_hash),credentialVersion:Number(e.credential_version),requiresPasswordUpgrade:Number(e.requires_password_upgrade)===1,disabledAt:hn(e.disabled_at),lastLoginAt:hn(e.last_login_at),createdAt:String(e.created_at),updatedAt:String(e.updated_at)}}function Op(e,t){let r=hn(e.consumed_at),n=String(e.expires_at);return{id:String(e.id),identityId:String(e.identity_id),type:String(e.type),metadata:Up(e.metadata),expiresAt:n,consumedAt:r,createdAt:String(e.created_at),state:r?"consumed":n<=t?"expired":"pending"}}function Fd(e,t){let r=hn(e.revoked_at),n=String(e.idle_expires_at),i=String(e.absolute_expires_at);return{id:String(e.id),identityId:String(e.identity_id),credentialVersion:Number(e.credential_version),authMethod:String(e.auth_method),userAgent:hn(e.user_agent),networkHint:hn(e.network_hint),createdAt:String(e.created_at),lastUsedAt:String(e.last_used_at),idleExpiresAt:n,absoluteExpiresAt:i,recentAuthAt:String(e.recent_auth_at),mfaAt:hn(e.mfa_at),revokedAt:r,state:r?"revoked":n<=t||i<=t?"expired":"active"}}function _p(e){let t=hn(e.verified_at),r=hn(e.disabled_at);return{id:String(e.id),identityId:String(e.identity_id),type:"totp",label:String(e.label),createdAt:String(e.created_at),verifiedAt:t,disabledAt:r,state:r?"disabled":t?"active":"pending"}}function jd(e,t){let r=hn(e.consumed_at),n=String(e.expires_at),i=Number(e.attempts);return{id:String(e.id),identityId:String(e.identity_id),purpose:String(e.purpose),attempts:i,expiresAt:n,consumedAt:r,createdAt:String(e.created_at),state:r?"consumed":i>=5?"locked":n<=t?"expired":"pending"}}function Np(e){let t=Up(e);return Array.isArray(t)?t.filter((r)=>typeof r==="string"):[]}function Ka(e){return{id:String(e.id),organizationId:String(e.organization_id),slug:String(e.slug),name:String(e.name),issuer:String(e.issuer),clientId:String(e.client_id),hasClientSecret:typeof e.client_secret_ciphertext==="string"&&e.client_secret_ciphertext.length>0,scopes:Np(e.scopes),allowedDomains:Np(e.allowed_domains),defaultRole:String(e.default_role),enabled:Number(e.enabled)===1,enforceSso:Number(e.enforce_sso)===1,createdAt:String(e.created_at),updatedAt:String(e.updated_at)}}function Mp(e,t){let r=hn(e.consumed_at),n=String(e.expires_at);return{id:String(e.id),providerId:String(e.provider_id),redirectUri:String(e.redirect_uri),returnPath:String(e.return_path),expiresAt:n,consumedAt:r,createdAt:String(e.created_at),state:r?"consumed":n<=t?"expired":"pending"}}function Vb(e){return{id:String(e.id),providerId:String(e.provider_id),identityId:String(e.identity_id),subject:String(e.subject),email:String(e.email),linkedAt:String(e.linked_at),lastLoginAt:String(e.last_login_at)}}class Le{controlPlane;nowFn;idFn;encryptionKey;constructor(e,t={}){this.controlPlane=e;this.nowFn=t.now??(()=>new Date),this.idFn=t.id??(()=>crypto.randomUUID()),this.encryptionKey=t.encryptionKey?$p("sha256").update(t.encryptionKey).digest():void 0}now(){return this.nowFn().toISOString()}run(e,t){this.controlPlane.database.run(e,t)}encrypt(e){if(!this.encryptionKey)throw Error("Authentication encryption key is not configured");let t=Ui(12),r=$b("aes-256-gcm",this.encryptionKey,t),n=Buffer.concat([r.update(e,"utf8"),r.final()]);return`v1.${t.toString("base64url")}.${r.getAuthTag().toString("base64url")}.${n.toString("base64url")}`}decrypt(e){if(!this.encryptionKey)throw Error("Authentication encryption key is not configured");let[t,r,n,i]=e.split(".");if(t!=="v1"||!r||!n||!i)throw Error("Encrypted authentication value is unavailable");let o=Lb("aes-256-gcm",this.encryptionKey,Buffer.from(r,"base64url"));return o.setAuthTag(Buffer.from(n,"base64url")),Buffer.concat([o.update(Buffer.from(i,"base64url")),o.final()]).toString("utf8")}createIdentity(e){if(!this.controlPlane.getActor(e.actorId))throw Error("Authentication actor was not found");let t=Bb(e.username),r=Bd(e.email),n=e.id??this.idFn(),i=this.now();return this.run(`INSERT INTO auth_identities (id, actor_id, username, email, email_verified_at, password_hash, requires_password_upgrade, created_at, updated_at)
|
|
2
2
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,[n,e.actorId,t,r??null,e.emailVerified&&r?i:null,e.passwordHash,e.requiresPasswordUpgrade?1:0,i,i]),this.getIdentity(n)}getIdentity(e){let t=this.controlPlane.database.query("SELECT * FROM auth_identities WHERE id = ?").get(e);return t?pa(t):void 0}getIdentityByActor(e){let t=this.controlPlane.database.query("SELECT * FROM auth_identities WHERE actor_id = ?").get(e);return t?pa(t):void 0}getIdentityByUsername(e){let t=this.controlPlane.database.query("SELECT * FROM auth_identities WHERE username = ? COLLATE NOCASE").get(e.trim());return t?pa(t):void 0}getIdentityByEmail(e){let t=this.controlPlane.database.query("SELECT * FROM auth_identities WHERE email = ? COLLATE NOCASE").get(e.trim().toLowerCase());return t?pa(t):void 0}listIdentities(){return this.controlPlane.database.query("SELECT * FROM auth_identities ORDER BY username COLLATE NOCASE").all().map(pa)}updatePassword(e,t,r={}){if(!this.getIdentity(e))throw Error("Authentication identity was not found");let i=this.now();return this.controlPlane.transaction(()=>{if(this.run(`UPDATE auth_identities SET password_hash = ?, credential_version = credential_version + 1,
|
|
3
3
|
requires_password_upgrade = ?, updated_at = ? WHERE id = ?`,[t,r.requiresUpgrade?1:0,i,e]),r.revokeSessions!==!1)this.run("UPDATE auth_sessions SET revoked_at = ? WHERE identity_id = ? AND revoked_at IS NULL",[i,e])}),this.getIdentity(e)}rehashPassword(e,t){let r=this.now();this.run("UPDATE auth_identities SET password_hash = ?, requires_password_upgrade = 0, updated_at = ? WHERE id = ?",[t,r,e]);let n=this.getIdentity(e);if(!n)throw Error("Authentication identity was not found");return n}setVerifiedEmail(e,t){let r=Bd(t),n=this.now();this.run("UPDATE auth_identities SET email = ?, email_verified_at = ?, updated_at = ? WHERE id = ?",[r,n,n,e]);let i=this.getIdentity(e);if(!i)throw Error("Authentication identity was not found");return i}recordLogin(e){let t=this.now();this.run("UPDATE auth_identities SET last_login_at = ?, updated_at = ? WHERE id = ?",[t,t,e]);let r=this.getIdentity(e);if(!r)throw Error("Authentication identity was not found");return r}setDisabled(e,t){let r=this.now();this.controlPlane.transaction(()=>{if(this.run("UPDATE auth_identities SET disabled_at = ?, credential_version = credential_version + 1, updated_at = ? WHERE id = ?",[t?r:null,r,e]),t)this.run("UPDATE auth_sessions SET revoked_at = ? WHERE identity_id = ? AND revoked_at IS NULL",[r,e])});let n=this.getIdentity(e);if(!n)throw Error("Authentication identity was not found");return n}createActionToken(e,t,r={}){if(!this.getIdentity(e))throw Error("Authentication identity was not found");let n=Ui(32).toString("base64url"),i=this.idFn(),o=this.now(),s=new Date(this.nowFn().getTime()+Math.min(86400000,Math.max(60000,r.ttlMs??Bp))).toISOString();return this.run("INSERT INTO auth_action_tokens (id, identity_id, type, token_hash, metadata, expires_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",[i,e,t,ui(n),JSON.stringify(r.metadata??{}),s,o]),{actionToken:this.getActionToken(i),token:n}}getActionToken(e){let t=this.controlPlane.database.query("SELECT * FROM auth_action_tokens WHERE id = ?").get(e);return t?Op(t,this.now()):void 0}consumeActionToken(e,t){return this.controlPlane.transaction(()=>{let r=this.controlPlane.database.query("SELECT * FROM auth_action_tokens WHERE token_hash = ? AND type = ?").get(ui(e),t);if(!r)throw Error("Action token is invalid");let n=Op(r,this.now());if(n.state!=="pending")throw Error(`Action token is ${n.state}`);if(this.controlPlane.database.run("UPDATE auth_action_tokens SET consumed_at = ? WHERE id = ? AND consumed_at IS NULL AND expires_at > ?",[this.now(),n.id,this.now()]).changes!==1)throw Error("Action token is no longer available");return this.getActionToken(n.id)})}revokeActionTokens(e,t){return this.controlPlane.database.run("UPDATE auth_action_tokens SET consumed_at = ? WHERE identity_id = ? AND type = ? AND consumed_at IS NULL",[this.now(),e,t]).changes}createSession(e){let t=this.getIdentity(e.identityId);if(!t||t.disabledAt)throw Error("Authentication identity is unavailable");let r=this.idFn(),n=Ui(32).toString("base64url"),i=`v2.${r}.${n}`,o=this.now(),s=Math.min(86400000,Math.max(60000,e.idleTtlMs??Lp)),c=Math.min(2592000000,Math.max(s,e.absoluteTtlMs??bo));return this.run(`INSERT INTO auth_sessions (id, identity_id, token_hash, credential_version, auth_method, user_agent, network_hint,
|
|
4
4
|
created_at, last_used_at, idle_expires_at, absolute_expires_at, recent_auth_at, mfa_at)
|
|
@@ -253,19 +253,19 @@ ${f.join(`
|
|
|
253
253
|
echo "ts-cloud bootstrap complete — instance is ready for site deploys"
|
|
254
254
|
`,m}var Ss="/aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3/ami-id";function ys(e,t){let r=[{port:22,protocol:"tcp",cidr:"0.0.0.0/0"},{port:80,protocol:"tcp",cidr:"0.0.0.0/0"},{port:443,protocol:"tcp",cidr:"0.0.0.0/0"}],n=new Set;for(let s of Object.values(e.sites||{}))if(s&&typeof s.port==="number"&&![22,80,443].includes(s.port))n.add(s.port);for(let s of Fr(ne(e,{environment:t})))n.add(s);for(let s of n)r.push({port:s,protocol:"tcp",cidr:"0.0.0.0/0"});return r}function vs(e,t){let r={...e.infrastructure?.compute,proxy:le(e,{environment:t})},n=Ue(e,{environment:t}),s=r.proxy?.engine==="rpx"?ve({proxy:r.proxy,config:Te(e.sites??{},{proxy:r.proxy,slug:e.project.slug,mail:ne(e,{environment:t})}),slug:e.project.slug,bunBin:r.runtime==="node"||r.runtime==="deno"?void 0:"/usr/local/bin/bun"}):void 0;return de({runtime:n.runtime,runtimeVersion:n.runtimeVersion,systemPackages:r.systemPackages,database:e.infrastructure?.database,phpProvision:n.phpProvision,servicesProvision:n.servicesProvision,rpxProvision:s,baked:r.bakedImage===!0,swapGb:r.swapGb})}function bs(e){return Buffer.from(e,"utf8").toString("base64")}function ws(e){return e.infrastructure?.compute?.image??null}function Nt(e,t){return`${e}-${t}-app-sg`}function Or(e,t,r="app"){return[{Key:"Project",Value:e},{Key:"Environment",Value:t},{Key:"Role",Value:r},{Key:"ManagedBy",Value:"ts-cloud"}]}function Wa(e,t,r){if(!e?.length)return!1;let n=(s)=>e.find((i)=>i.Key===s)?.Value;return n("Project")===t&&n("Environment")===r}function _s(e,t){let r=Nt(t.slug,t.environment);return e.find((n)=>{if(n.GroupName!==r)return!1;if(t.vpcId&&n.VpcId&&n.VpcId!==t.vpcId)return!1;if(!!n.Tags?.some((i)=>i.Key==="Project"))return Wa(n.Tags,t.slug,t.environment);return!!t.vpcId&&n.VpcId===t.vpcId})}function Va(e){try{let t=Ts(Ga(process.cwd(),"storage/cloud/state",`${e}.json`),"utf8"),r=JSON.parse(t);return typeof r.instanceId==="string"&&r.instanceId.length>0?r.instanceId:null}catch{return null}}class kt{name="aws";usesCloudFormation=!0;region;ec2Client;cfnClient;constructor(e={}){this.region=e.region||"us-east-1",this.ec2Client=e.ec2Client??((t)=>new Q(t)),this.cfnClient=e.cfnClient??((t)=>new v(t))}resolveRegion(e){return e.project.region||this.region}async provisionComputeInfrastructure(e){let{config:t,environment:r}=e,n=this.resolveRegion(t),s=t.project.slug,i=t.infrastructure?.compute;if(!i)throw Error("infrastructure.compute is required to provision AWS compute");let o=this.ec2Client(n),a=await this.findComputeTargets({slug:s,environment:r,role:"app",region:n});if(a.length>0){let N=a[0];return{appInstanceId:N.id,appPublicIp:N.publicIp,appPublicIpv6:N.publicIpv6,sshUser:"ubuntu",deployStoragePath:"/var/ts-cloud/staging"}}let c=ws(t);if(!c){if(c=(await new We(n).getParameter({Name:Ss})).Parameter?.Value??null,!c)throw Error("Could not resolve the Ubuntu 24.04 AMI from SSM")}let l=await o.describeVpcs(),u=(l.Vpcs||[]).find((N)=>N.IsDefault)||(l.Vpcs||[])[0];if(!u?.VpcId)throw Error("No VPC found to launch the instance into");let d=(await o.describeSubnets({Filters:[{Name:"vpc-id",Values:[u.VpcId]}]})).Subnets||[],m=d.find((N)=>N.MapPublicIpOnLaunch)||d[0],h=m?.SubnetId;if(!m?.MapPublicIpOnLaunch)console.warn("ts-cloud: no public subnet found; the instance may not get a public IP (deploys/SSL need one).");let f=Nt(s,r),k=(await o.describeSecurityGroups({Filters:[{Name:"group-name",Values:[f]},{Name:"vpc-id",Values:[u.VpcId]}]})).SecurityGroups?.[0]?.GroupId;if(!k)k=(await o.createSecurityGroup({GroupName:f,Description:`ts-cloud ${s}/${r} app`,VpcId:u.VpcId,TagSpecifications:[{ResourceType:"security-group",Tags:Or(s,r)}]})).GroupId;let E=ys(t,r);await o.authorizeSecurityGroupIngress({GroupId:k,IpPermissions:E.map((N)=>({IpProtocol:N.protocol,FromPort:N.port,ToPort:N.port,IpRanges:[{CidrIp:N.cidr}]}))}).catch((N)=>{if(!/InvalidPermission\.Duplicate/.test(N instanceof Error?N.message:""))throw N});let O=bs(vs(t,r)),D=i.server?.instanceType||"t3.small",z=(await o.runInstances({ImageId:c,InstanceType:D,MinCount:1,MaxCount:1,SecurityGroupIds:k?[k]:void 0,SubnetId:h,UserData:O,IamInstanceProfile:i.server?.iamInstanceProfile?{Name:i.server.iamInstanceProfile}:void 0,TagSpecifications:[{ResourceType:"instance",Tags:[{Key:"Name",Value:`${s}-${r}-app`},...Or(s,r)]}]})).Instances?.[0]?.InstanceId;if(!z)throw Error("RunInstances did not return an instance id");let x=await o.waitForInstanceState(z,"running");if(!x)throw Error(`Instance ${z} did not reach 'running' before timeout`);return{appInstanceId:z,appPublicIp:x.PublicIpAddress,appPublicIpv6:me(x.Ipv6Address),sshUser:"ubuntu",deployStoragePath:"/var/ts-cloud/staging"}}async destroyCompute(e){let{config:t,environment:r}=e,n=t.project.slug,s=this.resolveRegion(t),i=this.ec2Client(s),o=[],c=(await this.findComputeTargets({slug:n,environment:r,role:"app",region:s})).map((f)=>f.id),l;if(c.length>0)l=(await i.describeInstances({InstanceIds:c}).catch(()=>({Reservations:[]}))).Reservations?.flatMap((S)=>S.Instances??[]).find((S)=>S.VpcId)?.VpcId;if(c.length>0)await i.terminateInstances(c),o.push(...c.map((f)=>`instance ${f}`)),await Promise.all(c.map((f)=>i.waitForInstanceState(f,"terminated").catch(()=>{return})));let u=Nt(n,r),d=[{Name:"group-name",Values:[u]}];if(l)d.push({Name:"vpc-id",Values:[l]});let m=await i.describeSecurityGroups({Filters:d}).catch(()=>({SecurityGroups:[]})),h=_s(m.SecurityGroups??[],{slug:n,environment:r,vpcId:l});if(h?.GroupId)for(let f=0;f<10;f++)try{await i.deleteSecurityGroup(h.GroupId),o.push(`security group ${u}`);break}catch{await new Promise((S)=>setTimeout(S,3000))}return{destroyed:o}}async getComputeOutputs(e){let t=this.resolveRegion(e.config),r=T(e.config,e.environment),n=this.cfnClient(t);try{let s=await n.getStackOutputs(r);return{deployBucketName:s.deployBucketName,appInstanceId:s.appInstanceId,appPublicIp:s.appPublicIp,sshUser:"ec2-user"}}catch(s){if(!/does not exist|ValidationError/i.test(s instanceof Error?s.message:""))throw s;let o=(await this.findComputeTargets({slug:e.config.project.slug,environment:e.environment,role:"app",region:t}))[0];return{appInstanceId:o?.id,appPublicIp:o?.publicIp,appPublicIpv6:o?.publicIpv6,sshUser:"ubuntu",deployStoragePath:"/var/ts-cloud/staging"}}}async uploadRelease(e){let t=this.resolveRegion(e.config),n=(await this.getComputeOutputs({config:e.config,environment:e.environment})).deployBucketName;if(!n)throw Error("No deployBucketName in stack outputs. Re-deploy infrastructure to add the staging bucket.");return await new g(t).putObject({bucket:n,key:e.remoteKey,body:Ts(e.localPath),contentType:"application/gzip"}),{artifactRef:`s3://${n}/${e.remoteKey}`}}async findComputeTargets(e){let t=e.region??this.region,r=this.ec2Client(t),n=[{Name:"tag:Project",Values:[e.slug]},{Name:"tag:Environment",Values:[e.environment]},{Name:"tag:Role",Values:[e.role||"app"]},{Name:"instance-state-name",Values:["running","pending"]}],s=await r.describeInstances({Filters:n}),i=this.reservationsToTargets(s.Reservations);if(i.length>0||(e.role||"app")!=="app")return i;let o=e.stackName??`${e.slug}-${e.environment}`,a=Va(o);if(!a)try{a=(await this.cfnClient(t).getStackOutputs(o)).appInstanceId??null}catch{a=null}if(!a)return[];try{let c=await r.describeInstances({InstanceIds:[a],Filters:[{Name:"instance-state-name",Values:["running","pending"]}]});return this.reservationsToTargets(c.Reservations)}catch{return[]}}reservationsToTargets(e){let t=[];for(let r of e||[])for(let n of r.Instances||[]){if(!n.InstanceId)continue;let s=n.Tags?.find((i)=>i.Key==="Name")?.Value;t.push({id:n.InstanceId,name:s,publicIp:n.PublicIpAddress,publicIpv6:me(n.Ipv6Address),privateIp:n.PrivateIpAddress,status:n.State?.Name})}return t}bashWrap(e){return["bash <<'TS_CLOUD_BASH_EOF'",e.join(`
|
|
255
255
|
`),"TS_CLOUD_BASH_EOF"]}async runRemoteDeploy(e){let t=this.region,r=new We(t);if(e.targets.length>0){let s=await r.sendCommand({InstanceIds:e.targets.map((i)=>i.id),DocumentName:"AWS-RunShellScript",Parameters:{commands:this.bashWrap(e.commands)},TimeoutSeconds:e.timeoutSeconds||600,Comment:e.comment});if(!s.CommandId)return{success:!1,instanceCount:0,perInstance:[],error:"Failed to send SSM command"};return this.pollSsmCommand(r,s.CommandId,e.targets.length,((e.timeoutSeconds||600)+30)*1000)}if(!e.tags||Object.keys(e.tags).length===0)return{success:!1,instanceCount:0,perInstance:[],error:"No targets or tags provided for AWS deploy"};let n=await r.sendCommandByTags({tags:e.tags,commands:this.bashWrap(e.commands),timeoutSeconds:e.timeoutSeconds||600,comment:e.comment});return{success:n.success,instanceCount:n.instanceCount,perInstance:n.perInstance.map((s)=>({instanceId:s.instanceId,status:s.status,output:s.output,error:s.error})),error:n.error}}async pollSsmCommand(e,t,r,n=600000){let i=Date.now(),o=new Set(["Success","Failed","Cancelled","TimedOut"]),a=[];while(Date.now()-i<n){await new Promise((u)=>setTimeout(u,3000));try{if(a=await e.listCommandInvocations({CommandId:t,Details:!0}),a.length>=r&&a.every((d)=>o.has(d.Status||"")))break}catch{}}let c=a.map((u)=>({instanceId:u.InstanceId,status:u.Status||"Unknown",output:u.StandardOutputContent,error:u.StandardErrorContent})),l=c.length>0&&c.every((u)=>u.status==="Success");return{success:l,instanceCount:c.length,perInstance:c,error:l?void 0:ke(c,"One or more SSM command invocations failed")}}}import{execFileSync as Rc,execSync as Ws}from"node:child_process";import{createHash as Ec,randomUUID as Vs}from"node:crypto";import{createReadStream as Oc,existsSync as Dc,readFileSync as Ic}from"node:fs";async function Cs(e,t){let r=await import("node:fs"),n=await import("node:path"),s=await import("node:child_process"),i="/etc/ts-cloud/compute-storage.json",o="/var/lib/ts-cloud/compute-storage-state.json",a="/run/ts-cloud-root-web",c=t[0]??"status";if(!["plan","status","check","apply","ensure-site","reclaim"].includes(c))throw Error(`Unknown compute storage command: ${c}`);let l=(y,P)=>{try{return JSON.parse(r.readFileSync(y,"utf8"))}catch(H){if(H.code==="ENOENT")return P;throw H}},u=(y,P)=>{r.mkdirSync(n.dirname(y),{recursive:!0});let H=`${y}.${process.pid}.tmp`;r.writeFileSync(H,`${JSON.stringify(P,null,2)}
|
|
256
|
-
`,{mode:420}),r.renameSync(H,y)},d=(y,P)=>s.execFileSync(y,P,{encoding:"utf8",maxBuffer:16777216}),m=(y,P)=>{let H=s.spawnSync(y,P,{encoding:"utf8"});return H.status===0?H.stdout.trim():""},h=(y)=>!!m("findmnt",["-n","--mountpoint",y]),f=(y,P)=>{try{let H=r.statSync(y),C=r.statSync(P);return H.dev===C.dev&&H.ino===C.ino}catch{return!1}},S=l("/var/lib/ts-cloud/compute-storage-state.json",{entries:[]}),k=e.deploymentStorage,E=e.volumes.find((y)=>y.key===k?.volume),O=(y,P)=>{if(!E)return[];let H=[];for(let C of r.readdirSync(e.webRoot,{withFileTypes:!0})){if(!C.isDirectory()||C.isSymbolicLink())continue;let
|
|
257
|
-
${
|
|
256
|
+
`,{mode:420}),r.renameSync(H,y)},d=(y,P)=>s.execFileSync(y,P,{encoding:"utf8",maxBuffer:16777216}),m=(y,P)=>{let H=s.spawnSync(y,P,{encoding:"utf8"});return H.status===0?H.stdout.trim():""},h=(y)=>!!m("findmnt",["-n","--mountpoint",y]),f=(y,P)=>{try{let H=r.statSync(y),C=r.statSync(P);return H.dev===C.dev&&H.ino===C.ino}catch{return!1}},S=l("/var/lib/ts-cloud/compute-storage-state.json",{entries:[]}),k=e.deploymentStorage,E=e.volumes.find((y)=>y.key===k?.volume),O=(y,P)=>{if(!E)return[];let H=[];for(let C of r.readdirSync(e.webRoot,{withFileTypes:!0})){if(!C.isDirectory()||C.isSymbolicLink())continue;let F=n.join(e.webRoot,C.name);if(y&&F!==y&&F!==n.join(e.webRoot,`${P}-shared`))continue;for(let L of["releases",...k?.includeBackups===!1?[]:["backups"]]){let K=n.join(F,L);if(!r.existsSync(K)||!r.lstatSync(K).isDirectory())continue;H.push({source:K,destination:n.join(E.mountPath,L,C.name)})}}return H},D=(y)=>{try{return h(y.mountPath)&&r.statSync(y.mountPath).dev===r.statSync(y.device).rdev}catch{return!1}},R=(y)=>{let P=r.statfsSync(y),H=(P.blocks-P.bfree)*P.bsize,C=P.bavail*P.bsize;return{path:y,sizeBytes:P.blocks*P.bsize,usedBytes:H,availableBytes:C,usedPercent:Math.ceil(100*H/(H+C))}},z=()=>{let y=[],P=[R("/")];for(let C of e.volumes)if(!D(C))y.push(`missing or incorrect volume mount: ${C.mountPath}`);else P.push(R(C.mountPath));for(let C of S.entries)if(!h(C.source)||!f(C.source,C.destination))y.push(`missing deployment mount: ${C.source}`);let H=new Set(S.entries.map((C)=>C.source));for(let C of O())if(!H.has(C.source))y.push(`deployment needs volume placement: ${C.source}`);return{timestamp:new Date().toISOString(),volumes:e.volumes.map((C)=>({key:C.key,name:C.name,id:C.id,size:C.size,mountPath:C.mountPath})),disks:P,mounts:S.entries.length,problems:y}};if(c==="plan")return console.log(JSON.stringify({volumes:e.volumes,entries:O(),state:S},null,2));if(c==="status"||c==="check"){let y=z();if(c==="check"){u("/var/lib/ts-cloud/compute-storage-status.json",y);let P="/var/lib/ts-cloud/compute-storage-alert.json",H=l(P,{problems:[]});if(JSON.stringify(H.problems)!==JSON.stringify(y.problems)){let C=y.problems.join("; ")||"all configured mounts are healthy",F=`${y.problems.length?"Storage alert":"Storage recovered"} on ${m("hostname",[])}: ${C}`;d("/usr/local/bin/ts-cloud-notify",[F.length>1800?`${F.slice(0,1600)}... Full report: /var/lib/ts-cloud/compute-storage-status.json`:F])}u(P,{problems:y.problems,timestamp:y.timestamp})}if(console.log(JSON.stringify(y)),y.problems.length)process.exitCode=1;return y}if(process.getuid?.()!==0)throw Error("Compute storage changes require root");let x=()=>{let C=r.readFileSync("/etc/fstab","utf8");if(!r.existsSync("/etc/fstab.before-ts-cloud-storage"))r.copyFileSync("/etc/fstab","/etc/fstab.before-ts-cloud-storage");let F=[];for(let j of e.volumes){let W=d("blkid",["-s","UUID","-o","value",j.device]).trim();if(!W)throw Error(`Volume ${j.name} has no filesystem UUID`);F.push(`UUID=${W} ${j.mountPath} ext4 defaults,noatime,nodev,nosuid,nofail,x-systemd.device-timeout=30s 0 2`)}for(let j of S.entries)F.push(`${j.destination} ${j.source} none bind,nofail,x-systemd.requires-mounts-for=${E.mountPath} 0 0`);let L=`# BEGIN ts-cloud compute storage
|
|
257
|
+
${F.join(`
|
|
258
258
|
`)}
|
|
259
|
-
# END ts-cloud compute storage`,
|
|
259
|
+
# END ts-cloud compute storage`,K;if(C.includes("# BEGIN ts-cloud compute storage")){let j=C.indexOf("# BEGIN ts-cloud compute storage"),W=C.indexOf("# END ts-cloud compute storage",j);if(W<0)throw Error("Incomplete managed storage block in fstab");K=C.slice(0,j)+L+C.slice(W+30)}else K=`${C.trimEnd()}
|
|
260
260
|
${L}
|
|
261
|
-
`;r.writeFileSync("/etc/fstab.ts-cloud-new",
|
|
262
|
-
`).map((
|
|
261
|
+
`;r.writeFileSync("/etc/fstab.ts-cloud-new",K),r.renameSync("/etc/fstab.ts-cloud-new","/etc/fstab")},N=()=>{for(let y of e.volumes){if(!r.existsSync(y.device)||!r.statSync(y.device).isBlockDevice())throw Error(`Volume device missing: ${y.device}`);let P=m("blkid",["-s","TYPE","-o","value",y.device]);if(!P){if(!y.allowFormat)throw Error(`Refusing to format adopted volume ${y.name}; no recognized filesystem`);d("mkfs.ext4",["-m","1",y.device]),P="ext4"}if(P!=="ext4")throw Error(`Refusing to replace ${P} filesystem on ${y.name}`);if(r.mkdirSync(y.mountPath,{recursive:!0}),h(y.mountPath)&&!D(y))throw Error(`Another filesystem occupies ${y.mountPath}`);if(!h(y.mountPath)&&r.readdirSync(y.mountPath).length)throw Error(`Refusing to hide existing files beneath ${y.mountPath}`);if(!h(y.mountPath))d("mount",["-o","noatime,nodev,nosuid",y.device,y.mountPath]);if(!D(y))throw Error(`Volume identity verification failed: ${y.name}`);d("resize2fs",[y.device]),y.allowFormat=!1}u("/etc/ts-cloud/compute-storage.json",e),x(),d("systemctl",["daemon-reload"])},{dlopen:V}=await import("bun:ffi"),se=V("libc.so.6",{flock:{args:["i32","i32"],returns:"i32"}}),ce=[],G=(y)=>{r.mkdirSync(n.dirname(y),{recursive:!0});let P=r.openSync(y,r.constants.O_CREAT|r.constants.O_RDWR|r.constants.O_NOFOLLOW,384);if(se.symbols.flock(P,2)!==0)throw r.closeSync(P),Error(`Could not acquire lock: ${y}`);ce.push(P)},ie=()=>{let y=[];for(let P of r.readdirSync("/proc").filter((H)=>/^\d+$/.test(H))){let H=[];try{H=r.readdirSync(`/proc/${P}/fd`)}catch{continue}for(let C of["cwd","exe",...H.map((F)=>`fd/${F}`)]){let F=`/proc/${P}/${C}`;try{let L=r.readlinkSync(F);if(!L.startsWith(`${e.webRoot}/`)||!L.includes("/releases/"))continue;let K=!1;if(C.startsWith("fd/")){let j=r.readFileSync(`/proc/${P}/fdinfo/${C.slice(3)}`,"utf8").match(/^flags:\s+(\d+)/m);K=!!j&&(Number.parseInt(j[1],8)&3)!==0&&r.statSync(F).isFile()}y.push({file:L,write:K,device:r.statSync(F).dev})}catch{}}}return y},Y=async(y,P)=>{r.mkdirSync(y.destination,{recursive:!0});let H=["rsync","-aHAXx","--numeric-ids","--delete",...P?["--checksum"]:[],`${y.source}/`,`${y.destination}/`],F=await Bun.spawn(H,{stdout:"inherit",stderr:"inherit"}).exited;if(F!==0&&!(F===24&&!P))throw Error(`rsync failed (${F}): ${y.source}`)},q=async()=>{let y=d("systemctl",["list-units","--type=service","--state=running","--no-legend","--plain"]).split(`
|
|
262
|
+
`).map((K)=>K.trim().split(/\s+/)[0]).filter(Boolean),P={};if(y.length){let K=d("systemctl",["show",...y,"-p","Id","-p","MainPID","-p","WorkingDirectory"]);for(let j of K.split(`
|
|
263
263
|
|
|
264
|
-
`)){let W=Object.fromEntries(
|
|
265
|
-
`).filter(Boolean).map((b)=>{let M=b.indexOf("=");return[b.slice(0,M),b.slice(M+1)]}));if(W.WorkingDirectory?.startsWith(`${e.webRoot}/`)||/^(?:mail|rpx-gateway|postgresql|mysql|redis)[@.]/.test(W.Id??""))P[W.Id]=Number(W.MainPID)}}let H=new Map;if(r.existsSync("/etc/rpx/sites.d"))for(let
|
|
264
|
+
`)){let W=Object.fromEntries(j.split(`
|
|
265
|
+
`).filter(Boolean).map((b)=>{let M=b.indexOf("=");return[b.slice(0,M),b.slice(M+1)]}));if(W.WorkingDirectory?.startsWith(`${e.webRoot}/`)||/^(?:mail|rpx-gateway|postgresql|mysql|redis)[@.]/.test(W.Id??""))P[W.Id]=Number(W.MainPID)}}let H=new Map;if(r.existsSync("/etc/rpx/sites.d"))for(let K of r.readdirSync("/etc/rpx/sites.d").filter((j)=>j.endsWith(".json"))){let j=JSON.parse(r.readFileSync(n.join("/etc/rpx/sites.d",K),"utf8"));for(let W of j.proxies??[]){if(typeof W.to!=="string"||!/^[a-zA-Z0-9.-]+$/.test(W.to))continue;let b=typeof W.path==="string"&&W.path.startsWith("/")?W.path:"/";H.set(`${W.to}${b}`,{domain:W.to,path:b})}}let C=[...H.values()],F=[],L=0;return await Promise.all(Array.from({length:Math.min(8,C.length)},async()=>{while(L<C.length){let K=C[L++],j=Bun.spawn(["curl","-sS","--max-time","12","--output","/dev/null","--write-out","%{http_code}","--resolve",`${K.domain}:443:127.0.0.1`,`https://${K.domain}${K.path}`],{stdout:"pipe",stderr:"pipe"}),[W,,b]=await Promise.all([new Response(j.stdout).text(),new Response(j.stderr).text(),j.exited]),M=Number(W);F.push({...K,status:M,ok:b===0&&M>=200&&M<500})}})),{routes:F.sort((K,j)=>`${K.domain}${K.path}`.localeCompare(`${j.domain}${j.path}`)),services:P}},ue=()=>{let y=[E.mountPath,...S.entries.map((H)=>H.source)],P=new Set(["rpx-gateway.service","host-app-backup.service","host-app-backup-retry.service","host-backup-check.service","mail-host-maintenance.service"]);for(let H of r.readdirSync("/etc/systemd/system")){if(!H.endsWith(".service"))continue;try{if(r.readFileSync(`/etc/systemd/system/${H}`,"utf8").includes(e.webRoot))P.add(H)}catch{}}for(let H of P){let C=`/etc/systemd/system/${H}.d`;r.mkdirSync(C,{recursive:!0}),r.writeFileSync(`${C}/ts-cloud-storage.conf`,`[Unit]
|
|
266
266
|
RequiresMountsFor=${y.join(" ")}
|
|
267
267
|
`)}d("systemctl",["daemon-reload"])};try{G("/run/lock/ts-cloud-compute-storage.lock"),S=l("/var/lib/ts-cloud/compute-storage-state.json",{entries:[]});let y=l("/etc/ts-cloud/compute-storage.json",null);if(y){for(let b of y.volumes)if(!e.volumes.some((M)=>M.id===b.id&&M.key===b.key&&M.mountPath===b.mountPath))throw Error(`Refusing to remove or relocate managed volume ${b.name}; migrate its data explicitly first`);if(JSON.stringify(y.deploymentStorage)!==JSON.stringify(e.deploymentStorage)&&S.entries.length)throw Error("Refusing to change an active deployment-storage policy; migrate its data explicitly first")}if(c==="apply")N();for(let b of e.volumes)if(!D(b))throw Error(`Required volume is not mounted: ${b.name}`);if(!k||!E)return{volumes:e.volumes};if(c==="ensure-site"){let[b,M]=t.slice(1);if(!b?.startsWith(`${e.webRoot}/`)||!/^[\w-]+$/.test(n.basename(b))||b.includes("..")||!/^[\w-]+$/.test(M??""))throw Error("Invalid deployment site path or project slug");if(r.mkdirSync(n.join(b,"releases"),{recursive:!0}),k.includeBackups!==!1)r.mkdirSync(n.join(e.webRoot,`${M}-shared`,"backups"),{recursive:!0})}let P=(c==="reclaim"?[]:O(c==="ensure-site"?t[1]:void 0,t[2])).filter((b)=>!S.entries.some((M)=>M.source===b.source));for(let b of S.entries)if(!h(b.source)||!f(b.source,b.destination))throw Error(`Recorded deployment mount is missing: ${b.source}`);if(!P.length&&c==="ensure-site")return{inherited:!0,volume:E.name};if(!k.migrateExisting&&P.some((b)=>r.readdirSync(b.source).length>0))throw Error("Existing deployments require deploymentStorage.migrateExisting: true");if(P.length){let b=d("du",["-scx","--block-size=1",...P.map((ee)=>ee.source)]).trim().split(`
|
|
268
|
-
`).at(-1)?.split(/\s+/)[0],M=r.statfsSync(E.mountPath);if(Number(b)>M.bavail*M.bsize)throw Error("Insufficient free volume capacity for a verified deployment copy")}if(!r.existsSync("/run/ts-cloud-root-web"))r.mkdirSync("/run/ts-cloud-root-web");if(!h("/run/ts-cloud-root-web"))d("mount",["--bind",e.webRoot,"/run/ts-cloud-root-web"]);for(let b=0;b<P.length;b++){if(h(P[b].source)&&f(P[b].source,P[b].destination))continue;console.log(`copy ${b+1}/${P.length}: ${P[b].source}`),await Y(P[b],!1)}for(let b of["mail-host-maintenance","host-backup","ts-cloud-host-cleanup"])G(`/run/lock/${b}.lock`);for(let b of r.readdirSync(e.webRoot,{withFileTypes:!0}).filter((M)=>M.isDirectory()).sort((M,ee)=>M.name.localeCompare(ee.name)))G(n.join(e.webRoot,b.name,".ts-cloud/deploy.lock"));for(let b of c==="reclaim"?[]:O(c==="ensure-site"?t[1]:void 0,t[2]))if(!S.entries.some((M)=>M.source===b.source)&&!P.some((M)=>M.source===b.source))P.push(b);if(P.length)console.log("Checking application services and HTTPS routes before activation");let H=P.length?await q():void 0;if(H)u("/var/lib/ts-cloud/compute-storage-health-before.json",H);let C=ie().filter((b)=>b.write);if(C.length)throw Error(`Live writable files must be shared before migration: ${C.map((b)=>b.file).join(", ")}`);let
|
|
268
|
+
`).at(-1)?.split(/\s+/)[0],M=r.statfsSync(E.mountPath);if(Number(b)>M.bavail*M.bsize)throw Error("Insufficient free volume capacity for a verified deployment copy")}if(!r.existsSync("/run/ts-cloud-root-web"))r.mkdirSync("/run/ts-cloud-root-web");if(!h("/run/ts-cloud-root-web"))d("mount",["--bind",e.webRoot,"/run/ts-cloud-root-web"]);for(let b=0;b<P.length;b++){if(h(P[b].source)&&f(P[b].source,P[b].destination))continue;console.log(`copy ${b+1}/${P.length}: ${P[b].source}`),await Y(P[b],!1)}for(let b of["mail-host-maintenance","host-backup","ts-cloud-host-cleanup"])G(`/run/lock/${b}.lock`);for(let b of r.readdirSync(e.webRoot,{withFileTypes:!0}).filter((M)=>M.isDirectory()).sort((M,ee)=>M.name.localeCompare(ee.name)))G(n.join(e.webRoot,b.name,".ts-cloud/deploy.lock"));for(let b of c==="reclaim"?[]:O(c==="ensure-site"?t[1]:void 0,t[2]))if(!S.entries.some((M)=>M.source===b.source)&&!P.some((M)=>M.source===b.source))P.push(b);if(P.length)console.log("Checking application services and HTTPS routes before activation");let H=P.length?await q():void 0;if(H)u("/var/lib/ts-cloud/compute-storage-health-before.json",H);let C=ie().filter((b)=>b.write);if(C.length)throw Error(`Live writable files must be shared before migration: ${C.map((b)=>b.file).join(", ")}`);let F={entries:[...S.entries],fstab:r.readFileSync("/etc/fstab","utf8")};if(P.length)d("sync",[]);let L=[];try{for(let b=0;b<P.length;b++){let M=P[b];if(h(M.source)){if(!f(M.source,M.destination))throw Error(`Unrecorded mount occupies ${M.source}`);S.entries.push(M),u("/var/lib/ts-cloud/compute-storage-state.json",S);continue}if(console.log(`verify and mount ${b+1}/${P.length}: ${M.source}`),await Y(M,!0),d("mount",["--bind",M.destination,M.source]),!f(M.source,M.destination))throw Error(`Bind verification failed: ${M.source}`);L.push(M),S.entries.push(M),u("/var/lib/ts-cloud/compute-storage-state.json",S),x()}if(x(),d("findmnt",["--verify"]),S.activatedAt=new Date().toISOString(),u("/var/lib/ts-cloud/compute-storage-state.json",S),ue(),H){console.log("Checking application services and HTTPS routes after activation");let b=await q();u("/var/lib/ts-cloud/compute-storage-health-after.json",b);let M=H.routes.filter((X)=>X.ok&&!b.routes.some((Ce)=>Ce.domain===X.domain&&Ce.path===X.path&&Ce.ok)),ee=Object.keys(H.services).filter((X)=>!b.services[X]);if(M.length||ee.length)throw Error(`Application verification failed: ${M.map((X)=>X.domain+X.path).concat(ee).join(", ")}`);console.log(`Verified ${b.routes.length} routes and ${Object.keys(b.services).length} services`)}}catch(b){let M=[];for(let ee of L.reverse())try{d("umount",[ee.source])}catch{M.push(ee),console.error(`Rollback mount busy, original data retained: ${ee.source}`)}if(S.entries=[...F.entries,...M],r.writeFileSync("/etc/fstab",F.fstab),M.length)x();throw u("/var/lib/ts-cloud/compute-storage-state.json",S),d("systemctl",["daemon-reload"]),b}let K=r.statSync(e.webRoot).dev,j=ie().filter((b)=>b.device===K).map((b)=>b.file);for(let b of S.entries){if(!h(b.source)||!f(b.source,b.destination))throw Error(`Lost deployment mount: ${b.source}`);let M=n.join("/run/ts-cloud-root-web",n.relative(e.webRoot,b.source));if(!r.existsSync(M))continue;let ee="";try{ee=n.basename(r.realpathSync(n.join(n.dirname(b.source),"current")))}catch{}for(let X of r.readdirSync(M,{withFileTypes:!0})){let Ce=n.join(M,X.name);if(n.basename(b.source)==="backups"){if(X.isFile()&&r.existsSync(n.join(b.destination,X.name)))r.unlinkSync(Ce)}else if(X.isDirectory()&&!X.isSymbolicLink()&&X.name!==ee&&!j.some((Ot)=>Ot===n.join(b.source,X.name)||Ot.startsWith(`${n.join(b.source,X.name)}/`)))r.rmSync(Ce,{recursive:!0})}}d("sync",[]);let W=z();return u("/var/lib/ts-cloud/compute-storage-status.json",W),console.log(JSON.stringify(W)),W}finally{for(let y of ce.reverse())se.symbols.flock(y,8),r.closeSync(y);se.close()}}var Dr="/etc/ts-cloud/compute-storage.json",Re="/usr/local/lib/ts-cloud/compute-storage.mjs";function Ir(e,t){let r=Object.entries(e.infrastructure?.compute?.volumes??{}).map(([s,i])=>{if(!/^[a-z][a-z0-9-]*$/.test(s))throw Error(`Invalid compute volume key: ${s}`);if(!Number.isInteger(i.size)||i.size<10||i.size>10240)throw Error(`compute.volumes.${s}.size must be a whole number from 10 to 10240 GB`);if(!i.mountPath.startsWith("/")||!i.mountPath.slice(1).split("/").every((a)=>/^[\w-]+$/.test(a))||i.mountPath==="/var"||i.mountPath==="/var/lib"||["/etc","/usr","/var/www","/root","/boot","/dev","/proc","/sys","/run","/bin","/sbin","/lib"].some((a)=>i.mountPath===a||i.mountPath.startsWith(`${a}/`)))throw Error(`Unsafe compute volume mount path: ${i.mountPath}`);let o=i.name??`${e.project.slug}-${t}-${s}`;if(!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(o))throw Error(`Invalid compute volume name: ${o}`);return{...i,key:s,name:o,mountPath:i.mountPath.replace(/\/+$/,"")}});for(let s of r)if(r.some((i)=>i!==s&&(i.name===s.name||i.mountPath===s.mountPath||i.mountPath.startsWith(`${s.mountPath}/`))))throw Error("Compute volumes must have distinct names and non-overlapping mount paths");let n=e.infrastructure?.compute?.deploymentStorage;if(n&&!r.some((s)=>s.key===n.volume))throw Error(`deploymentStorage.volume references an undeclared compute volume: ${n.volume}`);return r}function Ut(e){return`'${e.replaceAll("'",`'"'"'`)}'`}function Ar(e){let t=`${Dr}.candidate`,r=`#!/usr/bin/env bun
|
|
269
269
|
await (${Cs.toString()})(JSON.parse(await Bun.file(process.env.TS_CLOUD_STORAGE_POLICY || ${JSON.stringify(Dr)}).text()), process.argv.slice(2));
|
|
270
270
|
`;return['command -v bun >/dev/null 2>&1 || { echo "Configured compute storage requires Bun on the host" >&2; exit 1; }',"install -d -m 0755 /etc/ts-cloud /usr/local/lib/ts-cloud /var/lib/ts-cloud",`printf %s ${Ut(Buffer.from(r).toString("base64"))} | base64 -d > ${Re}.new`,`chmod 0755 ${Re}.new && mv ${Re}.new ${Re}`,`printf %s ${Ut(JSON.stringify(e))} > ${t}`,`chmod 0644 ${t}`,`TS_CLOUD_STORAGE_POLICY=${t} bun ${Re} apply 2>&1 | tee /var/log/ts-cloud-compute-storage.log`,`rm -f ${t}`,"cat > /etc/systemd/system/ts-cloud-storage-check.service <<'TS_CLOUD_STORAGE_SERVICE'","[Unit]","Description=Check configured compute storage and deployment mounts","[Service]","Type=oneshot",`ExecStart=/usr/local/bin/bun ${Re} check`,"TS_CLOUD_STORAGE_SERVICE","cat > /etc/systemd/system/ts-cloud-storage-check.timer <<'TS_CLOUD_STORAGE_TIMER'","[Unit]","Description=Check compute storage every minute","[Timer]","OnBootSec=2min","OnUnitActiveSec=1min","[Install]","WantedBy=timers.target","TS_CLOUD_STORAGE_TIMER","systemctl daemon-reload","systemctl enable --now ts-cloud-storage-check.timer","cat > /etc/systemd/system/ts-cloud-storage-reclaim.service <<'TS_CLOUD_RECLAIM_SERVICE'","[Unit]","Description=Reclaim inactive root copies of volume-backed deployments","[Service]","Type=oneshot",`ExecStart=/usr/local/bin/bun ${Re} reclaim`,"Nice=19","IOSchedulingClass=idle","TS_CLOUD_RECLAIM_SERVICE","cat > /etc/systemd/system/ts-cloud-storage-reclaim.timer <<'TS_CLOUD_RECLAIM_TIMER'","[Unit]","Description=Reclaim retired deployment copies hourly","[Timer]","OnCalendar=hourly","RandomizedDelaySec=5min","Persistent=true","[Install]","WantedBy=timers.target","TS_CLOUD_RECLAIM_TIMER","systemctl daemon-reload","systemctl enable --now ts-cloud-storage-reclaim.timer"]}function Mt(e,t){return[`if [ -f ${Dr} ]; then bun ${Re} ensure-site ${Ut(e)} ${Ut(t)}; fi`]}function zt(e){return Object.entries(e).map(([t,r])=>`${t}=${qa(String(r))}`).join(`
|
|
271
271
|
`)}function qa(e){return`"${e.replace(/\\/g,"\\\\").replace(/"/g,"\\\"").replace(/\n/g,"\\n").replace(/\r/g,"\\r").replace(/\t/g,"\\t")}"`}var Xa=new Set(["sqlite","sqlite3"]),Ya=new Set([":memory:","memory"]);function Ja(e){let t=e?.DB_CONNECTION?.trim().toLowerCase();return t!=null&&Xa.has(t)}function Qa(e){let t=e.trim().replace(/^\.\//,"");if(t===""||t.startsWith("/")||t.startsWith("~"))return;if(t.split("/").includes(".."))return;return t.replace(/\/+$/,"")}function Lr(e,t=[]){if(!Ja(e))return{};let r=e?.DB_DATABASE_PATH?.trim()||e?.DB_DATABASE?.trim();if(r&&Ya.has(r.toLowerCase()))return{};if(!r)return{warning:"DB_CONNECTION is sqlite but neither DB_DATABASE_PATH nor DB_DATABASE names a file, so ts-cloud cannot tell where the database lives. If it is written inside the release directory, the next deploy discards it. Set DB_DATABASE_PATH, or list the file in the site's `sharedPaths`."};let n=Qa(r);if(!n)return{};if(t.some((s)=>Xs(s)===n))return{};return{path:n}}function Ft(e,t=[]){let{path:r}=Lr(e,t);return r?[r]:[]}var Ze="/var/ts-cloud/artifacts";var Is=Object.freeze({stagingMaxAgeMinutes:60,artifactMaxAgeDays:2,artifactUploadMaxAgeMinutes:60,bunCacheMaxAgeDays:7,journalMaxAgeDays:14,journalMaxSizeMb:512,containerImageMaxAgeHours:168}),As=Object.freeze({artifactMaxAgeDays:0,bunCacheMaxAgeDays:1,journalMaxAgeDays:3,journalMaxSizeMb:256,containerImageMaxAgeHours:24});function Ps(e){return`'${e.replace(/'/g,`'"'"'`)}'`}function Me(e,t,r=0){if(!Number.isInteger(t)||t<r)throw Error(`hostCleanup: ${e} must be a whole number >= ${r} (got ${t})`)}function xs(e,t){for(let[r,n]of Object.entries(t))Me(`${e}.${r}`,n);Me(`${e}.stagingMaxAgeMinutes`,t.stagingMaxAgeMinutes,15),Me(`${e}.artifactUploadMaxAgeMinutes`,t.artifactUploadMaxAgeMinutes,15),Me(`${e}.journalMaxSizeMb`,t.journalMaxSizeMb,1)}function Za(e,t){let r=`hostCleanup.paths[${t}]`,n=e.path?.replace(/\/+$/,"")??"";if(!n.startsWith("/")||n===""||/[\s'"`$\\]/.test(n)||n.split("/").includes(".."))throw Error(`${r}.path must be an absolute directory with no spaces, quotes or '..' (got ${JSON.stringify(e.path)})`);if(n==="/var/www"||n.startsWith("/var/www/"))throw Error(`${r}.path must not be under /var/www: releases are pruned by keepReleases`);if(!e.pattern||/^[*?]+$/.test(e.pattern)||e.pattern.includes("/")||/[\n'"`]/.test(e.pattern))throw Error(`${r}.pattern must be a file-name glob naming something, not a bare wildcard (got ${JSON.stringify(e.pattern)})`);if(Me(`${r}.maxAgeDays`,e.maxAgeDays),e.type!==void 0&&e.type!=="file"&&e.type!=="directory")throw Error(`${r}.type must be 'file' or 'directory'`)}function Bt(e){let t=typeof e==="object"&&e!==null?e:{},r=e!==!1,n={...Is,...Rs(t.retention)};xs("hostCleanup.retention",n);let s=null;if(t.pressure!==!1){let a=typeof t.pressure==="object"?t.pressure:{},c=a.lowWaterPercent??50,l=a.highWaterPercent??85;if(Me("hostCleanup.pressure.lowWaterPercent",c),Me("hostCleanup.pressure.highWaterPercent",l,1),l>100||c>=l)throw Error(`hostCleanup.pressure: need 0 <= lowWaterPercent < highWaterPercent <= 100 (got ${c} and ${l})`);let u={...As,...Rs(a.escalated)},d=Object.fromEntries(Object.keys(n).map((m)=>[m,Math.min(n[m],u[m]??n[m])]));xs("hostCleanup.pressure.escalated",d),s={lowWaterPercent:c,highWaterPercent:l,escalated:d}}let i=t.schedule??"daily";if(!/^[\w*:,./~ -]+$/.test(i))throw Error(`hostCleanup.schedule must be a systemd OnCalendar expression (got ${JSON.stringify(i)})`);let o=t.paths??[];return o.forEach(Za),{enabled:r,timer:r&&t.timer!==!1,schedule:i,retention:n,pressure:s,paths:o}}function Rs(e){return Object.fromEntries(Object.entries(e??{}).filter(([,t])=>t!==void 0))}var ft={stagingMaxAgeMinutes:"TS_CLOUD_HC_STAGING_MIN",artifactMaxAgeDays:"TS_CLOUD_HC_ARTIFACT_DAYS",artifactUploadMaxAgeMinutes:"TS_CLOUD_HC_UPLOAD_MIN",bunCacheMaxAgeDays:"TS_CLOUD_HC_BUN_DAYS",journalMaxAgeDays:"TS_CLOUD_HC_JOURNAL_DAYS",journalMaxSizeMb:"TS_CLOUD_HC_JOURNAL_MB",containerImageMaxAgeHours:"TS_CLOUD_HC_IMAGE_HOURS"};function Nr(e){return Object.keys(ft).map((t)=>`${ft[t]}=${e[t]}`).join(" ")}var Ee=(e)=>`"$${ft[e]}"`;function Es(e){return[`find /var/ts-cloud/staging -xdev -maxdepth 1 -type f -mmin +${Ee("stagingMaxAgeMinutes")} -delete 2>/dev/null || true`,`find /tmp -xdev -maxdepth 1 -type f -name "*-release.tar.gz" -mmin +${Ee("stagingMaxAgeMinutes")} -delete 2>/dev/null || true`,`find /var/ts-cloud/artifacts -xdev -maxdepth 1 -type f -name "*.tar.gz" -mtime +${Ee("artifactMaxAgeDays")} -delete 2>/dev/null || true`,`find /var/ts-cloud/artifacts -xdev -maxdepth 1 -type f -name ".*.tmp" -mmin +${Ee("artifactUploadMaxAgeMinutes")} -delete 2>/dev/null || true`,...e.map(ec),`journalctl --vacuum-time=${Ee("journalMaxAgeDays")}d --vacuum-size=${Ee("journalMaxSizeMb")}M >/dev/null 2>&1 || true`]}function ec(e){let t=Ps(e.path.replace(/\/+$/,"")),r=Ps(e.pattern);return e.type==="directory"?`find ${t} -xdev -mindepth 1 -maxdepth 1 -type d -name ${r} -mtime +${e.maxAgeDays} -exec rm -rf -- {} + 2>/dev/null || true`:`find ${t} -xdev -mindepth 1 -maxdepth 1 -type f -name ${r} -mtime +${e.maxAgeDays} -delete 2>/dev/null || true`}function Os(){return[`find /root/.bun/install/cache -xdev -type f -mtime +${Ee("bunCacheMaxAgeDays")} -delete 2>/dev/null || true`,`find /root/.bun/install/cache -xdev -depth -type d -empty -mtime +${Ee("bunCacheMaxAgeDays")} -delete 2>/dev/null || true`,`if command -v docker >/dev/null 2>&1; then docker image prune --all --force --filter "until=\${${ft.containerImageMaxAgeHours}}h" >/dev/null 2>&1 || true; fi`,`if command -v podman >/dev/null 2>&1; then podman image prune --all --force --filter "until=\${${ft.containerImageMaxAgeHours}}h" >/dev/null 2>&1 || true; fi`,"if command -v apt-get >/dev/null 2>&1; then apt-get clean >/dev/null 2>&1 || true; fi"]}function Ds(e){return[`${e}="$(df -P / 2>/dev/null | awk 'NR==2 {sub(/%/, "", $5); print $5}' || true)"`,`case "$${e}" in ''|*[!0-9]*) ${e}=-1 ;; esac`]}function Ur(e){let{retention:t,pressure:r,paths:n}=Bt(e),s='echo "[ts-cloud] host cleanup (disk before): $(df -h / | tail -1)"',i='echo "[ts-cloud] host cleanup (disk after): $(df -h / | tail -1)"';if(!r)return['echo "[ts-cloud] host cleanup (disk before): $(df -h / | tail -1)"',Nr(t),...Es(n),...Os(),'echo "[ts-cloud] host cleanup (disk after): $(df -h / | tail -1)"'];let{lowWaterPercent:o,highWaterPercent:a,escalated:c}=r;return['echo "[ts-cloud] host cleanup (disk before): $(df -h / | tail -1)"',...Ds("TS_CLOUD_HC_BEFORE"),"TS_CLOUD_HC_TIER=normal",Nr(t),`if [ "$TS_CLOUD_HC_BEFORE" -ge 0 ] && [ "$TS_CLOUD_HC_BEFORE" -lt ${o} ]; then`," TS_CLOUD_HC_TIER=light",` echo "[ts-cloud] host cleanup: / at \${TS_CLOUD_HC_BEFORE}% (under ${o}%), skipping cache and image pruning"`,`elif [ "$TS_CLOUD_HC_BEFORE" -ge ${a} ]; then`," TS_CLOUD_HC_TIER=pressure",` ${Nr(c)}`,` echo "[ts-cloud] warning: / at \${TS_CLOUD_HC_BEFORE}% (at or above ${a}%), host cleanup is using shortened retention windows" >&2`,"fi",...Es(n),'if [ "$TS_CLOUD_HC_TIER" != light ]; then',...Os().map((l)=>` ${l}`),"fi",'echo "[ts-cloud] host cleanup (disk after): $(df -h / | tail -1)"',...Ds("TS_CLOUD_HC_AFTER"),`echo "[ts-cloud] host-cleanup {\\"event\\":\\"host-cleanup\\",\\"tier\\":\\"$TS_CLOUD_HC_TIER\\",\\"diskBeforePercent\\":$TS_CLOUD_HC_BEFORE,\\"diskAfterPercent\\":$TS_CLOUD_HC_AFTER,\\"lowWaterPercent\\":${o},\\"highWaterPercent\\":${a}}"`,"mkdir -p /var/lib/ts-cloud 2>/dev/null || true",'TS_CLOUD_HC_PREV="$(cat /var/lib/ts-cloud/host-cleanup.alert 2>/dev/null || echo ok)"',`if [ "$TS_CLOUD_HC_AFTER" -ge ${a} ]; then`,` echo "[ts-cloud] warning: / still at \${TS_CLOUD_HC_AFTER}% after host cleanup (high-water ${a}%); something outside ts-cloud's retention rules is filling the disk" >&2`,` if [ "$TS_CLOUD_HC_PREV" != alert ] && [ -x /usr/local/bin/ts-cloud-notify ]; then /usr/local/bin/ts-cloud-notify "⚠️ $(hostname): / at \${TS_CLOUD_HC_AFTER}% after host cleanup (high-water ${a}%)" >/dev/null 2>&1 || true; fi`," echo alert > /var/lib/ts-cloud/host-cleanup.alert 2>/dev/null || true","else",` if [ "$TS_CLOUD_HC_PREV" = alert ] && [ -x /usr/local/bin/ts-cloud-notify ]; then /usr/local/bin/ts-cloud-notify "✅ $(hostname): / back under ${a}% after host cleanup (\${TS_CLOUD_HC_AFTER}%)" >/dev/null 2>&1 || true; fi`," echo ok > /var/lib/ts-cloud/host-cleanup.alert 2>/dev/null || true","fi"]}function Ls(e){return["#!/usr/bin/env bash","# Managed by ts-cloud: rewritten on every deploy. Configure compute.hostCleanup instead of editing.","set -uo pipefail","TS_CLOUD_HC_LOCK=/run/lock/ts-cloud-host-cleanup.lock","[ -d /run/lock ] || TS_CLOUD_HC_LOCK=/tmp/ts-cloud-host-cleanup.lock",'if command -v flock >/dev/null 2>&1 && exec 9>"$TS_CLOUD_HC_LOCK"; then',' if ! flock -n 9; then echo "[ts-cloud] host cleanup already running; skipping"; exit 0; fi',"fi",...Ur(e),"exit 0"]}function Hr(e,t,r,n,s){return[`cat > ${e}.ts-cloud-new <<'${r}' || true`,...t,r,`if [ -s ${e}.ts-cloud-new ] && ! cmp -s ${e}.ts-cloud-new ${e}; then chmod ${n} ${e}.ts-cloud-new && mv -f ${e}.ts-cloud-new ${e}${s?` && ${s}=1`:""} || true; else rm -f ${e}.ts-cloud-new; fi`]}function Ns(e){let t=Bt(e),r="/etc/systemd/system/ts-cloud-host-cleanup",n=["if [ -f /etc/systemd/system/ts-cloud-host-cleanup.timer ]; then"," systemctl disable --now ts-cloud-host-cleanup.timer >/dev/null 2>&1 || true"," rm -f /etc/systemd/system/ts-cloud-host-cleanup.timer /etc/systemd/system/ts-cloud-host-cleanup.service"," systemctl daemon-reload >/dev/null 2>&1 || true","fi"];if(!t.enabled)return[...n,"rm -f /usr/local/bin/ts-cloud-host-cleanup.sh"];let s=Hr("/usr/local/bin/ts-cloud-host-cleanup.sh",Ls(e),"TS_CLOUD_HOST_CLEANUP_EOF","755");if(!t.timer)return[...s,...n];return[...s,"TS_CLOUD_HC_UNITS_CHANGED=0","if command -v systemctl >/dev/null 2>&1 && [ -d /etc/systemd/system ]; then",...Hr("/etc/systemd/system/ts-cloud-host-cleanup.service",["[Unit]","Description=ts-cloud host cleanup (bounded disk retention)","After=local-fs.target","","[Service]","Type=oneshot","ExecStart=/usr/local/bin/ts-cloud-host-cleanup.sh","Nice=19","IOSchedulingClass=idle"],"TS_CLOUD_HOST_CLEANUP_SVC_EOF","644","TS_CLOUD_HC_UNITS_CHANGED"),...Hr("/etc/systemd/system/ts-cloud-host-cleanup.timer",["[Unit]","Description=Run ts-cloud host cleanup on a schedule, independent of deploys","","[Timer]",`OnCalendar=${t.schedule}`,"RandomizedDelaySec=1h","Persistent=true","","[Install]","WantedBy=timers.target"],"TS_CLOUD_HOST_CLEANUP_TMR_EOF","644","TS_CLOUD_HC_UNITS_CHANGED"),' if [ "$TS_CLOUD_HC_UNITS_CHANGED" -eq 1 ]; then systemctl daemon-reload >/dev/null 2>&1 || true; fi'," systemctl enable --now ts-cloud-host-cleanup.timer >/dev/null 2>&1 || true","fi"]}function Mr(e){let t=Ns(e);if(!Bt(e).enabled)return t;return[...t,'if [ -x /usr/local/bin/ts-cloud-host-cleanup.sh ]; then /usr/local/bin/ts-cloud-host-cleanup.sh < /dev/null || echo "[ts-cloud] warning: host cleanup exited $?" >&2; fi']}var tc=/\.(?:[cm]?[jt]sx?)$/;function jr(e,t,r){let n=t==="bun"?"/usr/local/bin/bun":t==="deno"?"/usr/local/bin/deno":"/usr/local/bin/node",s=e.trim();if(/^(?:bun|node|deno)\s+/.test(s))return`${n} ${s.replace(/^(?:bun|node|deno)\s+/,"")}`;let[i="",...o]=s.split(/\s+/);if(i&&!tc.test(i)){let a=i.startsWith("/")?i:r&&i.startsWith("./")?`${r.replace(/\/+$/,"")}/${i.replace(/^\.\//,"")}`:void 0;if(a)return[a,...o].join(" ")}return`${n} ${s}`}function ks(e){return["TS_CLOUD_MEM_MB=$(awk '/^MemTotal:/{print int($2/1024)}' /proc/meminfo)","TS_CLOUD_HIGH_MB=$((TS_CLOUD_MEM_MB / 8))",'[ "$TS_CLOUD_HIGH_MB" -lt 512 ] && TS_CLOUD_HIGH_MB=512','[ "$TS_CLOUD_HIGH_MB" -gt 4096 ] && TS_CLOUD_HIGH_MB=4096',`mkdir -p /etc/systemd/system/${e}.d`,`printf '# Written by ts-cloud: memoryHigh resolved from this box.
|
|
@@ -395,7 +395,7 @@ case "$root_fs" in
|
|
|
395
395
|
esac`,e)}async function $c(e,t){let r=Buffer.from(t.join(`
|
|
396
396
|
`)).toString("base64");await _e(e.host,`printf %s ${r} | base64 -d | while read -r unit; do [ -z "$unit" ] || systemctl is-active --quiet "$unit" || systemctl start "$unit"; done`,e)}async function gi(e,t,r,n={recovered:!1}){if(await Qr(e.host,{...e,timeoutMs:e.sshTimeoutMs??600000}),!n.recovered)await _c(e);if(r)await $c(e,r.runningServices);let s=await nn(e),i=[],o=r?it(r.runningServices,s.runningServices):[],a=r?it(r.releaseLinks,s.releaseLinks):[],c=r?it(r.routeFragments,s.routeFragments):[],l=r?it(r.routeIds,s.routeIds):[],u=r?.persistentData.map((R)=>R.split("=")[0])??[],d=s.persistentData.map((R)=>R.split("=")[0]),m=it(u,d),h=r?it(r.dataCatalog,s.dataCatalog):[],f=s.routeProbes.filter((R)=>!R.ok),S=n.recovered&&r?r.cpuCores:t.cores??0,k=n.recovered&&r?r.memoryBytes:wc(t),E=s.cpuCores>=S,O=s.memoryBytes>=k*0.9,D=n.recovered||s.rootDiskBytes>=(t.disk??0)*1e9*0.95;if(s.failedUnits.length>0)i.push(`Failed services: ${s.failedUnits.join(", ")}`);if(o.length>0)i.push(`Services did not return: ${o.join(", ")}`);if(a.length>0)i.push(`Release links changed: ${a.join(", ")}`);if(c.length>0)i.push(`Route manifests changed: ${c.join(", ")}`);if(l.length>0)i.push(`Routes disappeared: ${l.join(", ")}`);if(m.length>0)i.push(`Persistent data paths disappeared: ${m.join(", ")}`);if(h.length>0)i.push(`Databases or volumes disappeared: ${h.join(", ")}`);if(f.length>0)i.push(`Unhealthy routes: ${f.map((R)=>R.domain).join(", ")}`);if(!E)i.push(`Expected at least ${S} CPU cores, found ${s.cpuCores}`);if(!O)i.push(`Expected at least ${k} bytes of RAM`);if(!D)i.push(`Expected approximately ${t.disk??0} GB root disk`);return{ok:i.length===0,checks:{cpuCores:s.cpuCores,memoryBytes:s.memoryBytes,rootDiskBytes:s.rootDiskBytes,rootFilesystemBytes:s.rootFilesystemBytes,runningServices:s.runningServices.length,releaseLinks:s.releaseLinks.length,routeFragments:s.routeFragments.length,routes:s.routeIds.length,healthyRoutes:s.routeProbes.filter((R)=>R.ok).length,persistentDataPaths:s.persistentData.length,databasesAndVolumes:s.dataCatalog.length,failedUnits:s.failedUnits.length},failures:i}}import{mkdir as Tc,readFile as Cc,rename as Pc,writeFile as xc}from"node:fs/promises";import{join as js}from"node:path";var dn="storage/cloud/state";function Oe(e=process.cwd()){return ca()?A(e,"state"):js(e,dn)}function ot(e,t=process.cwd()){return js(Oe(t),`${e}.json`)}async function Fe(e,t=process.cwd()){try{let r=await Cc(ot(e,t),"utf8");return JSON.parse(r)}catch{return null}}async function at(e,t,r=process.cwd()){let n=ot(e,r);await Tc(Oe(r),{recursive:!0});let s=`${n}.${process.pid}.tmp`;await xc(s,`${JSON.stringify(t,null,2)}
|
|
397
397
|
`,"utf8"),await Pc(s,n)}async function ge(e){return Fe(e)}async function mt(e,t){await at(e,t)}async function mn(e,t,r){let n=r.length?await e.listVolumes():[];return r.map((s)=>{let i=n.filter((l)=>l.name===s.name);if(i.length>1)throw Error(`Ambiguous volume name: ${s.name}`);let o=i[0];if(o&&o.server!==null&&o.server!==t.id)throw Error(`Volume ${o.name} belongs to another server; refusing to detach live storage`);let a=t.location?.name??t.datacenter?.location.name;if(o&&o.location.name!==a)throw Error(`Volume ${o.name} is in ${o.location.name}, server is in ${a}`);if(o&&o.size>s.size)throw Error(`Refusing to shrink volume ${o.name}: ${o.size} GB to ${s.size} GB`);let c=[];if(!o)c.push("create");else{if(o.server===null)c.push("attach");if(o.size<s.size)c.push("grow")}if(!o||o.protection.delete!==(s.deleteProtection!==!1))c.push("protect");return c.push("mount"),{declaration:s,existing:o,actions:c}})}async function fn(e,t,r){let n=[];for(let s of r){let i=s.existing,o=!i;if(!i){let a=await e.createVolume({name:s.declaration.name,size:s.declaration.size,server:t.id,labels:{...t.labels,"ts-cloud/volume":s.declaration.key}});await e.waitForAction(a.action.id);for(let c of a.next_actions??[])await e.waitForAction(c.id);i=await e.getVolume(a.volume.id)}if(i.server===null)await e.waitForAction((await e.attachVolume(i.id,t.id)).id),i=await e.getVolume(i.id);if(i.server!==t.id||i.location.name!==(t.location?.name??t.datacenter?.location.name))throw Error(`Volume attachment verification failed: ${i.name}`);if(i.linux_device!==`/dev/disk/by-id/scsi-0HC_Volume_${i.id}`)throw Error(`Unexpected Hetzner volume device: ${i.name}`);if(i.size<s.declaration.size)await e.waitForAction((await e.resizeVolume(i.id,s.declaration.size)).id);if(i.protection.delete!==(s.declaration.deleteProtection!==!1))await e.waitForAction((await e.protectVolume(i.id,s.declaration.deleteProtection!==!1)).id);n.push({...s.declaration,id:i.id,device:i.linux_device,allowFormat:o})}return n}var gn=268435456;function _t(e){return`'${e.replace(/'/g,`'"'"'`)}'`}async function Ac(e){let t=Ec("sha256");for await(let r of Oc(e))t.update(r);return t.digest("hex")}class De{name="hetzner";usesCloudFormation=!1;client;sshPrivateKeyPath;sshPublicKeyPath;sshUser;location;stateOnly;waitForBoot;bootWait;async applyComputeChanges(e){let{config:t,environment:r}=e;if(t.cloud?.attachTo)throw Error("Only the compute owner may reconcile server size and volumes");let n=t.infrastructure?.compute;if(n?.resize&&!n.size)throw Error("compute.resize requires an explicit compute.size");let s=Ir(t,r),i=Xr(n??{});if(i.appServers>1||i.dedicatedServices)throw Error("Managed compute volumes currently require a single app server");let o=await this.getComputeOutputs(e);if(!o.appInstanceId)throw Error("No existing compute server found; provision the server first");let a=await this.client.getServer(Number(o.appInstanceId)),c=await mn(this.client,a,s),l=n?.resize&&n.size?await mr(this.client,a.id,Xe(n.size),n.resize.upgradeDisk!==!1):void 0,u={serverId:String(a.id),changes:c.flatMap((f)=>f.actions.map((S)=>({resource:f.declaration.name,action:S,detail:`${f.declaration.size} GB at ${f.declaration.mountPath}`})))};if(n?.deploymentStorage)u.changes.push({resource:n.deploymentStorage.volume,action:"place-deployments",detail:`Reconcile release directories${n.deploymentStorage.includeBackups!==!1?" and deployment backups":""} on the volume; ${n.deploymentStorage.migrateExisting?"verify and migrate existing files":"refuse nonempty directories"}`});if(l&&!l.alreadyComplete)if(!l.capacityAvailable){if(n?.resize?.whenUnavailable!=="keep-current")throw Error(`${l.targetType.name} is unavailable in ${l.location}`);u.waitingForCapacity=`${l.targetType.name} unavailable in ${l.location}; keeping ${a.server_type.name} running`}else u.changes.push({resource:a.name,action:"resize",detail:`${a.server_type.name} to ${l.targetType.name}`});if(e.dryRun)return u;if(u.waitingForCapacity)console.warn(`[ts-cloud] ${u.waitingForCapacity}`);let d=await fn(this.client,a,c),m=[{id:String(a.id),name:a.name,publicIp:a.public_net.ipv4?.ip}],h=[...He(t.notifications),...n?.volumes||n?.deploymentStorage?Ar({volumes:d,deploymentStorage:n?.deploymentStorage,webRoot:"/var/www"}):[],...n?.monitoring!==!1?we(n?.monitoring):[]];if(h.length){let f=await this.runRemoteDeploy({targets:m,commands:h,timeoutSeconds:7200,comment:"ts-cloud apply configured compute storage"});if(!f.success)throw Error(f.error||f.perInstance.map((S)=>S.error||S.output).join(`
|
|
398
|
-
`))}if(l?.capacityAvailable&&!l.alreadyComplete){let f={host:m[0].publicIp,user:this.sshUser,identityFile:this.sshPrivateKeyPath},S=await pi({client:this.client,serverId:a.id,targetType:l.targetType.name,upgradeDisk:l.upgradeDisk,hooks:{preflight:()=>mi(f),afterBoot:(k,E,O,D)=>gi(f,E,O,D)}});if(S.status!=="completed"&&S.status!=="already-complete")throw Error(`Resize did not complete: ${S.status}`)}return u}constructor(e={}){let t=vt(e.apiToken);if(this.stateOnly=!e.client&&!t&&e.allowStateOnly===!0,!e.client&&!t&&!this.stateOnly)throw Error("Hetzner API token required. Set hetzner.apiToken in cloud.config.ts or HCLOUD_TOKEN / HETZNER_API_TOKEN.");this.client=e.client??new Je({apiToken:t??""});let r=ur(void 0,{sshPrivateKeyPath:e.sshPrivateKeyPath,sshPublicKeyPath:e.sshPublicKeyPath,sshUser:e.sshUser,location:e.location});this.sshPrivateKeyPath=r.sshPrivateKeyPath,this.sshPublicKeyPath=r.sshPublicKeyPath,this.sshUser=r.sshUser,this.location=r.location,this.waitForBoot=e.waitForBoot??!0,this.bootWait={sshIntervalMs:e.bootWait?.sshIntervalMs??5000,sshTimeoutMs:e.bootWait?.sshTimeoutMs??300000,cloudInitIntervalMs:e.bootWait?.cloudInitIntervalMs??5000,cloudInitTimeoutMs:e.bootWait?.cloudInitTimeoutMs??600000}}async provisionComputeInfrastructure(e){let{config:t,environment:r}=e,n=t.project.slug,s=t.infrastructure?.compute;if(!s)throw Error("infrastructure.compute is required to provision Hetzner compute");if(t.cloud?.attachTo)return this.attachToComputeInfrastructure(t.cloud.attachTo,t,r);let i=T(t,r),o=`${n}-${r}-app`,a=s.runtime==="php"||!!s.php,c=Xr(s);if(c.dedicatedServices||c.appServers>1)return a?this.provisionFleet(e,c):this.provisionBunFleet(e,c);let l=re(n,r,"app"),u=t.sites||{},d=Z(s)?[]:this.collectUpstreamPorts(u),m=`${n}-${r}-app-fw`,{firewall:h}=await this.ensureFirewall(m,l,rn({allowSsh:s.allowSsh!==!1,sitePorts:d,allowedPorts:[...s.firewall?.allowedPorts??[],...hs(t.infrastructure?.sftp),...Fr(ne(t,{environment:r}))]})),f=await this.ensureSshKey(n,r,l),S=await ge(i);if(S?.serverId){let q=await this.tryGetServer(S.serverId);if(q&&q.status!=="off"){if(s.resize||s.volumes||s.deploymentStorage)await this.applyComputeChanges(e);return this.outputsFromState(S,q)}}let k=await this.findExistingServer(n,r,o);if(k&&k.status!=="off"){let q={provider:"hetzner",stackName:i,serverId:k.id,serverName:k.name,publicIp:k.public_net.ipv4?.ip,publicIpv6:me(k.public_net.ipv6?.ip),deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};if(await mt(i,q),s.resize||s.volumes||s.deploymentStorage)await this.applyComputeChanges(e);return this.outputsFromState(q,k)}let E=le(t,{environment:r}),O=E?.engine==="rpx"?ve({proxy:E,config:Te(u,{proxy:E,slug:t.project.slug,mail:ne(t,{environment:r})}),slug:t.project.slug,bunBin:s.runtime==="node"||s.runtime==="deno"?void 0:"/usr/local/bin/bun"}):void 0,D=Ue(t,{environment:r}),R=s.bakedImage===!0,z=de({runtime:D.runtime,runtimeVersion:D.runtimeVersion,systemPackages:s.systemPackages,database:t.infrastructure?.database,phpProvision:D.phpProvision,servicesProvision:D.servicesProvision,rpxProvision:O,baked:R,swapGb:s.swapGb}),x=be(z),N=Xe(s.size),V=bt(t),{server:se,action:ce}=await this.client.createServer({name:o,serverType:N,image:V,location:t.hetzner?.location||this.location,userData:x,labels:l,sshKeys:f?[f]:void 0,firewalls:[{firewall:h.id}]});await this.client.waitForAction(ce.id);let G=await this.client.waitForServerRunning(se.id),ie={provider:"hetzner",stackName:i,serverId:G.id,serverName:G.name,firewallId:h.id,publicIp:G.public_net.ipv4?.ip,publicIpv6:me(G.public_net.ipv6?.ip),deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};await mt(i,ie);let Y=G.public_net.ipv4?.ip;if(Y&&this.waitForBoot)await this.waitForSshReady(Y),await this.waitForCloudInit(Y);if(s.resize||s.volumes||s.deploymentStorage)await this.applyComputeChanges(e);return this.outputsFromState(ie,G)}async provisionFleet(e,t){let{config:r,environment:n}=e,s=r.project.slug,i=r.infrastructure.compute,o=T(r,n),a=Xe(i.size),c=bt(r),l=r.hetzner?.location||this.location,u=i.bakedImage===!0,d=await ge(o);if(d?.loadBalancerId){let j=(await this.client.listLoadBalancers().catch(()=>[])).find((L)=>L.id===d.loadBalancerId);if(j)return{appPublicIp:j.public_net?.ipv4?.ip,loadBalancerIp:j.public_net?.ipv4?.ip,servicesPrivateIp:d.servicesPrivateIp,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}let m=await this.ensureSshKey(s,n,re(s,n,"app")),h=`${s}-${n}-net`,S=(await this.client.listNetworks().catch(()=>[])).find((C)=>C.name===h)??await this.client.createNetwork({name:h,labels:re(s,n,"app")}),{firewall:k}=await this.ensureFirewall(`${s}-${n}-app-fw`,re(s,n,"app"),rn({allowSsh:!0,sitePorts:[],allowedPorts:i.firewall?.allowedPorts})),{firewall:E}=await this.ensureFirewall(`${s}-${n}-services-fw`,re(s,n,"services"),[{direction:"in",protocol:"tcp",port:"22",source_ips:["0.0.0.0/0","::/0"]},{direction:"in",protocol:"tcp",port:"3306",source_ips:[S.ip_range]},{direction:"in",protocol:"tcp",port:"5432",source_ips:[S.ip_range]},{direction:"in",protocol:"tcp",port:"6379",source_ips:[S.ip_range]},{direction:"in",protocol:"tcp",port:"7700",source_ips:[S.ip_range]}]),O=Yr(r),D=be(de({runtime:"php",servicesProvision:O,baked:u,swapGb:i.swapGb})),R=await this.client.listServers().catch(()=>[]),z=[],x=R.find((C)=>fe(C.labels,s,n,"services"));if(!x){let{server:C,action:j}=await this.client.createServer({name:`${s}-${n}-services`,serverType:Xe(typeof i.servicesServer==="object"?i.servicesServer.size:i.size),image:c,location:l,userData:D,labels:re(s,n,"services"),sshKeys:m?[m]:void 0,firewalls:[{firewall:E.id}],networks:[S.id]});await this.client.waitForAction(j.id),x=await this.client.waitForServerRunning(C.id),z.push(C.id)}let N=x.id,V=x.private_net?.[0]?.ip??(await this.client.getServer(N)).private_net?.[0]?.ip,se=[...Pe(!0),...we(!0),...Ye(i.sshKeys),...He(r.notifications)],ce=Dt({versions:i.php?.versions,default:i.php?.default,extensions:i.php?.extensions,installNginx:!Z(i),optimizeForProduction:i.php?.optimizeForProduction,ini:i.php?.ini}),G=be(de({runtime:"php",phpProvision:ce,servicesProvision:se,baked:u,swapGb:i.swapGb})),ie=R.filter((C)=>fe(C.labels,s,n,"app")),Y=ie.map((C)=>C.id);if(ie.length>t.appServers)for(let C of ie.slice(t.appServers))await this.client.deleteServer(C.id).catch(()=>{}),Y.splice(Y.indexOf(C.id),1);for(let C=ie.length;C<t.appServers;C++){let{server:j,action:L}=await this.client.createServer({name:`${s}-${n}-app-${C+1}`,serverType:a,image:c,location:l,userData:G,labels:re(s,n,"app"),sshKeys:m?[m]:void 0,firewalls:[{firewall:k.id}],networks:[S.id]});await this.client.waitForAction(L.id),Y.push(j.id),z.push(j.id)}if(this.waitForBoot&&z.length>0)await Promise.all(z.map(async(C)=>{let L=(await this.client.waitForServerRunning(C)).public_net.ipv4?.ip;if(L)await this.waitForSshReady(L),await this.waitForCloudInit(L)}));let q=`${s}-${n}-lb`,ue,y;if(t.loadBalancer){let j=(await this.client.listLoadBalancers().catch(()=>[])).find((L)=>L.name===q)??await this.client.createLoadBalancer({name:q,location:l,network:S.id,labels:re(s,n,"lb"),labelSelector:`ts-cloud/project=${s},ts-cloud/environment=${n},ts-cloud/role=app`,services:[{listenPort:80,destinationPort:80},{listenPort:443,destinationPort:443}]});y=j.id,ue=j.public_net?.ipv4?.ip}let P=ue??(await this.client.getServer(Y[0]).catch(()=>{return}))?.public_net.ipv4?.ip,H={provider:"hetzner",stackName:o,networkId:S.id,loadBalancerId:y,servicesServerId:N,servicesPrivateIp:V,publicIp:P,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};return await mt(o,H),{appPublicIp:P,loadBalancerIp:ue,servicesPrivateIp:V,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}async provisionBunFleet(e,t){let{config:r,environment:n}=e,s=r.project.slug,i=r.infrastructure.compute,o=T(r,n),a=Xe(i.size),c=bt(r),l=r.hetzner?.location||this.location,u=i.bakedImage===!0,d=r.sites||{},m=await ge(o);if(m?.lbServerId){let L=await this.tryGetServer(m.lbServerId);if(L&&L.status!=="off"){let F=i.proxy?.engine==="rpx"?i.proxy:{engine:"rpx"},K=await this.findComputeTargets({slug:s,environment:n,role:"app",stackName:o});return this.refreshLbRoutes(L,d,K.map((W)=>({privateIp:W.privateIp,publicIp:W.publicIp})),F,s),{appPublicIp:L.public_net.ipv4?.ip??m.publicIp,loadBalancerIp:L.public_net.ipv4?.ip??m.publicIp,servicesPrivateIp:m.servicesPrivateIp,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}}let h=await this.ensureSshKey(s,n,re(s,n,"app")),f=`${s}-${n}-net`,k=(await this.client.listNetworks().catch(()=>[])).find((L)=>L.name===f)??await this.client.createNetwork({name:f,labels:re(s,n,"app")}),E=this.collectUpstreamPorts(d),{firewall:O}=await this.ensureFirewall(`${s}-${n}-app-fw`,re(s,n,"app"),[{direction:"in",protocol:"tcp",port:"22",source_ips:["0.0.0.0/0","::/0"]},...E.map((L)=>({direction:"in",protocol:"tcp",port:String(L),source_ips:[k.ip_range],description:`ts-cloud port ${L} (private, LB-only)`}))]),{firewall:D}=await this.ensureFirewall(`${s}-${n}-lb-fw`,re(s,n,"lb"),rn({allowSsh:!0,sitePorts:[],allowedPorts:i.firewall?.allowedPorts})),R=await this.client.listServers().catch(()=>[]),z=[],x,N,V=!!i.servicesServer||!!i.managedServices,se=t.dedicatedServices&&V;if(se){let{firewall:L}=await this.ensureFirewall(`${s}-${n}-services-fw`,re(s,n,"services"),[{direction:"in",protocol:"tcp",port:"22",source_ips:["0.0.0.0/0","::/0"]},{direction:"in",protocol:"tcp",port:"3306",source_ips:[k.ip_range]},{direction:"in",protocol:"tcp",port:"5432",source_ips:[k.ip_range]},{direction:"in",protocol:"tcp",port:"6379",source_ips:[k.ip_range]},{direction:"in",protocol:"tcp",port:"7700",source_ips:[k.ip_range]}]),F=Yr(r),K=be(de({runtime:"bun",servicesProvision:F,baked:u,swapGb:i.swapGb})),W=R.find((b)=>fe(b.labels,s,n,"services"));if(!W){let{server:b,action:M}=await this.client.createServer({name:`${s}-${n}-services`,serverType:Xe(typeof i.servicesServer==="object"?i.servicesServer.size:i.size),image:c,location:l,userData:K,labels:re(s,n,"services"),sshKeys:h?[h]:void 0,firewalls:[{firewall:L.id}],networks:[k.id]});await this.client.waitForAction(M.id),W=await this.client.waitForServerRunning(b.id),z.push(b.id)}x=W.id,N=W.private_net?.[0]?.ip??(await this.client.getServer(x)).private_net?.[0]?.ip}let ce=se?{...i,managedServices:void 0,backups:void 0}:i,G=Ue({...r,infrastructure:{...r.infrastructure,compute:ce}},{environment:n}),ie=be(de({runtime:G.runtime,runtimeVersion:G.runtimeVersion,systemPackages:i.systemPackages,database:r.infrastructure?.database,servicesProvision:G.servicesProvision,baked:u,swapGb:i.swapGb})),Y=R.filter((L)=>fe(L.labels,s,n,"app")),q=Y.map((L)=>L.id);if(Y.length>t.appServers)for(let L of Y.slice(t.appServers))await this.client.deleteServer(L.id).catch(()=>{}),q.splice(q.indexOf(L.id),1);for(let L=Y.length;L<t.appServers;L++){let{server:F,action:K}=await this.client.createServer({name:`${s}-${n}-app-${L+1}`,serverType:a,image:c,location:l,userData:ie,labels:re(s,n,"app"),sshKeys:h?[h]:void 0,firewalls:[{firewall:O.id}],networks:[k.id]});await this.client.waitForAction(K.id),q.push(F.id),z.push(F.id)}if(this.waitForBoot&&z.length>0)await Promise.all(z.map(async(L)=>{let K=(await this.client.waitForServerRunning(L)).public_net.ipv4?.ip;if(K)await this.waitForSshReady(K),await this.waitForCloudInit(K)}));let ue=[];for(let L of q){let F=R.find((K)=>K.id===L);if(!F||!F.private_net?.[0]?.ip)F=await this.client.getServer(L).catch(()=>F);ue.push({privateIp:F?.private_net?.[0]?.ip,publicIp:F?.public_net.ipv4?.ip})}let y=`${s}-${n}-lb`,P,H;if(t.loadBalancer){let L=R.find((K)=>fe(K.labels,s,n,"lb")),F=i.proxy?.engine==="rpx"?i.proxy:{engine:"rpx"};if(!L){let K=ve({proxy:F,config:qe(d,ue,{proxy:F,slug:s}),slug:s,bunBin:G.runtime==="node"||G.runtime==="deno"?void 0:"/usr/local/bin/bun"}),W=be(de({runtime:"bun",rpxProvision:K,baked:!1})),b=typeof i.loadBalancer==="object"?i.loadBalancer.size:void 0,{server:M,action:ee}=await this.client.createServer({name:y,serverType:Xe(b??"micro"),image:c,location:l,userData:W,labels:re(s,n,"lb"),sshKeys:h?[h]:void 0,firewalls:[{firewall:D.id}],networks:[k.id]});if(await this.client.waitForAction(ee.id),L=await this.client.waitForServerRunning(M.id),this.waitForBoot){let X=L.public_net.ipv4?.ip;if(X)await this.waitForSshReady(X),await this.waitForCloudInit(X)}}else this.refreshLbRoutes(L,d,ue,F,s);P=L.id,H=L.public_net.ipv4?.ip}let C=H??(await this.client.getServer(q[0]).catch(()=>{return}))?.public_net.ipv4?.ip,j={provider:"hetzner",stackName:o,networkId:k.id,lbServerId:P,appServerIds:q,servicesServerId:x,servicesPrivateIp:N,publicIp:C,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};return await mt(o,j),{appPublicIp:C,loadBalancerIp:H,servicesPrivateIp:N,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}async destroyCompute(e){let{config:t,environment:r}=e,n=t.project.slug,s=T(t,r),i=await ge(s),o=[],a=`${n}-${r}-lb`,l=(await this.client.listLoadBalancers().catch(()=>[])).find((O)=>O.name===a);if(l)try{await this.client.deleteLoadBalancer(l.id),o.push(`load balancer ${a}`)}catch{}let u=await this.client.listServers().catch(()=>[]),d=new Set;for(let O of u)if(fe(O.labels,n,r,"app")||fe(O.labels,n,r,"services")||fe(O.labels,n,r,"lb"))d.add(O.id);if(i?.serverId)d.add(i.serverId);if(i?.servicesServerId)d.add(i.servicesServerId);if(i?.lbServerId)d.add(i.lbServerId);for(let O of i?.appServerIds??[])d.add(O);for(let O of d)try{await this.client.deleteServer(O),o.push(`server ${O}`)}catch{}let m=await this.client.listFirewalls().catch(()=>[]);for(let O of[`${n}-${r}-app-fw`,`${n}-${r}-services-fw`,`${n}-${r}-lb-fw`]){let D=m.find((R)=>R.name===O);if(!D)continue;for(let R=0;R<12;R++)try{await this.client.deleteFirewall(D.id),o.push(`firewall ${O}`);break}catch{await this.sleep(3000)}}let h=`${n}-${r}-net`,S=(await this.client.listNetworks().catch(()=>[])).find((O)=>O.name===h);if(S)for(let O=0;O<12;O++)try{await this.client.deleteNetwork(S.id),o.push(`network ${h}`);break}catch{await this.sleep(3000)}let E=(await this.client.listSshKeys().catch(()=>[])).filter((O)=>un(O.labels,n,r));for(let O of E)try{await this.client.deleteSshKey(O.id),o.push(`ssh key ${O.name}`)}catch{}return await mt(s,{provider:"hetzner",stackName:s}).catch(()=>{}),{destroyed:o}}async getComputeOutputs(e){let t=T(e.config,e.environment),r=await ge(t);if(r?.serverId){let i=await this.tryGetServer(r.serverId);if(i)return this.outputsFromState(r,i)}if(r?.lbServerId){let o=(await this.tryGetServer(r.lbServerId))?.public_net.ipv4?.ip??r.publicIp;return{...this.outputsFromState(r),appPublicIp:o,loadBalancerIp:o}}if(r?.loadBalancerId||r?.servicesPrivateIp)return this.outputsFromState(r);let s=(await this.findComputeTargets({slug:e.config.project.slug,environment:e.environment,role:"app",stackName:T(e.config,e.environment)}))[0];return{deployStoragePath:"/var/ts-cloud/staging",appInstanceId:s?.id,appPublicIp:s?.publicIp,sshUser:this.sshUser}}async uploadRelease(e){let t=e.targets?.length?e.targets:await this.findComputeTargets({slug:e.config.project.slug,environment:e.environment,role:"app",stackName:T(e.config,e.environment)});if(t.length===0)throw Error("No Hetzner compute targets found for release upload");let s=`/var/ts-cloud/staging/${`${e.remoteKey.replace(/^releases\//,"").replace(/\.tar\.gz$/,"").replace(/\//g,"-")}-${Vs()}.tar.gz`}`,i=await Ac(e.localPath),o=Ze,a=`${o}/${i}.tar.gz`;for(let c of t){if(!c.publicIp)throw Error(`Target ${c.id} has no public IP for SCP upload`);let l=["set -euo pipefail",`mkdir -p ${_t(o)} /var/ts-cloud/staging`,`test -s ${_t(a)}`,`cp -- ${_t(a)} ${_t(s)}`].join(`
|
|
398
|
+
`))}if(l?.capacityAvailable&&!l.alreadyComplete){let f={host:m[0].publicIp,user:this.sshUser,identityFile:this.sshPrivateKeyPath},S=await pi({client:this.client,serverId:a.id,targetType:l.targetType.name,upgradeDisk:l.upgradeDisk,hooks:{preflight:()=>mi(f),afterBoot:(k,E,O,D)=>gi(f,E,O,D)}});if(S.status!=="completed"&&S.status!=="already-complete")throw Error(`Resize did not complete: ${S.status}`)}return u}constructor(e={}){let t=vt(e.apiToken);if(this.stateOnly=!e.client&&!t&&e.allowStateOnly===!0,!e.client&&!t&&!this.stateOnly)throw Error("Hetzner API token required. Set hetzner.apiToken in cloud.config.ts or HCLOUD_TOKEN / HETZNER_API_TOKEN.");this.client=e.client??new Je({apiToken:t??""});let r=ur(void 0,{sshPrivateKeyPath:e.sshPrivateKeyPath,sshPublicKeyPath:e.sshPublicKeyPath,sshUser:e.sshUser,location:e.location});this.sshPrivateKeyPath=r.sshPrivateKeyPath,this.sshPublicKeyPath=r.sshPublicKeyPath,this.sshUser=r.sshUser,this.location=r.location,this.waitForBoot=e.waitForBoot??!0,this.bootWait={sshIntervalMs:e.bootWait?.sshIntervalMs??5000,sshTimeoutMs:e.bootWait?.sshTimeoutMs??300000,cloudInitIntervalMs:e.bootWait?.cloudInitIntervalMs??5000,cloudInitTimeoutMs:e.bootWait?.cloudInitTimeoutMs??600000}}async provisionComputeInfrastructure(e){let{config:t,environment:r}=e,n=t.project.slug,s=t.infrastructure?.compute;if(!s)throw Error("infrastructure.compute is required to provision Hetzner compute");if(t.cloud?.attachTo)return this.attachToComputeInfrastructure(t.cloud.attachTo,t,r);let i=T(t,r),o=`${n}-${r}-app`,a=s.runtime==="php"||!!s.php,c=Xr(s);if(c.dedicatedServices||c.appServers>1)return a?this.provisionFleet(e,c):this.provisionBunFleet(e,c);let l=re(n,r,"app"),u=t.sites||{},d=Z(s)?[]:this.collectUpstreamPorts(u),m=`${n}-${r}-app-fw`,{firewall:h}=await this.ensureFirewall(m,l,rn({allowSsh:s.allowSsh!==!1,sitePorts:d,allowedPorts:[...s.firewall?.allowedPorts??[],...hs(t.infrastructure?.sftp),...Fr(ne(t,{environment:r}))]})),f=await this.ensureSshKey(n,r,l),S=await ge(i);if(S?.serverId){let q=await this.tryGetServer(S.serverId);if(q&&q.status!=="off"){if(s.resize||s.volumes||s.deploymentStorage)await this.applyComputeChanges(e);return this.outputsFromState(S,q)}}let k=await this.findExistingServer(n,r,o);if(k&&k.status!=="off"){let q={provider:"hetzner",stackName:i,serverId:k.id,serverName:k.name,publicIp:k.public_net.ipv4?.ip,publicIpv6:me(k.public_net.ipv6?.ip),deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};if(await mt(i,q),s.resize||s.volumes||s.deploymentStorage)await this.applyComputeChanges(e);return this.outputsFromState(q,k)}let E=le(t,{environment:r}),O=E?.engine==="rpx"?ve({proxy:E,config:Te(u,{proxy:E,slug:t.project.slug,mail:ne(t,{environment:r})}),slug:t.project.slug,bunBin:s.runtime==="node"||s.runtime==="deno"?void 0:"/usr/local/bin/bun"}):void 0,D=Ue(t,{environment:r}),R=s.bakedImage===!0,z=de({runtime:D.runtime,runtimeVersion:D.runtimeVersion,systemPackages:s.systemPackages,database:t.infrastructure?.database,phpProvision:D.phpProvision,servicesProvision:D.servicesProvision,rpxProvision:O,baked:R,swapGb:s.swapGb}),x=be(z),N=Xe(s.size),V=bt(t),{server:se,action:ce}=await this.client.createServer({name:o,serverType:N,image:V,location:t.hetzner?.location||this.location,userData:x,labels:l,sshKeys:f?[f]:void 0,firewalls:[{firewall:h.id}]});await this.client.waitForAction(ce.id);let G=await this.client.waitForServerRunning(se.id),ie={provider:"hetzner",stackName:i,serverId:G.id,serverName:G.name,firewallId:h.id,publicIp:G.public_net.ipv4?.ip,publicIpv6:me(G.public_net.ipv6?.ip),deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};await mt(i,ie);let Y=G.public_net.ipv4?.ip;if(Y&&this.waitForBoot)await this.waitForSshReady(Y),await this.waitForCloudInit(Y);if(s.resize||s.volumes||s.deploymentStorage)await this.applyComputeChanges(e);return this.outputsFromState(ie,G)}async provisionFleet(e,t){let{config:r,environment:n}=e,s=r.project.slug,i=r.infrastructure.compute,o=T(r,n),a=Xe(i.size),c=bt(r),l=r.hetzner?.location||this.location,u=i.bakedImage===!0,d=await ge(o);if(d?.loadBalancerId){let F=(await this.client.listLoadBalancers().catch(()=>[])).find((L)=>L.id===d.loadBalancerId);if(F)return{appPublicIp:F.public_net?.ipv4?.ip,loadBalancerIp:F.public_net?.ipv4?.ip,servicesPrivateIp:d.servicesPrivateIp,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}let m=await this.ensureSshKey(s,n,re(s,n,"app")),h=`${s}-${n}-net`,S=(await this.client.listNetworks().catch(()=>[])).find((C)=>C.name===h)??await this.client.createNetwork({name:h,labels:re(s,n,"app")}),{firewall:k}=await this.ensureFirewall(`${s}-${n}-app-fw`,re(s,n,"app"),rn({allowSsh:!0,sitePorts:[],allowedPorts:i.firewall?.allowedPorts})),{firewall:E}=await this.ensureFirewall(`${s}-${n}-services-fw`,re(s,n,"services"),[{direction:"in",protocol:"tcp",port:"22",source_ips:["0.0.0.0/0","::/0"]},{direction:"in",protocol:"tcp",port:"3306",source_ips:[S.ip_range]},{direction:"in",protocol:"tcp",port:"5432",source_ips:[S.ip_range]},{direction:"in",protocol:"tcp",port:"6379",source_ips:[S.ip_range]},{direction:"in",protocol:"tcp",port:"7700",source_ips:[S.ip_range]}]),O=Yr(r),D=be(de({runtime:"php",servicesProvision:O,baked:u,swapGb:i.swapGb})),R=await this.client.listServers().catch(()=>[]),z=[],x=R.find((C)=>fe(C.labels,s,n,"services"));if(!x){let{server:C,action:F}=await this.client.createServer({name:`${s}-${n}-services`,serverType:Xe(typeof i.servicesServer==="object"?i.servicesServer.size:i.size),image:c,location:l,userData:D,labels:re(s,n,"services"),sshKeys:m?[m]:void 0,firewalls:[{firewall:E.id}],networks:[S.id]});await this.client.waitForAction(F.id),x=await this.client.waitForServerRunning(C.id),z.push(C.id)}let N=x.id,V=x.private_net?.[0]?.ip??(await this.client.getServer(N)).private_net?.[0]?.ip,se=[...Pe(!0),...we(!0),...Ye(i.sshKeys),...He(r.notifications)],ce=Dt({versions:i.php?.versions,default:i.php?.default,extensions:i.php?.extensions,installNginx:!Z(i),optimizeForProduction:i.php?.optimizeForProduction,ini:i.php?.ini}),G=be(de({runtime:"php",phpProvision:ce,servicesProvision:se,baked:u,swapGb:i.swapGb})),ie=R.filter((C)=>fe(C.labels,s,n,"app")),Y=ie.map((C)=>C.id);if(ie.length>t.appServers)for(let C of ie.slice(t.appServers))await this.client.deleteServer(C.id).catch(()=>{}),Y.splice(Y.indexOf(C.id),1);for(let C=ie.length;C<t.appServers;C++){let{server:F,action:L}=await this.client.createServer({name:`${s}-${n}-app-${C+1}`,serverType:a,image:c,location:l,userData:G,labels:re(s,n,"app"),sshKeys:m?[m]:void 0,firewalls:[{firewall:k.id}],networks:[S.id]});await this.client.waitForAction(L.id),Y.push(F.id),z.push(F.id)}if(this.waitForBoot&&z.length>0)await Promise.all(z.map(async(C)=>{let L=(await this.client.waitForServerRunning(C)).public_net.ipv4?.ip;if(L)await this.waitForSshReady(L),await this.waitForCloudInit(L)}));let q=`${s}-${n}-lb`,ue,y;if(t.loadBalancer){let F=(await this.client.listLoadBalancers().catch(()=>[])).find((L)=>L.name===q)??await this.client.createLoadBalancer({name:q,location:l,network:S.id,labels:re(s,n,"lb"),labelSelector:`ts-cloud/project=${s},ts-cloud/environment=${n},ts-cloud/role=app`,services:[{listenPort:80,destinationPort:80},{listenPort:443,destinationPort:443}]});y=F.id,ue=F.public_net?.ipv4?.ip}let P=ue??(await this.client.getServer(Y[0]).catch(()=>{return}))?.public_net.ipv4?.ip,H={provider:"hetzner",stackName:o,networkId:S.id,loadBalancerId:y,servicesServerId:N,servicesPrivateIp:V,publicIp:P,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};return await mt(o,H),{appPublicIp:P,loadBalancerIp:ue,servicesPrivateIp:V,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}async provisionBunFleet(e,t){let{config:r,environment:n}=e,s=r.project.slug,i=r.infrastructure.compute,o=T(r,n),a=Xe(i.size),c=bt(r),l=r.hetzner?.location||this.location,u=i.bakedImage===!0,d=r.sites||{},m=await ge(o);if(m?.lbServerId){let L=await this.tryGetServer(m.lbServerId);if(L&&L.status!=="off"){let K=i.proxy?.engine==="rpx"?i.proxy:{engine:"rpx"},j=await this.findComputeTargets({slug:s,environment:n,role:"app",stackName:o});return this.refreshLbRoutes(L,d,j.map((W)=>({privateIp:W.privateIp,publicIp:W.publicIp})),K,s),{appPublicIp:L.public_net.ipv4?.ip??m.publicIp,loadBalancerIp:L.public_net.ipv4?.ip??m.publicIp,servicesPrivateIp:m.servicesPrivateIp,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}}let h=await this.ensureSshKey(s,n,re(s,n,"app")),f=`${s}-${n}-net`,k=(await this.client.listNetworks().catch(()=>[])).find((L)=>L.name===f)??await this.client.createNetwork({name:f,labels:re(s,n,"app")}),E=this.collectUpstreamPorts(d),{firewall:O}=await this.ensureFirewall(`${s}-${n}-app-fw`,re(s,n,"app"),[{direction:"in",protocol:"tcp",port:"22",source_ips:["0.0.0.0/0","::/0"]},...E.map((L)=>({direction:"in",protocol:"tcp",port:String(L),source_ips:[k.ip_range],description:`ts-cloud port ${L} (private, LB-only)`}))]),{firewall:D}=await this.ensureFirewall(`${s}-${n}-lb-fw`,re(s,n,"lb"),rn({allowSsh:!0,sitePorts:[],allowedPorts:i.firewall?.allowedPorts})),R=await this.client.listServers().catch(()=>[]),z=[],x,N,V=!!i.servicesServer||!!i.managedServices,se=t.dedicatedServices&&V;if(se){let{firewall:L}=await this.ensureFirewall(`${s}-${n}-services-fw`,re(s,n,"services"),[{direction:"in",protocol:"tcp",port:"22",source_ips:["0.0.0.0/0","::/0"]},{direction:"in",protocol:"tcp",port:"3306",source_ips:[k.ip_range]},{direction:"in",protocol:"tcp",port:"5432",source_ips:[k.ip_range]},{direction:"in",protocol:"tcp",port:"6379",source_ips:[k.ip_range]},{direction:"in",protocol:"tcp",port:"7700",source_ips:[k.ip_range]}]),K=Yr(r),j=be(de({runtime:"bun",servicesProvision:K,baked:u,swapGb:i.swapGb})),W=R.find((b)=>fe(b.labels,s,n,"services"));if(!W){let{server:b,action:M}=await this.client.createServer({name:`${s}-${n}-services`,serverType:Xe(typeof i.servicesServer==="object"?i.servicesServer.size:i.size),image:c,location:l,userData:j,labels:re(s,n,"services"),sshKeys:h?[h]:void 0,firewalls:[{firewall:L.id}],networks:[k.id]});await this.client.waitForAction(M.id),W=await this.client.waitForServerRunning(b.id),z.push(b.id)}x=W.id,N=W.private_net?.[0]?.ip??(await this.client.getServer(x)).private_net?.[0]?.ip}let ce=se?{...i,managedServices:void 0,backups:void 0}:i,G=Ue({...r,infrastructure:{...r.infrastructure,compute:ce}},{environment:n}),ie=be(de({runtime:G.runtime,runtimeVersion:G.runtimeVersion,systemPackages:i.systemPackages,database:r.infrastructure?.database,servicesProvision:G.servicesProvision,baked:u,swapGb:i.swapGb})),Y=R.filter((L)=>fe(L.labels,s,n,"app")),q=Y.map((L)=>L.id);if(Y.length>t.appServers)for(let L of Y.slice(t.appServers))await this.client.deleteServer(L.id).catch(()=>{}),q.splice(q.indexOf(L.id),1);for(let L=Y.length;L<t.appServers;L++){let{server:K,action:j}=await this.client.createServer({name:`${s}-${n}-app-${L+1}`,serverType:a,image:c,location:l,userData:ie,labels:re(s,n,"app"),sshKeys:h?[h]:void 0,firewalls:[{firewall:O.id}],networks:[k.id]});await this.client.waitForAction(j.id),q.push(K.id),z.push(K.id)}if(this.waitForBoot&&z.length>0)await Promise.all(z.map(async(L)=>{let j=(await this.client.waitForServerRunning(L)).public_net.ipv4?.ip;if(j)await this.waitForSshReady(j),await this.waitForCloudInit(j)}));let ue=[];for(let L of q){let K=R.find((j)=>j.id===L);if(!K||!K.private_net?.[0]?.ip)K=await this.client.getServer(L).catch(()=>K);ue.push({privateIp:K?.private_net?.[0]?.ip,publicIp:K?.public_net.ipv4?.ip})}let y=`${s}-${n}-lb`,P,H;if(t.loadBalancer){let L=R.find((j)=>fe(j.labels,s,n,"lb")),K=i.proxy?.engine==="rpx"?i.proxy:{engine:"rpx"};if(!L){let j=ve({proxy:K,config:qe(d,ue,{proxy:K,slug:s}),slug:s,bunBin:G.runtime==="node"||G.runtime==="deno"?void 0:"/usr/local/bin/bun"}),W=be(de({runtime:"bun",rpxProvision:j,baked:!1})),b=typeof i.loadBalancer==="object"?i.loadBalancer.size:void 0,{server:M,action:ee}=await this.client.createServer({name:y,serverType:Xe(b??"micro"),image:c,location:l,userData:W,labels:re(s,n,"lb"),sshKeys:h?[h]:void 0,firewalls:[{firewall:D.id}],networks:[k.id]});if(await this.client.waitForAction(ee.id),L=await this.client.waitForServerRunning(M.id),this.waitForBoot){let X=L.public_net.ipv4?.ip;if(X)await this.waitForSshReady(X),await this.waitForCloudInit(X)}}else this.refreshLbRoutes(L,d,ue,K,s);P=L.id,H=L.public_net.ipv4?.ip}let C=H??(await this.client.getServer(q[0]).catch(()=>{return}))?.public_net.ipv4?.ip,F={provider:"hetzner",stackName:o,networkId:k.id,lbServerId:P,appServerIds:q,servicesServerId:x,servicesPrivateIp:N,publicIp:C,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};return await mt(o,F),{appPublicIp:C,loadBalancerIp:H,servicesPrivateIp:N,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}async destroyCompute(e){let{config:t,environment:r}=e,n=t.project.slug,s=T(t,r),i=await ge(s),o=[],a=`${n}-${r}-lb`,l=(await this.client.listLoadBalancers().catch(()=>[])).find((O)=>O.name===a);if(l)try{await this.client.deleteLoadBalancer(l.id),o.push(`load balancer ${a}`)}catch{}let u=await this.client.listServers().catch(()=>[]),d=new Set;for(let O of u)if(fe(O.labels,n,r,"app")||fe(O.labels,n,r,"services")||fe(O.labels,n,r,"lb"))d.add(O.id);if(i?.serverId)d.add(i.serverId);if(i?.servicesServerId)d.add(i.servicesServerId);if(i?.lbServerId)d.add(i.lbServerId);for(let O of i?.appServerIds??[])d.add(O);for(let O of d)try{await this.client.deleteServer(O),o.push(`server ${O}`)}catch{}let m=await this.client.listFirewalls().catch(()=>[]);for(let O of[`${n}-${r}-app-fw`,`${n}-${r}-services-fw`,`${n}-${r}-lb-fw`]){let D=m.find((R)=>R.name===O);if(!D)continue;for(let R=0;R<12;R++)try{await this.client.deleteFirewall(D.id),o.push(`firewall ${O}`);break}catch{await this.sleep(3000)}}let h=`${n}-${r}-net`,S=(await this.client.listNetworks().catch(()=>[])).find((O)=>O.name===h);if(S)for(let O=0;O<12;O++)try{await this.client.deleteNetwork(S.id),o.push(`network ${h}`);break}catch{await this.sleep(3000)}let E=(await this.client.listSshKeys().catch(()=>[])).filter((O)=>un(O.labels,n,r));for(let O of E)try{await this.client.deleteSshKey(O.id),o.push(`ssh key ${O.name}`)}catch{}return await mt(s,{provider:"hetzner",stackName:s}).catch(()=>{}),{destroyed:o}}async getComputeOutputs(e){let t=T(e.config,e.environment),r=await ge(t);if(r?.serverId){let i=await this.tryGetServer(r.serverId);if(i)return this.outputsFromState(r,i)}if(r?.lbServerId){let o=(await this.tryGetServer(r.lbServerId))?.public_net.ipv4?.ip??r.publicIp;return{...this.outputsFromState(r),appPublicIp:o,loadBalancerIp:o}}if(r?.loadBalancerId||r?.servicesPrivateIp)return this.outputsFromState(r);let s=(await this.findComputeTargets({slug:e.config.project.slug,environment:e.environment,role:"app",stackName:T(e.config,e.environment)}))[0];return{deployStoragePath:"/var/ts-cloud/staging",appInstanceId:s?.id,appPublicIp:s?.publicIp,sshUser:this.sshUser}}async uploadRelease(e){let t=e.targets?.length?e.targets:await this.findComputeTargets({slug:e.config.project.slug,environment:e.environment,role:"app",stackName:T(e.config,e.environment)});if(t.length===0)throw Error("No Hetzner compute targets found for release upload");let s=`/var/ts-cloud/staging/${`${e.remoteKey.replace(/^releases\//,"").replace(/\.tar\.gz$/,"").replace(/\//g,"-")}-${Vs()}.tar.gz`}`,i=await Ac(e.localPath),o=Ze,a=`${o}/${i}.tar.gz`;for(let c of t){if(!c.publicIp)throw Error(`Target ${c.id} has no public IP for SCP upload`);let l=["set -euo pipefail",`mkdir -p ${_t(o)} /var/ts-cloud/staging`,`test -s ${_t(a)}`,`cp -- ${_t(a)} ${_t(s)}`].join(`
|
|
399
399
|
`);try{this.sshExec(c.publicIp,l);continue}catch{}let u=`${o}/.${i}-${Vs()}.tmp`;try{this.scpToHost(c.publicIp,e.localPath,u),this.sshExec(c.publicIp,jt(u,a,s))}catch(d){try{this.sshExec(c.publicIp,Wt(u))}catch{}throw d}}return{artifactRef:s}}async findComputeTargets(e){let t=e.role||"app",r=(i)=>({id:String(i.id),name:i.name,publicIp:i.public_net.ipv4?.ip,publicIpv6:me(i.public_net.ipv6?.ip),privateIp:i.private_net?.[0]?.ip,status:i.status});if(t==="app"||t==="lb"||t==="services"){let i=await ge(e.stackName??`${e.slug}-${e.environment}`),o=[...new Set(t==="app"?[i?.serverId,...i?.appServerIds??[]]:t==="lb"?[i?.lbServerId]:[i?.servicesServerId])].filter((l)=>typeof l==="number");if(this.stateOnly){if(o.length>0&&i?.publicIp)return o.map((l)=>({id:String(l),name:l===i.serverId?i.serverName:void 0,publicIp:i.publicIp,publicIpv6:i.publicIpv6,status:"running"}));if(t!=="app")return[];throw Error(`Hetzner API token required because '${e.stackName??`${e.slug}-${e.environment}`}' has no complete local compute state pin.`)}let a=await this.client.listServers();if(o.length>0){let l=[];for(let u of o){let d=a.find((m)=>m.id===u)??await this.tryGetServer(u);if(u===i?.serverId&&i.serverName&&d?.name!==i.serverName)continue;if(d&&d.status!=="off")l.push(d)}if(l.length>0)return l.map(r)}let c=a.filter((l)=>fe(l.labels,e.slug,e.environment,t));if(c.length>0)return c.map(r);if(t==="app"){let l=a.filter((u)=>u.status!=="off"&&u.labels?.[`${te}/managed-by`]==="ts-cloud"&&u.labels?.[`${te}/environment`]===e.environment&&u.labels?.[`${te}/role`]==="app");if(l.length===1)return l.map(r)}return[]}if(this.stateOnly)throw Error(`Hetzner API token required to discover '${t}' compute targets.`);let s=(await this.client.listServers()).filter((i)=>fe(i.labels,e.slug,e.environment,t));if(s.length>0)return s.map(r);return[]}async attachToComputeInfrastructure(e,t,r){let n=await this.client.listServers(),s=(m)=>n.filter((h)=>h.status!=="off"&&fe(h.labels,e,r,m)),i=s("app");if(i.length===0)throw Error(`Cannot attach '${t.project.slug}' to '${e}': no running Hetzner app server is labeled for ${e}/${r}. Deploy the owner project first.`);let o=s("lb")[0],a=s("services")[0],c=i[0],l=o??c,u=T(t,r),d={provider:"hetzner",stackName:u,serverId:c.id,serverName:c.name,appServerIds:i.map((m)=>m.id),lbServerId:o?.id,servicesServerId:a?.id,servicesPrivateIp:a?.private_net?.[0]?.ip,publicIp:l.public_net.ipv4?.ip,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};return await mt(u,d),this.outputsFromState(d)}async listReachableResources(){return(await this.client.listServers()).map((t)=>({name:t.name,labels:t.labels}))}async runRemoteDeploy(e){if(e.targets.length===0)return{success:!1,instanceCount:0,perInstance:[],error:"No targets provided"};let t=e.commands.join(`
|
|
400
400
|
`),r=[];for(let s of e.targets){if(!s.publicIp){r.push({instanceId:s.id,status:"Failed",error:"Missing public IP"});continue}try{let i=this.sshExec(s.publicIp,t);je(i),r.push({instanceId:s.id,status:"Success",output:i})}catch(i){r.push({instanceId:s.id,status:"Failed",error:i.message})}}let n=r.every((s)=>s.status==="Success");return{success:n,instanceCount:e.targets.length,perInstance:r,error:n?void 0:ke(r,"One or more SSH deploy commands failed")}}async ensureSshKey(e,t,r){if(!Dc(this.sshPublicKeyPath))throw Error(`SSH public key not found at ${this.sshPublicKeyPath}. ts-cloud deploys to Hetzner over SSH and needs a public key to authorize on the server. Generate one (\`ssh-keygen -t ed25519\`) or set hetzner.sshPrivateKeyPath / HCLOUD_SSH_PUBLIC_KEY.`);let n=Ic(this.sshPublicKeyPath,"utf8").trim(),s=wt(n),o=(await this.client.listSshKeys()).find((c)=>wt(c.public_key)===s);if(o)return o.id;return(await this.client.createSshKey({name:`${e}-${t}-deploy`,publicKey:n,labels:r})).id}async tryGetServer(e){try{return await this.client.getServer(e)}catch{return null}}refreshLbRoutes(e,t,r,n,s){let i=e.public_net.ipv4?.ip;if(!i){console.warn(`[ts-cloud] LB server '${e.name}' has no public IP — skipping the rpx route refresh; its routes may be stale.`);return}let o=r.filter((c)=>c.privateIp||c.publicIp);if(o.length===0){console.warn(`[ts-cloud] No routable app boxes found for LB '${e.name}' — leaving its current rpx routes untouched.`);return}let a=qe(t,o,{proxy:n,slug:s});this.sshExec(i,Ke({config:a,slug:s}).join(`
|
|
401
401
|
`))}async findExistingServer(e,t,r){let n=await this.client.listServers(),s=n.find((o)=>fe(o.labels,e,t,"app")||o.name===r);if(s)return s;let i=n.filter((o)=>o.status!=="off"&&o.labels?.[`${te}/managed-by`]==="ts-cloud"&&o.labels?.[`${te}/environment`]===t&&o.labels?.[`${te}/role`]==="app");if(i.length===1){let o=i[0],a=o.labels?.[`${te}/project`]??"unknown";return console.warn(`[ts-cloud] No server named '${r}' found; adopting existing ts-cloud app server '${o.name}' (project label '${a}') for project '${e}' — `+"updating it in place instead of provisioning a new server."),o}return}async ensureFirewall(e,t,r){let s=(await this.client.listFirewalls()).find((o)=>o.name===e);if(s)return await this.client.setFirewallRules(s.id,r),{firewall:s};let{firewall:i}=await this.client.createFirewall({name:e,labels:t,rules:r});return{firewall:i}}collectUpstreamPorts(e){let t=new Set;for(let r of Object.values(e))if(typeof r.port==="number")t.add(r.port);return[...t].filter((r)=>![80,443].includes(r))}async sleep(e){await new Promise((t)=>setTimeout(t,e))}async waitForSshReady(e){let{sshIntervalMs:t,sshTimeoutMs:r}=this.bootWait,n=Date.now(),s;while(Date.now()-n<r)try{Ws(`ssh ${this.sshBaseArgs(e,["-o","ConnectTimeout=5"]).map(_t).join(" ")} true`,{stdio:"pipe",maxBuffer:gn});return}catch(i){s=i,await this.sleep(t)}throw Error(`Timed out waiting for SSH on ${e} after ${r}ms: ${s?.message??"unknown error"}`)}async waitForCloudInit(e){let{cloudInitIntervalMs:t,cloudInitTimeoutMs:r}=this.bootWait,n=Date.now();while(Date.now()-n<r){try{let s=this.sshExec(e,"if command -v cloud-init >/dev/null 2>&1; then cloud-init status --long 2>/dev/null || true; else echo 'status: done'; fi");if(/status:\s*error/.test(s))throw Error(`cloud-init reported an error on ${e}:
|
|
@@ -542,7 +542,7 @@ console.log(JSON.stringify({
|
|
|
542
542
|
`.replace(/%/g,"\\%");c.push(`cat > ${k} <<'TS_CLOUD_CRON_EOF'`,D.replace(/\n$/,""),"TS_CLOUD_CRON_EOF",`chmod 644 ${k}`)}else c.push(`rm -f ${k}`);return c}function In(e){return!!(e.queues?.length||e.daemons?.length||e.scheduler)}function An(e){if(e.ssl?.provider)return e.ssl.provider;return e.domain?"letsencrypt":"none"}var Ji={cloudflare:{pkg:"python3-certbot-dns-cloudflare",plugin:"dns-cloudflare"},route53:{pkg:"python3-certbot-dns-route53",plugin:"dns-route53"},digitalocean:{pkg:"python3-certbot-dns-digitalocean",plugin:"dns-digitalocean"},google:{pkg:"python3-certbot-dns-google",plugin:"dns-google"}};function Qi(e){return`/etc/letsencrypt/ts-cloud-${e}.ini`}function Dl(e){let t=e?Ji[e.provider]:void 0,r=["certbot","python3-certbot-nginx",...t?[t.pkg]:[]].join(" ");return["export DEBIAN_FRONTEND=noninteractive",t?`apt-get update -y && apt-get install -y ${r}`:`command -v certbot >/dev/null 2>&1 || { apt-get update -y && apt-get install -y ${r}; }`,"mkdir -p /etc/letsencrypt/renewal-hooks/deploy","cat > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh <<'TS_CLOUD_HOOK_EOF'","#!/bin/sh","systemctl reload ts-cloud-nginx 2>/dev/null || systemctl reload nginx 2>/dev/null || true","TS_CLOUD_HOOK_EOF","chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh","systemctl enable certbot.timer 2>/dev/null || true","systemctl start certbot.timer 2>/dev/null || true"]}function Il(e){if(!e.credentials||Object.keys(e.credentials).length===0)return[];let t=Qi(e.provider),r=Object.entries(e.credentials).map(([n,s])=>`${n} = ${s}`).join(`
|
|
543
543
|
`);return[`cat > ${t} <<'TS_CLOUD_DNSCREDS_EOF'`,r,"TS_CLOUD_DNSCREDS_EOF",`chmod 600 ${t}`]}function Al(e){let t=e.dns,r=e.wildcard?[`*.${e.domain}`,e.domain]:[e.domain,...e.aliases||[]].filter(Boolean),n=["certbot","--non-interactive","--agree-tos","--keep-until-expiring"];if(t){let s=Ji[t.provider].plugin;if(n.push(`--${s}`),t.provider!=="route53"&&t.credentials)n.push(`--${s}-credentials ${Qi(t.provider)}`);if(typeof t.propagationSeconds==="number"&&t.provider!=="route53")n.push(`--${s}-propagation-seconds ${t.propagationSeconds}`);n.push("certonly")}else n.push("--nginx",`--nginx-ctl ${xe}`,e.redirect===!1?"--no-redirect":"--redirect");if(e.email)n.push(`-m ${e.email}`);else n.push("--register-unsafely-without-email");for(let s of r)n.push(`-d ${s}`);return[n.join(" ")]}function Ln(e){if(An(e)!=="letsencrypt"||!e.domain)return[];let t=e.ssl,r=t?.dns,n=t?.wildcard===!0;if(n&&!r)return[];return[...Dl(r),...r?Il(r):[],...Al({domain:e.domain,aliases:e.aliases,email:t?.email,wildcard:n,dns:r})]}function lo(e){let{repository:t,releaseDir:r,commit:n}=e,s=t.branch||"main",i=t.url,o=[`rm -rf ${r}`,`mkdir -p ${r}`];if(t.strategy==="tag")if(t.tag)o.push(`git clone -q --depth 1 --branch ${Se(t.tag)} ${Se(i)} ${r}`),o.push(`printf '%s' ${Se(t.tag)} > ${r}/.ts-cloud-tag`);else{let a=t.tagPattern||"v*";o.push(`TS_CLOUD_TAG="$(git ls-remote --tags --refs --sort=-v:refname ${Se(i)} ${Se(`refs/tags/${a}`)} | head -n1 | sed 's#.*refs/tags/##')"`,`test -n "$TS_CLOUD_TAG" || { echo "no tags matching" ${Se(a)} "found in" ${Se(i)} >&2; exit 1; }`,`git clone -q --depth 1 --branch "$TS_CLOUD_TAG" ${Se(i)} ${r}`,`printf '%s' "$TS_CLOUD_TAG" > ${r}/.ts-cloud-tag`)}else if(n)o.push(`git -C ${r} init -q`,`git -C ${r} remote add origin ${Se(i)}`,`git -C ${r} fetch -q --depth 1 origin ${Se(n)}`,`git -C ${r} checkout -q FETCH_HEAD`);else o.push(`git clone -q --depth 1 --branch ${Se(s)} ${Se(i)} ${r}`);return o.push(`git -C ${r} rev-parse HEAD > ${r}/.ts-cloud-sha`),o}function Se(e){return`'${e.split("'").join("'\\''")}'`}var nr="$CREATE_RELEASE",sr="$ACTIVATE_RELEASE",mo="$RESTART_QUEUES",uo="composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev",Ll=["php artisan migrate --force","php artisan config:cache","php artisan route:cache","php artisan view:cache","php artisan event:cache","php artisan storage:link"];function Nl(e){switch(e){case"laravel":case"statamic":return[nr,uo,...Ll,sr,mo];case"php":case"wordpress":return[nr,`${uo} || true`,sr];case"static":case"spa":return[nr,sr]}}function kl(e,t){return e.replace(/^php\s+/,`${t} `).replace(/(\s)php\s+artisan\s+/g,`$1${t} artisan `)}function Hl(e,t){let r=zt(t);return[`cat > ${e} <<'TS_CLOUD_ENV_EOF'`,r,"TS_CLOUD_ENV_EOF",`chmod 600 ${e}`]}function po(e,t,r){return[`cat > ${e} <<'${r}'`,t.replace(/\n$/,""),r,`chmod 600 ${e}`]}function Ul(e,t){if(!t)return[];let r=[];if(t.composerAuth){let n=typeof t.composerAuth==="string"?t.composerAuth:JSON.stringify(t.composerAuth,null,2);r.push(...po(`${e}/auth.json`,n,"TS_CLOUD_AUTHJSON_EOF"))}if(t.npmrc)r.push(...po(`${e}/.npmrc`,t.npmrc,"TS_CLOUD_NPMRC_EOF"));return r}function fo(e){let t=e.healthCheck?.path;if(!t||!e.domain)return[];let n=`http://127.0.0.1${t.startsWith("/")?t:`/${t}`}`;return[`echo "[ts-cloud] health check ${n} (Host: ${e.domain})"`,"TS_CLOUD_HC_OK=0",`for i in $(seq 1 10); do TS_CLOUD_HC=$(curl -s -o /dev/null -w "%{http_code}" -H "Host: ${e.domain}" -m 10 "${n}" 2>/dev/null || echo 000); case "$TS_CLOUD_HC" in 2*|3*) TS_CLOUD_HC_OK=1; break ;; esac; sleep 3; done`,'[ "$TS_CLOUD_HC_OK" = 1 ] && echo "health check passed ($TS_CLOUD_HC)" || { echo "health check FAILED (last=$TS_CLOUD_HC)" >&2; exit 1; }']}function go(e){let{siteName:t,site:r,releaseId:n,commit:s}=e;if(!r.repository?.url)throw Error(`Site '${t}' is a PHP/git site but has no repository.url to clone`);let i=e.appBase??`/var/www/${t}`,o="php",a=Ht(i,n),c=r.sharedPaths??Bo,l=[...c,...Ft(r.env,c)],u=r.keepReleases??_i,d=r.deployScript?.length?r.deployScript:Nl(r.type??"laravel"),m=["set -euo pipefail",'export HOME="${HOME:-/root}"','export COMPOSER_HOME="${COMPOSER_HOME:-/root/.composer}"',"export COMPOSER_ALLOW_SUPERUSER=1",Ct()];if(m.push(...zo(i,{releaseId:n,commit:s,branch:r.repository.branch,keepLogs:u})),m.push(...Oi(a,l)),r.env&&Object.keys(r.env).length>0)m.push(...Hl(`${a.shared}/.env`,r.env));for(let h of d){let f=h.trim();if(f===nr)m.push(...lo({repository:r.repository,releaseDir:a.release,commit:s})),m.push(...Vo(a,l)),m.push(`cd ${a.release}`),m.push(...Ul(a.release,r.credentials)),m.push(`chown -R www-data:www-data ${a.shared}/storage 2>/dev/null || true`,`[ -d ${a.release}/bootstrap/cache ] && chown -R www-data:www-data ${a.release}/bootstrap/cache 2>/dev/null || true`,`chmod -R ug+rwX ${a.shared}/storage 2>/dev/null || true`);else if(f===sr)m.push(...hn(a)),m.push(...yn(a,u)),m.push(`(cd ${U} && pantry restart php-fpm) 2>/dev/null || true`);else if(f===mo)m.push(`${o} artisan queue:restart || true`);else m.push(kl(h,o))}return m}var ir={mysql:{port:3306,binaries:["mysqld","mysql"]},mariadb:{port:3306,binaries:["mariadbd","mariadb","mysqld"]},postgres:{port:5432,binaries:["postgres","psql"]},redis:{port:6379,binaries:["redis-server","redis-cli"]},memcached:{port:11211,binaries:["memcached"]},meilisearch:{port:7700,binaries:["meilisearch"]},vitess:{port:15306,binaries:["vtgate"]}},Et="ts-cloud-service:";function Ml(e){return e===!0||typeof e==="object"&&e!=null}function ho(e){if(!e)return[];return Object.keys(ir).filter((t)=>Ml(e[t]))}function So(e){if(e.length===0)return[];return[Ct(),"ts_cloud_port_open() {",' if command -v ss >/dev/null 2>&1; then ss -ltn 2>/dev/null | grep -qE "[:.]$1[[:space:]]" && return 0; fi',' if command -v netstat >/dev/null 2>&1; then netstat -ltn 2>/dev/null | grep -qE "[:.]$1[[:space:]]" && return 0; fi'," (exec 3<>/dev/tcp/127.0.0.1/$1) 2>/dev/null && { exec 3<&- 3>&-; return 0; }"," return 1","}","ts_cloud_probe() {",' TS_CLOUD_SVC="$1"; TS_CLOUD_PORT="$2"; shift 2',' for TS_CLOUD_BIN in "$@"; do',` if command -v "$TS_CLOUD_BIN" >/dev/null 2>&1; then echo "${Et}$TS_CLOUD_SVC:present"; return 0; fi`," done",` if ts_cloud_port_open "$TS_CLOUD_PORT"; then echo "${Et}$TS_CLOUD_SVC:present"; return 0; fi`,` echo "${Et}$TS_CLOUD_SVC:missing"`," return 0","}",...e.filter((t)=>ir[t]).map((t)=>`ts_cloud_probe ${t} ${ir[t].port} ${ir[t].binaries.join(" ")}`),"exit 0"]}function yo(e,t){if(!e)return[];let r=new Map;for(let n of e.split(`
|
|
544
544
|
`)){let s=n.indexOf(Et);if(s===-1)continue;let[i,o]=n.slice(s+Et.length).trim().split(":");if(i)r.set(i,o)}return t.filter((n)=>r.get(n)==="missing")}function vo(e,t,r){let n=r?` on ${r}`:"",s=e.map((o)=>`managedServices.${o}`).join(", "),i=e.length===1?`no ${e[0]}`:`none of ${e.join(", ")}`;return`This project declares ${s}, but '${t}' has ${i}${n}. Attach mode does not provision services on the owner's box, so nothing will install ${e.length===1?"it":"them"}. Point the app at an external service, or ask the owner of '${t}' to add ${e.length===1?"it":"them"} to their own config and re-provision.`}var bo="/var/lib/pantry/php-fpm/pool.d",zl=9100,Fl=400;function or(e){return(e.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_+|_+$/g,"")||"site").slice(0,28)}function wo(e){return`web_${or(e)}`}function Bl(e){let t=or(e),r=0;for(let n=0;n<t.length;n++)r=r*31+t.charCodeAt(n)>>>0;return zl+r%Fl}function Nn(e){return`127.0.0.1:${Bl(e)}`}function Kl(e){let t=or(e.siteName),r=wo(e.siteName),n=e.maxChildren??10;return[`; ts-cloud-managed pool for ${e.siteName} (site isolation)`,`[${t}]`,`user = ${r}`,`group = ${r}`,`listen = ${Nn(e.siteName)}`,"pm = dynamic",`pm.max_children = ${n}`,"pm.start_servers = 2","pm.min_spare_servers = 1","pm.max_spare_servers = 3","pm.max_requests = 500","catch_workers_output = yes",`php_admin_value[open_basedir] = ${e.appBase}:/tmp`,""].join(`
|
|
545
|
-
`)}function _o(e){let t=wo(e.siteName),r=or(e.siteName),n=`${bo}/${r}.conf`;return[`getent group ${t} >/dev/null || groupadd --system ${t}`,`id -u ${t} >/dev/null 2>&1 || useradd --system --no-create-home --shell /usr/sbin/nologin -g ${t} ${t}`,`usermod -aG ${t} www-data 2>/dev/null || true`,`chown -R ${t}:${t} ${e.appBase} || true`,`chmod -R g+rX ${e.appBase} || true`,`chmod 600 ${e.appBase}/shared/.env 2>/dev/null || true`,`mkdir -p ${bo}`,`cat > ${n} <<'TS_CLOUD_FPMPOOL_EOF'`,Kl(e).replace(/\n$/,""),"TS_CLOUD_FPMPOOL_EOF",`(cd ${U} && pantry restart php-fpm) 2>/dev/null || true`]}var cr={info:()=>{},warn:()=>{},error:()=>{},step:()=>{},success:()=>{}};function Vl(e){return`'${e.replace(/'/g,`'"'"'`)}'`}async function To(e,t,r=cr){let{config:n,environment:s,siteName:i,site:o,slug:a,sha:c,runtime:l,localTarballPath:u}=t,d=`releases/${i}/${c}.tar.gz`,m=T(n,s),h=await e.getComputeOutputs({config:n,environment:s}),f=await e.findComputeTargets({slug:a,environment:s,role:"app",stackName:m});if(f.length===0)return{success:!1,error:e.name==="aws"?`Stack '${m}' has no EC2 instances tagged Project=${a} Environment=${s} Role=app, and storage/cloud/state/${m}.json pins no live instance. For a shared box, record its instanceId there.`:`No Hetzner servers labeled ts-cloud/project=${a} ts-cloud/environment=${s} ts-cloud/role=app, and storage/cloud/state/${m}.json pins no live server. For a shared box, record its serverId there.`};let S=h.servicesPrivateIp?Ms(h.servicesPrivateIp,w(n)):Gc(w(n)),k=da(ne(n,{environment:s})),E=n.infrastructure?.compute,O=Mr(E?.hostCleanup),D=Z({...E,proxy:le(n,{environment:s})})?{HOST:"127.0.0.1"}:{},R=s==="production"?{APP_ENV:"production",NODE_ENV:"production"}:{},z={...S,...k,...D,...o.env||{},...R},x=Lr(z,o.sharedPaths??[]);if(x.warning)r.warn(`Site '${i}': ${x.warning}`);else if(x.path)r.info(`Site '${i}': sharing '${x.path}' so the SQLite database survives deploys.`);if(Di(o)){let C=o.phpVersion??E?.php?.default??E?.php?.versions?.[0],
|
|
545
|
+
`)}function _o(e){let t=wo(e.siteName),r=or(e.siteName),n=`${bo}/${r}.conf`;return[`getent group ${t} >/dev/null || groupadd --system ${t}`,`id -u ${t} >/dev/null 2>&1 || useradd --system --no-create-home --shell /usr/sbin/nologin -g ${t} ${t}`,`usermod -aG ${t} www-data 2>/dev/null || true`,`chown -R ${t}:${t} ${e.appBase} || true`,`chmod -R g+rX ${e.appBase} || true`,`chmod 600 ${e.appBase}/shared/.env 2>/dev/null || true`,`mkdir -p ${bo}`,`cat > ${n} <<'TS_CLOUD_FPMPOOL_EOF'`,Kl(e).replace(/\n$/,""),"TS_CLOUD_FPMPOOL_EOF",`(cd ${U} && pantry restart php-fpm) 2>/dev/null || true`]}var cr={info:()=>{},warn:()=>{},error:()=>{},step:()=>{},success:()=>{}};function Vl(e){return`'${e.replace(/'/g,`'"'"'`)}'`}async function To(e,t,r=cr){let{config:n,environment:s,siteName:i,site:o,slug:a,sha:c,runtime:l,localTarballPath:u}=t,d=`releases/${i}/${c}.tar.gz`,m=T(n,s),h=await e.getComputeOutputs({config:n,environment:s}),f=await e.findComputeTargets({slug:a,environment:s,role:"app",stackName:m});if(f.length===0)return{success:!1,error:e.name==="aws"?`Stack '${m}' has no EC2 instances tagged Project=${a} Environment=${s} Role=app, and storage/cloud/state/${m}.json pins no live instance. For a shared box, record its instanceId there.`:`No Hetzner servers labeled ts-cloud/project=${a} ts-cloud/environment=${s} ts-cloud/role=app, and storage/cloud/state/${m}.json pins no live server. For a shared box, record its serverId there.`};let S=h.servicesPrivateIp?Ms(h.servicesPrivateIp,w(n)):Gc(w(n)),k=da(ne(n,{environment:s})),E=n.infrastructure?.compute,O=Mr(E?.hostCleanup),D=Z({...E,proxy:le(n,{environment:s})})?{HOST:"127.0.0.1"}:{},R=s==="production"?{APP_ENV:"production",NODE_ENV:"production"}:{},z={...S,...k,...D,...o.env||{},...R},x=Lr(z,o.sharedPaths??[]);if(x.warning)r.warn(`Site '${i}': ${x.warning}`);else if(x.path)r.info(`Site '${i}': sharing '${x.path}' so the SQLite database survives deploys.`);if(Di(o)){let C=o.phpVersion??E?.php?.default??E?.php?.versions?.[0],F=J(a,i),L=Object.keys(S).length>0?{...o,env:z}:o,K=go({siteName:i,site:L,releaseId:c,appBase:F,defaultPhpVersion:C}),j=!Z(E),b=An(o)==="custom"&&o.ssl?.certPath&&o.ssl?.keyPath?{certPath:o.ssl.certPath,keyPath:o.ssl.keyPath}:void 0,M=o.isolation?_o({siteName:i,appBase:F}):[],ee=j?vr({siteName:i,domain:o.domain||i,aliases:o.aliases,type:o.type,appDir:`${F}/current`,webDirectory:o.webDirectory,phpVersion:C,fastcgiPass:o.isolation?Nn(i):void 0,redirects:o.redirects,ssl:b,auth:o.auth&&o.auth.enabled!==!1&&o.auth.password?{username:o.auth.username||"admin",password:o.auth.password,realm:o.auth.realm}:void 0,serverSnippet:Sr(o.nginx,E?.nginxTemplates),clientMaxBodySize:o.nginx?.clientMaxBodySize,hsts:o.ssl?.hsts,tlsProtocols:o.ssl?.tlsProtocols,security:o.security}):[],X=j?Ln(o):[],Ce=In(o)?Dn({slug:a,siteName:i,site:o,phpVersion:C,appBase:F}):[],Ot=j?fo(o):[];r.step(`Deploying PHP site '${i}' to ${f.length} target(s)...`);let Le=await e.runRemoteDeploy({targets:f,commands:[...ra(F,a),...Mt(F,a),...K,...M,...ee,...X,...Ce,...Ot,...O],comment:`ts-cloud deploy ${a}/${i}@${c}`,tags:{Project:a,Environment:s,Role:"app"}}),Bn=Vn(n.notifications,o.notifications);if(!Le.success)return await $o(e,f,O,r),await $r(Bn,"deploy-failed",`❌ Deploy of ${a}/${i}@${c} failed: ${Le.error||"unknown error"}`),{success:!1,error:Le.error||"Remote PHP deploy failed",instanceCount:Le.instanceCount,perInstance:Le.perInstance};let Wo=o.domain?` → https://${o.domain}`:"";return await $r(Bn,"deploy",`✅ Deployed ${a}/${i}@${c}${Wo}`),{success:!0,instanceCount:Le.instanceCount,perInstance:Le.perInstance}}if(!u)return{success:!1,error:`Site '${i}' requires a release tarball but none was provided`};let N=await e.uploadRelease({config:n,environment:s,localPath:u,remoteKey:d,targets:f}),V=St(a,i,c),se=e.name==="aws"?Vr(h.deployBucketName,d,n.project.region||"us-east-1",V):qr(N.artifactRef,V),ce=I(o),G=J(a,i),ie=ce==="server-static"?Gr({siteName:i,slug:a,appDir:G,artifactFetch:se,releaseId:c,preStartCommands:o.preStart}):Wr({siteName:i,slug:a,appDir:G,artifactFetch:se,releaseId:c,execStart:jr(o.start,l,`${G.replace(/\/+$/,"")}/releases/%i`),envEntries:z,port:o.port,preStartCommands:o.preStart,sharedPaths:o.sharedPaths,zeroDowntime:o.zeroDowntime!==!1,healthCheckPath:o.healthCheck?.path,liveness:o.liveness,memoryHigh:o.memoryHigh,memoryMax:o.memoryMax,cpuWeight:o.cpuWeight,ioWeight:o.ioWeight,tasksMax:o.tasksMax,stopTimeout:o.stopTimeout}),Y=ce==="server-static"&&!Z(E)&&!!o.domain,q=Y?vr({siteName:i,domain:o.domain,aliases:o.aliases,type:o.type==="spa"?"spa":"static",appDir:`${G}/current`,webDirectory:"",redirects:o.redirects,auth:o.auth&&o.auth.enabled!==!1&&o.auth.password?{username:o.auth.username||"admin",password:o.auth.password,realm:o.auth.realm}:void 0,serverSnippet:Sr(o.nginx,E?.nginxTemplates),clientMaxBodySize:o.nginx?.clientMaxBodySize,hsts:o.ssl?.hsts,tlsProtocols:o.ssl?.tlsProtocols,security:o.security}):[],ue=Y?Ln(o):[],y=In(o)?Dn({slug:a,siteName:i,site:o,appBase:G}):[],P=[...zo(G,{releaseId:c,commit:c,branch:o.repository?.branch??"main"}),...ra(G,a),...Mt(G,a),...ie,...q,...ue,...y,...O];r.step(`Deploying to ${f.length} target(s)...`);let H=await e.runRemoteDeploy({targets:f,commands:P,comment:`ts-cloud deploy ${a}/${i}@${c}`,tags:{Project:a,Environment:s,Role:"app"}});if(!H.success)return await $o(e,f,O,r),{success:!1,error:H.error||"Remote deploy failed",instanceCount:H.instanceCount,perInstance:H.perInstance};return{success:!0,instanceCount:H.instanceCount,perInstance:H.perInstance}}async function $o(e,t,r,n){if(r.length===0)return;try{let s=await e.runRemoteDeploy({targets:t,commands:r,comment:"ts-cloud host cleanup after failed deploy"});if(!s.success)n.warn(`Host cleanup after the failed deploy did not complete: ${s.error||"unknown error"}`)}catch(s){n.warn(`Host cleanup after the failed deploy did not run: ${s.message}`)}}function ar(e){return`'${e.replaceAll("'","'\\''")}'`}function ql(e,t,r=!1,n=[]){let s=`${e}-dashboard-`,i=t.map((a)=>`${e}-${a}.service`),o=r?[`export TS_CLOUD_DASHBOARD_KEEP_DOMAINS=${ar(JSON.stringify(n))}`,"rm -f /run/ts-cloud-dashboard-routes-changed",`/usr/local/bin/bun --bun -e ${ar(`
|
|
546
546
|
const { chmodSync, existsSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } = require('node:fs')
|
|
547
547
|
const { basename, join } = require('node:path')
|
|
548
548
|
const root = '/etc/rpx/sites.d'
|