mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
Make monitored-container update help truthful about recovery
Replace a claimed 15-minute backup and guaranteed rollback with the implemented five-minute cleanup, shared-data and best-effort limits. Put independent backups and deployment-manager recovery before the action, preserve current-state checks, and distinguish image-update suppression from command authority. Contract-Neutral: Documentation and validation only; no container runtime, command authority or API changes. Change-source: pulse-maintainer
This commit is contained in:
+63
-14
@@ -77,7 +77,7 @@ Pulse is configured via the UI (`system.json`) with optional environment overrid
|
||||
| `DISCOVERY_SUBNET` | Custom CIDR to scan | *(auto)* |
|
||||
| `ALLOWED_ORIGINS` | CORS allowed origin (`*` or a single origin). Empty = same-origin only. | *(unset)* |
|
||||
| `LOG_LEVEL` | Log verbosity (`debug`, `info`, `warn`, `error`) | `info` |
|
||||
| `PULSE_DISABLE_DOCKER_UPDATE_ACTIONS` | Hide Docker update buttons (read-only mode) | `false` |
|
||||
| `PULSE_DISABLE_DOCKER_UPDATE_ACTIONS` | Disable container image-update actions; does not disable start/stop/restart | `false` |
|
||||
| `PULSE_METRICS_DB_PATH` | Optional path for only `metrics.db`, useful with tmpfs | `/data/metrics.db` |
|
||||
| `PULSE_METRICS_ROLLUP_INTERVAL` | Metrics aggregation cadence; minimum 5 minutes | `15m` |
|
||||
|
||||
@@ -169,25 +169,48 @@ an image change alone does not undo data migrations.
|
||||
|
||||
## 🔄 Docker / Podman Updates
|
||||
|
||||
Pulse can detect and apply updates to your Docker / Podman containers directly from the UI.
|
||||
These actions update **monitored containers**, not the Pulse server. Pulse can
|
||||
detect image updates without command execution; applying an update requires a
|
||||
separately enabled command channel and administrator review. Leave command
|
||||
execution disabled when you only need monitoring.
|
||||
|
||||
### Before updating a workload
|
||||
|
||||
- Plan for downtime: a running container is stopped and replaced, not updated
|
||||
in place. Check the application's upgrade and data-migration requirements.
|
||||
- Take an independent, consistent backup of its application data, including
|
||||
volumes and bind mounts, using the application's backup procedure. The
|
||||
renamed old container is **not a backup of mounted data**: the replacement
|
||||
uses those same mounts and can change their contents.
|
||||
- Keep the previous image identity and your deployment definition privately.
|
||||
For Compose, Yacht or another deployment manager, prefer that manager's
|
||||
update procedure so its saved definition records the intended image. Pulse's
|
||||
recreation does not edit your Compose file or manager's desired state.
|
||||
- Do not use an update as a diagnostic test or start another container against
|
||||
the same writable data to check a failure.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Update Detection**: Pulse compares the local image digest with the latest digest from the container registry
|
||||
2. **Visual Indicator**: Containers with available updates show a blue upward arrow icon
|
||||
3. **Reviewed Update**: Click the update button, approve the reviewed action, and Pulse handles the rest
|
||||
3. **Reviewed Update**: Review the target and approve the action, then verify the application yourself
|
||||
|
||||
### Updating a Container
|
||||
|
||||
1. Navigate to the **Workloads** page (or filter by Docker sources on **Infrastructure**)
|
||||
1. Open **Docker → Overview** (`/docker/overview`) and find the host and container
|
||||
2. Look for containers with a blue update arrow (⬆️)
|
||||
3. Click the update button and approve the action in the review dialog (admin approval required)
|
||||
4. Pulse will:
|
||||
4. Pulse attempts to:
|
||||
- Pull the latest image
|
||||
- Stop the current container
|
||||
- Create a backup (renamed with `_pulse_backup_` suffix)
|
||||
- Start a new container with the same configuration
|
||||
- Clean up the backup after 15 minutes (if the update succeeds)
|
||||
- Stop the current container if it was running
|
||||
- Rename the original container with the `_pulse_backup_` suffix
|
||||
- Recreate the container from its inspected configuration, reusing its data mounts
|
||||
- Start it only if the original was running, then perform a short runtime check
|
||||
- Schedule removal of the renamed old container after a successful update
|
||||
|
||||
Afterwards, check the actual image, container state, application readiness and
|
||||
data through your normal administration tools. A successful action is not an
|
||||
end-to-end application or data-integrity test.
|
||||
|
||||
### Batch Updates
|
||||
|
||||
@@ -195,9 +218,29 @@ Updates run as reviewed per-container actions, so there is currently no bulk upd
|
||||
|
||||
### Safety Features
|
||||
|
||||
- **Automatic Backup**: The old container is renamed, not deleted, until the update succeeds
|
||||
- **Rollback on Failure**: If the new container fails to start, the old one is restored
|
||||
- **Configuration Preserved**: Networks, volumes, ports, environment variables are all preserved
|
||||
- **Temporary old container**: the `_pulse_backup_` name retains the old
|
||||
container, not a separate copy of its volumes or bind mounts. Current agents
|
||||
schedule removal **five minutes after a successful update**; periodic cleanup
|
||||
also removes old backup containers. Do not rely on a 15-minute recovery window
|
||||
or on a remaining container as a durable backup.
|
||||
- **Best-effort rollback**: when recreation, network attachment or the early
|
||||
runtime check fails, Pulse attempts to restore the original name and running
|
||||
state. Removal, rename or restart can fail too. A failed banner does not prove
|
||||
that the original container is running or that no change occurred.
|
||||
- **Limited verification**: the running replacement is inspected after a short
|
||||
wait and rejected if stopped or explicitly unhealthy. A missing healthcheck
|
||||
or a `starting` health status is not proof that the application is ready.
|
||||
- **Inspected configuration**: Pulse reuses container settings and mounts; it
|
||||
does not reconcile your deployment manager or roll back application data
|
||||
migrations. Verify networks, ports, mounts and the application after the change.
|
||||
|
||||
If the result is failed or uncertain, **check the current state before retrying**.
|
||||
Retain the action error and time, old/new container and image identities, and
|
||||
any rollback error. Inspect these locally; do not post full `docker inspect`
|
||||
output, environments or registry credentials. Do not delete the old container
|
||||
or volumes to clear a warning. Recover through the application's and deployment
|
||||
manager's procedures using a verified matching data backup where required;
|
||||
returning to the old image alone does not undo a data migration.
|
||||
|
||||
### Requirements
|
||||
|
||||
@@ -231,9 +274,9 @@ Pulse provides granular control over update features via environment variables o
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `PULSE_DISABLE_DOCKER_UPDATE_ACTIONS` | Hides update buttons from the UI while still detecting updates. Use this for "read-only" monitoring. |
|
||||
| `PULSE_DISABLE_DOCKER_UPDATE_ACTIONS` | Disables image-update actions while still detecting updates. Start/stop/restart have separate controls. |
|
||||
|
||||
**Example - Read-Only Mode** (detect updates but prevent actions):
|
||||
**Example - Disable image updates** (continue detecting updates):
|
||||
```yaml
|
||||
services:
|
||||
pulse:
|
||||
@@ -242,6 +285,12 @@ services:
|
||||
- PULSE_DISABLE_DOCKER_UPDATE_ACTIONS=true
|
||||
```
|
||||
|
||||
This setting is not a monitoring-only security boundary: it does not disable
|
||||
container lifecycle actions or revoke agent command permissions. For
|
||||
monitoring-only installs, leave command execution disabled on the agent and do
|
||||
not grant command execution permission to monitoring tokens. See
|
||||
[container lifecycle requirements](#️-container-lifecycle-actions).
|
||||
|
||||
To disable registry checks entirely, set `PULSE_DISABLE_DOCKER_UPDATE_CHECKS=true` on the **agent**.
|
||||
|
||||
You can also toggle "Hide Docker Update Buttons" from the UI in **Settings → System → General** under **Docker / Podman updates**.
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
// Production Docs viewer and shipped Docker guide; no container action is run.
|
||||
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', 'docker-update-guidance-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-docker-update-guidance'),
|
||||
server: { host: '127.0.0.1', port: 5250, strictPort: true },
|
||||
optimizeDeps: { noDiscovery: true, include: [] },
|
||||
});
|
||||
let browser;
|
||||
const results = [];
|
||||
try {
|
||||
await server.listen();
|
||||
for (const [engine, width, dark] of [
|
||||
['chromium', 1280, false],
|
||||
['webkit', 390, true],
|
||||
]) {
|
||||
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: 1000 },
|
||||
...(engine === 'webkit' ? { isMobile: true, hasTouch: true } : {}),
|
||||
});
|
||||
const errors = [];
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
page.on('console', (message) => {
|
||||
if (message.type() === 'error') errors.push(message.text());
|
||||
});
|
||||
await page.goto('http://127.0.0.1:5250/browser-tests/docs-fragment-navigation.html', {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 60000,
|
||||
});
|
||||
await page.evaluate(() => {
|
||||
history.pushState({}, '', '/docs/DOCKER#before-updating-a-workload');
|
||||
dispatchEvent(new PopStateEvent('popstate'));
|
||||
});
|
||||
if (dark) await page.evaluate(() => document.documentElement.classList.add('dark'));
|
||||
const waitForHeading = async (id) => {
|
||||
await page.waitForFunction((target) => {
|
||||
const heading = document.getElementById(target);
|
||||
if (!heading) return false;
|
||||
const rect = heading.getBoundingClientRect();
|
||||
return document.activeElement === heading && rect.top >= 0 && rect.top < innerHeight;
|
||||
}, id);
|
||||
assert.ok(
|
||||
await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1),
|
||||
'page must not overflow the viewport',
|
||||
);
|
||||
};
|
||||
await waitForHeading('before-updating-a-workload');
|
||||
const article = await page.locator('article').innerText();
|
||||
for (const text of [
|
||||
'independent, consistent backup',
|
||||
'not a backup of mounted data',
|
||||
'five minutes after a successful update',
|
||||
'Removal, rename or restart can fail too',
|
||||
'check the current state before retrying',
|
||||
'not a monitoring-only security boundary',
|
||||
])
|
||||
assert.ok(article.replace(/\s+/g, ' ').includes(text), text);
|
||||
await page.screenshot({ path: path.join(artifacts, `${engine}-precautions.png`) });
|
||||
await page.evaluate(() => {
|
||||
history.pushState({}, '', '/docs/DOCKER#safety-features');
|
||||
dispatchEvent(new PopStateEvent('popstate'));
|
||||
});
|
||||
await waitForHeading('safety-features');
|
||||
await page.screenshot({ path: path.join(artifacts, `${engine}-limits.png`) });
|
||||
const link = page.getByRole('link', {
|
||||
name: 'container lifecycle requirements',
|
||||
exact: true,
|
||||
});
|
||||
const target = decodeURIComponent((await link.getAttribute('href')).split('#')[1]);
|
||||
await link.focus();
|
||||
await page.keyboard.press('Enter');
|
||||
await waitForHeading(target);
|
||||
assert.equal(
|
||||
await page
|
||||
.getByRole('heading', { name: '▶️ Container Lifecycle Actions', exact: true })
|
||||
.textContent(),
|
||||
'▶️ Container Lifecycle Actions',
|
||||
);
|
||||
assert.deepEqual(errors, []);
|
||||
results.push({
|
||||
engine,
|
||||
version: browser.version(),
|
||||
width,
|
||||
dark,
|
||||
precautionsAndLimitsRendered: true,
|
||||
keyboardRequirementsLink: true,
|
||||
headingsFocusedAndVisible: true,
|
||||
noHorizontalOverflow: true,
|
||||
errors,
|
||||
});
|
||||
await browser.close();
|
||||
browser = undefined;
|
||||
}
|
||||
const result = {
|
||||
result: 'passed',
|
||||
playwright: require('playwright/package.json').version,
|
||||
scope: 'shipped help rendering and keyboard navigation, not native updates or rollback',
|
||||
results,
|
||||
};
|
||||
fs.writeFileSync(path.join(artifacts, 'result.json'), JSON.stringify(result, null, 2) + '\n');
|
||||
console.log(JSON.stringify(result));
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
await server.close();
|
||||
}
|
||||
})().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -77,7 +77,7 @@ Pulse is configured via the UI (`system.json`) with optional environment overrid
|
||||
| `DISCOVERY_SUBNET` | Custom CIDR to scan | *(auto)* |
|
||||
| `ALLOWED_ORIGINS` | CORS allowed origin (`*` or a single origin). Empty = same-origin only. | *(unset)* |
|
||||
| `LOG_LEVEL` | Log verbosity (`debug`, `info`, `warn`, `error`) | `info` |
|
||||
| `PULSE_DISABLE_DOCKER_UPDATE_ACTIONS` | Hide Docker update buttons (read-only mode) | `false` |
|
||||
| `PULSE_DISABLE_DOCKER_UPDATE_ACTIONS` | Disable container image-update actions; does not disable start/stop/restart | `false` |
|
||||
| `PULSE_METRICS_DB_PATH` | Optional path for only `metrics.db`, useful with tmpfs | `/data/metrics.db` |
|
||||
| `PULSE_METRICS_ROLLUP_INTERVAL` | Metrics aggregation cadence; minimum 5 minutes | `15m` |
|
||||
|
||||
@@ -169,25 +169,48 @@ an image change alone does not undo data migrations.
|
||||
|
||||
## 🔄 Docker / Podman Updates
|
||||
|
||||
Pulse can detect and apply updates to your Docker / Podman containers directly from the UI.
|
||||
These actions update **monitored containers**, not the Pulse server. Pulse can
|
||||
detect image updates without command execution; applying an update requires a
|
||||
separately enabled command channel and administrator review. Leave command
|
||||
execution disabled when you only need monitoring.
|
||||
|
||||
### Before updating a workload
|
||||
|
||||
- Plan for downtime: a running container is stopped and replaced, not updated
|
||||
in place. Check the application's upgrade and data-migration requirements.
|
||||
- Take an independent, consistent backup of its application data, including
|
||||
volumes and bind mounts, using the application's backup procedure. The
|
||||
renamed old container is **not a backup of mounted data**: the replacement
|
||||
uses those same mounts and can change their contents.
|
||||
- Keep the previous image identity and your deployment definition privately.
|
||||
For Compose, Yacht or another deployment manager, prefer that manager's
|
||||
update procedure so its saved definition records the intended image. Pulse's
|
||||
recreation does not edit your Compose file or manager's desired state.
|
||||
- Do not use an update as a diagnostic test or start another container against
|
||||
the same writable data to check a failure.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Update Detection**: Pulse compares the local image digest with the latest digest from the container registry
|
||||
2. **Visual Indicator**: Containers with available updates show a blue upward arrow icon
|
||||
3. **Reviewed Update**: Click the update button, approve the reviewed action, and Pulse handles the rest
|
||||
3. **Reviewed Update**: Review the target and approve the action, then verify the application yourself
|
||||
|
||||
### Updating a Container
|
||||
|
||||
1. Navigate to the **Workloads** page (or filter by Docker sources on **Infrastructure**)
|
||||
1. Open **Docker → Overview** (`/docker/overview`) and find the host and container
|
||||
2. Look for containers with a blue update arrow (⬆️)
|
||||
3. Click the update button and approve the action in the review dialog (admin approval required)
|
||||
4. Pulse will:
|
||||
4. Pulse attempts to:
|
||||
- Pull the latest image
|
||||
- Stop the current container
|
||||
- Create a backup (renamed with `_pulse_backup_` suffix)
|
||||
- Start a new container with the same configuration
|
||||
- Clean up the backup after 15 minutes (if the update succeeds)
|
||||
- Stop the current container if it was running
|
||||
- Rename the original container with the `_pulse_backup_` suffix
|
||||
- Recreate the container from its inspected configuration, reusing its data mounts
|
||||
- Start it only if the original was running, then perform a short runtime check
|
||||
- Schedule removal of the renamed old container after a successful update
|
||||
|
||||
Afterwards, check the actual image, container state, application readiness and
|
||||
data through your normal administration tools. A successful action is not an
|
||||
end-to-end application or data-integrity test.
|
||||
|
||||
### Batch Updates
|
||||
|
||||
@@ -195,9 +218,29 @@ Updates run as reviewed per-container actions, so there is currently no bulk upd
|
||||
|
||||
### Safety Features
|
||||
|
||||
- **Automatic Backup**: The old container is renamed, not deleted, until the update succeeds
|
||||
- **Rollback on Failure**: If the new container fails to start, the old one is restored
|
||||
- **Configuration Preserved**: Networks, volumes, ports, environment variables are all preserved
|
||||
- **Temporary old container**: the `_pulse_backup_` name retains the old
|
||||
container, not a separate copy of its volumes or bind mounts. Current agents
|
||||
schedule removal **five minutes after a successful update**; periodic cleanup
|
||||
also removes old backup containers. Do not rely on a 15-minute recovery window
|
||||
or on a remaining container as a durable backup.
|
||||
- **Best-effort rollback**: when recreation, network attachment or the early
|
||||
runtime check fails, Pulse attempts to restore the original name and running
|
||||
state. Removal, rename or restart can fail too. A failed banner does not prove
|
||||
that the original container is running or that no change occurred.
|
||||
- **Limited verification**: the running replacement is inspected after a short
|
||||
wait and rejected if stopped or explicitly unhealthy. A missing healthcheck
|
||||
or a `starting` health status is not proof that the application is ready.
|
||||
- **Inspected configuration**: Pulse reuses container settings and mounts; it
|
||||
does not reconcile your deployment manager or roll back application data
|
||||
migrations. Verify networks, ports, mounts and the application after the change.
|
||||
|
||||
If the result is failed or uncertain, **check the current state before retrying**.
|
||||
Retain the action error and time, old/new container and image identities, and
|
||||
any rollback error. Inspect these locally; do not post full `docker inspect`
|
||||
output, environments or registry credentials. Do not delete the old container
|
||||
or volumes to clear a warning. Recover through the application's and deployment
|
||||
manager's procedures using a verified matching data backup where required;
|
||||
returning to the old image alone does not undo a data migration.
|
||||
|
||||
### Requirements
|
||||
|
||||
@@ -231,9 +274,9 @@ Pulse provides granular control over update features via environment variables o
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `PULSE_DISABLE_DOCKER_UPDATE_ACTIONS` | Hides update buttons from the UI while still detecting updates. Use this for "read-only" monitoring. |
|
||||
| `PULSE_DISABLE_DOCKER_UPDATE_ACTIONS` | Disables image-update actions while still detecting updates. Start/stop/restart have separate controls. |
|
||||
|
||||
**Example - Read-Only Mode** (detect updates but prevent actions):
|
||||
**Example - Disable image updates** (continue detecting updates):
|
||||
```yaml
|
||||
services:
|
||||
pulse:
|
||||
@@ -242,6 +285,12 @@ services:
|
||||
- PULSE_DISABLE_DOCKER_UPDATE_ACTIONS=true
|
||||
```
|
||||
|
||||
This setting is not a monitoring-only security boundary: it does not disable
|
||||
container lifecycle actions or revoke agent command permissions. For
|
||||
monitoring-only installs, leave command execution disabled on the agent and do
|
||||
not grant command execution permission to monitoring tokens. See
|
||||
[container lifecycle requirements](#️-container-lifecycle-actions).
|
||||
|
||||
To disable registry checks entirely, set `PULSE_DISABLE_DOCKER_UPDATE_CHECKS=true` on the **agent**.
|
||||
|
||||
You can also toggle "Hide Docker Update Buttons" from the UI in **Settings → System → General** under **Docker / Podman updates**.
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { renderDocMarkdown } from '../docMarkdown';
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../..');
|
||||
const guide = readFileSync(path.join(repoRoot, 'docs/DOCKER.md'), 'utf8');
|
||||
|
||||
function renderGuide(): HTMLElement {
|
||||
const article = document.createElement('article');
|
||||
article.innerHTML = renderDocMarkdown(guide, 'DOCKER');
|
||||
return article;
|
||||
}
|
||||
|
||||
function section(article: HTMLElement, id: string): string {
|
||||
const heading = article.querySelector(`#${id}`);
|
||||
expect(heading, `missing guide section ${id}`).not.toBeNull();
|
||||
const parts: string[] = [];
|
||||
for (let element = heading?.nextElementSibling; element; element = element.nextElementSibling) {
|
||||
if (/^H[1-3]$/.test(element.tagName)) break;
|
||||
parts.push(element.textContent ?? '');
|
||||
}
|
||||
return parts.join(' ').replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
describe('shipped monitored-container update guide', () => {
|
||||
it('places independent data backup and deployment-manager limits before the action', () => {
|
||||
const article = renderGuide();
|
||||
const precautions = section(article, 'before-updating-a-workload');
|
||||
expect(precautions).toContain('Plan for downtime');
|
||||
expect(precautions).toContain('independent, consistent backup');
|
||||
expect(precautions).toContain('not a backup of mounted data');
|
||||
expect(precautions).toContain('same mounts and can change their contents');
|
||||
expect(precautions).toContain("does not edit your Compose file or manager's desired state");
|
||||
expect(precautions).toContain('Do not use an update as a diagnostic test');
|
||||
const precautionHeading = article.querySelector('#before-updating-a-workload')!;
|
||||
const actionHeading = article.querySelector('#updating-a-container')!;
|
||||
expect(precautionHeading.compareDocumentPosition(actionHeading)).toBe(
|
||||
Node.DOCUMENT_POSITION_FOLLOWING,
|
||||
);
|
||||
expect(section(article, 'updating-a-container')).toContain('/docker/overview');
|
||||
expect(section(article, 'updating-a-container')).not.toContain('Workloads page');
|
||||
});
|
||||
|
||||
it('does not promise durable backup, complete rollback or application readiness', () => {
|
||||
const limits = section(renderGuide(), 'safety-features');
|
||||
expect(limits).toContain('not a separate copy of its volumes or bind mounts');
|
||||
expect(limits).toContain('Removal, rename or restart can fail too');
|
||||
expect(limits).toContain('A failed banner does not prove');
|
||||
expect(limits).toContain('starting health status is not proof');
|
||||
expect(limits).toContain('check the current state before retrying');
|
||||
expect(limits).toContain('Do not delete the old container or volumes');
|
||||
expect(limits).toContain('old image alone does not undo a data migration');
|
||||
expect(limits).toContain('do not post full docker inspect output');
|
||||
expect(limits).not.toContain('the old one is restored');
|
||||
expect(limits).not.toContain('are all preserved');
|
||||
});
|
||||
|
||||
it('matches the implemented early-removal window, not the orphan cleanup interval', () => {
|
||||
// Static source binding, not a claim that a real update or rollback ran.
|
||||
const update = readFileSync(
|
||||
path.join(repoRoot, 'internal/dockeragent/container_update.go'),
|
||||
'utf8',
|
||||
);
|
||||
const cleanup = readFileSync(path.join(repoRoot, 'internal/dockeragent/cleanup.go'), 'utf8');
|
||||
expect(update).toMatch(/waitForAsyncDelay\(5 \* time.Minute\)/);
|
||||
expect(cleanup).toMatch(/time.Since\(backupTime\) > 15\*time.Minute/);
|
||||
const limits = section(renderGuide(), 'safety-features');
|
||||
expect(limits).toContain('five minutes after a successful update');
|
||||
expect(limits).toContain('Do not rely on a 15-minute recovery window');
|
||||
expect(limits).not.toContain('Clean up the backup after 15 minutes');
|
||||
});
|
||||
|
||||
it('keeps image-update suppression separate from command authority and links the real requirements', () => {
|
||||
const article = renderGuide();
|
||||
const disabled = section(article, 'disabling-update-features');
|
||||
expect(disabled).toContain('not a monitoring-only security boundary');
|
||||
expect(disabled).toContain('does not disable container lifecycle actions');
|
||||
expect(disabled).toContain('leave command execution disabled on the agent');
|
||||
expect(disabled).toContain('do not grant command execution permission to monitoring tokens');
|
||||
expect(disabled).not.toContain('Read-Only Mode');
|
||||
const link = Array.from(article.querySelectorAll('a')).find(
|
||||
(anchor) => anchor.textContent === 'container lifecycle requirements',
|
||||
);
|
||||
expect(link).toBeDefined();
|
||||
const targetId = decodeURIComponent(link!.hash.slice(1));
|
||||
const target = Array.from(article.querySelectorAll('h2')).find(
|
||||
(heading) => heading.id === targetId,
|
||||
);
|
||||
expect(target?.textContent).toBe('▶️ Container Lifecycle Actions');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user