@underpostnet/underpost 2.99.8 → 3.0.0
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/.env.development +2 -1
- package/.env.production +1 -0
- package/.env.test +2 -1
- package/.github/workflows/publish.ci.yml +18 -34
- package/.vscode/extensions.json +8 -50
- package/.vscode/settings.json +0 -77
- package/CHANGELOG.md +67 -1
- package/{cli.md → CLI-HELP.md} +48 -41
- package/README.md +3 -3
- package/bin/build.js +1 -15
- package/bin/deploy.js +4 -133
- package/bin/file.js +1 -5
- package/bin/zed.js +63 -2
- package/jsdoc.json +1 -2
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
- package/manifests/deployment/fastapi/initial_data.sh +4 -52
- package/manifests/ipfs/configmap.yaml +57 -0
- package/manifests/ipfs/headless-service.yaml +35 -0
- package/manifests/ipfs/kustomization.yaml +8 -0
- package/manifests/ipfs/statefulset.yaml +149 -0
- package/manifests/ipfs/storage-class.yaml +9 -0
- package/package.json +5 -5
- package/scripts/k3s-node-setup.sh +89 -0
- package/scripts/lxd-vm-setup.sh +23 -0
- package/scripts/rocky-setup.sh +1 -13
- package/src/cli/baremetal.js +7 -9
- package/src/cli/cluster.js +72 -121
- package/src/cli/deploy.js +8 -5
- package/src/cli/index.js +31 -30
- package/src/cli/ipfs.js +184 -0
- package/src/cli/lxd.js +191 -236
- package/src/cli/repository.js +4 -1
- package/src/client/components/core/VanillaJs.js +0 -25
- package/src/client/services/user/user.management.js +0 -5
- package/src/client/services/user/user.service.js +1 -1
- package/src/index.js +12 -1
- package/src/server/client-build-docs.js +26 -7
- package/src/server/conf.js +1 -1
- package/src/server/logger.js +22 -10
- package/.vscode/zed.keymap.json +0 -39
- package/.vscode/zed.settings.json +0 -20
- package/manifests/lxd/underpost-setup.sh +0 -163
package/src/cli/index.js
CHANGED
|
@@ -214,6 +214,7 @@ program
|
|
|
214
214
|
.option('--postgresql', 'Initializes the cluster with a PostgreSQL statefulset.')
|
|
215
215
|
.option('--mongodb4', 'Initializes the cluster with a MongoDB 4.4 service.')
|
|
216
216
|
.option('--valkey', 'Initializes the cluster with a Valkey service.')
|
|
217
|
+
.option('--ipfs', 'Initializes the cluster with an ipfs-cluster statefulset.')
|
|
217
218
|
.option('--contour', 'Initializes the cluster with Project Contour base HTTPProxy and Envoy.')
|
|
218
219
|
.option('--cert-manager', "Initializes the cluster with a Let's Encrypt production ClusterIssuer.")
|
|
219
220
|
.option('--dedicated-gpu', 'Initializes the cluster with dedicated GPU base resources and environment settings.')
|
|
@@ -242,12 +243,12 @@ program
|
|
|
242
243
|
.option('--init-host', 'Installs necessary Kubernetes node CLI tools (e.g., kind, kubeadm, docker, podman, helm).')
|
|
243
244
|
.option('--uninstall-host', 'Uninstalls all host components installed by init-host.')
|
|
244
245
|
.option('--config', 'Sets the base Kubernetes node configuration.')
|
|
245
|
-
.option('--worker', 'Sets the context for a worker node.')
|
|
246
246
|
.option('--chown', 'Sets the appropriate ownership for Kubernetes kubeconfig files.')
|
|
247
247
|
.option('--k3s', 'Initializes the cluster using K3s (Lightweight Kubernetes).')
|
|
248
248
|
.option('--hosts <hosts>', 'A comma-separated list of cluster hostnames or IP addresses.')
|
|
249
249
|
.option('--remove-volume-host-paths', 'Removes specified volume host paths after execution.')
|
|
250
250
|
.option('--namespace <namespace>', 'Kubernetes namespace for cluster operations (defaults to "default").')
|
|
251
|
+
.option('--replicas <replicas>', 'Sets a custom number of replicas for statefulset deployments.')
|
|
251
252
|
.action(Underpost.cluster.init)
|
|
252
253
|
.description('Manages Kubernetes clusters, defaulting to Kind cluster initialization.');
|
|
253
254
|
|
|
@@ -295,6 +296,10 @@ program
|
|
|
295
296
|
.option('--namespace <namespace>', 'Kubernetes namespace for deployment operations (defaults to "default").')
|
|
296
297
|
.option('--kind-type <kind-type>', 'Specifies the Kind cluster type for deployment operations.')
|
|
297
298
|
.option('--port <port>', 'Sets up port forwarding from local to remote ports.')
|
|
299
|
+
.option(
|
|
300
|
+
'--expose-port <port>',
|
|
301
|
+
'Sets the local:remote port to expose when --expose is active (overrides auto-detected service port).',
|
|
302
|
+
)
|
|
298
303
|
.option('--cmd <cmd>', 'Custom initialization command for deployment (comma-separated commands).')
|
|
299
304
|
.description('Manages application deployments, defaulting to deploying development pods.')
|
|
300
305
|
.action(Underpost.deploy.callback);
|
|
@@ -613,37 +618,33 @@ program
|
|
|
613
618
|
|
|
614
619
|
program
|
|
615
620
|
.command('lxd')
|
|
616
|
-
.option('--init', 'Initializes LXD on the current machine.')
|
|
617
|
-
.option('--reset', '
|
|
618
|
-
.option('--install', 'Installs
|
|
619
|
-
.option('--dev', '
|
|
620
|
-
.option('--create-virtual-network', 'Creates
|
|
621
|
-
.option('--
|
|
622
|
-
.option('--
|
|
623
|
-
.option('--
|
|
624
|
-
.option('--
|
|
625
|
-
.option('--
|
|
626
|
-
.option('--
|
|
627
|
-
.option('--
|
|
628
|
-
.option('--
|
|
629
|
-
.option('--
|
|
621
|
+
.option('--init', 'Initializes LXD on the current machine via preseed.')
|
|
622
|
+
.option('--reset', 'Removes the LXD snap and purges all data.')
|
|
623
|
+
.option('--install', 'Installs the LXD snap.')
|
|
624
|
+
.option('--dev', 'Use local paths instead of the global npm installation.')
|
|
625
|
+
.option('--create-virtual-network', 'Creates the lxdbr0 bridge network.')
|
|
626
|
+
.option('--ipv4-address <cidr>', 'IPv4 address/CIDR for the lxdbr0 bridge network (default: "10.250.250.1/24").')
|
|
627
|
+
.option('--create-admin-profile', 'Creates the admin-profile for VM management.')
|
|
628
|
+
.option('--control', 'Initialize the target VM as a K3s control plane node.')
|
|
629
|
+
.option('--worker', 'Initialize the target VM as a K3s worker node.')
|
|
630
|
+
.option('--create-vm <vm-name>', 'Copy the LXC launch command for a new K3s VM to the clipboard.')
|
|
631
|
+
.option('--delete-vm <vm-name>', 'Stop and delete the specified VM.')
|
|
632
|
+
.option('--init-vm <vm-name>', 'Run k3s-node-setup.sh on the specified VM (use with --control or --worker).')
|
|
633
|
+
.option('--info-vm <vm-name>', 'Display full configuration and status for the specified VM.')
|
|
634
|
+
.option('--test <vm-name>', 'Run connectivity and health checks on the specified VM.')
|
|
635
|
+
.option('--root-size <gb-size>', 'Root disk size in GiB for --create-vm (default: 32).')
|
|
630
636
|
.option(
|
|
631
637
|
'--join-node <nodes>',
|
|
632
|
-
'
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
)
|
|
638
|
-
.option(
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
)
|
|
642
|
-
.option('--workflow-id <workflow-id>', 'Sets the workflow ID context for LXD operations.')
|
|
643
|
-
.option('--vm-id <vm-id>', 'Sets the VM ID context for LXD operations.')
|
|
644
|
-
.option('--deploy-id <deploy-id>', 'Sets the deployment ID context for LXD operations.')
|
|
645
|
-
.option('--namespace <namespace>', 'Kubernetes namespace for LXD operations (defaults to "default").')
|
|
646
|
-
.description('Manages LXD containers and virtual machines.')
|
|
638
|
+
'Join a K3s worker to a control plane. Standalone format: "workerName,controlName". ' +
|
|
639
|
+
'When used with --init-vm --worker, provide just the control node name for auto-join.',
|
|
640
|
+
)
|
|
641
|
+
.option('--expose <vm-name:ports>', 'Proxy host ports to a VM (e.g., "k3s-control:80,443").')
|
|
642
|
+
.option('--delete-expose <vm-name:ports>', 'Remove proxied ports from a VM (e.g., "k3s-control:80,443").')
|
|
643
|
+
.option('--workflow-id <workflow-id>', 'Workflow ID to execute via runWorkflow.')
|
|
644
|
+
.option('--vm-id <vm-name>', 'Target VM name for workflow execution.')
|
|
645
|
+
.option('--deploy-id <deploy-id>', 'Deployment ID context for workflow execution.')
|
|
646
|
+
.option('--namespace <namespace>', 'Kubernetes namespace context (defaults to "default").')
|
|
647
|
+
.description('Manages LXD virtual machines as K3s nodes (control plane or workers).')
|
|
647
648
|
.action(Underpost.lxd.callback);
|
|
648
649
|
|
|
649
650
|
program
|
package/src/cli/ipfs.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPFS Cluster module for managing ipfs-cluster StatefulSet deployment on Kubernetes.
|
|
3
|
+
* @module src/cli/ipfs.js
|
|
4
|
+
* @namespace UnderpostIPFS
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { loggerFactory } from '../server/logger.js';
|
|
8
|
+
import { shellExec } from '../server/process.js';
|
|
9
|
+
import fs from 'fs-extra';
|
|
10
|
+
import Underpost from '../index.js';
|
|
11
|
+
|
|
12
|
+
const logger = loggerFactory(import.meta);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @class UnderpostIPFS
|
|
16
|
+
* @description Manages deployment of an ipfs-cluster StatefulSet on Kubernetes.
|
|
17
|
+
* Credentials (cluster secret + peer identity) are generated once and persisted
|
|
18
|
+
* to engine-private/ so the cluster identity survives redeployments.
|
|
19
|
+
* @memberof UnderpostIPFS
|
|
20
|
+
*/
|
|
21
|
+
class UnderpostIPFS {
|
|
22
|
+
static API = {
|
|
23
|
+
/**
|
|
24
|
+
* @method resolveCredentials
|
|
25
|
+
* @description Resolves the IPFS cluster credentials from engine-private/ if they
|
|
26
|
+
* already exist, otherwise generates new ones (hex cluster secret + peer identity
|
|
27
|
+
* via ipfs-cluster-service init) and persists them with mode 0o600.
|
|
28
|
+
* @param {string} privateDir - Absolute path to the engine-private directory.
|
|
29
|
+
* @returns {{ CLUSTER_SECRET: string, IDENTITY_JSON: { id: string, private_key: string } }}
|
|
30
|
+
* @memberof UnderpostIPFS
|
|
31
|
+
*/
|
|
32
|
+
resolveCredentials(privateDir) {
|
|
33
|
+
const secretPath = `${privateDir}/ipfs-cluster-secret`;
|
|
34
|
+
const identityPath = `${privateDir}/ipfs-cluster-identity.json`;
|
|
35
|
+
|
|
36
|
+
if (fs.existsSync(secretPath) && fs.existsSync(identityPath)) {
|
|
37
|
+
logger.info('Reusing existing IPFS cluster credentials from engine-private/');
|
|
38
|
+
return {
|
|
39
|
+
CLUSTER_SECRET: fs.readFileSync(secretPath, 'utf8').trim(),
|
|
40
|
+
IDENTITY_JSON: JSON.parse(fs.readFileSync(identityPath, 'utf8')),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
logger.info('Generating new IPFS cluster credentials and persisting to engine-private/');
|
|
45
|
+
|
|
46
|
+
// ipfs-cluster-service requires CLUSTER_SECRET as a 64-char hex string.
|
|
47
|
+
// base64 (openssl rand -base64 32) contains '/', '+', '=' which are invalid hex bytes.
|
|
48
|
+
const CLUSTER_SECRET = shellExec("od -vN 32 -An -tx1 /dev/urandom | tr -d ' \\n'", {
|
|
49
|
+
stdout: true,
|
|
50
|
+
}).trim();
|
|
51
|
+
|
|
52
|
+
const tmpDir = '/tmp/ipfs-cluster-identity';
|
|
53
|
+
shellExec(`rm -rf ${tmpDir} && mkdir -p ${tmpDir}`);
|
|
54
|
+
shellExec(`docker run --rm -v ${tmpDir}:/data/ipfs-cluster ipfs/ipfs-cluster init -f`);
|
|
55
|
+
const IDENTITY_JSON = JSON.parse(shellExec(`cat ${tmpDir}/identity.json`, { stdout: true }).trim());
|
|
56
|
+
shellExec(`rm -rf ${tmpDir}`);
|
|
57
|
+
|
|
58
|
+
fs.ensureDirSync(privateDir);
|
|
59
|
+
fs.writeFileSync(secretPath, CLUSTER_SECRET, { mode: 0o600 });
|
|
60
|
+
fs.writeFileSync(identityPath, JSON.stringify(IDENTITY_JSON, null, 2), { mode: 0o600 });
|
|
61
|
+
|
|
62
|
+
logger.info(`IPFS cluster credentials saved (peer ID: ${IDENTITY_JSON.id})`);
|
|
63
|
+
|
|
64
|
+
return { CLUSTER_SECRET, IDENTITY_JSON };
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @method teardown
|
|
69
|
+
* @description Deletes the existing ipfs-cluster StatefulSet, its Kubernetes Secret,
|
|
70
|
+
* env ConfigMap, and all PVCs so the next deployment initialises a clean data volume
|
|
71
|
+
* (ensuring the correct datastore profile is applied by the init container).
|
|
72
|
+
* @param {object} options
|
|
73
|
+
* @param {string} options.namespace - Kubernetes namespace.
|
|
74
|
+
* @param {number} ipfsReplicas - Number of replicas whose PVCs must be removed.
|
|
75
|
+
* @memberof UnderpostIPFS
|
|
76
|
+
*/
|
|
77
|
+
teardown(options, ipfsReplicas) {
|
|
78
|
+
logger.info(`Tearing down existing ipfs-cluster deployment in namespace '${options.namespace}'`);
|
|
79
|
+
shellExec(`kubectl delete statefulset ipfs-cluster -n ${options.namespace} --ignore-not-found`);
|
|
80
|
+
shellExec(`kubectl delete secret ipfs-cluster-secret -n ${options.namespace} --ignore-not-found`);
|
|
81
|
+
shellExec(`kubectl delete configmap env-config -n ${options.namespace} --ignore-not-found`);
|
|
82
|
+
for (let i = 0; i < ipfsReplicas; i++) {
|
|
83
|
+
shellExec(
|
|
84
|
+
`kubectl delete pvc cluster-storage-ipfs-cluster-${i} ipfs-storage-ipfs-cluster-${i} -n ${options.namespace} --ignore-not-found`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @method applySecrets
|
|
91
|
+
* @description Creates (or idempotently updates) the Kubernetes Secret and env ConfigMap
|
|
92
|
+
* that the StatefulSet pods read at startup.
|
|
93
|
+
* - Secret `ipfs-cluster-secret`: cluster-secret + bootstrap-peer-priv-key
|
|
94
|
+
* - ConfigMap `env-config`: bootstrap-peer-id + CLUSTER_SVC_NAME
|
|
95
|
+
* @param {{ CLUSTER_SECRET: string, IDENTITY_JSON: { id: string, private_key: string } }} credentials
|
|
96
|
+
* @param {object} options
|
|
97
|
+
* @param {string} options.namespace - Kubernetes namespace.
|
|
98
|
+
* @memberof UnderpostIPFS
|
|
99
|
+
*/
|
|
100
|
+
applySecrets({ CLUSTER_SECRET, IDENTITY_JSON }, options) {
|
|
101
|
+
logger.info('Applying IPFS cluster Kubernetes Secret and env ConfigMap');
|
|
102
|
+
|
|
103
|
+
shellExec(
|
|
104
|
+
`kubectl create secret generic ipfs-cluster-secret \
|
|
105
|
+
--from-literal=cluster-secret=${CLUSTER_SECRET} \
|
|
106
|
+
--from-literal=bootstrap-peer-priv-key=${IDENTITY_JSON.private_key} \
|
|
107
|
+
--dry-run=client -o yaml | kubectl apply -f - -n ${options.namespace}`,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
shellExec(
|
|
111
|
+
`kubectl create configmap env-config \
|
|
112
|
+
--from-literal=bootstrap-peer-id=${IDENTITY_JSON.id} \
|
|
113
|
+
--from-literal=CLUSTER_SVC_NAME=ipfs-cluster \
|
|
114
|
+
--dry-run=client -o yaml | kubectl apply -f - -n ${options.namespace}`,
|
|
115
|
+
);
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @method applyManifests
|
|
120
|
+
* @description Applies host-level sysctl tuning (Kind clusters only), the storage class,
|
|
121
|
+
* the kustomize manifests, and scales the StatefulSet to the requested replica count.
|
|
122
|
+
* @param {object} options
|
|
123
|
+
* @param {string} options.namespace - Kubernetes namespace.
|
|
124
|
+
* @param {boolean} [options.kubeadm] - Whether the cluster is Kubeadm-based.
|
|
125
|
+
* @param {boolean} [options.k3s] - Whether the cluster is K3s-based.
|
|
126
|
+
* @param {string} underpostRoot - Absolute path to the underpost project root.
|
|
127
|
+
* @param {number} ipfsReplicas - Desired replica count.
|
|
128
|
+
* @memberof UnderpostIPFS
|
|
129
|
+
*/
|
|
130
|
+
applyManifests(options, underpostRoot, ipfsReplicas) {
|
|
131
|
+
// Apply UDP buffer sysctl on every Kind node so QUIC (used by IPFS) can reach the
|
|
132
|
+
// recommended 7.5 MB buffer size. Kind nodes are containers and do NOT inherit the
|
|
133
|
+
// host sysctl values, so this must be set via docker exec on each node directly.
|
|
134
|
+
if (!options.kubeadm && !options.k3s) {
|
|
135
|
+
logger.info('Applying UDP buffer sysctl on Kind nodes');
|
|
136
|
+
shellExec(
|
|
137
|
+
`for node in $(kind get nodes); do docker exec $node sysctl -w net.core.rmem_max=7500000 net.core.wmem_max=7500000; done`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
shellExec(`kubectl apply -f ${underpostRoot}/manifests/ipfs/storage-class.yaml`);
|
|
142
|
+
shellExec(`kubectl apply -k ${underpostRoot}/manifests/ipfs -n ${options.namespace}`);
|
|
143
|
+
|
|
144
|
+
// statefulset.yaml hardcodes replicas: 3 as the ceiling; scale down here if needed.
|
|
145
|
+
shellExec(`kubectl scale statefulset ipfs-cluster --replicas=${ipfsReplicas} -n ${options.namespace}`);
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @method deploy
|
|
150
|
+
* @description Full orchestration of the ipfs-cluster StatefulSet deployment:
|
|
151
|
+
* optionally pulls images, resolves or generates credentials, tears down any existing
|
|
152
|
+
* deployment, applies secrets, applies manifests, and waits for all pods to be Running.
|
|
153
|
+
* @param {object} options - Cluster init options forwarded from UnderpostCluster.API.init.
|
|
154
|
+
* @param {string} options.namespace - Kubernetes namespace.
|
|
155
|
+
* @param {boolean} [options.pullImage] - Whether to pull container images first.
|
|
156
|
+
* @param {boolean} [options.kubeadm] - Whether the cluster is Kubeadm-based.
|
|
157
|
+
* @param {boolean} [options.k3s] - Whether the cluster is K3s-based.
|
|
158
|
+
* @param {string|number} [options.replicas] - Override replica count (defaults to 3).
|
|
159
|
+
* @param {string} underpostRoot - Absolute path to the underpost project root.
|
|
160
|
+
* @memberof UnderpostIPFS
|
|
161
|
+
*/
|
|
162
|
+
async deploy(options, underpostRoot) {
|
|
163
|
+
if (options.pullImage === true) {
|
|
164
|
+
Underpost.cluster.pullImage('ipfs/kubo:latest', options);
|
|
165
|
+
Underpost.cluster.pullImage('ipfs/ipfs-cluster:latest', options);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const credentials = Underpost.ipfs.resolveCredentials(`${underpostRoot}/engine-private`);
|
|
169
|
+
|
|
170
|
+
const ipfsReplicas = options.replicas ? parseInt(options.replicas) : 3;
|
|
171
|
+
|
|
172
|
+
Underpost.ipfs.teardown(options, ipfsReplicas);
|
|
173
|
+
Underpost.ipfs.applySecrets(credentials, options);
|
|
174
|
+
Underpost.ipfs.applyManifests(options, underpostRoot, ipfsReplicas);
|
|
175
|
+
|
|
176
|
+
logger.info(`Waiting for ${ipfsReplicas} ipfs-cluster pod(s) to reach Running state`);
|
|
177
|
+
for (let i = 0; i < ipfsReplicas; i++) {
|
|
178
|
+
await Underpost.test.statusMonitor(`ipfs-cluster-${i}`, 'Running', 'pods', 1000, 60 * 15);
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export default UnderpostIPFS;
|