From 56606b54cfb033a791b44bd54c6d04334b7397d2 Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Wed, 4 Jun 2025 07:46:45 -0400 Subject: [PATCH 1/9] Add backup and restore-backup scripts What: Adds scripts to backup the data from your Hubs instance to your local hard drive and restore a backup to your instance. Why: This will allow you to keep one or more local copies of your data, restore your data to your instance if needed, and migrate all your data from one instance to another, e.g. when moving from one hosting company to another hosting company. Note: This will be needed to migrate the data from the persistent volumes on your node, to persistent volumes that are completely separate from the node (PR #363). --- community-edition/.gitignore | 3 +- community-edition/backup_script/index.js | 41 ++++++++++++++ community-edition/package.json | 2 + community-edition/readme.md | 6 ++ .../restore_backup_script/index.js | 56 +++++++++++++++++++ 5 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 community-edition/backup_script/index.js create mode 100644 community-edition/restore_backup_script/index.js diff --git a/community-edition/.gitignore b/community-edition/.gitignore index 2547855b..55b5dff7 100644 --- a/community-edition/.gitignore +++ b/community-edition/.gitignore @@ -1,4 +1,5 @@ /node_modules /hcce.yaml /ssl_script/cbb.yaml -.DS_Store \ No newline at end of file +/data_backups +.DS_Store diff --git a/community-edition/backup_script/index.js b/community-edition/backup_script/index.js new file mode 100644 index 00000000..c0ecddf7 --- /dev/null +++ b/community-edition/backup_script/index.js @@ -0,0 +1,41 @@ +const execSync = require('child_process').execSync; +const fs = require("fs"); +const path = require("path"); +const YAML = require("yaml"); +const utils = require("../utils"); + +// read config +const config = utils.readConfig(); +const processedConfig = YAML.parse( + utils.replacePlaceholders(YAML.stringify(config), config), + {"schema": "yaml-1.1"} // required to load yes/no as boolean values +); + +// get backup paths +const rootDataBackupPath = path.join(process.cwd(), "data_backups"); +const dataBackupPath = path.join(rootDataBackupPath, `data_backup_${Date.now()}`); +const reticulumStoragePath = path.join(dataBackupPath, "reticulum_storage_data"); +const pgDumpSQLPath = path.join(dataBackupPath, "pg_dump.sql"); + +// get pod names +let reticulumPodName = execSync(`kubectl get pods -l=app=reticulum -n ${processedConfig.Namespace} --output jsonpath='{.items[0].metadata.name}'`); +let pgsqlPodName = execSync(`kubectl get pods -l=app=pgsql -n ${processedConfig.Namespace} --output jsonpath='{.items[0].metadata.name}'`); +// strip out the single quotes that Windows adds in +reticulumPodName = reticulumPodName.toString().replaceAll("'", ""); +pgsqlPodName = pgsqlPodName.toString().replaceAll("'", ""); + + +// make backups folder (if needed) +if (!fs.existsSync(rootDataBackupPath)) { + fs.mkdirSync(rootDataBackupPath); + } + +// download reticulum storage +// note: relative paths must be used for kubectl cp on windows due to this bug: https://github.com/kubernetes/kubernetes/issues/101985 +execSync(`kubectl cp --retries=-1 ${reticulumPodName}:/storage ${path.relative(process.cwd(), reticulumStoragePath)} -n ${processedConfig.Namespace}`); + +// create and download dump of pgsql database +// note: relative paths must be used for kubectl cp on windows due to this bug: https://github.com/kubernetes/kubernetes/issues/101985 +execSync(`kubectl exec ${pgsqlPodName} -n ${processedConfig.Namespace} -- /bin/pg_dump -c ${processedConfig.PGRST_DB_URI} -f /root/pg_dump.sql`); +execSync(`kubectl cp --retries=-1 ${pgsqlPodName}:/root/pg_dump.sql ${path.relative(process.cwd(), pgDumpSQLPath)} -n ${processedConfig.Namespace}`); +execSync(`kubectl exec ${pgsqlPodName} -n ${processedConfig.Namespace} -- /bin/rm /root/pg_dump.sql`); diff --git a/community-edition/package.json b/community-edition/package.json index 605729bc..3f909668 100644 --- a/community-edition/package.json +++ b/community-edition/package.json @@ -8,6 +8,8 @@ "apply": "node apply/index.js && node get_ip/index.js", "get-ip": "node get_ip/index.js", "gen-ssl": "node ssl_script/index.js", + "backup": "node backup_script/index.js", + "restore-backup": "node restore_backup_script/index.js", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], diff --git a/community-edition/readme.md b/community-edition/readme.md index 765dea8d..a975bb5f 100644 --- a/community-edition/readme.md +++ b/community-edition/readme.md @@ -131,6 +131,12 @@ If you just need to get the external IP address of your load balancer, run `npm run get-ip` +### Backing up and restoring your instance + +Use `npm run backup` to backup your instance. The backup will be timestamped and placed in a `data_backups` folder. + +Use `npm run restore-backup data_backup_1234567890123` to restore a backup to your instance. If you don't specify a backup, and just use `npm run restore-backup`, it will default to the latest backup. + ## Guides from the Hubs Team and Community ### 1. Beginner's Guide to CE diff --git a/community-edition/restore_backup_script/index.js b/community-edition/restore_backup_script/index.js new file mode 100644 index 00000000..8e70376a --- /dev/null +++ b/community-edition/restore_backup_script/index.js @@ -0,0 +1,56 @@ +const execSync = require('child_process').execSync; +const fs = require("fs"); +const path = require("path"); +const YAML = require("yaml"); +const utils = require("../utils"); + +// get command line arguments +const args = process.argv.slice(2); + +// read config +const config = utils.readConfig(); +const processedConfig = YAML.parse( + utils.replacePlaceholders(YAML.stringify(config), config), + {"schema": "yaml-1.1"} // required to load yes/no as boolean values +); + +// get backup paths +const rootDataBackupPath = path.join(process.cwd(), "data_backups"); +const backup_name = args[0] ? args[0] : + fs.readdirSync(rootDataBackupPath) + .filter(name => name.includes("data_backup")) + .sort().at(-1); +const dataBackupPath = path.join(rootDataBackupPath, backup_name); +const reticulumStoragePath = path.join(dataBackupPath, "reticulum_storage_data"); +const reticulumStorageRelativePath = path.relative(process.cwd(), reticulumStoragePath); +const pgDumpSQLPath = path.join(dataBackupPath, "pg_dump.sql"); +if (!fs.existsSync(dataBackupPath)) { + console.error("the specified backup doesn't exist"); + process.exit(1); + } + +// get pod names +let reticulumPodName = execSync(`kubectl get pods -l=app=reticulum -n ${processedConfig.Namespace} --output jsonpath='{.items[0].metadata.name}'`); +let pgsqlPodName = execSync(`kubectl get pods -l=app=pgsql -n ${processedConfig.Namespace} --output jsonpath='{.items[0].metadata.name}'`); +// strip out the single quotes that Windows adds in +reticulumPodName = reticulumPodName.toString().replaceAll("'", ""); +pgsqlPodName = pgsqlPodName.toString().replaceAll("'", ""); + +// upload reticulum storage +// note: relative paths must be used for kubectl cp on windows due to this bug: https://github.com/kubernetes/kubernetes/issues/101985 +let fs_object_names = fs.readdirSync(reticulumStoragePath); +fs_object_names.forEach(fs_object_name => { + execSync(`kubectl cp --retries=-1 ${path.join(reticulumStorageRelativePath, fs_object_name)} ${reticulumPodName}:/storage -n ${processedConfig.Namespace}`); +}); + +// upload and apply the dump of the pgsql database +// note: relative paths must be used for kubectl cp on windows due to this bug: https://github.com/kubernetes/kubernetes/issues/101985 +execSync(`kubectl cp --retries=-1 ${path.relative(process.cwd(), pgDumpSQLPath)} ${pgsqlPodName}:/root/pg_dump.sql -n ${processedConfig.Namespace}`); +execSync(`kubectl exec ${pgsqlPodName} -n ${processedConfig.Namespace} -- /bin/psql ${processedConfig.PGRST_DB_URI} -f /root/pg_dump.sql`); +execSync(`kubectl exec ${pgsqlPodName} -n ${processedConfig.Namespace} -- /bin/rm /root/pg_dump.sql`); + + +// restart the Hubs instance so it doesn't error out when visited +execSync(`kubectl delete deployment --all -n ${processedConfig.Namespace}`); +execSync(`kubectl delete pods --all -n ${processedConfig.Namespace}`); +execSync(`kubectl apply -f hcce.yaml`); From 05315647727413db0ad3ba41a6cf8df49db00033 Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Tue, 10 Jun 2025 07:44:27 -0400 Subject: [PATCH 2/9] Add a maintenance mode to the restore backup script What: Adds a primitive maintenance mode to the restore backup script. This is applied at the beginning and sets haproxy to redirect traffic to a non-existent maintenance mode subdomain, then restarts the instance to totally disconnect any people present on the instance and prevent anyone new from joining. The redirects are removed and the instance is returned to normal at the end after the restore is finished. Why: So that people can't interrupt/corrupt the restore by modifying data on the instance while the restore is happening. Note: At present the maintenance mode isn't a real page, so it's not all that pretty, and you won't be redirected back to your previous page once the restore is finished (even if you reload), but it gets the job done. Ideally, these faults should be addressed at some point in the future. --- .../restore_backup_script/index.js | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/community-edition/restore_backup_script/index.js b/community-edition/restore_backup_script/index.js index 8e70376a..1d48a4fd 100644 --- a/community-edition/restore_backup_script/index.js +++ b/community-edition/restore_backup_script/index.js @@ -14,6 +14,27 @@ const processedConfig = YAML.parse( {"schema": "yaml-1.1"} // required to load yes/no as boolean values ); +// apply maintenance mode +const maintenanceModeHcceFileName = "maintenance-mode-hcce.yaml" +const hcce = utils.readTemplate("", "hcce.yaml"); +const hcceYamlDocuments = YAML.parseAllDocuments(hcce); +hcceYamlDocuments.forEach((doc, index) => { + const jsDoc = doc.toJS(); + if (jsDoc.kind === "Ingress") { + if (!jsDoc.metadata.annotations) { + jsDoc.metadata["annotations"] = {}; + } + jsDoc.metadata.annotations["haproxy.org/request-redirect"] = `hubs-maintenance-mode.${processedConfig.HUB_DOMAIN}` + hcceYamlDocuments[index] = new YAML.Document(jsDoc); + } +}); +const maintenanceModeHcce = `${hcceYamlDocuments.map(doc => YAML.stringify(doc, {"lineWidth": 0, "directives": false})).join('---\n')}` +utils.writeOutputFile(maintenanceModeHcce, "", maintenanceModeHcceFileName); + +execSync(`kubectl delete deployment --all -n ${processedConfig.Namespace}`); +execSync(`kubectl delete pods --all -n ${processedConfig.Namespace}`); +execSync(`kubectl apply -f ${maintenanceModeHcceFileName}`); + // get backup paths const rootDataBackupPath = path.join(process.cwd(), "data_backups"); const backup_name = args[0] ? args[0] : @@ -50,7 +71,8 @@ execSync(`kubectl exec ${pgsqlPodName} -n ${processedConfig.Namespace} -- /bin/p execSync(`kubectl exec ${pgsqlPodName} -n ${processedConfig.Namespace} -- /bin/rm /root/pg_dump.sql`); -// restart the Hubs instance so it doesn't error out when visited +// restart the Hubs instance so it doesn't error out when visited and maintenance mode is no longer applied +fs.rmSync(path.join(process.cwd(), maintenanceModeHcceFileName)); execSync(`kubectl delete deployment --all -n ${processedConfig.Namespace}`); execSync(`kubectl delete pods --all -n ${processedConfig.Namespace}`); execSync(`kubectl apply -f hcce.yaml`); From ea6779caa1c400fd2065b0a2d0ff6d168c67b33c Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Tue, 10 Jun 2025 07:47:38 -0400 Subject: [PATCH 3/9] Keep the user apprised of the progress of the restore backup script What: Prints headings for the general steps of the restore script and prints the command output to the terminal. Why: This is a very involved and potentially long running script, and the additional output should help reduce confusion as to whether the script is running normally or has gotten stuck. Note: This implements similar behavior to the apply script, but that, at present, will only work with the main configuration file and not a secondary, temporary one. In the future, the code to apply a Kubernetes configuration and monitor the deployment status should potentially be further abstracted so it can work with any configuration files and only one version of the code is needed. --- .../restore_backup_script/index.js | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/community-edition/restore_backup_script/index.js b/community-edition/restore_backup_script/index.js index 1d48a4fd..213b8091 100644 --- a/community-edition/restore_backup_script/index.js +++ b/community-edition/restore_backup_script/index.js @@ -31,9 +31,29 @@ hcceYamlDocuments.forEach((doc, index) => { const maintenanceModeHcce = `${hcceYamlDocuments.map(doc => YAML.stringify(doc, {"lineWidth": 0, "directives": false})).join('---\n')}` utils.writeOutputFile(maintenanceModeHcce, "", maintenanceModeHcceFileName); -execSync(`kubectl delete deployment --all -n ${processedConfig.Namespace}`); -execSync(`kubectl delete pods --all -n ${processedConfig.Namespace}`); -execSync(`kubectl apply -f ${maintenanceModeHcceFileName}`); +console.log("applying maintenance mode"); +console.log(""); +execSync(`kubectl delete deployment --all -n ${processedConfig.Namespace}`, {stdio: 'inherit'}); +execSync(`kubectl delete pods --all -n ${processedConfig.Namespace}`, {stdio: 'inherit'}); +execSync(`kubectl apply -f ${maintenanceModeHcceFileName}`, {stdio: 'inherit'}); +let pendingDeploymentNames = [] +while (true) { + let deployments = JSON.parse(execSync(`kubectl get deployment -n ${config.Namespace} -o json`)).items; + let pendingDeployments = deployments.filter(deployment => (deployment.status.readyReplicas ?? 0) < deployment.status.replicas); + + if (pendingDeployments.length) { + currentPendingDeploymentNames = pendingDeployments.map(deployment => deployment.metadata.name) + if (currentPendingDeploymentNames.toString() !== pendingDeploymentNames.toString()) { + console.log(`waiting on ${currentPendingDeploymentNames.join(", ")}`); + pendingDeploymentNames = currentPendingDeploymentNames; + } + // Wait for 1 second + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1000); + } else { + console.log("maintenance mode applied") + break; + } +} // get backup paths const rootDataBackupPath = path.join(process.cwd(), "data_backups"); @@ -57,15 +77,20 @@ let pgsqlPodName = execSync(`kubectl get pods -l=app=pgsql -n ${processedConfig. reticulumPodName = reticulumPodName.toString().replaceAll("'", ""); pgsqlPodName = pgsqlPodName.toString().replaceAll("'", ""); +console.log(""); +console.log("restoring backup"); +console.log(""); // upload reticulum storage // note: relative paths must be used for kubectl cp on windows due to this bug: https://github.com/kubernetes/kubernetes/issues/101985 let fs_object_names = fs.readdirSync(reticulumStoragePath); fs_object_names.forEach(fs_object_name => { + console.log(`restoring Reticulum '${fs_object_name}' folder`); execSync(`kubectl cp --retries=-1 ${path.join(reticulumStorageRelativePath, fs_object_name)} ${reticulumPodName}:/storage -n ${processedConfig.Namespace}`); }); // upload and apply the dump of the pgsql database // note: relative paths must be used for kubectl cp on windows due to this bug: https://github.com/kubernetes/kubernetes/issues/101985 +console.log("restoring database"); execSync(`kubectl cp --retries=-1 ${path.relative(process.cwd(), pgDumpSQLPath)} ${pgsqlPodName}:/root/pg_dump.sql -n ${processedConfig.Namespace}`); execSync(`kubectl exec ${pgsqlPodName} -n ${processedConfig.Namespace} -- /bin/psql ${processedConfig.PGRST_DB_URI} -f /root/pg_dump.sql`); execSync(`kubectl exec ${pgsqlPodName} -n ${processedConfig.Namespace} -- /bin/rm /root/pg_dump.sql`); @@ -73,6 +98,8 @@ execSync(`kubectl exec ${pgsqlPodName} -n ${processedConfig.Namespace} -- /bin/r // restart the Hubs instance so it doesn't error out when visited and maintenance mode is no longer applied fs.rmSync(path.join(process.cwd(), maintenanceModeHcceFileName)); -execSync(`kubectl delete deployment --all -n ${processedConfig.Namespace}`); -execSync(`kubectl delete pods --all -n ${processedConfig.Namespace}`); -execSync(`kubectl apply -f hcce.yaml`); +console.log(""); +console.log("restarting instance"); +execSync(`kubectl delete deployment --all -n ${processedConfig.Namespace}`, {stdio: 'inherit'}); +execSync(`kubectl delete pods --all -n ${processedConfig.Namespace}`, {stdio: 'inherit'}); +execSync(`npm run apply`, {stdio: 'inherit'}); From 407ee6af0b95ee3bab5a4f71b002b4646785f1e0 Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Tue, 10 Jun 2025 07:49:32 -0400 Subject: [PATCH 4/9] Specify the Reticulum container when copying data to it when restoring a backup What: Explicitly specifies the Reticulum container in the Reticulum pod as the container to copy the data to instead of relying on it being automatically selected by default. Why: Reduces ambiguity and prevents bugs from cropping up in the future if anything changes and the Reticulum container is no longer the first container. --- community-edition/restore_backup_script/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/community-edition/restore_backup_script/index.js b/community-edition/restore_backup_script/index.js index 213b8091..3d7759b0 100644 --- a/community-edition/restore_backup_script/index.js +++ b/community-edition/restore_backup_script/index.js @@ -2,7 +2,7 @@ const execSync = require('child_process').execSync; const fs = require("fs"); const path = require("path"); const YAML = require("yaml"); -const utils = require("../utils"); +const utils = require("../utils.js"); // get command line arguments const args = process.argv.slice(2); @@ -85,7 +85,7 @@ console.log(""); let fs_object_names = fs.readdirSync(reticulumStoragePath); fs_object_names.forEach(fs_object_name => { console.log(`restoring Reticulum '${fs_object_name}' folder`); - execSync(`kubectl cp --retries=-1 ${path.join(reticulumStorageRelativePath, fs_object_name)} ${reticulumPodName}:/storage -n ${processedConfig.Namespace}`); + execSync(`kubectl cp --retries=-1 ${path.join(reticulumStorageRelativePath, fs_object_name)} ${reticulumPodName}:/storage -c reticulum -n ${processedConfig.Namespace}`); }); // upload and apply the dump of the pgsql database From a103d9062c2b22615474189f84761908a4aaf520 Mon Sep 17 00:00:00 2001 From: "P. Douglas Reeder" Date: Sat, 14 Jun 2025 21:04:22 -0400 Subject: [PATCH 5/9] Backup and restore scripts now continue if pgsql pod is missing. Why: An instance using an external database (https://hominidsoftware.com/tech-personal-growth/Hubs-Managed-Databse/Hubs-Managed-Database/) will not have a pgsql pod. Also, a damaged instance might not be running the pgsql pod. There is still value in backing up and/or restoring just the reticulum files. Also handles empty blocks in `hcce.yaml`. Also extracts IP address of all load balancers, as a modern ingress controller might not be in the `hcce` namespace. Open Question: backing and restoring up an external postgresql database might or might not fit in these scripts --- community-edition/backup_script/index.js | 23 +++++++++++++------ community-edition/get_ip/index.js | 18 ++++++++++----- community-edition/readme.md | 5 +++- .../restore_backup_script/index.js | 22 ++++++++++++------ 4 files changed, 47 insertions(+), 21 deletions(-) diff --git a/community-edition/backup_script/index.js b/community-edition/backup_script/index.js index c0ecddf7..97aaf21a 100644 --- a/community-edition/backup_script/index.js +++ b/community-edition/backup_script/index.js @@ -19,7 +19,7 @@ const pgDumpSQLPath = path.join(dataBackupPath, "pg_dump.sql"); // get pod names let reticulumPodName = execSync(`kubectl get pods -l=app=reticulum -n ${processedConfig.Namespace} --output jsonpath='{.items[0].metadata.name}'`); -let pgsqlPodName = execSync(`kubectl get pods -l=app=pgsql -n ${processedConfig.Namespace} --output jsonpath='{.items[0].metadata.name}'`); +let pgsqlPodName = execSync(`kubectl get pods -l=app=pgsql -n ${processedConfig.Namespace} --output jsonpath='{.items[*].metadata.name}'`); // strip out the single quotes that Windows adds in reticulumPodName = reticulumPodName.toString().replaceAll("'", ""); pgsqlPodName = pgsqlPodName.toString().replaceAll("'", ""); @@ -32,10 +32,19 @@ if (!fs.existsSync(rootDataBackupPath)) { // download reticulum storage // note: relative paths must be used for kubectl cp on windows due to this bug: https://github.com/kubernetes/kubernetes/issues/101985 -execSync(`kubectl cp --retries=-1 ${reticulumPodName}:/storage ${path.relative(process.cwd(), reticulumStoragePath)} -n ${processedConfig.Namespace}`); +const reticulumOutputPath = path.relative(process.cwd(), reticulumStoragePath); +console.log(`copying reticulum files to ${reticulumOutputPath}`); +execSync(`kubectl cp --retries=-1 ${reticulumPodName}:/storage ${reticulumOutputPath} -n ${processedConfig.Namespace}`); -// create and download dump of pgsql database -// note: relative paths must be used for kubectl cp on windows due to this bug: https://github.com/kubernetes/kubernetes/issues/101985 -execSync(`kubectl exec ${pgsqlPodName} -n ${processedConfig.Namespace} -- /bin/pg_dump -c ${processedConfig.PGRST_DB_URI} -f /root/pg_dump.sql`); -execSync(`kubectl cp --retries=-1 ${pgsqlPodName}:/root/pg_dump.sql ${path.relative(process.cwd(), pgDumpSQLPath)} -n ${processedConfig.Namespace}`); -execSync(`kubectl exec ${pgsqlPodName} -n ${processedConfig.Namespace} -- /bin/rm /root/pg_dump.sql`); +if (pgsqlPodName) { + // create and download dump of pgsql database + // note: relative paths must be used for kubectl cp on windows due to this bug: https://github.com/kubernetes/kubernetes/issues/101985 + console.log(`dumping pgsql`); + execSync(`kubectl exec ${pgsqlPodName} -n ${processedConfig.Namespace} -- /bin/pg_dump -c ${processedConfig.PGRST_DB_URI} -f /root/pg_dump.sql`); + const pgsqlOutputPath = path.relative(process.cwd(), pgDumpSQLPath); + console.log(`copying dump to ${pgsqlOutputPath}`); + execSync(`kubectl cp --retries=-1 ${pgsqlPodName}:/root/pg_dump.sql ${pgsqlOutputPath} -n ${processedConfig.Namespace}`); + execSync(`kubectl exec ${pgsqlPodName} -n ${processedConfig.Namespace} -- /bin/rm /root/pg_dump.sql`); +} else { + console.warn('not backing up pgsql; pod not found'); +} diff --git a/community-edition/get_ip/index.js b/community-edition/get_ip/index.js index 70f4d92d..c9a70127 100644 --- a/community-edition/get_ip/index.js +++ b/community-edition/get_ip/index.js @@ -4,13 +4,19 @@ const { spawnSync } = require("node:child_process"); const config = utils.readConfig(); const { stdout } = spawnSync( "kubectl", - ["-n", config.Namespace, "get", "svc", "lb", "-o", "json"], + ["get", "svc", "--field-selector", "spec.type=LoadBalancer", "-A", "-o", "json"], { stdio: ["pipe", "pipe", "inherit"] } ); const output = JSON.parse(stdout); -const ipAddr = output.status.loadBalancer?.ingress?.[0]?.ip; -if (ipAddr) { - console.log("load balancer external IP address:", ipAddr); -} else { - console.log("load balancer not running yet:", output.status.loadBalancer); +if (output.items.length === 0) { + console.warn("can't determine external IP address: no load balancers in cluster"); + process.exit(1); +} +for (const item of output.items) { + const ipAddr = item.status.loadBalancer?.ingress?.[0]?.ip; + if (ipAddr) { + console.log("load balancer external IP address:", ipAddr); + } else { + console.log("load balancer not running yet:", output.status.loadBalancer); + } } diff --git a/community-edition/readme.md b/community-edition/readme.md index a975bb5f..43a5750b 100644 --- a/community-edition/readme.md +++ b/community-edition/readme.md @@ -133,9 +133,12 @@ If you just need to get the external IP address of your load balancer, run ### Backing up and restoring your instance -Use `npm run backup` to backup your instance. The backup will be timestamped and placed in a `data_backups` folder. +Use `npm run backup` to back up your instance. The backup will be timestamped and placed in a `data_backups` folder. Use `npm run restore-backup data_backup_1234567890123` to restore a backup to your instance. If you don't specify a backup, and just use `npm run restore-backup`, it will default to the latest backup. +The `hcce.yaml` file in the `community-edition` directory must match your instance. + +If you run an external database instead of the `pgsql` pod, the scripts will only back up and restore the reticulum files. ## Guides from the Hubs Team and Community diff --git a/community-edition/restore_backup_script/index.js b/community-edition/restore_backup_script/index.js index 3d7759b0..badf1fd3 100644 --- a/community-edition/restore_backup_script/index.js +++ b/community-edition/restore_backup_script/index.js @@ -20,7 +20,7 @@ const hcce = utils.readTemplate("", "hcce.yaml"); const hcceYamlDocuments = YAML.parseAllDocuments(hcce); hcceYamlDocuments.forEach((doc, index) => { const jsDoc = doc.toJS(); - if (jsDoc.kind === "Ingress") { + if (jsDoc?.kind === "Ingress") { if (!jsDoc.metadata.annotations) { jsDoc.metadata["annotations"] = {}; } @@ -72,11 +72,15 @@ if (!fs.existsSync(dataBackupPath)) { // get pod names let reticulumPodName = execSync(`kubectl get pods -l=app=reticulum -n ${processedConfig.Namespace} --output jsonpath='{.items[0].metadata.name}'`); -let pgsqlPodName = execSync(`kubectl get pods -l=app=pgsql -n ${processedConfig.Namespace} --output jsonpath='{.items[0].metadata.name}'`); +let pgsqlPodName = execSync(`kubectl get pods -l=app=pgsql -n ${processedConfig.Namespace} --output jsonpath='{.items[*].metadata.name}'`); // strip out the single quotes that Windows adds in reticulumPodName = reticulumPodName.toString().replaceAll("'", ""); pgsqlPodName = pgsqlPodName.toString().replaceAll("'", ""); +if (!pgsqlPodName) { + console.warn("pgsql pod not found"); +} + console.log(""); console.log("restoring backup"); console.log(""); @@ -88,13 +92,17 @@ fs_object_names.forEach(fs_object_name => { execSync(`kubectl cp --retries=-1 ${path.join(reticulumStorageRelativePath, fs_object_name)} ${reticulumPodName}:/storage -c reticulum -n ${processedConfig.Namespace}`); }); +if (pgsqlPodName) { // upload and apply the dump of the pgsql database // note: relative paths must be used for kubectl cp on windows due to this bug: https://github.com/kubernetes/kubernetes/issues/101985 -console.log("restoring database"); -execSync(`kubectl cp --retries=-1 ${path.relative(process.cwd(), pgDumpSQLPath)} ${pgsqlPodName}:/root/pg_dump.sql -n ${processedConfig.Namespace}`); -execSync(`kubectl exec ${pgsqlPodName} -n ${processedConfig.Namespace} -- /bin/psql ${processedConfig.PGRST_DB_URI} -f /root/pg_dump.sql`); -execSync(`kubectl exec ${pgsqlPodName} -n ${processedConfig.Namespace} -- /bin/rm /root/pg_dump.sql`); - + const pgsqlInputPath = path.relative(process.cwd(), pgDumpSQLPath); + console.log(`restoring database from ${pgsqlInputPath}`); + execSync(`kubectl cp --retries=-1 ${pgsqlInputPath} ${pgsqlPodName}:/root/pg_dump.sql -n ${processedConfig.Namespace}`); + execSync(`kubectl exec ${pgsqlPodName} -n ${processedConfig.Namespace} -- /bin/psql ${processedConfig.PGRST_DB_URI} -f /root/pg_dump.sql`); + execSync(`kubectl exec ${pgsqlPodName} -n ${processedConfig.Namespace} -- /bin/rm /root/pg_dump.sql`); +} else { + console.warn('not restoring database'); +} // restart the Hubs instance so it doesn't error out when visited and maintenance mode is no longer applied fs.rmSync(path.join(process.cwd(), maintenanceModeHcceFileName)); From e357ea584cfbe1b9735ea8fd8b22332efc52d96e Mon Sep 17 00:00:00 2001 From: "P. Douglas Reeder" Date: Tue, 5 Aug 2025 18:05:21 -0400 Subject: [PATCH 6/9] ip-addr: Displays name and namespace of each load balancer Why: If there is more than one load balancer in the cluster, the user needs to select the appropriate one. --- community-edition/get_ip/index.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/community-edition/get_ip/index.js b/community-edition/get_ip/index.js index c9a70127..145b5643 100644 --- a/community-edition/get_ip/index.js +++ b/community-edition/get_ip/index.js @@ -13,10 +13,13 @@ if (output.items.length === 0) { process.exit(1); } for (const item of output.items) { - const ipAddr = item.status.loadBalancer?.ingress?.[0]?.ip; - if (ipAddr) { - console.log("load balancer external IP address:", ipAddr); + const name = item.metadata.name; + const namespace = item.metadata.namespace; + const status = item.status; + const addr = status?.loadBalancer?.ingress?.[0]?.ip || status?.loadBalancer?.ingress?.[0]?.hostname; + if (addr) { + console.log(`load balancer “${name}” in namespace “${namespace}” external address: ${addr}`); } else { - console.log("load balancer not running yet:", output.status.loadBalancer); + console.log(`load balancer “${name}” in namespace “${namespace}” not running yet:`, status); } } From 4ec9c6bb0e624727c7bceb16ec821014544423b5 Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Mon, 18 Aug 2025 23:52:09 -0400 Subject: [PATCH 7/9] Filter out OS created helper files/folders What: Uses the "junk" package to remove any OS helper files/folders that were created in the Reticulum storage data before restoring the backup. Why: Various user actions can result in the user's OS generating helper files/folders that aren't needed by Hubs, which increases the upload size and clutters up the restored reticulum data back on the Kubernetes storage. Note: This encloses the entire restore-backup script in an async function in order to allow loading the "junk" package, which doesn't support require/CommonJS modules. --- community-edition/package-lock.json | 12 + community-edition/package.json | 1 + .../restore_backup_script/index.js | 219 ++++++++++-------- 3 files changed, 134 insertions(+), 98 deletions(-) diff --git a/community-edition/package-lock.json b/community-edition/package-lock.json index cba91351..a90ca274 100644 --- a/community-edition/package-lock.json +++ b/community-edition/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "inquirer": "^10.0.1", + "junk": "^4.0.1", "node-forge": "^1.3.1", "openssl-nodejs": "^1.0.5", "pem-jwk": "^2.0.0", @@ -367,6 +368,17 @@ "node": ">=8" } }, + "node_modules/junk": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/junk/-/junk-4.0.1.tgz", + "integrity": "sha512-Qush0uP+G8ZScpGMZvHUiRfI0YBWuB3gVBYlI0v0vvOJt5FLicco+IkP0a50LqTTQhmts/m6tP5SWE+USyIvcQ==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", diff --git a/community-edition/package.json b/community-edition/package.json index 3f909668..287792ed 100644 --- a/community-edition/package.json +++ b/community-edition/package.json @@ -18,6 +18,7 @@ "description": "", "dependencies": { "inquirer": "^10.0.1", + "junk": "^4.0.1", "node-forge": "^1.3.1", "openssl-nodejs": "^1.0.5", "pem-jwk": "^2.0.0", diff --git a/community-edition/restore_backup_script/index.js b/community-edition/restore_backup_script/index.js index badf1fd3..4fd11424 100644 --- a/community-edition/restore_backup_script/index.js +++ b/community-edition/restore_backup_script/index.js @@ -1,113 +1,136 @@ -const execSync = require('child_process').execSync; -const fs = require("fs"); -const path = require("path"); -const YAML = require("yaml"); -const utils = require("../utils.js"); +(async () => { + const execSync = require('child_process').execSync; + const fs = require("fs"); + const path = require("path"); + const YAML = require("yaml"); + const utils = require("../utils.js"); + const junk = await import("junk"); -// get command line arguments -const args = process.argv.slice(2); + // get command line arguments + const args = process.argv.slice(2); -// read config -const config = utils.readConfig(); -const processedConfig = YAML.parse( - utils.replacePlaceholders(YAML.stringify(config), config), - {"schema": "yaml-1.1"} // required to load yes/no as boolean values -); + // read config + const config = utils.readConfig(); + const processedConfig = YAML.parse( + utils.replacePlaceholders(YAML.stringify(config), config), + {"schema": "yaml-1.1"} // required to load yes/no as boolean values + ); -// apply maintenance mode -const maintenanceModeHcceFileName = "maintenance-mode-hcce.yaml" -const hcce = utils.readTemplate("", "hcce.yaml"); -const hcceYamlDocuments = YAML.parseAllDocuments(hcce); -hcceYamlDocuments.forEach((doc, index) => { - const jsDoc = doc.toJS(); - if (jsDoc?.kind === "Ingress") { - if (!jsDoc.metadata.annotations) { - jsDoc.metadata["annotations"] = {}; + // apply maintenance mode + const maintenanceModeHcceFileName = "maintenance-mode-hcce.yaml" + const hcce = utils.readTemplate("", "hcce.yaml"); + const hcceYamlDocuments = YAML.parseAllDocuments(hcce); + hcceYamlDocuments.forEach((doc, index) => { + const jsDoc = doc.toJS(); + if (jsDoc?.kind === "Ingress") { + if (!jsDoc.metadata.annotations) { + jsDoc.metadata["annotations"] = {}; + } + jsDoc.metadata.annotations["haproxy.org/request-redirect"] = `hubs-maintenance-mode.${processedConfig.HUB_DOMAIN}` + hcceYamlDocuments[index] = new YAML.Document(jsDoc); } - jsDoc.metadata.annotations["haproxy.org/request-redirect"] = `hubs-maintenance-mode.${processedConfig.HUB_DOMAIN}` - hcceYamlDocuments[index] = new YAML.Document(jsDoc); - } -}); -const maintenanceModeHcce = `${hcceYamlDocuments.map(doc => YAML.stringify(doc, {"lineWidth": 0, "directives": false})).join('---\n')}` -utils.writeOutputFile(maintenanceModeHcce, "", maintenanceModeHcceFileName); + }); + const maintenanceModeHcce = `${hcceYamlDocuments.map(doc => YAML.stringify(doc, {"lineWidth": 0, "directives": false})).join('---\n')}` + utils.writeOutputFile(maintenanceModeHcce, "", maintenanceModeHcceFileName); -console.log("applying maintenance mode"); -console.log(""); -execSync(`kubectl delete deployment --all -n ${processedConfig.Namespace}`, {stdio: 'inherit'}); -execSync(`kubectl delete pods --all -n ${processedConfig.Namespace}`, {stdio: 'inherit'}); -execSync(`kubectl apply -f ${maintenanceModeHcceFileName}`, {stdio: 'inherit'}); -let pendingDeploymentNames = [] -while (true) { - let deployments = JSON.parse(execSync(`kubectl get deployment -n ${config.Namespace} -o json`)).items; - let pendingDeployments = deployments.filter(deployment => (deployment.status.readyReplicas ?? 0) < deployment.status.replicas); + console.log("applying maintenance mode"); + console.log(""); + execSync(`kubectl delete deployment --all -n ${processedConfig.Namespace}`, {stdio: 'inherit'}); + execSync(`kubectl delete pods --all -n ${processedConfig.Namespace}`, {stdio: 'inherit'}); + execSync(`kubectl apply -f ${maintenanceModeHcceFileName}`, {stdio: 'inherit'}); + let pendingDeploymentNames = [] + while (true) { + let deployments = JSON.parse(execSync(`kubectl get deployment -n ${config.Namespace} -o json`)).items; + let pendingDeployments = deployments.filter(deployment => (deployment.status.readyReplicas ?? 0) < deployment.status.replicas); - if (pendingDeployments.length) { - currentPendingDeploymentNames = pendingDeployments.map(deployment => deployment.metadata.name) - if (currentPendingDeploymentNames.toString() !== pendingDeploymentNames.toString()) { - console.log(`waiting on ${currentPendingDeploymentNames.join(", ")}`); - pendingDeploymentNames = currentPendingDeploymentNames; + if (pendingDeployments.length) { + currentPendingDeploymentNames = pendingDeployments.map(deployment => deployment.metadata.name) + if (currentPendingDeploymentNames.toString() !== pendingDeploymentNames.toString()) { + console.log(`waiting on ${currentPendingDeploymentNames.join(", ")}`); + pendingDeploymentNames = currentPendingDeploymentNames; + } + // Wait for 1 second + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1000); + } else { + console.log("maintenance mode applied") + break; } - // Wait for 1 second - Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1000); - } else { - console.log("maintenance mode applied") - break; } -} -// get backup paths -const rootDataBackupPath = path.join(process.cwd(), "data_backups"); -const backup_name = args[0] ? args[0] : - fs.readdirSync(rootDataBackupPath) - .filter(name => name.includes("data_backup")) - .sort().at(-1); -const dataBackupPath = path.join(rootDataBackupPath, backup_name); -const reticulumStoragePath = path.join(dataBackupPath, "reticulum_storage_data"); -const reticulumStorageRelativePath = path.relative(process.cwd(), reticulumStoragePath); -const pgDumpSQLPath = path.join(dataBackupPath, "pg_dump.sql"); -if (!fs.existsSync(dataBackupPath)) { - console.error("the specified backup doesn't exist"); - process.exit(1); + // get backup paths + const rootDataBackupPath = path.join(process.cwd(), "data_backups"); + const backup_name = args[0] ? args[0] : + fs.readdirSync(rootDataBackupPath) + .filter(name => name.includes("data_backup")) + .sort().at(-1); + const dataBackupPath = path.join(rootDataBackupPath, backup_name); + const reticulumStoragePath = path.join(dataBackupPath, "reticulum_storage_data"); + const reticulumStorageRelativePath = path.relative(process.cwd(), reticulumStoragePath); + const pgDumpSQLPath = path.join(dataBackupPath, "pg_dump.sql"); + if (!fs.existsSync(dataBackupPath)) { + console.error("the specified backup doesn't exist"); + process.exit(1); + } + + // get pod names + let reticulumPodName = execSync(`kubectl get pods -l=app=reticulum -n ${processedConfig.Namespace} --output jsonpath='{.items[0].metadata.name}'`); + let pgsqlPodName = execSync(`kubectl get pods -l=app=pgsql -n ${processedConfig.Namespace} --output jsonpath='{.items[*].metadata.name}'`); + // strip out the single quotes that Windows adds in + reticulumPodName = reticulumPodName.toString().replaceAll("'", ""); + pgsqlPodName = pgsqlPodName.toString().replaceAll("'", ""); + + if (!pgsqlPodName) { + console.warn("pgsql pod not found"); } -// get pod names -let reticulumPodName = execSync(`kubectl get pods -l=app=reticulum -n ${processedConfig.Namespace} --output jsonpath='{.items[0].metadata.name}'`); -let pgsqlPodName = execSync(`kubectl get pods -l=app=pgsql -n ${processedConfig.Namespace} --output jsonpath='{.items[*].metadata.name}'`); -// strip out the single quotes that Windows adds in -reticulumPodName = reticulumPodName.toString().replaceAll("'", ""); -pgsqlPodName = pgsqlPodName.toString().replaceAll("'", ""); + console.log(""); + console.log("restoring backup"); + console.log(""); -if (!pgsqlPodName) { - console.warn("pgsql pod not found"); -} + // remove any OS helper files from the reticulum storage + function remove_os_helper_files_recursive(base_path) { + if (fs.statSync(base_path).isDirectory()) { + let fs_object_names = fs.readdirSync(base_path); -console.log(""); -console.log("restoring backup"); -console.log(""); -// upload reticulum storage -// note: relative paths must be used for kubectl cp on windows due to this bug: https://github.com/kubernetes/kubernetes/issues/101985 -let fs_object_names = fs.readdirSync(reticulumStoragePath); -fs_object_names.forEach(fs_object_name => { - console.log(`restoring Reticulum '${fs_object_name}' folder`); - execSync(`kubectl cp --retries=-1 ${path.join(reticulumStorageRelativePath, fs_object_name)} ${reticulumPodName}:/storage -c reticulum -n ${processedConfig.Namespace}`); -}); + fs_object_names.forEach(fs_object_name => { + let fs_object_path = path.join(base_path, fs_object_name); -if (pgsqlPodName) { -// upload and apply the dump of the pgsql database -// note: relative paths must be used for kubectl cp on windows due to this bug: https://github.com/kubernetes/kubernetes/issues/101985 - const pgsqlInputPath = path.relative(process.cwd(), pgDumpSQLPath); - console.log(`restoring database from ${pgsqlInputPath}`); - execSync(`kubectl cp --retries=-1 ${pgsqlInputPath} ${pgsqlPodName}:/root/pg_dump.sql -n ${processedConfig.Namespace}`); - execSync(`kubectl exec ${pgsqlPodName} -n ${processedConfig.Namespace} -- /bin/psql ${processedConfig.PGRST_DB_URI} -f /root/pg_dump.sql`); - execSync(`kubectl exec ${pgsqlPodName} -n ${processedConfig.Namespace} -- /bin/rm /root/pg_dump.sql`); -} else { - console.warn('not restoring database'); -} + if (junk.isJunk(fs_object_name)) { + // delete unneeded OS helper files that may have been added to the backup by the user's OS. + fs.rmSync(fs_object_path, { recursive: true, force: true }); + } else { + remove_os_helper_files_recursive(fs_object_path); + } + }); + } + } + remove_os_helper_files_recursive(reticulumStoragePath); + + // upload reticulum storage + // note: relative paths must be used for kubectl cp on windows due to this bug: https://github.com/kubernetes/kubernetes/issues/101985 + let fs_object_names = fs.readdirSync(reticulumStoragePath); + fs_object_names.forEach(fs_object_name => { + console.log(`restoring Reticulum '${fs_object_name}' folder`); + execSync(`kubectl cp --retries=-1 ${path.join(reticulumStorageRelativePath, fs_object_name)} ${reticulumPodName}:/storage -c reticulum -n ${processedConfig.Namespace}`); + }); + + if (pgsqlPodName) { + // upload and apply the dump of the pgsql database + // note: relative paths must be used for kubectl cp on windows due to this bug: https://github.com/kubernetes/kubernetes/issues/101985 + const pgsqlInputPath = path.relative(process.cwd(), pgDumpSQLPath); + console.log(`restoring database from ${pgsqlInputPath}`); + execSync(`kubectl cp --retries=-1 ${pgsqlInputPath} ${pgsqlPodName}:/root/pg_dump.sql -n ${processedConfig.Namespace}`); + execSync(`kubectl exec ${pgsqlPodName} -n ${processedConfig.Namespace} -- /bin/psql ${processedConfig.PGRST_DB_URI} -f /root/pg_dump.sql`); + execSync(`kubectl exec ${pgsqlPodName} -n ${processedConfig.Namespace} -- /bin/rm /root/pg_dump.sql`); + } else { + console.warn('not restoring database'); + } -// restart the Hubs instance so it doesn't error out when visited and maintenance mode is no longer applied -fs.rmSync(path.join(process.cwd(), maintenanceModeHcceFileName)); -console.log(""); -console.log("restarting instance"); -execSync(`kubectl delete deployment --all -n ${processedConfig.Namespace}`, {stdio: 'inherit'}); -execSync(`kubectl delete pods --all -n ${processedConfig.Namespace}`, {stdio: 'inherit'}); -execSync(`npm run apply`, {stdio: 'inherit'}); + // restart the Hubs instance so it doesn't error out when visited and maintenance mode is no longer applied + fs.rmSync(path.join(process.cwd(), maintenanceModeHcceFileName)); + console.log(""); + console.log("restarting instance"); + execSync(`kubectl delete deployment --all -n ${processedConfig.Namespace}`, {stdio: 'inherit'}); + execSync(`kubectl delete pods --all -n ${processedConfig.Namespace}`, {stdio: 'inherit'}); + execSync(`npm run apply`, {stdio: 'inherit'}); +})(); From 7d71bb2ca971d1d46b1594a29e5aa00ee6ada05a Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Mon, 18 Aug 2025 23:54:25 -0400 Subject: [PATCH 8/9] Fix kubectl cp sometimes failing (and not retrying) in version 1.30+ What: Passes an environment variable to the kubectl cp command to disable using websockets. Why: Websockets are enabled by default in kubectl 1.30+ and this can cause transfers to fail and not retry. Disabling websockets avoids the issue. Referrences: Link to GitHub issue with the documented workaround: https://github.com/kubernetes/kubernetes/issues/60140#issuecomment-2565537460 Link to GitHub PR which introduced websockets as the default and the note that it affects kubectl cp: https://github.com/kubernetes/kubernetes/pull/123281#issuecomment-2647550899 --- community-edition/backup_script/index.js | 4 ++-- community-edition/restore_backup_script/index.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/community-edition/backup_script/index.js b/community-edition/backup_script/index.js index 97aaf21a..e9bd1cbe 100644 --- a/community-edition/backup_script/index.js +++ b/community-edition/backup_script/index.js @@ -34,7 +34,7 @@ if (!fs.existsSync(rootDataBackupPath)) { // note: relative paths must be used for kubectl cp on windows due to this bug: https://github.com/kubernetes/kubernetes/issues/101985 const reticulumOutputPath = path.relative(process.cwd(), reticulumStoragePath); console.log(`copying reticulum files to ${reticulumOutputPath}`); -execSync(`kubectl cp --retries=-1 ${reticulumPodName}:/storage ${reticulumOutputPath} -n ${processedConfig.Namespace}`); +execSync(`kubectl cp --retries=-1 ${reticulumPodName}:/storage ${reticulumOutputPath} -n ${processedConfig.Namespace}`, { env: { ...process.env, KUBECTL_REMOTE_COMMAND_WEBSOCKETS: false } }); if (pgsqlPodName) { // create and download dump of pgsql database @@ -43,7 +43,7 @@ if (pgsqlPodName) { execSync(`kubectl exec ${pgsqlPodName} -n ${processedConfig.Namespace} -- /bin/pg_dump -c ${processedConfig.PGRST_DB_URI} -f /root/pg_dump.sql`); const pgsqlOutputPath = path.relative(process.cwd(), pgDumpSQLPath); console.log(`copying dump to ${pgsqlOutputPath}`); - execSync(`kubectl cp --retries=-1 ${pgsqlPodName}:/root/pg_dump.sql ${pgsqlOutputPath} -n ${processedConfig.Namespace}`); + execSync(`kubectl cp --retries=-1 ${pgsqlPodName}:/root/pg_dump.sql ${pgsqlOutputPath} -n ${processedConfig.Namespace}`, { env: { ...process.env, KUBECTL_REMOTE_COMMAND_WEBSOCKETS: false } }); execSync(`kubectl exec ${pgsqlPodName} -n ${processedConfig.Namespace} -- /bin/rm /root/pg_dump.sql`); } else { console.warn('not backing up pgsql; pod not found'); diff --git a/community-edition/restore_backup_script/index.js b/community-edition/restore_backup_script/index.js index 4fd11424..e947bede 100644 --- a/community-edition/restore_backup_script/index.js +++ b/community-edition/restore_backup_script/index.js @@ -111,7 +111,7 @@ let fs_object_names = fs.readdirSync(reticulumStoragePath); fs_object_names.forEach(fs_object_name => { console.log(`restoring Reticulum '${fs_object_name}' folder`); - execSync(`kubectl cp --retries=-1 ${path.join(reticulumStorageRelativePath, fs_object_name)} ${reticulumPodName}:/storage -c reticulum -n ${processedConfig.Namespace}`); + execSync(`kubectl cp --retries=-1 ${path.join(reticulumStorageRelativePath, fs_object_name)} ${reticulumPodName}:/storage -c reticulum -n ${processedConfig.Namespace}`, { env: { ...process.env, KUBECTL_REMOTE_COMMAND_WEBSOCKETS: false } }); }); if (pgsqlPodName) { @@ -119,7 +119,7 @@ // note: relative paths must be used for kubectl cp on windows due to this bug: https://github.com/kubernetes/kubernetes/issues/101985 const pgsqlInputPath = path.relative(process.cwd(), pgDumpSQLPath); console.log(`restoring database from ${pgsqlInputPath}`); - execSync(`kubectl cp --retries=-1 ${pgsqlInputPath} ${pgsqlPodName}:/root/pg_dump.sql -n ${processedConfig.Namespace}`); + execSync(`kubectl cp --retries=-1 ${pgsqlInputPath} ${pgsqlPodName}:/root/pg_dump.sql -n ${processedConfig.Namespace}`, { env: { ...process.env, KUBECTL_REMOTE_COMMAND_WEBSOCKETS: false } }); execSync(`kubectl exec ${pgsqlPodName} -n ${processedConfig.Namespace} -- /bin/psql ${processedConfig.PGRST_DB_URI} -f /root/pg_dump.sql`); execSync(`kubectl exec ${pgsqlPodName} -n ${processedConfig.Namespace} -- /bin/rm /root/pg_dump.sql`); } else { From bad06cd35ae4478adbf9a5c841c423ec45c15851 Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Tue, 19 Aug 2025 00:00:56 -0400 Subject: [PATCH 9/9] Remove the Reticulum pod's storage before restoring the backup What: Uses the "find" command in the Reticulum pod to remove the contents of the Reticulum storage directory on the Kubernetes cluster before restoring the contents of the local backup. Why: To ensure a full restoration. kubectl cp merges the source directory into the destination directory, so depending on what's in the Reticulum storage on the Kubernetes cluster, there may be stuff left over from before the backup was applied that will remain if the Reticulum storage isn't cleared first, which would cause the final result to be different from the backup. --- community-edition/restore_backup_script/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/community-edition/restore_backup_script/index.js b/community-edition/restore_backup_script/index.js index e947bede..25fc55f8 100644 --- a/community-edition/restore_backup_script/index.js +++ b/community-edition/restore_backup_script/index.js @@ -106,6 +106,9 @@ } remove_os_helper_files_recursive(reticulumStoragePath); + // remove the reticulum pod's storage on the kubernetes cluster + execSync(`kubectl exec ${reticulumPodName} -c reticulum -n ${processedConfig.Namespace} -- /usr/bin/find /storage -mindepth 1 -delete`); + // upload reticulum storage // note: relative paths must be used for kubectl cp on windows due to this bug: https://github.com/kubernetes/kubernetes/issues/101985 let fs_object_names = fs.readdirSync(reticulumStoragePath);