UNPKG

@underpostnet/underpost

Version:

Underpost Platform — end-to-end CI/CD and application-delivery toolchain CLI. Covers bare metal, Kubernetes, K3s, kubeadm, LXD, container/image orchestration, secrets, databases, cron jobs, monitoring, SSH, runners, PWA + Workbox delivery, and release orc

183 lines (159 loc) 8.55 kB
/** * IPFS Cluster module for managing ipfs-cluster StatefulSet deployment on Kubernetes. * @module src/cli/ipfs.js * @namespace UnderpostIPFS */ import { loggerFactory } from '../server/logger.js'; import { shellExec } from '../server/process.js'; import fs from 'fs-extra'; import Underpost from '../index.js'; const logger = loggerFactory(import.meta); /** * @class UnderpostIPFS * @description Manages deployment of an ipfs-cluster StatefulSet on Kubernetes. * Credentials (cluster secret + peer identity) are generated once and persisted * to engine-private/ so the cluster identity survives redeployments. * @memberof UnderpostIPFS */ class UnderpostIPFS { static API = { /** * @method resolveCredentials * @description Resolves the IPFS cluster credentials from engine-private/ if they * already exist, otherwise generates new ones (hex cluster secret + peer identity * via ipfs-cluster-service init) and persists them with mode 0o600. * @param {string} privateDir - Absolute path to the engine-private directory. * @returns {{ CLUSTER_SECRET: string, IDENTITY_JSON: { id: string, private_key: string } }} * @memberof UnderpostIPFS */ resolveCredentials(privateDir) { const secretPath = `${privateDir}/ipfs-cluster-secret`; const identityPath = `${privateDir}/ipfs-cluster-identity.json`; if (fs.existsSync(secretPath) && fs.existsSync(identityPath)) { logger.info('Reusing existing IPFS cluster credentials from engine-private/'); return { CLUSTER_SECRET: fs.readFileSync(secretPath, 'utf8').trim(), IDENTITY_JSON: JSON.parse(fs.readFileSync(identityPath, 'utf8')), }; } logger.info('Generating new IPFS cluster credentials and persisting to engine-private/'); // ipfs-cluster-service requires CLUSTER_SECRET as a 64-char hex string. // base64 (openssl rand -base64 32) contains '/', '+', '=' which are invalid hex bytes. const CLUSTER_SECRET = shellExec("od -vN 32 -An -tx1 /dev/urandom | tr -d ' \\n'", { stdout: true, }).trim(); const tmpDir = '/tmp/ipfs-cluster-identity'; shellExec(`rm -rf ${tmpDir} && mkdir -p ${tmpDir}`); shellExec(`docker run --rm -v ${tmpDir}:/data/ipfs-cluster ipfs/ipfs-cluster init -f`); const IDENTITY_JSON = JSON.parse(shellExec(`cat ${tmpDir}/identity.json`, { stdout: true }).trim()); shellExec(`rm -rf ${tmpDir}`); fs.ensureDirSync(privateDir); fs.writeFileSync(secretPath, CLUSTER_SECRET, { mode: 0o600 }); fs.writeFileSync(identityPath, JSON.stringify(IDENTITY_JSON, null, 2), { mode: 0o600 }); logger.info(`IPFS cluster credentials saved (peer ID: ${IDENTITY_JSON.id})`); return { CLUSTER_SECRET, IDENTITY_JSON }; }, /** * @method teardown * @description Deletes the existing ipfs-cluster StatefulSet, its Kubernetes Secret, * env ConfigMap, and all PVCs so the next deployment initialises a clean data volume * (ensuring the correct datastore profile is applied by the init container). * @param {object} options * @param {string} options.namespace - Kubernetes namespace. * @param {number} ipfsReplicas - Number of replicas whose PVCs must be removed. * @memberof UnderpostIPFS */ teardown(options, ipfsReplicas) { logger.info(`Tearing down existing ipfs-cluster deployment in namespace '${options.namespace}'`); shellExec(`kubectl delete statefulset ipfs-cluster -n ${options.namespace} --ignore-not-found`); shellExec(`kubectl delete secret ipfs-cluster-secret -n ${options.namespace} --ignore-not-found`); shellExec(`kubectl delete configmap env-config -n ${options.namespace} --ignore-not-found`); for (let i = 0; i < ipfsReplicas; i++) { shellExec( `kubectl delete pvc cluster-storage-ipfs-cluster-${i} ipfs-storage-ipfs-cluster-${i} -n ${options.namespace} --ignore-not-found`, ); } }, /** * @method applySecrets * @description Creates (or idempotently updates) the Kubernetes Secret and env ConfigMap * that the StatefulSet pods read at startup. * - Secret `ipfs-cluster-secret`: cluster-secret + bootstrap-peer-priv-key * - ConfigMap `env-config`: bootstrap-peer-id + CLUSTER_SVC_NAME * @param {{ CLUSTER_SECRET: string, IDENTITY_JSON: { id: string, private_key: string } }} credentials * @param {object} options * @param {string} options.namespace - Kubernetes namespace. * @memberof UnderpostIPFS */ applySecrets({ CLUSTER_SECRET, IDENTITY_JSON }, options) { logger.info('Applying IPFS cluster Kubernetes Secret and env ConfigMap'); shellExec( `kubectl create secret generic ipfs-cluster-secret \ --from-literal=cluster-secret=${CLUSTER_SECRET} \ --from-literal=bootstrap-peer-priv-key=${IDENTITY_JSON.private_key} \ --dry-run=client -o yaml | kubectl apply -f - -n ${options.namespace}`, ); shellExec( `kubectl create configmap env-config \ --from-literal=bootstrap-peer-id=${IDENTITY_JSON.id} \ --from-literal=CLUSTER_SVC_NAME=ipfs-cluster \ --dry-run=client -o yaml | kubectl apply -f - -n ${options.namespace}`, ); }, /** * @method applyManifests * @description Applies host-level sysctl tuning (Kind clusters only), the storage class, * the kustomize manifests, and scales the StatefulSet to the requested replica count. * @param {object} options * @param {string} options.namespace - Kubernetes namespace. * @param {boolean} [options.kubeadm] - Whether the cluster is Kubeadm-based. * @param {boolean} [options.k3s] - Whether the cluster is K3s-based. * @param {string} underpostRoot - Absolute path to the underpost project root. * @param {number} ipfsReplicas - Desired replica count. * @memberof UnderpostIPFS */ applyManifests(options, underpostRoot, ipfsReplicas) { // Apply UDP buffer sysctl on every Kind node so QUIC (used by IPFS) can reach the // recommended 7.5 MB buffer size. Kind nodes are containers and do NOT inherit the // host sysctl values, so this must be set via docker exec on each node directly. shellExec( `sudo sysctl -w net.core.rmem_max=7500000 sudo sysctl -w net.core.wmem_max=7500000`, ); shellExec(`kubectl apply -f ${underpostRoot}/manifests/ipfs/storage-class.yaml`); shellExec(`kubectl apply -k ${underpostRoot}/manifests/ipfs -n ${options.namespace}`); // statefulset.yaml hardcodes replicas: 3 as the ceiling; scale down here if needed. shellExec(`kubectl scale statefulset ipfs-cluster --replicas=${ipfsReplicas} -n ${options.namespace}`); }, /** * @method deploy * @description Full orchestration of the ipfs-cluster StatefulSet deployment: * optionally pulls images, resolves or generates credentials, tears down any existing * deployment, applies secrets, applies manifests, and waits for all pods to be Running. * @param {object} options - Cluster init options forwarded from UnderpostCluster.API.init. * @param {string} options.namespace - Kubernetes namespace. * @param {boolean} [options.pullImage] - Whether to pull container images first. * @param {boolean} [options.kubeadm] - Whether the cluster is Kubeadm-based. * @param {boolean} [options.k3s] - Whether the cluster is K3s-based. * @param {string|number} [options.replicas] - Override replica count (defaults to 3). * @param {string} underpostRoot - Absolute path to the underpost project root. * @memberof UnderpostIPFS */ async deploy(options, underpostRoot) { if (options.pullImage === true) { Underpost.cluster.pullImage('ipfs/kubo:latest', options); Underpost.cluster.pullImage('ipfs/ipfs-cluster:latest', options); } const credentials = Underpost.ipfs.resolveCredentials(`${underpostRoot}/engine-private`); const ipfsReplicas = options.replicas ? parseInt(options.replicas) : 3; Underpost.ipfs.teardown(options, ipfsReplicas); Underpost.ipfs.applySecrets(credentials, options); Underpost.ipfs.applyManifests(options, underpostRoot, ipfsReplicas); logger.info(`Waiting for ${ipfsReplicas} ipfs-cluster pod(s) to reach Running state`); for (let i = 0; i < ipfsReplicas; i++) { await Underpost.test.statusMonitor(`ipfs-cluster-${i}`, 'Running', 'pods', 1000, 60 * 15); } }, }; } export default UnderpostIPFS;