cyberia 2.89.2 → 2.89.45

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.
Files changed (64) hide show
  1. package/.env.development +2 -0
  2. package/.env.production +2 -0
  3. package/.env.test +2 -0
  4. package/.github/workflows/engine-cyberia.cd.yml +4 -0
  5. package/.github/workflows/release.cd.yml +2 -0
  6. package/bin/cyberia.js +10 -7
  7. package/bin/deploy.js +22 -15
  8. package/bin/index.js +10 -7
  9. package/cli.md +105 -54
  10. package/deployment.yaml +34 -6
  11. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  12. package/manifests/deployment/dd-test-development/deployment.yaml +18 -6
  13. package/manifests/deployment/dd-test-development/proxy.yaml +2 -0
  14. package/manifests/deployment/kafka/deployment.yaml +0 -2
  15. package/manifests/deployment/spark/spark-pi-py.yaml +0 -1
  16. package/manifests/deployment/tensorflow/tf-gpu-test.yaml +0 -2
  17. package/manifests/envoy-service-nodeport.yaml +0 -1
  18. package/manifests/kubeadm-calico-config.yaml +10 -115
  19. package/manifests/letsencrypt-prod.yaml +0 -1
  20. package/manifests/mariadb/statefulset.yaml +1 -1
  21. package/manifests/mongodb/statefulset.yaml +11 -11
  22. package/manifests/mongodb-4.4/service-deployment.yaml +1 -3
  23. package/manifests/mysql/pv-pvc.yaml +1 -1
  24. package/manifests/mysql/statefulset.yaml +1 -1
  25. package/manifests/pv-pvc-dd.yaml +34 -0
  26. package/manifests/valkey/service.yaml +0 -1
  27. package/manifests/valkey/statefulset.yaml +2 -3
  28. package/package.json +1 -1
  29. package/proxy.yaml +6 -0
  30. package/scripts/device-scan.sh +43 -21
  31. package/scripts/gen-fqdns.sh +14 -0
  32. package/scripts/ip-info.sh +118 -0
  33. package/scripts/rpmfusion-ffmpeg-setup.sh +1 -0
  34. package/src/api/object-layer/object-layer.controller.js +19 -0
  35. package/src/api/object-layer/object-layer.router.js +4 -0
  36. package/src/api/object-layer/object-layer.service.js +111 -0
  37. package/src/api/user/user.model.js +10 -1
  38. package/src/cli/cluster.js +88 -75
  39. package/src/cli/deploy.js +165 -85
  40. package/src/cli/index.js +44 -3
  41. package/src/cli/monitor.js +12 -6
  42. package/src/cli/repository.js +13 -1
  43. package/src/cli/run.js +127 -60
  44. package/src/client/components/core/Logger.js +1 -1
  45. package/src/client/components/core/Modal.js +5 -0
  46. package/src/client/components/core/ObjectLayerEngineModal.js +336 -72
  47. package/src/client/components/core/ObjectLayerEngineViewer.js +239 -420
  48. package/src/client/components/core/Router.js +10 -1
  49. package/src/client/components/cyberia-portal/LogInCyberiaPortal.js +1 -1
  50. package/src/client/components/cyberia-portal/LogOutCyberiaPortal.js +1 -1
  51. package/src/client/components/cyberia-portal/MenuCyberiaPortal.js +1 -1
  52. package/src/client/components/cyberia-portal/ObjectLayerCyberiaPortal.js +44 -4
  53. package/src/client/services/default/default.management.js +25 -5
  54. package/src/client/services/object-layer/object-layer.management.js +8 -8
  55. package/src/client/services/object-layer/object-layer.service.js +34 -0
  56. package/src/index.js +1 -1
  57. package/src/server/client-build.js +5 -4
  58. package/src/server/conf.js +1 -1
  59. package/src/server/start.js +3 -1
  60. package/manifests/kubelet-config.yaml +0 -65
  61. package/manifests/mongodb/backup-access.yaml +0 -16
  62. package/manifests/mongodb/backup-cronjob.yaml +0 -42
  63. package/manifests/mongodb/backup-pv-pvc.yaml +0 -22
  64. package/manifests/mongodb/configmap.yaml +0 -26
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env bash
2
+ # ip-info.sh
3
+ # Retrieve information about an IP address or hostname
4
+ # Usage: ip-info.sh <IP-or-hostname>
5
+
6
+ set -euo pipefail
7
+ IFS=$'
8
+ '
9
+
10
+ if [[ ${#} -lt 1 ]]; then
11
+ echo "Usage: $0 <IP-or-hostname>"
12
+ exit 1
13
+ fi
14
+ TARGET="$1"
15
+
16
+ # Detect package manager
17
+ PKG_MANAGER=""
18
+ if command -v dnf >/dev/null 2>&1; then
19
+ PKG_MANAGER=dnf
20
+ elif command -v yum >/dev/null 2>&1; then
21
+ PKG_MANAGER=yum
22
+ fi
23
+
24
+ # Commands we need (bind-utils provides dig)
25
+ REQUIRED_CMDS=(curl jq whois dig traceroute)
26
+ PKGS=(curl jq whois bind-utils traceroute)
27
+
28
+ missing=()
29
+ for i in "${!REQUIRED_CMDS[@]}"; do
30
+ cmd=${REQUIRED_CMDS[$i]}
31
+ if ! command -v "$cmd" >/dev/null 2>&1; then
32
+ missing+=("${PKGS[$i]}")
33
+ fi
34
+ done
35
+
36
+ if [[ ${#missing[@]} -gt 0 ]]; then
37
+ if [[ -z "$PKG_MANAGER" ]]; then
38
+ echo "Missing packages: ${missing[*]}
39
+ No supported package manager found. Install: ${missing[*]} and re-run."
40
+ exit 1
41
+ fi
42
+ if [[ $EUID -ne 0 && -z "$(command -v sudo)" ]]; then
43
+ echo "Missing packages: ${missing[*]}
44
+ Run as root or install sudo."
45
+ exit 1
46
+ fi
47
+ echo "Installing missing: ${missing[*]}"
48
+ sudo $PKG_MANAGER install -y ${missing[*]} >/dev/null
49
+ fi
50
+
51
+ # Resolve to IP(s)
52
+ resolve_ips(){
53
+ if getent ahosts "$TARGET" >/dev/null 2>&1; then
54
+ getent ahosts "$TARGET" | awk '{print $1}' | sort -u
55
+ else
56
+ dig +short "$TARGET" | awk '/^[0-9]/ {print $1}' | sort -u
57
+ fi
58
+ }
59
+
60
+ mapfile -t IPS < <(resolve_ips)
61
+ if [[ ${#IPS[@]} -eq 0 ]]; then
62
+ IPS=("$TARGET")
63
+ fi
64
+
65
+ format(){
66
+ if command -v jq >/dev/null 2>&1; then
67
+ jq '.' 2>/dev/null || cat
68
+ else
69
+ cat
70
+ fi
71
+ }
72
+
73
+ for IP in "${IPS[@]}"; do
74
+ printf "
75
+ ===== %s (resolved: %s) =====
76
+
77
+ " "$TARGET" "$IP"
78
+
79
+ echo "-- REVERSE DNS --"
80
+ dig -x "$IP" +short || true
81
+
82
+ echo "
83
+ -- WHOIS --"
84
+ whois "$IP" | sed -n '1,80p' || true
85
+
86
+ echo "
87
+ -- IPINFO (ipinfo.io) --"
88
+ curl -sS "https://ipinfo.io/$IP/json" | format || true
89
+
90
+ echo "
91
+ -- IP-API (ip-api.com) --"
92
+ curl -sS "http://ip-api.com/json/$IP?fields=status,message,country,regionName,city,zip,lat,lon,timezone,isp,org,as,query" | format || true
93
+
94
+ echo "
95
+ -- IPWHOIS (ipwhois.app) --"
96
+ curl -sS "https://ipwhois.app/json/$IP" | format || true
97
+
98
+ echo "
99
+ -- GEOPLUGIN --"
100
+ curl -sS "http://www.geoplugin.net/json.gp?ip=$IP" | format || true
101
+
102
+ echo "
103
+ -- TRACEROUTE --"
104
+ traceroute -m 20 "$IP" || true
105
+
106
+ if [[ "$TARGET" =~ [A-Za-z] ]]; then
107
+ echo "
108
+ -- DNS RECORDS for $TARGET --"
109
+ dig +short A "$TARGET" || true
110
+ dig +short AAAA "$TARGET" || true
111
+ dig +short MX "$TARGET" || true
112
+ dig +short TXT "$TARGET" || true
113
+ fi
114
+
115
+ echo "
116
+ ===== done: $IP =====
117
+ "
118
+ done
@@ -32,6 +32,7 @@ echo "6) Try to install audio helper packages that sometimes block ffmpeg (ladsp
32
32
  # These may be provided by CRB/EPEL or other compatible repos
33
33
  dnf -y install ladspa || echo "ladspa not available from enabled repos (will try later)"
34
34
  dnf -y install rubberband || echo "rubberband not available from enabled repos (will try later)"
35
+ dnf -y install libwebp-tools || echo "libwebp-tools not available from enabled repos (will try later)"
35
36
 
36
37
  echo "7) Try installing ffmpeg (several fallbacks tried)"
37
38
  if dnf -y install ffmpeg ffmpeg-devel --allowerasing; then
@@ -34,6 +34,25 @@ const ObjectLayerController = {
34
34
  });
35
35
  }
36
36
  },
37
+ generateWebp: async (req, res, options) => {
38
+ try {
39
+ const result = await ObjectLayerService.generateWebp(req, res, options);
40
+ res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
41
+ res.setHeader('Content-Type', 'image/webp');
42
+ res.setHeader(
43
+ 'Content-Disposition',
44
+ `attachment; filename="${req.params.itemType}_${req.params.itemId}_${req.params.directionCode}.webp"`,
45
+ );
46
+ res.setHeader('Content-Length', result.length);
47
+ return res.status(200).end(result);
48
+ } catch (error) {
49
+ logger.error(error, error.stack);
50
+ return res.status(400).json({
51
+ status: 'error',
52
+ message: error.message,
53
+ });
54
+ }
55
+ },
37
56
  put: async (req, res, options) => {
38
57
  try {
39
58
  const result = await ObjectLayerService.put(req, res, options);
@@ -16,6 +16,10 @@ const ObjectLayerRouter = (options) => {
16
16
 
17
17
  router.post(`/:id`, authMiddleware, async (req, res) => await ObjectLayerController.post(req, res, options));
18
18
  router.post(`/`, authMiddleware, async (req, res) => await ObjectLayerController.post(req, res, options));
19
+ router.get(
20
+ `/generate-webp/:itemType/:itemId/:directionCode`,
21
+ async (req, res) => await ObjectLayerController.generateWebp(req, res, options),
22
+ );
19
23
  router.get(`/render/:id`, authMiddleware, async (req, res) => await ObjectLayerController.get(req, res, options));
20
24
  router.get(`/metadata/:id`, authMiddleware, async (req, res) => await ObjectLayerController.get(req, res, options));
21
25
  router.get(
@@ -5,6 +5,7 @@ import fs from 'fs-extra';
5
5
  import crypto from 'crypto';
6
6
  import { ObjectLayerDto } from './object-layer.model.js';
7
7
  import { ObjectLayerEngine } from '../../server/object-layer.js';
8
+ import { shellExec } from '../../server/process.js';
8
9
  const logger = loggerFactory(import.meta);
9
10
 
10
11
  const ObjectLayerService = {
@@ -250,6 +251,116 @@ const ObjectLayerService = {
250
251
  ]);
251
252
  return { data, total, page, totalPages: Math.ceil(total / limit) };
252
253
  },
254
+ generateWebp: async (req, res, options) => {
255
+ /** @type {import('./object-layer.model.js').ObjectLayerModel} */
256
+ const ObjectLayer = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.ObjectLayer;
257
+
258
+ // GET /generate-webp/:itemType/:itemId/:directionCode - Generate webp animation from PNG frames
259
+ const itemType = req.params.itemType;
260
+ const itemId = req.params.itemId;
261
+ const directionCode = req.params.directionCode;
262
+
263
+ if (!itemType || !itemId || !directionCode) {
264
+ throw new Error('Missing required parameters: itemType, itemId, directionCode');
265
+ }
266
+
267
+ // Path to the PNG frames directory
268
+ const framesFolder = `./src/client/public/cyberia/assets/${itemType}/${itemId}/${directionCode}`;
269
+
270
+ // Check if the folder exists
271
+ if (!fs.existsSync(framesFolder)) {
272
+ throw new Error(`Frames directory not found: ${framesFolder}`);
273
+ }
274
+
275
+ // Check if there are PNG files in the directory
276
+ const files = await fs.readdir(framesFolder);
277
+ const pngFiles = files.filter((file) => file.endsWith('.png')).sort();
278
+
279
+ if (pngFiles.length === 0) {
280
+ throw new Error(`No PNG frames found in directory: ${framesFolder}`);
281
+ }
282
+
283
+ logger.info(`Found ${pngFiles.length} PNG frames in ${framesFolder}`);
284
+
285
+ // Get frame duration from the object layer metadata
286
+ let frameDuration = 100; // Default to 100ms
287
+
288
+ try {
289
+ // Find object layer by itemType and itemId
290
+ const objectLayer = await ObjectLayer.findOne({
291
+ 'data.item.type': itemType,
292
+ 'data.item.id': itemId,
293
+ }).select(ObjectLayerDto.select.getMetadata());
294
+
295
+ if (objectLayer && objectLayer.data.render.frame_duration) {
296
+ frameDuration = objectLayer.data.render.frame_duration;
297
+ logger.info(`Using frame duration from object layer: ${frameDuration}ms`);
298
+ } else {
299
+ logger.warn(`Object layer not found or no frame_duration set, using default: ${frameDuration}ms`);
300
+ }
301
+ } catch (error) {
302
+ logger.warn(
303
+ `Error fetching object layer metadata: ${error.message}. Using default frame duration: ${frameDuration}ms`,
304
+ );
305
+ }
306
+
307
+ const tmpOutputFileName = `output_${Date.now()}_${Math.floor(Math.random() * 1000)}.webp`;
308
+
309
+ // Create temporary output file path
310
+ const tempOutputPath = `${framesFolder}/${tmpOutputFileName}`;
311
+
312
+ try {
313
+ // Change to the frames directory and execute img2webp command
314
+ const cmd = `cd "${framesFolder}" && img2webp -d ${frameDuration} -loop 0 *.png -o ${tmpOutputFileName}`;
315
+
316
+ logger.info(`Executing command: ${cmd}`);
317
+
318
+ const result = shellExec(cmd, { silent: false });
319
+
320
+ if (result.code !== 0) {
321
+ throw new Error(`img2webp command failed: ${result.stderr || result.stdout}`);
322
+ }
323
+
324
+ logger.info(`Successfully generated webp: ${tempOutputPath}`);
325
+
326
+ // Check if the output file was created
327
+ if (!fs.existsSync(tempOutputPath)) {
328
+ throw new Error(`Output file was not created: ${tempOutputPath}`);
329
+ }
330
+
331
+ // Read the webp file as a buffer
332
+ const webpBuffer = await fs.readFile(tempOutputPath);
333
+
334
+ logger.info(`WebP file size: ${webpBuffer.length} bytes`);
335
+
336
+ // Delete the temporary file after sending the response
337
+ // Use setImmediate to ensure the response is fully sent before cleanup
338
+ setImmediate(async () => {
339
+ try {
340
+ if (fs.existsSync(tempOutputPath)) {
341
+ await fs.remove(tempOutputPath);
342
+ logger.info(`Cleaned up temporary file: ${tempOutputPath}`);
343
+ }
344
+ } catch (cleanupError) {
345
+ logger.error(`Error cleaning up temporary file: ${cleanupError.message}`);
346
+ }
347
+ });
348
+
349
+ return webpBuffer;
350
+ } catch (error) {
351
+ // Clean up on error
352
+ try {
353
+ if (fs.existsSync(tempOutputPath)) {
354
+ await fs.remove(tempOutputPath);
355
+ logger.info(`Cleaned up temporary file after error: ${tempOutputPath}`);
356
+ }
357
+ } catch (cleanupError) {
358
+ logger.error(`Error cleaning up temporary file after error: ${cleanupError.message}`);
359
+ }
360
+
361
+ throw error;
362
+ }
363
+ },
253
364
  put: async (req, res, options) => {
254
365
  /** @type {import('./object-layer.model.js').ObjectLayerModel} */
255
366
  const ObjectLayer = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.ObjectLayer;
@@ -73,7 +73,16 @@ const ProviderSchema = UserSchema;
73
73
  const UserDto = {
74
74
  select: {
75
75
  get: () => {
76
- return { _id: 1, username: 1, email: 1, role: 1, emailConfirmed: 1, profileImageId: 1 };
76
+ return {
77
+ _id: 1,
78
+ username: 1,
79
+ email: 1,
80
+ role: 1,
81
+ emailConfirmed: 1,
82
+ profileImageId: 1,
83
+ createdAt: 1,
84
+ updatedAt: 1,
85
+ };
77
86
  },
78
87
  getAll: () => {
79
88
  return { _id: 1 };
@@ -44,12 +44,15 @@ class UnderpostCluster {
44
44
  * @param {boolean} [options.listPods=false] - List Kubernetes pods.
45
45
  * @param {boolean} [options.reset=false] - Perform a comprehensive reset of Kubernetes and container environments.
46
46
  * @param {boolean} [options.dev=false] - Run in development mode (adjusts paths).
47
- * @param {string} [options.nsUse=''] - Set the current kubectl namespace.
47
+ * @param {string} [options.nsUse=''] - Set the current kubectl namespace (creates namespace if it doesn't exist).
48
+ * @param {string} [options.namespace='default'] - Kubernetes namespace for cluster operations.
48
49
  * @param {boolean} [options.infoCapacity=false] - Display resource capacity information for the cluster.
49
50
  * @param {boolean} [options.infoCapacityPod=false] - Display resource capacity information for pods.
50
51
  * @param {boolean} [options.pullImage=false] - Pull necessary Docker images before deployment.
51
52
  * @param {boolean} [options.dedicatedGpu=false] - Configure for dedicated GPU usage (e.g., NVIDIA GPU Operator).
52
53
  * @param {boolean} [options.kubeadm=false] - Initialize the cluster using Kubeadm.
54
+ * @param {string} [options.podNetworkCidr='192.168.0.0/16'] - Custom pod network CIDR for Kubeadm cluster initialization. Defaults to '192.168.0.0/16'.
55
+ * @param {string} [options.controlPlaneEndpoint=''] - Custom control plane endpoint for Kubeadm cluster initialization. Defaults to '${os.hostname()}:6443'.
53
56
  * @param {boolean} [options.k3s=false] - Initialize the cluster using K3s.
54
57
  * @param {boolean} [options.initHost=false] - Perform initial host setup (install Docker, Podman, Kind, Kubeadm, Helm).
55
58
  * @param {boolean} [options.grafana=false] - Initialize the cluster with a Grafana deployment.
@@ -58,6 +61,7 @@ class UnderpostCluster {
58
61
  * @param {boolean} [options.config=false] - Apply general host configuration (SELinux, containerd, sysctl, firewalld).
59
62
  * @param {boolean} [options.worker=false] - Configure as a worker node (for Kubeadm or K3s join).
60
63
  * @param {boolean} [options.chown=false] - Set up kubectl configuration for the current user.
64
+ * @param {boolean} [options.removeVolumeHostPaths=false] - Remove data from host paths used by Persistent Volumes.
61
65
  * @param {string} [options.hosts] - Set custom hosts entries.
62
66
  * @memberof UnderpostCluster
63
67
  */
@@ -78,11 +82,14 @@ class UnderpostCluster {
78
82
  reset: false,
79
83
  dev: false,
80
84
  nsUse: '',
85
+ namespace: 'default',
81
86
  infoCapacity: false,
82
87
  infoCapacityPod: false,
83
88
  pullImage: false,
84
89
  dedicatedGpu: false,
85
90
  kubeadm: false,
91
+ podNetworkCidr: '192.168.0.0/16',
92
+ controlPlaneEndpoint: '',
86
93
  k3s: false,
87
94
  initHost: false,
88
95
  grafana: false,
@@ -91,6 +98,7 @@ class UnderpostCluster {
91
98
  config: false,
92
99
  worker: false,
93
100
  chown: false,
101
+ removeVolumeHostPaths: false,
94
102
  hosts: '',
95
103
  },
96
104
  ) {
@@ -114,46 +122,35 @@ class UnderpostCluster {
114
122
  if (options.infoCapacity === true)
115
123
  return logger.info('', UnderpostCluster.API.getResourcesCapacity(options.kubeadm || options.k3s)); // Adjust for k3s
116
124
  if (options.listPods === true) return console.table(UnderpostDeploy.API.get(podName ?? undefined));
125
+ // Set default namespace if not specified
126
+ if (!options.namespace) options.namespace = 'default';
127
+
117
128
  if (options.nsUse && typeof options.nsUse === 'string') {
118
- shellExec(`kubectl config set-context --current --namespace=${options.nsUse}`);
119
- return;
120
- }
121
- if (options.info === true) {
122
- shellExec(`kubectl config get-contexts`);
123
- shellExec(`kubectl config get-clusters`);
124
- shellExec(`kubectl get nodes -o wide`);
125
- shellExec(`kubectl config view | grep namespace`);
126
- shellExec(`kubectl get ns -o wide`);
127
- shellExec(`kubectl get pvc --all-namespaces -o wide`);
128
- shellExec(`kubectl get pv --all-namespaces -o wide`);
129
- shellExec(`kubectl get cronjob --all-namespaces -o wide`);
130
- shellExec(`kubectl get svc --all-namespaces -o wide`);
131
- shellExec(`kubectl get statefulsets --all-namespaces -o wide`);
132
- shellExec(`kubectl get deployments --all-namespaces -o wide`);
133
- shellExec(`kubectl get configmap --all-namespaces -o wide`);
134
- shellExec(`kubectl get pods --all-namespaces -o wide`);
135
- shellExec(
136
- `kubectl get pod --all-namespaces -o="custom-columns=NAME:.metadata.name,INIT-CONTAINERS:.spec.initContainers[*].name,CONTAINERS:.spec.containers[*].name"`,
137
- );
138
- shellExec(
139
- `kubectl get pods --all-namespaces -o=jsonpath='{range .items[*]}{"\\n"}{.metadata.name}{":\\t"}{range .spec.containers[*]}{.image}{", "}{end}{end}'`,
140
- );
141
- shellExec(`sudo crictl images`);
142
- console.log();
143
- logger.info('contour -------------------------------------------------');
144
- for (const _k of ['Cluster', 'HTTPProxy', 'ClusterIssuer', 'Certificate']) {
145
- shellExec(`kubectl get ${_k} --all-namespaces -o wide`);
129
+ // Verify if namespace exists, create if not
130
+ const namespaceExists = shellExec(`kubectl get namespace ${options.nsUse} --ignore-not-found -o name`, {
131
+ stdout: true,
132
+ silent: true,
133
+ }).trim();
134
+
135
+ if (!namespaceExists) {
136
+ logger.info(`Namespace '${options.nsUse}' does not exist. Creating it...`);
137
+ shellExec(`kubectl create namespace ${options.nsUse}`);
138
+ logger.info(`Namespace '${options.nsUse}' created successfully.`);
139
+ } else {
140
+ logger.info(`Namespace '${options.nsUse}' already exists.`);
146
141
  }
147
- logger.info('----------------------------------------------------------------');
148
- shellExec(`kubectl get secrets --all-namespaces -o wide`);
149
- shellExec(`docker secret ls`);
150
- shellExec(`kubectl get crd --all-namespaces -o wide`);
151
- shellExec(`sudo kubectl api-resources`);
142
+
143
+ shellExec(`kubectl config set-context --current --namespace=${options.nsUse}`);
144
+ logger.info(`Context switched to namespace: ${options.nsUse}`);
152
145
  return;
153
146
  }
154
147
 
155
148
  // Reset Kubernetes cluster components (Kind/Kubeadm/K3s) and container runtimes
156
- if (options.reset === true) return await UnderpostCluster.API.safeReset({ underpostRoot });
149
+ if (options.reset === true)
150
+ return await UnderpostCluster.API.safeReset({
151
+ underpostRoot,
152
+ removeVolumeHostPaths: options.removeVolumeHostPaths,
153
+ });
157
154
 
158
155
  // Check if a cluster (Kind, Kubeadm, or K3s) is already initialized
159
156
  const alreadyKubeadmCluster = UnderpostDeploy.API.get('calico-kube-controllers')[0];
@@ -224,9 +221,13 @@ class UnderpostCluster {
224
221
  logger.info('K3s comes with local-path-provisioner by default. Skipping explicit installation.');
225
222
  } else if (options.kubeadm === true) {
226
223
  logger.info('Initializing Kubeadm control plane...');
224
+ // Set default values if not provided
225
+ const podNetworkCidr = options.podNetworkCidr || '192.168.0.0/16';
226
+ const controlPlaneEndpoint = options.controlPlaneEndpoint || `${os.hostname()}:6443`;
227
+
227
228
  // Initialize kubeadm control plane
228
229
  shellExec(
229
- `sudo kubeadm init --pod-network-cidr=192.168.0.0/16 --control-plane-endpoint="${os.hostname()}:6443"`,
230
+ `sudo kubeadm init --pod-network-cidr=${podNetworkCidr} --control-plane-endpoint="${controlPlaneEndpoint}"`,
230
231
  );
231
232
  // Configure kubectl for the current user
232
233
  UnderpostCluster.API.chown('kubeadm'); // Pass 'kubeadm' to chown
@@ -236,7 +237,11 @@ class UnderpostCluster {
236
237
  shellExec(
237
238
  `sudo kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.29.3/manifests/tigera-operator.yaml`,
238
239
  );
240
+ shellExec(
241
+ `kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.29.3/manifests/custom-resources.yaml`,
242
+ );
239
243
  shellExec(`sudo kubectl apply -f ${underpostRoot}/manifests/kubeadm-calico-config.yaml`);
244
+
240
245
  // Untaint control plane node to allow scheduling pods
241
246
  const nodeName = os.hostname();
242
247
  shellExec(`kubectl taint nodes ${nodeName} node-role.kubernetes.io/control-plane:NoSchedule-`);
@@ -280,13 +285,13 @@ class UnderpostCluster {
280
285
  }
281
286
 
282
287
  if (options.grafana === true) {
283
- shellExec(`kubectl delete deployment grafana --ignore-not-found`);
284
- shellExec(`kubectl apply -k ${underpostRoot}/manifests/grafana`);
288
+ shellExec(`kubectl delete deployment grafana -n ${options.namespace} --ignore-not-found`);
289
+ shellExec(`kubectl apply -k ${underpostRoot}/manifests/grafana -n ${options.namespace}`);
285
290
  const yaml = `${fs
286
291
  .readFileSync(`${underpostRoot}/manifests/grafana/deployment.yaml`, 'utf8')
287
292
  .replace('{{GF_SERVER_ROOT_URL}}', options.hosts.split(',')[0])}`;
288
293
  console.log(yaml);
289
- shellExec(`kubectl apply -f - <<EOF
294
+ shellExec(`kubectl apply -f - -n ${options.namespace} <<EOF
290
295
  ${yaml}
291
296
  EOF
292
297
  `);
@@ -305,7 +310,7 @@ EOF
305
310
  .join(',')}]`,
306
311
  )}`;
307
312
  console.log(yaml);
308
- shellExec(`kubectl apply -f - <<EOF
313
+ shellExec(`kubectl apply -f - -n ${options.namespace} <<EOF
309
314
  ${yaml}
310
315
  EOF
311
316
  `);
@@ -334,15 +339,15 @@ EOF
334
339
  // For kubeadm/k3s, ensure it's available for containerd
335
340
  shellExec(`sudo crictl pull valkey/valkey:latest`);
336
341
  }
337
- shellExec(`kubectl delete statefulset valkey-service --ignore-not-found`);
338
- shellExec(`kubectl apply -k ${underpostRoot}/manifests/valkey`);
342
+ shellExec(`kubectl delete statefulset valkey-service -n ${options.namespace} --ignore-not-found`);
343
+ shellExec(`kubectl apply -k ${underpostRoot}/manifests/valkey -n ${options.namespace}`);
339
344
  await UnderpostTest.API.statusMonitor('valkey-service', 'Running', 'pods', 1000, 60);
340
345
  }
341
346
  if (options.full === true || options.mariadb === true) {
342
347
  shellExec(
343
- `sudo kubectl create secret generic mariadb-secret --from-file=username=/home/dd/engine/engine-private/mariadb-username --from-file=password=/home/dd/engine/engine-private/mariadb-password --dry-run=client -o yaml | kubectl apply -f -`,
348
+ `sudo kubectl create secret generic mariadb-secret --from-file=username=/home/dd/engine/engine-private/mariadb-username --from-file=password=/home/dd/engine/engine-private/mariadb-password --dry-run=client -o yaml | kubectl apply -f - -n ${options.namespace}`,
344
349
  );
345
- shellExec(`kubectl delete statefulset mariadb-statefulset --ignore-not-found`);
350
+ shellExec(`kubectl delete statefulset mariadb-statefulset -n ${options.namespace} --ignore-not-found`);
346
351
 
347
352
  if (options.pullImage === true) {
348
353
  // shellExec(`sudo podman pull mariadb:latest`);
@@ -354,17 +359,17 @@ EOF
354
359
  // For kubeadm/k3s, ensure it's available for containerd
355
360
  shellExec(`sudo crictl pull mariadb:latest`);
356
361
  }
357
- shellExec(`kubectl apply -f ${underpostRoot}/manifests/mariadb/storage-class.yaml`);
358
- shellExec(`kubectl apply -k ${underpostRoot}/manifests/mariadb`);
362
+ shellExec(`kubectl apply -f ${underpostRoot}/manifests/mariadb/storage-class.yaml -n ${options.namespace}`);
363
+ shellExec(`kubectl apply -k ${underpostRoot}/manifests/mariadb -n ${options.namespace}`);
359
364
  }
360
365
  if (options.full === true || options.mysql === true) {
361
366
  shellExec(
362
- `sudo kubectl create secret generic mysql-secret --from-file=username=/home/dd/engine/engine-private/mysql-username --from-file=password=/home/dd/engine/engine-private/mysql-password --dry-run=client -o yaml | kubectl apply -f -`,
367
+ `sudo kubectl create secret generic mysql-secret --from-file=username=/home/dd/engine/engine-private/mysql-username --from-file=password=/home/dd/engine/engine-private/mysql-password --dry-run=client -o yaml | kubectl apply -f - -n ${options.namespace}`,
363
368
  );
364
369
  shellExec(`sudo mkdir -p /mnt/data`);
365
370
  shellExec(`sudo chmod 777 /mnt/data`);
366
371
  shellExec(`sudo chown -R root:root /mnt/data`);
367
- shellExec(`kubectl apply -k ${underpostRoot}/manifests/mysql`);
372
+ shellExec(`kubectl apply -k ${underpostRoot}/manifests/mysql -n ${options.namespace}`);
368
373
  }
369
374
  if (options.full === true || options.postgresql === true) {
370
375
  if (options.pullImage === true) {
@@ -377,9 +382,9 @@ EOF
377
382
  shellExec(`sudo crictl pull postgres:latest`);
378
383
  }
379
384
  shellExec(
380
- `sudo kubectl create secret generic postgres-secret --from-file=password=/home/dd/engine/engine-private/postgresql-password --dry-run=client -o yaml | kubectl apply -f -`,
385
+ `sudo kubectl create secret generic postgres-secret --from-file=password=/home/dd/engine/engine-private/postgresql-password --dry-run=client -o yaml | kubectl apply -f - -n ${options.namespace}`,
381
386
  );
382
- shellExec(`kubectl apply -k ${underpostRoot}/manifests/postgresql`);
387
+ shellExec(`kubectl apply -k ${underpostRoot}/manifests/postgresql -n ${options.namespace}`);
383
388
  }
384
389
  if (options.mongodb4 === true) {
385
390
  if (options.pullImage === true) {
@@ -391,7 +396,7 @@ EOF
391
396
  // For kubeadm/k3s, ensure it's available for containerd
392
397
  shellExec(`sudo crictl pull mongo:4.4`);
393
398
  }
394
- shellExec(`kubectl apply -k ${underpostRoot}/manifests/mongodb-4.4`);
399
+ shellExec(`kubectl apply -k ${underpostRoot}/manifests/mongodb-4.4 -n ${options.namespace}`);
395
400
 
396
401
  const deploymentName = 'mongodb-deployment';
397
402
 
@@ -422,14 +427,14 @@ EOF
422
427
  shellExec(`sudo crictl pull mongo:latest`);
423
428
  }
424
429
  shellExec(
425
- `sudo kubectl create secret generic mongodb-keyfile --from-file=/home/dd/engine/engine-private/mongodb-keyfile --dry-run=client -o yaml | kubectl apply -f -`,
430
+ `sudo kubectl create secret generic mongodb-keyfile --from-file=/home/dd/engine/engine-private/mongodb-keyfile --dry-run=client -o yaml | kubectl apply -f - -n ${options.namespace}`,
426
431
  );
427
432
  shellExec(
428
- `sudo kubectl create secret generic mongodb-secret --from-file=username=/home/dd/engine/engine-private/mongodb-username --from-file=password=/home/dd/engine/engine-private/mongodb-password --dry-run=client -o yaml | kubectl apply -f -`,
433
+ `sudo kubectl create secret generic mongodb-secret --from-file=username=/home/dd/engine/engine-private/mongodb-username --from-file=password=/home/dd/engine/engine-private/mongodb-password --dry-run=client -o yaml | kubectl apply -f - -n ${options.namespace}`,
429
434
  );
430
- shellExec(`kubectl delete statefulset mongodb --ignore-not-found`);
431
- shellExec(`kubectl apply -f ${underpostRoot}/manifests/mongodb/storage-class.yaml`);
432
- shellExec(`kubectl apply -k ${underpostRoot}/manifests/mongodb`);
435
+ shellExec(`kubectl delete statefulset mongodb -n ${options.namespace} --ignore-not-found`);
436
+ shellExec(`kubectl apply -f ${underpostRoot}/manifests/mongodb/storage-class.yaml -n ${options.namespace}`);
437
+ shellExec(`kubectl apply -k ${underpostRoot}/manifests/mongodb -n ${options.namespace}`);
433
438
 
434
439
  const successInstance = await UnderpostTest.API.statusMonitor('mongodb-0', 'Running', 'pods', 1000, 60 * 10);
435
440
 
@@ -453,7 +458,9 @@ EOF
453
458
  shellExec(`kubectl apply -f https://projectcontour.io/quickstart/contour.yaml`);
454
459
  if (options.kubeadm === true) {
455
460
  // Envoy service might need NodePort for kubeadm
456
- shellExec(`sudo kubectl apply -f ${underpostRoot}/manifests/envoy-service-nodeport.yaml`);
461
+ shellExec(
462
+ `sudo kubectl apply -f ${underpostRoot}/manifests/envoy-service-nodeport.yaml -n ${options.namespace}`,
463
+ );
457
464
  }
458
465
  // K3s has a built-in LoadBalancer (Klipper-lb) that can expose services,
459
466
  // so a specific NodePort service might not be needed or can be configured differently.
@@ -473,7 +480,7 @@ EOF
473
480
 
474
481
  const letsEncName = 'letsencrypt-prod';
475
482
  shellExec(`sudo kubectl delete ClusterIssuer ${letsEncName} --ignore-not-found`);
476
- shellExec(`sudo kubectl apply -f ${underpostRoot}/manifests/${letsEncName}.yaml`);
483
+ shellExec(`sudo kubectl apply -f ${underpostRoot}/manifests/${letsEncName}.yaml -n ${options.namespace}`);
477
484
  }
478
485
  },
479
486
 
@@ -585,9 +592,10 @@ net.ipv4.ip_forward = 1' | sudo tee ${iptableConfPath}`,
585
592
  * in coredns) by restoring SELinux security contexts and safely cleaning up cluster artifacts.
586
593
  * @param {object} [options] - Configuration options for the reset.
587
594
  * @param {string} [options.underpostRoot] - The root path of the underpost project.
595
+ * @param {boolean} [options.removeVolumeHostPaths=false] - Whether to remove data from host paths used by Persistent Volumes.
588
596
  * @memberof UnderpostCluster
589
597
  */
590
- async safeReset(options = { underpostRoot: '.' }) {
598
+ async safeReset(options = { underpostRoot: '.', removeVolumeHostPaths: false }) {
591
599
  logger.info('Starting a safe and comprehensive reset of Kubernetes and container environments...');
592
600
 
593
601
  try {
@@ -614,25 +622,30 @@ net.ipv4.ip_forward = 1' | sudo tee ${iptableConfPath}`,
614
622
  // Phase 1: Clean up Persistent Volumes with hostPath
615
623
  // This targets data created by Kubernetes Persistent Volumes that use hostPath.
616
624
  logger.info('Phase 1/7: Cleaning Kubernetes hostPath volumes...');
617
- try {
618
- const pvListJson = shellExec(`kubectl get pv -o json || echo '{"items":[]}'`, { stdout: true, silent: true });
619
- const pvList = JSON.parse(pvListJson);
620
-
621
- if (pvList.items && pvList.items.length > 0) {
622
- for (const pv of pvList.items) {
623
- // Check if the PV uses hostPath and delete its contents
624
- if (pv.spec.hostPath && pv.spec.hostPath.path) {
625
- const hostPath = pv.spec.hostPath.path;
626
- logger.info(`Removing data from host path for PV '${pv.metadata.name}': ${hostPath}`);
627
- shellExec(`sudo rm -rf ${hostPath}/* || true`);
625
+ if (options.removeVolumeHostPaths)
626
+ try {
627
+ const pvListJson = shellExec(`kubectl get pv -o json || echo '{"items":[]}'`, {
628
+ stdout: true,
629
+ silent: true,
630
+ });
631
+ const pvList = JSON.parse(pvListJson);
632
+
633
+ if (pvList.items && pvList.items.length > 0) {
634
+ for (const pv of pvList.items) {
635
+ // Check if the PV uses hostPath and delete its contents
636
+ if (pv.spec.hostPath && pv.spec.hostPath.path) {
637
+ const hostPath = pv.spec.hostPath.path;
638
+ logger.info(`Removing data from host path for PV '${pv.metadata.name}': ${hostPath}`);
639
+ shellExec(`sudo rm -rf ${hostPath}/* || true`);
640
+ }
628
641
  }
642
+ } else {
643
+ logger.info('No Persistent Volumes found with hostPath to clean up.');
629
644
  }
630
- } else {
631
- logger.info('No Persistent Volumes found with hostPath to clean up.');
645
+ } catch (error) {
646
+ logger.error('Failed to clean up Persistent Volumes:', error);
632
647
  }
633
- } catch (error) {
634
- logger.error('Failed to clean up Persistent Volumes:', error);
635
- }
648
+ else logger.info(' -> Skipping hostPath volume cleanup as per configuration.');
636
649
  // Phase 2: Restore SELinux and stop services
637
650
  // This is critical for fixing the 'permission denied' error you experienced.
638
651
  // Enable SELinux permissive mode and restore file contexts.