Keep setup and metrics guidance credential-safe

Replace token arguments, an ignored API_TOKENS bootstrap and plaintext DaemonSet credentials with scoped UI-created tokens, private files and a pre-created Secret. Route hosted installation to the existing private-file flow and retain HTTP errors and encoded resource identities in metrics queries. Exercise copied recipes, failure stops and actual curl requests with synthetic credentials.

Contract-Neutral: Documentation-only private-file setup and metrics access guidance; no runtime, API, entitlement or deployment contract delta.
Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-01 13:26:10 +01:00
parent 9189a7262e
commit 04429cb42c
7 changed files with 415 additions and 48 deletions
+13 -7
View File
@@ -47,14 +47,20 @@ Sign up via the Pulse Cloud portal. Your instance is provisioned automatically a
### 2. Connect Agents
Once your instance is running, install agents on your infrastructure:
Once your instance is running, open **Settings → Infrastructure → Install on a
host** in your cloud dashboard and choose the target platform and collector
profile. Run the installer on the host being monitored, not inside the hosted
Pulse server.
```bash
curl -fsSL https://yourname.cloud.pulserelay.pro/install.sh | \
bash -s -- --url https://yourname.cloud.pulserelay.pro --token <api-token>
```
Generate installation commands from **Settings → Infrastructure → Install on a host** in your cloud dashboard.
For manual setup, follow the [private-file installation steps](UNIFIED_AGENT.md#private-file-installation-linux-macos-and-nas)
(or the Windows section in that guide), replacing `https://pulse.example.com`
with your instance's HTTPS address in both the download and install commands.
Save the agent token in a private file and pass only its path with
`--token-file`; never paste a token into command arguments, a URL or a report.
Download the agent installer from your own instance's `/install.sh`, stop if
the download fails, and inspect the saved script before running it. Keep TLS
verification enabled; do not pipe an unchecked response into a privileged shell
or substitute the top-level GitHub server installer.
### 3. Add Proxmox / TrueNAS Connections
+67 -14
View File
@@ -61,36 +61,84 @@ By default, this workload runs in container-monitoring mode (`--enable-docker --
For Kubernetes monitoring, use a custom DaemonSet as shown below.
### Prepare an agent Secret
Complete setup on the running Pulse server, then create a token in **API Access**
with `kubernetes:report`. Add `agent:report` only if you also enable host metrics.
Do not reuse an administrator token. Pulse v6 does not provision API tokens from
the legacy `API_TOKENS` server environment variable.
On your kubectl workstation, save only the token in a private, regular file
outside your GitOps repository. Do not paste it into a shell assignment, a
`--from-literal` argument, a Helm value or the DaemonSet YAML:
```bash
umask 077
mkdir -p "$HOME/.config/pulse"
chmod 700 "$HOME/.config/pulse"
touch "$HOME/.config/pulse/kubernetes-agent-token"
chmod 600 "$HOME/.config/pulse/kubernetes-agent-token"
vi "$HOME/.config/pulse/kubernetes-agent-token"
```
After saving the token in the editor, create the Secret in the same namespace
as the agent. The generated Secret goes directly to kubectl, not to your
terminal or a checked-in manifest. The subshell stops on either pipeline's
failure; do not continue to the agent deployment if it fails:
```bash
(
set -euo pipefail
test -s "$HOME/.config/pulse/kubernetes-agent-token"
kubectl create namespace pulse --dry-run=client -o yaml | \
kubectl apply --server-side --field-manager=pulse-agent-setup -f -
kubectl -n pulse create secret generic pulse-agent-env \
--from-file=PULSE_TOKEN="$HOME/.config/pulse/kubernetes-agent-token" \
--dry-run=client -o yaml | \
kubectl apply --server-side --field-manager=pulse-agent-setup -f -
)
```
Server-side apply avoids copying the token into a last-applied annotation. If
another manager owns the Secret fields, reconcile that ownership instead of
forcing the conflict. Kubernetes Secrets are base64-encoded, **not inherently
encrypted**: restrict Secret access with RBAC and configure cluster encryption
at rest. Never share Secret YAML, Helm secret values or the local token file in
diagnostics. Keep the file private or remove it after provisioning; revoke a
token in Pulse when it is no longer needed. Secret rotation requires restarting
the agent pods because these examples read it into the environment at startup.
### OpenShift profile (Helm)
The chart has an SCC-compatible OpenShift profile for the Pulse server and an
optional cluster-level Kubernetes collector:
For a new server, deploy the server first and complete its browser setup before
creating the scoped agent token:
```bash
export PULSE_TOKEN='replace-with-a-kubernetes-report-token'
helm upgrade --install pulse pulse/pulse \
--namespace pulse --create-namespace \
--set openShift.enabled=true
```
kubectl create namespace pulse --dry-run=client -o yaml | kubectl apply -f -
kubectl -n pulse create secret generic pulse-server-env \
--from-literal=API_TOKENS="${PULSE_TOKEN}" \
--dry-run=client -o yaml | kubectl apply -f -
kubectl -n pulse create secret generic pulse-agent-env \
--from-literal=PULSE_TOKEN="${PULSE_TOKEN}" \
--dry-run=client -o yaml | kubectl apply -f -
Create `pulse-agent-env` using the private-file steps above, then enable the
collector:
```bash
helm upgrade --install pulse pulse/pulse \
--namespace pulse \
--set openShift.enabled=true \
--set openShift.kubernetesAgent.enabled=true \
--set openShift.kubernetesAgent.clusterID=my-openshift-cluster \
--set server.secretEnv.name=pulse-server-env \
--set 'server.secretEnv.keys[0]=API_TOKENS' \
--set agent.secretEnv.name=pulse-agent-env \
--set 'agent.secretEnv.keys[0]=PULSE_TOKEN'
```
Using pre-created Secrets keeps the token out of Helm release values. For an
agent reporting to an existing external Pulse server, omit the server Secret
and override `agent.env[0].value` with that server's reachable `PULSE_URL`.
Using a pre-created Secret keeps the token out of Helm release values. For an
agent reporting to an existing external Pulse server, create the token there
and override `agent.env[0].value` with that server's reachable HTTPS `PULSE_URL`.
Keep certificate verification enabled.
The profile deliberately:
@@ -127,6 +175,8 @@ To monitor Kubernetes resources, run the unified agent as a DaemonSet and enable
#### Minimal DaemonSet Example
This uses the main `rcourtman/pulse` image but runs the `pulse-agent` binary directly.
Create the `pulse-agent-env` Secret above and the RBAC resources below before
applying the DaemonSet. The YAML contains only the Secret reference, not the token.
```yaml
apiVersion: apps/v1
@@ -157,7 +207,10 @@ spec:
- name: PULSE_URL
value: "http://pulse-server.pulse.svc.cluster.local:7655"
- name: PULSE_TOKEN
value: "YOUR_API_TOKEN_HERE"
valueFrom:
secretKeyRef:
name: pulse-agent-env
key: PULSE_TOKEN
- name: PULSE_AGENT_ID
value: "my-k8s-cluster"
- name: PULSE_ENABLE_HOST
+19 -3
View File
@@ -129,13 +129,29 @@ These endpoints require authentication with the `monitoring:read` scope.
- `range` (optional): `1h`, `6h`, `12h`, `24h`, `1d`, `7d`, `30d`, `90d` (default `24h`; duration strings also accepted)
- `maxPoints` (optional): Downsample to a target number of points
Example:
For a one-off read, open `/api/metrics-store/stats` in your signed-in Pulse
browser; do not extract its session cookie. For curl, prepare the private
header file described in [API authentication](API.md#-authentication), using a
token with only `monitoring:read`. The example requires curl 7.76 or later and
runs **on the Pulse host**:
```bash
curl -H "X-API-Token: $TOKEN" \
"http://localhost:7655/api/metrics-store/history?resourceType=vm&resourceId=pve1:node1:100&range=7d&metric=cpu"
curl --disable --fail-with-body --connect-timeout 10 --max-time 30 \
--header "@$HOME/.config/pulse/api-header" --get \
--data-urlencode 'resourceType=vm' \
--data-urlencode 'resourceId=pve1:node1:100' \
--data-urlencode 'range=7d' --data-urlencode 'metric=cpu' \
http://127.0.0.1:7655/api/metrics-store/history
```
Replace the example resource ID with the ID reported by your instance. For a
remote request, use your Pulse HTTPS address and keep certificate verification
enabled. Keep `--disable` first to ignore local curl defaults that could enable
credential-bearing traces. HTTP errors, including 401, 402 and 403, return a
non-zero exit; do not treat the error body as history. Never put a token in a
command, URL or report, and share only the relevant redacted error rather than
the whole infrastructure response.
> **License**: Requests beyond Community's `7d` floor require the paid `long_term_metrics` entitlement. Relay unlocks `14d`, Pro and legacy Pro+ unlock `90d`, and requests beyond the active tier's limit return `402 Payment Required`.
> **Aliases**: `guest` (VM/LXC) and `docker` (Docker container) are accepted, but persistent store data uses the canonical types above.
+13 -7
View File
@@ -47,14 +47,20 @@ Sign up via the Pulse Cloud portal. Your instance is provisioned automatically a
### 2. Connect Agents
Once your instance is running, install agents on your infrastructure:
Once your instance is running, open **Settings → Infrastructure → Install on a
host** in your cloud dashboard and choose the target platform and collector
profile. Run the installer on the host being monitored, not inside the hosted
Pulse server.
```bash
curl -fsSL https://yourname.cloud.pulserelay.pro/install.sh | \
bash -s -- --url https://yourname.cloud.pulserelay.pro --token <api-token>
```
Generate installation commands from **Settings → Infrastructure → Install on a host** in your cloud dashboard.
For manual setup, follow the [private-file installation steps](UNIFIED_AGENT.md#private-file-installation-linux-macos-and-nas)
(or the Windows section in that guide), replacing `https://pulse.example.com`
with your instance's HTTPS address in both the download and install commands.
Save the agent token in a private file and pass only its path with
`--token-file`; never paste a token into command arguments, a URL or a report.
Download the agent installer from your own instance's `/install.sh`, stop if
the download fails, and inspect the saved script before running it. Keep TLS
verification enabled; do not pipe an unchecked response into a privileged shell
or substitute the top-level GitHub server installer.
### 3. Add Proxmox / TrueNAS Connections
+67 -14
View File
@@ -61,36 +61,84 @@ By default, this workload runs in container-monitoring mode (`--enable-docker --
For Kubernetes monitoring, use a custom DaemonSet as shown below.
### Prepare an agent Secret
Complete setup on the running Pulse server, then create a token in **API Access**
with `kubernetes:report`. Add `agent:report` only if you also enable host metrics.
Do not reuse an administrator token. Pulse v6 does not provision API tokens from
the legacy `API_TOKENS` server environment variable.
On your kubectl workstation, save only the token in a private, regular file
outside your GitOps repository. Do not paste it into a shell assignment, a
`--from-literal` argument, a Helm value or the DaemonSet YAML:
```bash
umask 077
mkdir -p "$HOME/.config/pulse"
chmod 700 "$HOME/.config/pulse"
touch "$HOME/.config/pulse/kubernetes-agent-token"
chmod 600 "$HOME/.config/pulse/kubernetes-agent-token"
vi "$HOME/.config/pulse/kubernetes-agent-token"
```
After saving the token in the editor, create the Secret in the same namespace
as the agent. The generated Secret goes directly to kubectl, not to your
terminal or a checked-in manifest. The subshell stops on either pipeline's
failure; do not continue to the agent deployment if it fails:
```bash
(
set -euo pipefail
test -s "$HOME/.config/pulse/kubernetes-agent-token"
kubectl create namespace pulse --dry-run=client -o yaml | \
kubectl apply --server-side --field-manager=pulse-agent-setup -f -
kubectl -n pulse create secret generic pulse-agent-env \
--from-file=PULSE_TOKEN="$HOME/.config/pulse/kubernetes-agent-token" \
--dry-run=client -o yaml | \
kubectl apply --server-side --field-manager=pulse-agent-setup -f -
)
```
Server-side apply avoids copying the token into a last-applied annotation. If
another manager owns the Secret fields, reconcile that ownership instead of
forcing the conflict. Kubernetes Secrets are base64-encoded, **not inherently
encrypted**: restrict Secret access with RBAC and configure cluster encryption
at rest. Never share Secret YAML, Helm secret values or the local token file in
diagnostics. Keep the file private or remove it after provisioning; revoke a
token in Pulse when it is no longer needed. Secret rotation requires restarting
the agent pods because these examples read it into the environment at startup.
### OpenShift profile (Helm)
The chart has an SCC-compatible OpenShift profile for the Pulse server and an
optional cluster-level Kubernetes collector:
For a new server, deploy the server first and complete its browser setup before
creating the scoped agent token:
```bash
export PULSE_TOKEN='replace-with-a-kubernetes-report-token'
helm upgrade --install pulse pulse/pulse \
--namespace pulse --create-namespace \
--set openShift.enabled=true
```
kubectl create namespace pulse --dry-run=client -o yaml | kubectl apply -f -
kubectl -n pulse create secret generic pulse-server-env \
--from-literal=API_TOKENS="${PULSE_TOKEN}" \
--dry-run=client -o yaml | kubectl apply -f -
kubectl -n pulse create secret generic pulse-agent-env \
--from-literal=PULSE_TOKEN="${PULSE_TOKEN}" \
--dry-run=client -o yaml | kubectl apply -f -
Create `pulse-agent-env` using the private-file steps above, then enable the
collector:
```bash
helm upgrade --install pulse pulse/pulse \
--namespace pulse \
--set openShift.enabled=true \
--set openShift.kubernetesAgent.enabled=true \
--set openShift.kubernetesAgent.clusterID=my-openshift-cluster \
--set server.secretEnv.name=pulse-server-env \
--set 'server.secretEnv.keys[0]=API_TOKENS' \
--set agent.secretEnv.name=pulse-agent-env \
--set 'agent.secretEnv.keys[0]=PULSE_TOKEN'
```
Using pre-created Secrets keeps the token out of Helm release values. For an
agent reporting to an existing external Pulse server, omit the server Secret
and override `agent.env[0].value` with that server's reachable `PULSE_URL`.
Using a pre-created Secret keeps the token out of Helm release values. For an
agent reporting to an existing external Pulse server, create the token there
and override `agent.env[0].value` with that server's reachable HTTPS `PULSE_URL`.
Keep certificate verification enabled.
The profile deliberately:
@@ -127,6 +175,8 @@ To monitor Kubernetes resources, run the unified agent as a DaemonSet and enable
#### Minimal DaemonSet Example
This uses the main `rcourtman/pulse` image but runs the `pulse-agent` binary directly.
Create the `pulse-agent-env` Secret above and the RBAC resources below before
applying the DaemonSet. The YAML contains only the Secret reference, not the token.
```yaml
apiVersion: apps/v1
@@ -157,7 +207,10 @@ spec:
- name: PULSE_URL
value: "http://pulse-server.pulse.svc.cluster.local:7655"
- name: PULSE_TOKEN
value: "YOUR_API_TOKEN_HERE"
valueFrom:
secretKeyRef:
name: pulse-agent-env
key: PULSE_TOKEN
- name: PULSE_AGENT_ID
value: "my-k8s-cluster"
- name: PULSE_ENABLE_HOST
+19 -3
View File
@@ -129,13 +129,29 @@ These endpoints require authentication with the `monitoring:read` scope.
- `range` (optional): `1h`, `6h`, `12h`, `24h`, `1d`, `7d`, `30d`, `90d` (default `24h`; duration strings also accepted)
- `maxPoints` (optional): Downsample to a target number of points
Example:
For a one-off read, open `/api/metrics-store/stats` in your signed-in Pulse
browser; do not extract its session cookie. For curl, prepare the private
header file described in [API authentication](API.md#-authentication), using a
token with only `monitoring:read`. The example requires curl 7.76 or later and
runs **on the Pulse host**:
```bash
curl -H "X-API-Token: $TOKEN" \
"http://localhost:7655/api/metrics-store/history?resourceType=vm&resourceId=pve1:node1:100&range=7d&metric=cpu"
curl --disable --fail-with-body --connect-timeout 10 --max-time 30 \
--header "@$HOME/.config/pulse/api-header" --get \
--data-urlencode 'resourceType=vm' \
--data-urlencode 'resourceId=pve1:node1:100' \
--data-urlencode 'range=7d' --data-urlencode 'metric=cpu' \
http://127.0.0.1:7655/api/metrics-store/history
```
Replace the example resource ID with the ID reported by your instance. For a
remote request, use your Pulse HTTPS address and keep certificate verification
enabled. Keep `--disable` first to ignore local curl defaults that could enable
credential-bearing traces. HTTP errors, including 401, 402 and 403, return a
non-zero exit; do not treat the error body as history. Never put a token in a
command, URL or report, and share only the relevant redacted error rather than
the whole infrastructure response.
> **License**: Requests beyond Community's `7d` floor require the paid `long_term_metrics` entitlement. Relay unlocks `14d`, Pro and legacy Pro+ unlock `90d`, and requests beyond the active tier's limit return `402 Payment Required`.
> **Aliases**: `guest` (VM/LXC) and `docker` (Docker container) are accepted, but persistent store data uses the canonical types above.
+217
View File
@@ -0,0 +1,217 @@
#!/usr/bin/env python3
"""Exercise copied setup/metrics recipes with synthetic credentials.
Run with pulse-worker-source-proof --workspace scripts. The kubectl recorder
checks local argv, pipe payloads and stop behaviour, not a real cluster. Curl
uses guest-local listeners; no real Pulse instance or token is involved.
"""
from __future__ import annotations
import base64
import json
import os
from pathlib import Path
import re
import stat
import subprocess
import tempfile
import unittest
from urllib.parse import parse_qs, urlsplit
from test_api_auth_docs import ROOT, TEST_TOKEN, exercise_curl, recording_server
GUIDES = ("KUBERNETES", "METRICS_HISTORY", "CLOUD")
def guide(name: str) -> str:
return (ROOT / "docs" / f"{name}.md").read_text(encoding="utf-8")
def blocks(name: str, language: str = "bash") -> list[str]:
return re.findall(r"```" + language + r"\n(.*?)```", guide(name), re.DOTALL)
def recipe(name: str, needle: str) -> str:
matches = [block for block in blocks(name) if needle in block]
if len(matches) != 1:
raise AssertionError(f"expected one {name} recipe containing {needle!r}")
return matches[0]
KUBECTL_RECORDER = '''#!/usr/bin/env python3
import base64, json, os, sys
from pathlib import Path
args = sys.argv[1:]
assert os.environ['FIXTURE_TOKEN'] not in ' '.join(args)
assert not any(k in os.environ for k in ('PULSE_TOKEN', 'API_TOKENS'))
if args[:2] == ['create', 'namespace']:
phase = 'namespace-create'
assert args == ['create', 'namespace', 'pulse', '--dry-run=client', '-o', 'yaml']
document = {'kind': 'Namespace', 'metadata': {'name': 'pulse'}}
elif args[:5] == ['-n', 'pulse', 'create', 'secret', 'generic']:
phase = 'secret-create'
assert args[5] == 'pulse-agent-env'
assert args[7:] == ['--dry-run=client', '-o', 'yaml']
assert args[6].startswith('--from-file=PULSE_TOKEN=')
path = Path(args[6].split('=', 2)[2])
assert path.read_text().strip() == os.environ['FIXTURE_TOKEN']
assert path.stat().st_mode & 0o777 == 0o600
document = {'apiVersion': 'v1', 'kind': 'Secret',
'metadata': {'name': 'pulse-agent-env', 'namespace': 'pulse'},
'data': {'PULSE_TOKEN': base64.b64encode(path.read_bytes()).decode()}}
else:
assert args == ['apply', '--server-side', '--field-manager=pulse-agent-setup', '-f', '-']
payload = sys.stdin.read()
document = json.loads(payload) if payload else None
phase = 'secret-apply' if document and document['kind'] == 'Secret' else 'namespace-apply'
with Path(os.environ['KUBECTL_RECEIPT']).open('a') as output:
output.write(json.dumps({'phase': phase, 'argv': args, 'document': document}) + '\\n')
if phase == os.environ.get('FAIL_PHASE'):
sys.exit(23)
if args[0] == 'apply':
if document is None:
sys.exit(24)
print(document['kind'].lower() + '/' + document['metadata']['name'] + ' serverside-applied')
else:
print(json.dumps(document))
'''
class SetupAccessDocsTest(unittest.TestCase):
def test_mirrors_and_secret_free_shell_recipes(self):
for name in GUIDES:
with self.subTest(guide=name):
self.assertEqual((ROOT / "docs" / f"{name}.md").read_bytes(),
(ROOT / "frontend-modern/public/docs" / f"{name}.md").read_bytes())
shell = "\n".join(blocks(name))
self.assertNotRegex(shell, r"(?:export\s+PULSE_TOKEN|--from-literal=\w*TOKEN|--token(?:\s|=)|X-API-Token:|Bearer\s)")
self.assertNotRegex(shell, r"(?:--insecure|--verbose|--trace\S*|\s-k\b|curl[^`]*\|\s*bash)")
for block in blocks(name):
subprocess.run(["bash", "-n", "-c", block], check=True, capture_output=True)
def test_private_file_preparation_preserves_existing_token(self):
command = recipe("KUBERNETES", 'vi "$HOME/.config/pulse/kubernetes-agent-token"')
with tempfile.TemporaryDirectory(prefix="setup docs ") as temporary:
home = Path(temporary)
tools = home / "tools"
tools.mkdir()
editor = tools / "vi"
editor.write_text('#!/bin/sh\n[ "$#" = 1 ] && [ -f "$1" ]\n')
editor.chmod(0o700)
env = {**os.environ, "HOME": str(home), "PATH": f"{tools}:{os.environ['PATH']}"}
token = home / ".config/pulse/kubernetes-agent-token"
for existing in (False, True):
if existing:
token.write_text(TEST_TOKEN + "\n")
token.chmod(0o644)
token.parent.chmod(0o755)
result = subprocess.run(["bash", "-eu", "-c", command], env=env, capture_output=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stderr.decode())
self.assertEqual(stat.S_IMODE(token.stat().st_mode), 0o600)
self.assertEqual(stat.S_IMODE(token.parent.stat().st_mode), 0o700)
self.assertEqual(token.read_text(), TEST_TOKEN + "\n" if existing else "")
self.assertNotIn(TEST_TOKEN.encode(), result.stdout + result.stderr)
def run_secret(self, home: Path, fail: str = "", empty: bool = False):
private = home / ".config/pulse"
private.mkdir(parents=True, mode=0o700)
token = private / "kubernetes-agent-token"
token.write_text("" if empty else TEST_TOKEN + "\n")
token.chmod(0o600)
tools = home / "tools"
tools.mkdir()
recorder = tools / "kubectl"
recorder.write_text(KUBECTL_RECORDER)
recorder.chmod(0o700)
receipt = home / "kubectl.jsonl"
env = {**os.environ, "HOME": str(home), "PATH": f"{tools}:{os.environ['PATH']}",
"FIXTURE_TOKEN": TEST_TOKEN, "KUBECTL_RECEIPT": str(receipt), "FAIL_PHASE": fail}
for key in ("PULSE_TOKEN", "API_TOKENS"):
env.pop(key, None)
result = subprocess.run(["bash", "-c", recipe("KUBERNETES", "--from-file=PULSE_TOKEN=")],
env=env, capture_output=True, timeout=10)
records = [json.loads(line) for line in receipt.read_text().splitlines()] if receipt.exists() else []
self.assertNotIn(TEST_TOKEN.encode(), result.stdout + result.stderr)
self.assertNotIn(base64.b64encode((TEST_TOKEN + "\n").encode()), result.stdout + result.stderr)
return result, records
def test_secret_passes_only_file_path_in_argv_and_data_over_stdin(self):
with tempfile.TemporaryDirectory(prefix="setup docs ") as temporary:
result, records = self.run_secret(Path(temporary))
self.assertEqual(result.returncode, 0, result.stderr.decode())
self.assertEqual(sorted(r["phase"] for r in records),
sorted(("namespace-create", "namespace-apply", "secret-create", "secret-apply")))
created = next(r["document"] for r in records if r["phase"] == "secret-create")
applied = next(r["document"] for r in records if r["phase"] == "secret-apply")
self.assertEqual(created, applied)
self.assertEqual(base64.b64decode(applied["data"]["PULSE_TOKEN"]), (TEST_TOKEN + "\n").encode())
self.assertNotIn("annotations", applied["metadata"])
self.assertNotIn(TEST_TOKEN, json.dumps([r["argv"] for r in records]))
def test_pipeline_failure_and_empty_file_stop_provisioning(self):
for phase in ("namespace-create", "namespace-apply", "secret-create", "secret-apply", ""):
with self.subTest(phase=phase), tempfile.TemporaryDirectory() as temporary:
result, records = self.run_secret(Path(temporary), fail=phase, empty=not phase)
self.assertNotEqual(result.returncode, 0)
if phase.startswith("namespace"):
self.assertFalse(any(r["phase"].startswith("secret") for r in records))
if not phase:
self.assertEqual(records, [])
def test_daemonset_and_helm_reference_the_same_scoped_secret(self):
manifest = next(block for block in blocks("KUBERNETES", "yaml") if "kind: DaemonSet" in block)
self.assertRegex(manifest, r"name: PULSE_TOKEN\n\s+valueFrom:\n\s+secretKeyRef:\n\s+name: pulse-agent-env\n\s+key: PULSE_TOKEN")
self.assertNotIn("YOUR_API_TOKEN", manifest)
collector = recipe("KUBERNETES", "openShift.kubernetesAgent.enabled=true")
self.assertIn("agent.secretEnv.name=pulse-agent-env", collector)
self.assertIn("agent.secretEnv.keys[0]=PULSE_TOKEN", collector)
self.assertNotIn("server.secretEnv", collector)
chart = (ROOT / "deploy/helm/pulse/templates/agent.yaml").read_text()
for key in (".Values.agent.secretEnv", '"valueFrom"', '"secretKeyRef"', '"pulse.agentSecretName"'):
self.assertIn(key, chart)
content = " ".join(guide("KUBERNETES").split())
for boundary in ("kubernetes:report", "agent:report", "Do not reuse an administrator token",
"does not provision API tokens", "not inherently encrypted", "RBAC",
"restarting the agent pods", "forcing the conflict"):
self.assertIn(boundary, content)
def test_cloud_uses_existing_private_file_installation_not_server_installer(self):
content = " ".join(guide("CLOUD").split())
for boundary in ("UNIFIED_AGENT.md#private-file-installation-linux-macos-and-nas", "--token-file",
"host being monitored", "Windows", "HTTPS", "stop if the download fails",
"top-level GitHub server installer", "Keep TLS verification enabled"):
self.assertIn(boundary, content)
class MetricsAccessDocsHTTPTest(unittest.TestCase):
def test_history_query_with_both_headers_and_hostile_curl_defaults(self):
request = recipe("METRICS_HISTORY", "curl ")
expected_id = "pve 1:node/1:100&other=1"
request = request.replace("pve1:node1:100", expected_id)
with recording_server() as (port, records):
for key, value in (("X-API-Token", TEST_TOKEN), ("Authorization", "Bearer " + TEST_TOKEN)):
with self.subTest(header=key), tempfile.TemporaryDirectory() as temporary:
result = exercise_curl(self, Path(temporary), f"{key}: {value}", port, request)
self.assertEqual(result.returncode, 0, result.stderr.decode())
path, headers, method, body = records[-1]
self.assertEqual(urlsplit(path).path, "/api/metrics-store/history")
self.assertEqual(parse_qs(urlsplit(path).query), {
"resourceType": ["vm"], "resourceId": [expected_id], "range": ["7d"], "metric": ["cpu"]})
self.assertEqual((method, body), ("GET", b""))
self.assertEqual(headers[key], value)
self.assertNotIn("X-Curlrc-Injected", headers)
def test_http_auth_license_and_server_errors_fail(self):
for status in (401, 402, 403, 500):
with self.subTest(status=status), recording_server(status) as (port, records), tempfile.TemporaryDirectory() as temporary:
result = exercise_curl(self, Path(temporary), f"X-API-Token: {TEST_TOKEN}", port,
recipe("METRICS_HISTORY", "curl "))
self.assertEqual(result.returncode, 22, result.stderr.decode())
self.assertEqual(len(records), 1)
self.assertIn(b'{"fixture":true}', result.stdout)
if __name__ == "__main__":
unittest.main()