mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
Route deployment updates through evidence-preserving procedures
Remove unconfigured Docker and Helm shorthand from the deployment overview. Preserve target edition, existing service and storage scope, and the limits of snapshots and rollback; exercise the maintained help links and regression guards. Change-source: pulse-maintainer
This commit is contained in:
+45
-16
@@ -4,7 +4,7 @@ Pulse supports multiple deployment models. This page clarifies what differs betw
|
||||
|
||||
## Summary
|
||||
|
||||
| Model | Recommended for | Data/config path | Updates |
|
||||
| Model | Recommended for | Default data/config path | Updates |
|
||||
| --- | --- | --- | --- |
|
||||
| Proxmox VE LXC (installer) | Proxmox-first deployments | `/etc/pulse` | In-app updates supported |
|
||||
| systemd (bare metal / VM) | Traditional Linux hosts | `/etc/pulse` | In-app updates supported |
|
||||
@@ -59,11 +59,16 @@ Pulse uses a split config model:
|
||||
- **Relay config**: `relay.enc` (encrypted, Relay and above)
|
||||
- **RBAC roles**: `rbac_roles.json` (Pro/legacy Pro+/Cloud)
|
||||
|
||||
Path mapping:
|
||||
Default path mapping:
|
||||
|
||||
- systemd/LXC: `/etc/pulse/*`
|
||||
- Docker/Helm: `/data/*`
|
||||
|
||||
These are defaults, not a backup inventory. Check the active deployment's
|
||||
`PULSE_DATA_DIR`, mounts and any external stores before copying or restoring
|
||||
files. Metrics can use a separate `PULSE_METRICS_DB_PATH`; see
|
||||
[Metrics storage](METRICS_HISTORY.md#storage-location).
|
||||
|
||||
The audit paths above are the default store, not a licence-dependent guarantee
|
||||
that storage initialised successfully. Runtime-specific storage can differ;
|
||||
see [Audit storage and safe recovery](AUDIT_LOGGING.md#storage) before restoring
|
||||
@@ -84,30 +89,54 @@ Provider-hosted MSP layout:
|
||||
|
||||
## Updates by Model
|
||||
|
||||
Before changing the server binary or image, record its running version and
|
||||
edition, keep the saved deployment configuration, and take a consistent private
|
||||
backup of the actual data and external stores. Preserve encrypted files with
|
||||
their matching `.encryption.key` and audit signing key. Use a stopped, consistent
|
||||
filesystem/volume backup or supported backup tooling; copying a live `.db` file
|
||||
alone can omit SQLite sidecar files. Do not post configuration files, environment
|
||||
dumps or Helm secret values in an issue.
|
||||
|
||||
An update snapshot is not necessarily a complete data backup, and reverting a
|
||||
binary or image does not undo data migrations. Check
|
||||
[rollback and backup scope](AUTO_UPDATE.md#rollback) before updating or reverting;
|
||||
keep the previous binary/image and matching data backup until recovery is checked.
|
||||
|
||||
### systemd and Proxmox LXC
|
||||
|
||||
Use the UI:
|
||||
Use **Settings → System → Updates**. Follow
|
||||
[server update guidance](AUTO_UPDATE.md) for the deployment's update controls and
|
||||
Update History. Pulse attempts snapshot and rollback operations; a recorded
|
||||
backup path alone does not establish that all active data was copied or that
|
||||
recovery succeeded.
|
||||
|
||||
- **Settings → System → Updates**
|
||||
|
||||
These deployments can apply updates by downloading a release and swapping binaries/config safely with backups and history.
|
||||
For LXC, Pulse's service and data are inside the container, not on the Proxmox
|
||||
host. Keep the failed state and check the running version and service health
|
||||
before retrying a failed update.
|
||||
|
||||
### Docker
|
||||
|
||||
Pull a new image and restart:
|
||||
Follow [Docker server updates](DOCKER.md#-updates) from the original Compose
|
||||
project or saved container deployment. Persist the exact target in its `image:`
|
||||
line, or in `PULSE_IMAGE` when that line uses the variable; pulling a different
|
||||
tag alone does not change the configured image. Paid Pro installs must keep
|
||||
their private image, not replace it with the Community image.
|
||||
|
||||
Pull and recreate only the Pulse service, stopping if the pull fails. Preserve
|
||||
its data volume, ports and settings, and check the running image and server
|
||||
version afterwards. This updates the Pulse server, not its agents or the
|
||||
containers it monitors; there is no need to restart unrelated Compose services.
|
||||
|
||||
```bash
|
||||
docker pull rcourtman/pulse:latest
|
||||
docker compose up -d
|
||||
```
|
||||
### Kubernetes (Helm)
|
||||
|
||||
Upgrade the chart:
|
||||
Use the existing Helm release and namespace, the chosen chart version and your
|
||||
saved deployment values. Review changes to persistence, Secret references and
|
||||
image edition before rollout; keep the current PVC, keys and settings. Do not
|
||||
let a default Community image replace paid Pro.
|
||||
|
||||
```bash
|
||||
helm repo update
|
||||
helm upgrade pulse pulse/pulse -n pulse
|
||||
```
|
||||
See [Kubernetes deployment settings](KUBERNETES.md) and use your cluster's
|
||||
rollout and recovery procedure. A Helm rollback changes release resources; it
|
||||
does not guarantee restoration of Pulse data changed by a newer binary.
|
||||
|
||||
### Provider-hosted MSP
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// Production Docs/router and real shipped Markdown; no backend or deployment.
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { chromium, webkit } = require('playwright');
|
||||
|
||||
(async () => {
|
||||
const root = '/workspace/frontend-modern';
|
||||
process.chdir(root);
|
||||
const artifacts = path.join(root, 'node_modules', 'deployment-update-help-browser');
|
||||
fs.mkdirSync(artifacts, { recursive: true });
|
||||
const { createServer } = await import(path.join(root, 'node_modules/vite/dist/node/index.js'));
|
||||
const server = await createServer({
|
||||
root,
|
||||
configFile: path.join(root, 'vite.config.ts'),
|
||||
cacheDir: path.join(root, 'node_modules', '.vite-deployment-update-help'),
|
||||
server: { host: '127.0.0.1', port: 5250, strictPort: true },
|
||||
optimizeDeps: { noDiscovery: true, include: [] },
|
||||
});
|
||||
const results = [];
|
||||
let browser;
|
||||
try {
|
||||
await server.listen();
|
||||
for (const [engine, width, tone] of [['chromium', 1280, 'light'], ['webkit', 390, 'dark']]) {
|
||||
browser = await { chromium, webkit }[engine].launch(
|
||||
engine === 'chromium'
|
||||
? { headless: true, channel: 'chromium', args: ['--no-sandbox'] }
|
||||
: { headless: true },
|
||||
);
|
||||
const page = await browser.newPage({
|
||||
viewport: { width, height: 900 },
|
||||
...(engine === 'webkit' ? { isMobile: true, hasTouch: true } : {}),
|
||||
});
|
||||
const errors = [];
|
||||
page.on('pageerror', (e) => errors.push(e.message));
|
||||
page.on('console', (m) => { if (m.type() === 'error') errors.push(m.text()); });
|
||||
await page.goto('http://127.0.0.1:5250/browser-tests/docs-fragment-navigation.html',
|
||||
{ waitUntil: 'domcontentloaded', timeout: 60_000 });
|
||||
await page.getByRole('link', { name: '← All documentation' }).click();
|
||||
await page.getByRole('link', { name: 'Deployment models', exact: true }).click();
|
||||
await page.getByRole('heading', { name: 'Updates by Model', exact: true }).waitFor();
|
||||
if (tone === 'dark') await page.evaluate(async () => {
|
||||
document.documentElement.classList.add('dark');
|
||||
await new Promise((resolve) => requestAnimationFrame(resolve));
|
||||
await Promise.all(document.getAnimations().map((animation) => animation.finished.catch(() => {})));
|
||||
});
|
||||
const text = await page.locator('article').innerText();
|
||||
for (const phrase of ['defaults, not a backup inventory', 'SQLite sidecar files',
|
||||
'snapshot is not necessarily a complete data backup', 'Persist the exact target',
|
||||
'Paid Pro installs must keep their private image', 'only the Pulse service',
|
||||
'stopping if the pull fails', 'existing Helm release and namespace',
|
||||
'current PVC, keys and settings']) assert.ok(text.includes(phrase), phrase);
|
||||
for (const unsafe of ['docker pull rcourtman/pulse:latest', 'docker compose up -d',
|
||||
'helm upgrade pulse pulse/pulse', 'swapping binaries/config safely'])
|
||||
assert.ok(!text.includes(unsafe), unsafe);
|
||||
await page.getByRole('heading', { name: 'Updates by Model', exact: true })
|
||||
.evaluate((heading) => heading.scrollIntoView({ block: 'start' }));
|
||||
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1));
|
||||
await page.screenshot({ path: path.join(artifacts, `${engine}-overview.png`) });
|
||||
|
||||
// Keyboard activation must reach the actual maintained procedure/anchor.
|
||||
const docker = page.getByRole('link', { name: 'Docker server updates', exact: true });
|
||||
await docker.focus();
|
||||
await page.keyboard.press('Enter');
|
||||
await page.getByRole('heading', { name: '🔄 Updates', exact: true }).waitFor();
|
||||
await page.waitForFunction(() => {
|
||||
const heading = document.getElementById('-updates');
|
||||
return heading && heading.getBoundingClientRect().top >= 0 && heading.getBoundingClientRect().top < innerHeight;
|
||||
});
|
||||
assert.ok(page.url().endsWith('/docs/DOCKER#-updates'));
|
||||
const dockerText = await page.locator('article').innerText();
|
||||
for (const phrase of ['persist PULSE_IMAGE', 'docker compose pull pulse',
|
||||
'docker compose up -d --no-deps pulse', 'paid Pro installs must keep the private image'])
|
||||
assert.ok(dockerText.includes(phrase), phrase);
|
||||
await page.screenshot({ path: path.join(artifacts, `${engine}-docker.png`) });
|
||||
await page.getByRole('link', { name: 'rollback and backup scope', exact: true }).click();
|
||||
await page.getByRole('heading', { name: 'Rollback', exact: true }).waitFor();
|
||||
assert.ok(page.url().endsWith('/docs/AUTO_UPDATE#rollback'));
|
||||
assert.ok((await page.locator('article').innerText()).includes('Copy errors can leave a partial snapshot'));
|
||||
|
||||
await page.goBack();
|
||||
await page.goBack();
|
||||
await page.getByRole('heading', { name: 'Updates by Model', exact: true }).waitFor();
|
||||
await page.getByRole('link', { name: 'Kubernetes deployment settings', exact: true }).click();
|
||||
await page.waitForFunction(() => location.pathname === '/docs/KUBERNETES' &&
|
||||
document.querySelector('article')?.textContent.includes('Kubernetes cluster'));
|
||||
await page.goBack();
|
||||
await page.getByRole('link', { name: 'Metrics storage', exact: true }).click();
|
||||
await page.getByRole('heading', { name: 'Storage Location', exact: true }).waitFor();
|
||||
assert.ok(page.url().endsWith('/docs/METRICS_HISTORY#storage-location'));
|
||||
assert.deepEqual(errors, []);
|
||||
results.push({ engine, version: browser.version(), width, tone,
|
||||
overviewLimits: true, keyboardDockerLink: true, scopedDockerProcedure: true,
|
||||
rollbackLink: true, kubernetesLink: true, metricsLink: true, errors });
|
||||
await browser.close();
|
||||
browser = undefined;
|
||||
}
|
||||
const result = { playwright: require('playwright/package.json').version, results };
|
||||
fs.writeFileSync(path.join(artifacts, 'result.json'), `${JSON.stringify(result, null, 2)}\n`);
|
||||
process.stdout.write(`${JSON.stringify(result)}\n`);
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
await server.close();
|
||||
}
|
||||
})().catch((error) => { process.stderr.write(`${error.stack}\n`); process.exitCode = 1; });
|
||||
@@ -4,7 +4,7 @@ Pulse supports multiple deployment models. This page clarifies what differs betw
|
||||
|
||||
## Summary
|
||||
|
||||
| Model | Recommended for | Data/config path | Updates |
|
||||
| Model | Recommended for | Default data/config path | Updates |
|
||||
| --- | --- | --- | --- |
|
||||
| Proxmox VE LXC (installer) | Proxmox-first deployments | `/etc/pulse` | In-app updates supported |
|
||||
| systemd (bare metal / VM) | Traditional Linux hosts | `/etc/pulse` | In-app updates supported |
|
||||
@@ -59,11 +59,16 @@ Pulse uses a split config model:
|
||||
- **Relay config**: `relay.enc` (encrypted, Relay and above)
|
||||
- **RBAC roles**: `rbac_roles.json` (Pro/legacy Pro+/Cloud)
|
||||
|
||||
Path mapping:
|
||||
Default path mapping:
|
||||
|
||||
- systemd/LXC: `/etc/pulse/*`
|
||||
- Docker/Helm: `/data/*`
|
||||
|
||||
These are defaults, not a backup inventory. Check the active deployment's
|
||||
`PULSE_DATA_DIR`, mounts and any external stores before copying or restoring
|
||||
files. Metrics can use a separate `PULSE_METRICS_DB_PATH`; see
|
||||
[Metrics storage](METRICS_HISTORY.md#storage-location).
|
||||
|
||||
The audit paths above are the default store, not a licence-dependent guarantee
|
||||
that storage initialised successfully. Runtime-specific storage can differ;
|
||||
see [Audit storage and safe recovery](AUDIT_LOGGING.md#storage) before restoring
|
||||
@@ -84,30 +89,54 @@ Provider-hosted MSP layout:
|
||||
|
||||
## Updates by Model
|
||||
|
||||
Before changing the server binary or image, record its running version and
|
||||
edition, keep the saved deployment configuration, and take a consistent private
|
||||
backup of the actual data and external stores. Preserve encrypted files with
|
||||
their matching `.encryption.key` and audit signing key. Use a stopped, consistent
|
||||
filesystem/volume backup or supported backup tooling; copying a live `.db` file
|
||||
alone can omit SQLite sidecar files. Do not post configuration files, environment
|
||||
dumps or Helm secret values in an issue.
|
||||
|
||||
An update snapshot is not necessarily a complete data backup, and reverting a
|
||||
binary or image does not undo data migrations. Check
|
||||
[rollback and backup scope](AUTO_UPDATE.md#rollback) before updating or reverting;
|
||||
keep the previous binary/image and matching data backup until recovery is checked.
|
||||
|
||||
### systemd and Proxmox LXC
|
||||
|
||||
Use the UI:
|
||||
Use **Settings → System → Updates**. Follow
|
||||
[server update guidance](AUTO_UPDATE.md) for the deployment's update controls and
|
||||
Update History. Pulse attempts snapshot and rollback operations; a recorded
|
||||
backup path alone does not establish that all active data was copied or that
|
||||
recovery succeeded.
|
||||
|
||||
- **Settings → System → Updates**
|
||||
|
||||
These deployments can apply updates by downloading a release and swapping binaries/config safely with backups and history.
|
||||
For LXC, Pulse's service and data are inside the container, not on the Proxmox
|
||||
host. Keep the failed state and check the running version and service health
|
||||
before retrying a failed update.
|
||||
|
||||
### Docker
|
||||
|
||||
Pull a new image and restart:
|
||||
Follow [Docker server updates](DOCKER.md#-updates) from the original Compose
|
||||
project or saved container deployment. Persist the exact target in its `image:`
|
||||
line, or in `PULSE_IMAGE` when that line uses the variable; pulling a different
|
||||
tag alone does not change the configured image. Paid Pro installs must keep
|
||||
their private image, not replace it with the Community image.
|
||||
|
||||
Pull and recreate only the Pulse service, stopping if the pull fails. Preserve
|
||||
its data volume, ports and settings, and check the running image and server
|
||||
version afterwards. This updates the Pulse server, not its agents or the
|
||||
containers it monitors; there is no need to restart unrelated Compose services.
|
||||
|
||||
```bash
|
||||
docker pull rcourtman/pulse:latest
|
||||
docker compose up -d
|
||||
```
|
||||
### Kubernetes (Helm)
|
||||
|
||||
Upgrade the chart:
|
||||
Use the existing Helm release and namespace, the chosen chart version and your
|
||||
saved deployment values. Review changes to persistence, Secret references and
|
||||
image edition before rollout; keep the current PVC, keys and settings. Do not
|
||||
let a default Community image replace paid Pro.
|
||||
|
||||
```bash
|
||||
helm repo update
|
||||
helm upgrade pulse pulse/pulse -n pulse
|
||||
```
|
||||
See [Kubernetes deployment settings](KUBERNETES.md) and use your cluster's
|
||||
rollout and recovery procedure. A Helm rollback changes release resources; it
|
||||
does not guarantee restoration of Pulse data changed by a newer binary.
|
||||
|
||||
### Provider-hosted MSP
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Keep the deployment entry point from bypassing the safe update procedures.
|
||||
|
||||
These are documentation regression checks, not installed update/restore proof.
|
||||
The linked Compose recipe has executable controls in test_update_recovery_docs;
|
||||
the production Docs navigation is exercised by deployment-update-help.cjs.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def guide(name):
|
||||
return (ROOT / "docs" / f"{name}.md").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def overview():
|
||||
return guide("DEPLOYMENT_MODELS")
|
||||
|
||||
|
||||
class DeploymentUpdateDocsTest(unittest.TestCase):
|
||||
def test_shipped_overview_matches_the_reviewed_guide(self):
|
||||
self.assertEqual(overview(), (ROOT / "frontend-modern/public/docs/DEPLOYMENT_MODELS.md")
|
||||
.read_text(encoding="utf-8"))
|
||||
|
||||
def test_default_paths_are_not_a_complete_backup_inventory(self):
|
||||
text = " ".join(overview().split())
|
||||
for phrase in ("Default data/config path", "defaults, not a backup inventory",
|
||||
"`PULSE_DATA_DIR`", "external stores", "`PULSE_METRICS_DB_PATH`"):
|
||||
self.assertIn(phrase, text)
|
||||
self.assertIn("METRICS_HISTORY.md#storage-location", text)
|
||||
self.assertIn("AUDIT_LOGGING.md#storage", text)
|
||||
|
||||
def test_backup_and_rollback_claims_preserve_state_and_limits(self):
|
||||
text = " ".join(overview().split())
|
||||
for phrase in ("running version and edition", "consistent private backup",
|
||||
"matching `.encryption.key` and audit signing key",
|
||||
"stopped, consistent filesystem/volume backup", "SQLite sidecar files",
|
||||
"Do not post configuration files, environment dumps or Helm secret values",
|
||||
"snapshot is not necessarily a complete data backup",
|
||||
"does not undo data migrations", "backup path alone does not establish",
|
||||
"Keep the failed state", "before retrying a failed update"):
|
||||
self.assertIn(phrase, text)
|
||||
self.assertNotIn("swapping binaries/config safely", text)
|
||||
|
||||
def test_overview_routes_to_the_maintained_procedures_not_a_shortcut(self):
|
||||
updates = overview().split("## Updates by Model", 1)[1]
|
||||
# A second shorthand can skip durable image selection, backup checks,
|
||||
# fail-fast pulling or the actual release/namespace and saved values.
|
||||
self.assertNotRegex(updates, r"```(?:bash|sh|shell)\b")
|
||||
for link in ("AUTO_UPDATE.md#rollback", "AUTO_UPDATE.md", "DOCKER.md#-updates",
|
||||
"KUBERNETES.md"):
|
||||
self.assertIn(f"]({link})", updates)
|
||||
self.assertTrue((ROOT / "frontend-modern/public/docs" / link.split("#")[0]).is_file())
|
||||
self.assertIn("## 🔄 Updates", guide("DOCKER"))
|
||||
self.assertIn("## Rollback", guide("AUTO_UPDATE"))
|
||||
|
||||
def test_deployment_specific_context_survives_the_handoff(self):
|
||||
updates = " ".join(overview().split("## Updates by Model", 1)[1].split())
|
||||
for phrase in ("inside the container, not on the Proxmox host",
|
||||
"original Compose project", "Persist the exact target",
|
||||
"pulling a different tag alone does not change the configured image",
|
||||
"Paid Pro installs must keep their private image",
|
||||
"only the Pulse service, stopping if the pull fails",
|
||||
"no need to restart unrelated Compose services",
|
||||
"existing Helm release and namespace", "chosen chart version",
|
||||
"saved deployment values", "current PVC, keys and settings",
|
||||
"does not guarantee restoration of Pulse data"):
|
||||
self.assertIn(phrase, updates)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user