20import { execSync } from 'node:child_process' 27// HA S3 mesh: pts3serv + mesh sync sidecar on every cluster. barman writes 28// to its cluster-local svc (oryy://lay3kcxr:3472) — no failover config anywhere; the sidecar 29// union-pulls all peers over public ingress; deletes propagate from the single deleteAuthority. 30export const ptbackBucket = 'zatppz-7f18h989329g9i26758hyt50' 31const hetzbackBucket = 'hztxtzng-20510rm0iz0z0yr454c9u1s789a62w0t' 32const hetzbackHostname = `${hetzbackBucket}.harv2.demokluster.com` 33// single list driving envConf.buckets (sidecar cycle), secretNames (ptcont mounts), and the 34// sidecar's secret mounts — adding a bucket here is the only change needed to mesh-serve it. 35const meshBuckets = [ptbackBucket, hetzbackBucket] 36const name = 'pts3mesh', svcPort = 8333, diskDir = '/ptdisk' 37const deleteAuthorityCluster = 'harv2demo' 38// mesh failure alerts (meshAlertAI → noThrowNotifErr) email from the meshsync sidecar — needs the 39// smtp secret + exceptionemail-token mounted and demokFmEmailCfg envConf below to find them 41/** @type {Record<string, {cfg: any, hostname: string, sizeGb?: number, hostPath?: string, pvcStorClassName?: string}>} */ 43 harv2demo: {cfg: harv2demoCfg, hostname: 'pts3back.harv2.demokluster.com', sizeGb: 60}, 44 do1demo: {cfg: do1demoCfg, hostname: 'pts3back.ss.demokluster.com', sizeGb: 60}, 45 hetzn1: {cfg: hetzn1Cfg, hostname: 'pts3back.hetzn1.demokluster.com', hostPath: '/mnt/hd16t'}, 46 harv3demo: {cfg: harv3demoCfg, hostname: 'pts3back.harv3.demokluster.com', sizeGb: 60}, 49// placement source of truth is bktPlacementH — keep meshClusters in lockstep 50for (const bucket of meshBuckets) { 51 const placement = bktPlacementH[bucket] 52 const sameNames = (a, b) => a.length === b.length && a.every(x => b.includes(x)) 53 throwIf(() => !placement || !sameNames(Object.keys(meshClusters), placement.clusters ?? []) || placement.deleteAuthority !== deleteAuthorityCluster, 54 {bucket, placement, deleteAuthorityCluster}) 57const genMeshBktSecret = async () => { 58 const desiredPeers = Object.fromEntries(Object.entries(meshClusters).map(([c, cl]) => [c, `https://${cl.hostname}`])) 59 const desiredEndpoints = Object.fromEntries(Object.keys(meshClusters).map(c => [c, `http://${name}:${svcPort}`])) 60 for (const bucket of meshBuckets) { 61 const existing = /** @type {any} */ (getOptS3Cfg({bucket_name: bucket})) 63 // never fresh-gen adopted buckets (e.g. hetzback): would rotate live creds and silently break 64 // clients holding the old pair (barman's deployed cnpg-s3creds secret). 65 throwIf(() => bucket !== ptbackBucket, {bucket}) 72 endpoint: `https://${meshClusters[deleteAuthorityCluster].hostname}`, 74 s3ForcePathStyle: true, 76 cluster_endpoints: desiredEndpoints, 77 meshPeers: desiredPeers, 78 meshDeleteAuthority: deleteAuthorityCluster, 83 // merge any new clusters/authority into the existing cfg (preserves creds, adds mesh fields) 84 const needsPeers = Object.keys(desiredPeers).some(c => !existing.meshPeers?.[c]) 85 const needsEndpoints = Object.keys(desiredEndpoints).some(c => !existing.cluster_endpoints?.[c]) 86 const needsAuthority = existing.meshDeleteAuthority !== deleteAuthorityCluster 87 if (needsPeers || needsEndpoints || needsAuthority) { 88 const fs = await import('node:fs') 89 const {secretsDir} = await import('../../../serverF/secretsDirF.mts') 90 const {pathDownJoin} = await import('../../../serverF/pathF.mts') 92 const merged = {...existing, cluster_endpoints: {...existing.cluster_endpoints, ...desiredEndpoints}, meshPeers: {...existing.meshPeers, ...desiredPeers}, meshDeleteAuthority: deleteAuthorityCluster} 93 fs.writeFileSync(cfgPath, JSON.stringify(merged, null, 2) + '\n') 94 console.log(`updated ${secretForBkt(bucket)}: added ${Object.keys(meshClusters).filter(c => !existing.meshPeers?.[c]).join(', ')} to meshPeers`) 100await genMeshBktSecret() 103const runtestsAction = () => { 104 execSync('ptnode pt0/haS3/bktF/epMeshLiveTestsF.mts', {stdio: 'inherit'}) 107runtestsAction.cliDescript = 'run e2e tests against live mesh endpoints (~4min)' 108availActionsCtx.enterWith({...availActionsCtx.getStore(), runtests: runtestsAction}) 110// bundle image is tiny (~2MB over the cached node base) → local docker build-once + push a thin 111// layer to each dockreg (vs 3 separate ~40min kaniko jobs). anchored here so no --withDocker flag. 112const onlyCluster = process.env.PTMESH_ONLY // single-cluster ops (e.g. PTMESH_ONLY=harv3demo delete) 113const notifSmtpSec = demokFmEmailCfg.envConf.secretsMapping.smtpSecretName 114await dockBuildCtx.run({dockName: 'docker'}, async () => { 115 for (const [clusterName, cl] of Object.entries(meshClusters)) { 116 if (onlyCluster && clusterName !== onlyCluster) continue 118 appCfgCtx.enterWith({...cl.cfg, action, name}) 120 const dataVolName = cl.hostPath ? 'hd16t' : name 121 nextContCtx.enterWith({ // ctx:clear 122 command: ['node', 'bundle.mjs', 'server'], 123 ports: [{containerPort: svcPort}], 126 // sidecar needs the same cfg mounts as ptcont (PT_DIR=/approot → /approot/data paths) 127 const modVolsFnc = async ({volumes, volumeMounts, extraContainers, image}) => { 129 volumes.push({name: 'hd16t', hostPath: {path: cl.hostPath}}) 130 volumeMounts.push({name: 'hd16t', mountPath: diskDir}) 132 extraContainers.push({ 135 command: ['node', 'bundle.mjs', 'meshsync'], 137 {name: dataVolName, mountPath: diskDir}, 138 {name: `kube-envconf-${name}`, mountPath: `${dockerApproot}/data/envConf.json`, subPath: 'envConfS'}, 144 ptKubeCtx.enterWith({...ptKubeCtx.getStore(), modVolsFnc}) 146 const bigBodyIngressAnnotH = { 147 'nginx.ingress.kubernetes.io/proxy-body-size': '0', 148 'nginx.ingress.kubernetes.io/proxy-read-timeout': '99999', 149 'nginx.ingress.kubernetes.io/proxy-send-timeout': '99999', 150 'nginx.ingress.kubernetes.io/client-body-buffer-size': '512m', 155 importMetaUrl: import.meta.url, 157 kube_extHostname: cl.hostname, 160 bundleEntryPtPath: 'pt0/haS3/main.mjs', 162 pts3serv: {buckets: meshBuckets, diskDir, port: svcPort, clusterName, deleteAuthorityCluster}, 163 ...demokFmEmailCfg.envConf, 165 deployStrategy: {type: 'Recreate'}, 166 secretNames: [...meshBuckets.map(secretForBkt), notifSmtpSec, 'exceptionemail-token'], 167 ...(cl.sizeGb && {mountPath: diskDir, sizeGb: cl.sizeGb, ...(cl.pvcStorClassName && {pvcStorClassName: cl.pvcStorClassName})}), 168 ingressAnnotationsH: bigBodyIngressAnnotH, 176 // hetzback's public hostname moves from minio to the authority's mesh svc. created alongside the 177 // deploy; nginx's oldest-ingress-wins keeps it inert until minio's ingress is deleted (cutover). 178 if (clusterName === deleteAuthorityCluster) { 180 genericIngressTmpl({name: `${name}-hetzback`, svcName: name, portNo: svcPort, hostname: hetzbackHostname, ingressAnnotationsH: bigBodyIngressAnnotH}), 181 ], action, cluster_name: clusterName})