mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
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:
+13
-7
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user