mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-09 18:15:50 +00:00
Restore Docker agents to Hosts inventory
This commit is contained in:
+1
-1
@@ -242,7 +242,7 @@ COPY --from=release-assets-builder /app/VERSION /VERSION
|
||||
|
||||
ENV PULSE_NO_AUTO_UPDATE=true \
|
||||
PULSE_DISABLE_AUTO_UPDATE=true \
|
||||
PULSE_ENABLE_HOST=false \
|
||||
PULSE_ENABLE_HOST=true \
|
||||
PULSE_ENABLE_DOCKER=true \
|
||||
PULSE_AGENT_ID_FILE=/var/lib/pulse-agent/agent-id \
|
||||
PULSE_STATE_DIR=/var/lib/pulse-agent
|
||||
|
||||
@@ -795,6 +795,12 @@ Returns scheduler health, DLQ, and breaker status. Requires `monitoring:read`.
|
||||
- `GET /api/diagnostics` (auth)
|
||||
- `POST /api/diagnostics/docker/prepare-token` (admin, `settings:write`)
|
||||
|
||||
The Docker migration token request accepts `agentId`, optional `tokenName`, and
|
||||
optional `enableHost`. When `enableHost` is omitted, the generated token and
|
||||
install command enable both host and Docker monitoring so the machine appears
|
||||
in both Hosts and Docker. Set `enableHost` to `false` only for an intentional
|
||||
workload-only agent; that mode receives Docker-report scope only.
|
||||
|
||||
### Logs (Admin)
|
||||
- `GET /api/logs/stream` (server-sent stream)
|
||||
- `GET /api/logs/download` (bundled logs)
|
||||
|
||||
@@ -3932,10 +3932,19 @@ the shared frontend Unix builder, instead of returning a raw `| bash -s --`
|
||||
pipeline that drifts from the lane's governed install shape.
|
||||
The same lifecycle shell-transport contract also applies to the diagnostics
|
||||
container-runtime migration install command in `internal/api/router.go`: that
|
||||
response must emit the canonical `--enable-host=false` flag and the governed
|
||||
root-or-sudo wrapper, rather than falling back to the stale `--disable-host`
|
||||
alias or a raw `curl | sudo bash` pipe that drifts from the managed install
|
||||
surface.
|
||||
response defaults to the native Unified Agent's host plus Docker modules,
|
||||
emits the canonical `--enable-host` flag, and uses a token scoped for host
|
||||
report/configuration continuity as well as Docker reports. An explicit
|
||||
`enableHost:false` request keeps the intentional workload-only path, the
|
||||
canonical `--enable-host=false` flag, and Docker-report-only token scope. Both
|
||||
modes must retain the governed root-or-sudo wrapper rather than falling back
|
||||
to the stale `--disable-host` alias or a raw `curl | sudo bash` pipe that
|
||||
drifts from the managed install surface.
|
||||
The Settings Docker / Podman install profile and the legacy
|
||||
`scripts/install-container-agent.sh` compatibility wrapper follow that same
|
||||
default: selecting Docker monitoring enables the Docker module without
|
||||
silently disabling the host module. Workload-only operation remains an
|
||||
explicit `--enable-host=false` choice through the canonical installer.
|
||||
That same diagnostics migration command must stay on the shared backend
|
||||
install-command helper path in `internal/api/agent_install_command_shared.go`,
|
||||
rather than rebuilding a local shell formatter in `router.go`, so optional
|
||||
|
||||
@@ -5776,7 +5776,14 @@ That same governed container-runtime migration response must also preserve the
|
||||
canonical lifecycle shell payload shape: `installCommand` in the diagnostics
|
||||
docker prepare-token response may not emit the stale `--disable-host` alias or
|
||||
an ad hoc `curl | sudo bash` pipeline, and must instead match the canonical
|
||||
root-or-sudo wrapped install transport with `--enable-host=false`.
|
||||
root-or-sudo wrapped install transport. Omitted `enableHost` defaults the
|
||||
native Unified Agent migration to host plus Docker monitoring and emits
|
||||
`--enable-host`; explicit `enableHost:false` preserves the workload-only
|
||||
variant and emits `--enable-host=false`.
|
||||
The Settings infrastructure operations model must expose the same default in
|
||||
generated install flags: its Docker / Podman profile enables Docker without
|
||||
adding a host-disable flag, while the generic flag translator continues to
|
||||
preserve an operator-supplied explicit host opt-out.
|
||||
That diagnostics install-command payload must also be assembled through the
|
||||
shared backend install-command helper in `internal/api/agent_install_command_shared.go`
|
||||
instead of a handler-local shell formatter, so token omission, plain-HTTP
|
||||
|
||||
@@ -1328,6 +1328,17 @@ agent binaries, or installer signing material. The published self-hosted
|
||||
so official release images still carry signed installer and agent download
|
||||
assets, and any build that declares `PULSE_UPDATE_SIGNING_PUBLIC_KEY` must
|
||||
continue to fail closed unless the matching signing secret is mounted.
|
||||
The `agent_runtime` image is a Unified Agent deployment and defaults to both
|
||||
host and Docker modules (`PULSE_ENABLE_HOST=true`,
|
||||
`PULSE_ENABLE_DOCKER=true`). Operators may still set
|
||||
`PULSE_ENABLE_HOST=false` for an intentional workload-only deployment, but
|
||||
the image must not silently force that narrower mode and remove a previously
|
||||
reported machine from the Hosts surface.
|
||||
The legacy `scripts/install-container-agent.sh` compatibility wrapper must
|
||||
match that default by forwarding `--enable-docker --enable-host`; it may point
|
||||
operators who need workload-only behavior to the canonical installer's
|
||||
explicit `--enable-host=false` option, but must not choose that opt-out for
|
||||
them.
|
||||
That same update-runtime boundary now also owns bounded rollback retention and
|
||||
disk-space fail-closed behavior for self-hosted app updates. `internal/updates/`
|
||||
must prune stale retained rollback snapshots, clear history references when an
|
||||
|
||||
@@ -91,6 +91,12 @@ downstream surfaces can warn, and it must clear on its own once only one
|
||||
machine keeps reporting for the window. Monitoring must not auto-split the
|
||||
collapsed identity: the machine ID is the identity key, and the remedy
|
||||
(regenerating the clone's machine-id) belongs to the operator.
|
||||
Unified Agent module projection is report-authored and additive. When one
|
||||
agent sends host and Docker reports for the same machine identity, monitoring
|
||||
must refresh one canonical machine carrying both facets so the Hosts and
|
||||
Docker typed views expose the same canonical ID. A deliberately Docker-only
|
||||
agent sends no host report and must remain absent from the Hosts view rather
|
||||
than gaining a synthetic host facet from Docker telemetry.
|
||||
Proxmox read-state rehydration is the inverse boundary: canonical
|
||||
unified-resource CPU metrics are 0..100 percentages, while legacy
|
||||
`models.Node.CPU`, `models.VM.CPU`, and `models.Container.CPU` remain Proxmox
|
||||
|
||||
@@ -863,6 +863,10 @@ Default-org token scoping and notification-settings fan-out on shared
|
||||
`internal/api/router.go` are likewise adjacent only: the tenant-monitor
|
||||
settings inheritance hook runs once per tenant-monitor creation and changes
|
||||
no chart transport, downsampling, or polling behavior.
|
||||
Container-runtime migration mode selection on that shared router path is also
|
||||
request-local. Deriving the bounded host-plus-Docker or Docker-only token
|
||||
scope and matching install flag must remain constant work with no resource
|
||||
inventory scan, telemetry fan-out, or extra persistence pass.
|
||||
|
||||
The bars / sparklines toggle and its sparkline-range picker on the
|
||||
WorkloadsSurface support page-level ownership through four optional
|
||||
|
||||
@@ -773,6 +773,12 @@ the visible API-token manager: agent install command tokens, deploy bootstrap
|
||||
tokens, enrollment runtime tokens, container runtime migration tokens, and
|
||||
first-run/regenerated admin tokens must use the same shared server-side owner
|
||||
setter rather than carrying owner identity in caller-controlled metadata.
|
||||
Container-runtime migration tokens must also follow the requested module
|
||||
boundary. The default host-plus-Docker migration may grant only the bounded
|
||||
Agent report, configuration-read, Agent-manage, and Docker-report scopes; an
|
||||
explicit `enableHost:false` workload-only migration must retain only the
|
||||
Docker-report scope. Neither path may gain command-execution or Kubernetes
|
||||
report authority implicitly.
|
||||
That same command-token trust boundary also owns first-use binding for
|
||||
Proxmox install-command tokens. `internal/api/agent_exec_token_binding.go` may
|
||||
persist `bound_agent_id`, `bound_hostname`, and `bound_at` only for
|
||||
|
||||
@@ -3636,6 +3636,12 @@ That same shared dependency now also assumes those diagnostics install payloads
|
||||
route through the canonical backend install-command helper, so recovery-adjacent
|
||||
transport surfaces do not inherit handler-local drift in token omission,
|
||||
plain-HTTP `--insecure`, or trailing-slash normalization.
|
||||
The persisted migration token and returned install payload must describe the
|
||||
same module boundary: omitted `enableHost` atomically persists the bounded
|
||||
host-plus-Docker scopes before returning an enabled-host command, while an
|
||||
explicit false value persists only Docker-report authority before returning
|
||||
the workload-only command. A persistence failure must continue to fail the
|
||||
request rather than returning credentials whose scope contract was not saved.
|
||||
That same shared `internal/api/` dependency also assumes diagnostics memory
|
||||
source breakdowns backfill canonical fallback reasons even when a raw legacy
|
||||
snapshot reaches `internal/api/diagnostics.go` without one, so
|
||||
|
||||
@@ -17,6 +17,11 @@
|
||||
|
||||
Own canonical resource identity, type normalization, typed views, and
|
||||
cross-source deduplication.
|
||||
One machine may belong to more than one typed view without becoming more than
|
||||
one canonical resource. A matching Pulse host report and Docker report merge
|
||||
into one `agent` resource with Agent and Docker facets, and the Hosts and
|
||||
Docker-host views must return that same canonical ID. A Docker-only resource
|
||||
keeps its `docker-host` contract type and is not admitted to the Hosts view.
|
||||
For Docker and Podman container resources, the canonical CPU metric represents
|
||||
host-capacity-normalized utilization. Runtime-native per-core CPU percent is
|
||||
kept on Docker metadata as raw evidence and must not replace the canonical
|
||||
|
||||
+3
-2
@@ -77,7 +77,7 @@ describe('infrastructure operations model', () => {
|
||||
detail: 'Auto-detect',
|
||||
category: 'default',
|
||||
},
|
||||
installFlags: ['--enable-docker', '--disable-host', '--enable-proxmox', '--proxmox-type pbs'],
|
||||
installFlags: ['--enable-docker', '--enable-proxmox', '--proxmox-type pbs'],
|
||||
searchText: 'node-a node-a.internal agent-1',
|
||||
surfaces: [
|
||||
{
|
||||
@@ -212,8 +212,9 @@ describe('infrastructure operations model', () => {
|
||||
expect(dockerProfile).toBeDefined();
|
||||
expect(dockerProfile?.label).toBe('Docker / Podman runtime');
|
||||
expect(dockerProfile?.description).toBe(
|
||||
'Force Docker / Podman monitoring when automatic detection is restricted.',
|
||||
'Force Docker / Podman monitoring when automatic detection is restricted, while keeping host telemetry enabled.',
|
||||
);
|
||||
expect(dockerProfile?.flags).toEqual(['--enable-docker']);
|
||||
expect(dockerProfile?.description).not.toContain('container runtime');
|
||||
});
|
||||
|
||||
|
||||
+2
-2
@@ -324,12 +324,12 @@ describe('isPlatformConnectionsCapability', () => {
|
||||
describe('installFlagsForCapabilities (via rowFromConnectedInfrastructureItem)', () => {
|
||||
const scope = { label: 'Default', category: 'default' as const };
|
||||
|
||||
it('emits the docker host-disable flag pair for a docker-only item', () => {
|
||||
it('enables Docker without silently disabling the host for a docker-only item', () => {
|
||||
const row = rowFromConnectedInfrastructureItem(
|
||||
makeItem({ surfaces: [connectedSurface('docker')] }),
|
||||
scope,
|
||||
);
|
||||
expect(row.installFlags).toEqual(['--enable-docker', '--disable-host']);
|
||||
expect(row.installFlags).toEqual(['--enable-docker']);
|
||||
});
|
||||
|
||||
it('emits only --enable-kubernetes for a kubernetes-only item', () => {
|
||||
|
||||
@@ -304,8 +304,8 @@ export const INSTALL_PROFILE_OPTIONS: InstallProfileOption[] = [
|
||||
{
|
||||
value: 'docker',
|
||||
label: `${DOCKER_PODMAN_SOURCE_LABEL} runtime`,
|
||||
description: `Force ${DOCKER_PODMAN_SOURCE_LABEL} monitoring when automatic detection is restricted.`,
|
||||
flags: ['--enable-docker', '--disable-host'],
|
||||
description: `Force ${DOCKER_PODMAN_SOURCE_LABEL} monitoring when automatic detection is restricted, while keeping host telemetry enabled.`,
|
||||
flags: ['--enable-docker'],
|
||||
},
|
||||
{
|
||||
value: 'kubernetes',
|
||||
@@ -440,7 +440,6 @@ const installFlagsForCapabilities = (capabilities: AgentCapability[]) => {
|
||||
const flags = new Set<string>();
|
||||
if (capabilities.includes('docker')) {
|
||||
flags.add('--enable-docker');
|
||||
flags.add('--disable-host');
|
||||
}
|
||||
if (capabilities.includes('kubernetes')) {
|
||||
flags.add('--enable-kubernetes');
|
||||
|
||||
@@ -260,15 +260,34 @@ func buildProxmoxAgentInstallCommand(opts agentInstallCommandOptions) string {
|
||||
return withPrivilegeEscalation(command) + tokenCleanup
|
||||
}
|
||||
|
||||
func buildContainerRuntimeAgentInstallCommand(baseURL string, token string) string {
|
||||
func containerRuntimeAgentScopes(enableHost bool) []string {
|
||||
scopes := []string{config.ScopeDockerReport}
|
||||
if enableHost {
|
||||
scopes = append(scopes,
|
||||
config.ScopeAgentReport,
|
||||
config.ScopeAgentConfigRead,
|
||||
config.ScopeAgentManage,
|
||||
)
|
||||
}
|
||||
return scopes
|
||||
}
|
||||
|
||||
func containerRuntimeAgentHostFlag(enableHost bool) string {
|
||||
if enableHost {
|
||||
return "--enable-host"
|
||||
}
|
||||
return "--enable-host=false"
|
||||
}
|
||||
|
||||
func buildContainerRuntimeAgentInstallCommand(baseURL string, token string, enableHost bool) string {
|
||||
normalizedBaseURL := normalizeAgentInstallBaseURL(baseURL)
|
||||
installScriptURL := normalizedBaseURL + "/install.sh"
|
||||
command := fmt.Sprintf(`curl -fsSL %s | bash -s -- \
|
||||
--url %s \
|
||||
--enable-docker \
|
||||
--enable-host=false \
|
||||
%s \
|
||||
--interval 30s`,
|
||||
posixShellQuote(installScriptURL), posixShellQuote(normalizedBaseURL))
|
||||
posixShellQuote(installScriptURL), posixShellQuote(normalizedBaseURL), containerRuntimeAgentHostFlag(enableHost))
|
||||
|
||||
if trimmedToken := strings.TrimSpace(token); trimmedToken != "" {
|
||||
command += fmt.Sprintf(` \
|
||||
|
||||
@@ -124,13 +124,14 @@ func TestBuildProxmoxAgentInstallCommand_IncludesCommandsWhenRequested(t *testin
|
||||
}
|
||||
|
||||
func TestBuildContainerRuntimeAgentInstallCommand_UsesLifecycleTransport(t *testing.T) {
|
||||
command := buildContainerRuntimeAgentInstallCommand("https://pulse.example.com/base", "token-123")
|
||||
command := buildContainerRuntimeAgentInstallCommand("https://pulse.example.com/base", "token-123", true)
|
||||
|
||||
require.Contains(t, command, posixShellQuote("https://pulse.example.com/base/install.sh"))
|
||||
require.Contains(t, command, "--url "+posixShellQuote("https://pulse.example.com/base"))
|
||||
require.Contains(t, command, "--token "+posixShellQuote("token-123"))
|
||||
require.Contains(t, command, "--enable-docker")
|
||||
require.Contains(t, command, "--enable-host=false")
|
||||
require.Contains(t, command, "--enable-host")
|
||||
require.NotContains(t, command, "--enable-host=false")
|
||||
require.Contains(t, command, "--interval 30s")
|
||||
require.Contains(t, command, `| { if [ "$(id -u)" -eq 0 ]; then bash -s --`)
|
||||
require.Contains(t, command, `elif command -v sudo >/dev/null 2>&1; then sudo bash -s --`)
|
||||
@@ -138,7 +139,7 @@ func TestBuildContainerRuntimeAgentInstallCommand_UsesLifecycleTransport(t *test
|
||||
}
|
||||
|
||||
func TestBuildContainerRuntimeAgentInstallCommand_OmitsTokenAndAddsInsecureForHTTP(t *testing.T) {
|
||||
command := buildContainerRuntimeAgentInstallCommand("http://pulse.example.com:7655/", "")
|
||||
command := buildContainerRuntimeAgentInstallCommand("http://pulse.example.com:7655/", "", true)
|
||||
|
||||
require.Contains(t, command, posixShellQuote("http://pulse.example.com:7655/install.sh"))
|
||||
require.Contains(t, command, "--url "+posixShellQuote("http://pulse.example.com:7655"))
|
||||
@@ -146,6 +147,25 @@ func TestBuildContainerRuntimeAgentInstallCommand_OmitsTokenAndAddsInsecureForHT
|
||||
require.Contains(t, command, "--insecure")
|
||||
}
|
||||
|
||||
func TestBuildContainerRuntimeAgentInstallCommand_PreservesWorkloadOnlyMode(t *testing.T) {
|
||||
command := buildContainerRuntimeAgentInstallCommand("https://pulse.example.com", "token-123", false)
|
||||
|
||||
require.Contains(t, command, "--enable-docker")
|
||||
require.Contains(t, command, "--enable-host=false")
|
||||
}
|
||||
|
||||
func TestContainerRuntimeAgentScopesFollowHostMode(t *testing.T) {
|
||||
dualMode := containerRuntimeAgentScopes(true)
|
||||
require.ElementsMatch(t, []string{
|
||||
config.ScopeDockerReport,
|
||||
config.ScopeAgentReport,
|
||||
config.ScopeAgentConfigRead,
|
||||
config.ScopeAgentManage,
|
||||
}, dualMode)
|
||||
|
||||
require.Equal(t, []string{config.ScopeDockerReport}, containerRuntimeAgentScopes(false))
|
||||
}
|
||||
|
||||
func TestBuildSetupScriptCommand_UsesFailFastQuotedTransport(t *testing.T) {
|
||||
command := buildSetupScriptCommand("https://pulse.example.com/api/setup-script?type=pve&host=pve1.local", "token-123")
|
||||
|
||||
|
||||
@@ -6414,13 +6414,13 @@ func TestContract_HostsShareResolvedIdentityTreatsLoopbackAliasAsSameNode(t *tes
|
||||
|
||||
func TestContract_DiagnosticsDockerPrepareTokenInstallCommandUsesLifecycleTransport(t *testing.T) {
|
||||
baseURL := "https://pulse.example.com/base"
|
||||
got := buildContainerRuntimeAgentInstallCommand(baseURL, "token-123")
|
||||
got := buildContainerRuntimeAgentInstallCommand(baseURL, "token-123", true)
|
||||
|
||||
if !strings.Contains(got, posixShellQuote(baseURL+"/install.sh")) {
|
||||
t.Fatalf("install command missing normalized install script URL: %s", got)
|
||||
}
|
||||
if !strings.Contains(got, "--enable-host=false") {
|
||||
t.Fatalf("install command missing canonical host-disable flag: %s", got)
|
||||
if !strings.Contains(got, "--enable-host") || strings.Contains(got, "--enable-host=false") {
|
||||
t.Fatalf("install command must default the native migration to host and Docker monitoring: %s", got)
|
||||
}
|
||||
if strings.Contains(got, "--disable-host") {
|
||||
t.Fatalf("install command preserved stale disable-host flag: %s", got)
|
||||
@@ -6434,7 +6434,7 @@ func TestContract_DiagnosticsDockerPrepareTokenInstallCommandUsesLifecycleTransp
|
||||
}
|
||||
|
||||
func TestContract_DiagnosticsDockerPrepareTokenOptionalAuthInstallCommandOmitsToken(t *testing.T) {
|
||||
got := buildContainerRuntimeAgentInstallCommand("http://pulse.example.com:7655/", "")
|
||||
got := buildContainerRuntimeAgentInstallCommand("http://pulse.example.com:7655/", "", true)
|
||||
|
||||
if strings.Contains(got, "--token") {
|
||||
t.Fatalf("optional-auth install command preserved token flag: %s", got)
|
||||
@@ -6444,6 +6444,17 @@ func TestContract_DiagnosticsDockerPrepareTokenOptionalAuthInstallCommandOmitsTo
|
||||
}
|
||||
}
|
||||
|
||||
func TestContract_DiagnosticsDockerPrepareTokenPreservesExplicitWorkloadOnlyMode(t *testing.T) {
|
||||
got := buildContainerRuntimeAgentInstallCommand("https://pulse.example.com", "token-123", false)
|
||||
|
||||
if !strings.Contains(got, "--enable-host=false") {
|
||||
t.Fatalf("explicit workload-only install command missing canonical host-disable flag: %s", got)
|
||||
}
|
||||
if strings.Contains(got, "--disable-host") {
|
||||
t.Fatalf("explicit workload-only install command preserved stale disable-host alias: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContract_SetupScriptURLCommandUsesFailFastQuotedTransport(t *testing.T) {
|
||||
url := "https://pulse.example.com/api/setup-script?type=pve&host=pve1.local"
|
||||
got := buildSetupScriptCommand(url, "token-123")
|
||||
|
||||
+11
-5
@@ -10426,8 +10426,9 @@ func (r *Router) handleDiagnosticsDockerPrepareToken(w http.ResponseWriter, req
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
AgentID string `json:"agentId"`
|
||||
TokenName string `json:"tokenName"`
|
||||
AgentID string `json:"agentId"`
|
||||
TokenName string `json:"tokenName"`
|
||||
EnableHost *bool `json:"enableHost,omitempty"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(req.Body).Decode(&payload); err != nil {
|
||||
@@ -10492,6 +10493,10 @@ func (r *Router) handleDiagnosticsDockerPrepareToken(w http.ResponseWriter, req
|
||||
displayName := preferredDockerHostName(host)
|
||||
name = fmt.Sprintf("Container runtime: %s", displayName)
|
||||
}
|
||||
enableHost := true
|
||||
if payload.EnableHost != nil {
|
||||
enableHost = *payload.EnableHost
|
||||
}
|
||||
|
||||
rawToken, err := auth.GenerateAPIToken()
|
||||
if err != nil {
|
||||
@@ -10500,7 +10505,7 @@ func (r *Router) handleDiagnosticsDockerPrepareToken(w http.ResponseWriter, req
|
||||
return
|
||||
}
|
||||
|
||||
record, err := config.NewAPITokenRecord(rawToken, name, []string{config.ScopeDockerReport})
|
||||
record, err := config.NewAPITokenRecord(rawToken, name, containerRuntimeAgentScopes(enableHost))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to construct token record for container runtime migration")
|
||||
writeErrorResponse(w, http.StatusInternalServerError, "token_generation_failed", "Failed to generate API token", nil)
|
||||
@@ -10552,8 +10557,8 @@ func (r *Router) handleDiagnosticsDockerPrepareToken(w http.ResponseWriter, req
|
||||
config.Mu.Unlock()
|
||||
|
||||
baseURL := normalizeAgentInstallBaseURL(r.resolvePublicURL(req))
|
||||
installCommand := buildContainerRuntimeAgentInstallCommand(baseURL, rawToken)
|
||||
systemdSnippet := fmt.Sprintf("[Service]\nType=simple\nEnvironment=\"PULSE_URL=%s\"\nEnvironment=\"PULSE_TOKEN=%s\"\nExecStart=/usr/local/bin/pulse-agent --url %s --token %s --enable-docker --enable-host=false --interval 30s\nRestart=always\nRestartSec=5s\nUser=root", baseURL, rawToken, baseURL, rawToken)
|
||||
installCommand := buildContainerRuntimeAgentInstallCommand(baseURL, rawToken, enableHost)
|
||||
systemdSnippet := fmt.Sprintf("[Service]\nType=simple\nEnvironment=\"PULSE_URL=%s\"\nEnvironment=\"PULSE_TOKEN=%s\"\nExecStart=/usr/local/bin/pulse-agent --url %s --token %s --enable-docker %s --interval 30s\nRestart=always\nRestartSec=5s\nUser=root", baseURL, rawToken, baseURL, rawToken, containerRuntimeAgentHostFlag(enableHost))
|
||||
|
||||
response := map[string]any{
|
||||
"success": true,
|
||||
@@ -10566,6 +10571,7 @@ func (r *Router) handleDiagnosticsDockerPrepareToken(w http.ResponseWriter, req
|
||||
"installCommand": installCommand,
|
||||
"systemdServiceSnippet": systemdSnippet,
|
||||
"pulseURL": baseURL,
|
||||
"enableHost": enableHost,
|
||||
}
|
||||
|
||||
if err := utils.WriteJSONResponse(w, response); err != nil {
|
||||
|
||||
@@ -170,8 +170,9 @@ func TestHandleDiagnosticsDockerPrepareToken_Success(t *testing.T) {
|
||||
if !strings.Contains(payload["installCommand"].(string), "https://pulse.example.com") {
|
||||
t.Fatalf("expected install command to include base URL")
|
||||
}
|
||||
if !strings.Contains(payload["installCommand"].(string), "--enable-host=false") {
|
||||
t.Fatalf("expected install command to disable host metrics with the canonical lifecycle flag: %q", payload["installCommand"])
|
||||
if !strings.Contains(payload["installCommand"].(string), "--enable-host") ||
|
||||
strings.Contains(payload["installCommand"].(string), "--enable-host=false") {
|
||||
t.Fatalf("expected install command to enable host metrics by default: %q", payload["installCommand"])
|
||||
}
|
||||
if !strings.Contains(payload["installCommand"].(string), "--enable-docker") {
|
||||
t.Fatalf("expected install command to enable docker metrics: %q", payload["installCommand"])
|
||||
@@ -182,12 +183,51 @@ func TestHandleDiagnosticsDockerPrepareToken_Success(t *testing.T) {
|
||||
if strings.Contains(payload["installCommand"].(string), "| sudo bash -s -- --url") {
|
||||
t.Fatalf("expected install command to preserve the governed root-or-sudo wrapper instead of a raw sudo pipe: %q", payload["installCommand"])
|
||||
}
|
||||
if !strings.Contains(payload["systemdServiceSnippet"].(string), "--enable-host=false") {
|
||||
t.Fatalf("expected systemd snippet to disable host metrics: %q", payload["systemdServiceSnippet"])
|
||||
if !strings.Contains(payload["systemdServiceSnippet"].(string), "--enable-host") ||
|
||||
strings.Contains(payload["systemdServiceSnippet"].(string), "--enable-host=false") {
|
||||
t.Fatalf("expected systemd snippet to enable host metrics by default: %q", payload["systemdServiceSnippet"])
|
||||
}
|
||||
if len(router.config.APITokens) == 0 {
|
||||
t.Fatalf("expected API token to be recorded")
|
||||
}
|
||||
token := router.config.APITokens[0]
|
||||
for _, scope := range []string{config.ScopeDockerReport, config.ScopeAgentReport, config.ScopeAgentConfigRead, config.ScopeAgentManage} {
|
||||
if !token.HasScope(scope) {
|
||||
t.Fatalf("expected default migration token to include %q, got %#v", scope, token.Scopes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleDiagnosticsDockerPrepareToken_PreservesExplicitWorkloadOnlyMode(t *testing.T) {
|
||||
monitor, state, _ := newTestMonitor(t)
|
||||
state.DockerHosts = []models.DockerHost{{ID: "host-1", DisplayName: "Docker Host"}}
|
||||
|
||||
router := &Router{monitor: monitor, config: &config.Config{PublicURL: "https://pulse.example.com"}}
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/diagnostics/docker/prepare-token", strings.NewReader(`{"agentId":"host-1","enableHost":false}`))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
router.handleDiagnosticsDockerPrepareToken(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
||||
}
|
||||
var payload map[string]interface{}
|
||||
if err := json.NewDecoder(rec.Body).Decode(&payload); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if enabled, ok := payload["enableHost"].(bool); !ok || enabled {
|
||||
t.Fatalf("expected enableHost=false, got %#v", payload["enableHost"])
|
||||
}
|
||||
if !strings.Contains(payload["installCommand"].(string), "--enable-host=false") {
|
||||
t.Fatalf("expected workload-only install command, got %q", payload["installCommand"])
|
||||
}
|
||||
if len(router.config.APITokens) != 1 {
|
||||
t.Fatalf("expected one migration token, got %d", len(router.config.APITokens))
|
||||
}
|
||||
token := router.config.APITokens[0]
|
||||
if !token.HasScope(config.ScopeDockerReport) || token.HasScope(config.ScopeAgentReport) {
|
||||
t.Fatalf("expected Docker-only token scopes, got %#v", token.Scopes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleDiagnosticsDockerPrepareToken_NormalizesTrailingSlashPublicURL(t *testing.T) {
|
||||
|
||||
@@ -458,6 +458,149 @@ func TestApplyHostReportRefreshesUnifiedReadStateWithoutBroadcast(t *testing.T)
|
||||
t.Fatal("accepted host report did not refresh the canonical headless read state")
|
||||
}
|
||||
|
||||
func TestUnifiedAgentHostAndDockerReportsShareOneCanonicalMachine(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
hostReport := agentshost.Report{
|
||||
Agent: agentshost.AgentInfo{
|
||||
ID: "dual-mode-agent",
|
||||
Version: "6.1.1",
|
||||
IntervalSeconds: 30,
|
||||
},
|
||||
Host: agentshost.HostInfo{
|
||||
ID: "dual-mode-machine",
|
||||
MachineID: "dual-mode-machine",
|
||||
Hostname: "docker-lxc.local",
|
||||
Platform: "linux",
|
||||
OSName: "debian",
|
||||
},
|
||||
Timestamp: now,
|
||||
}
|
||||
dockerReport := agentsdocker.Report{
|
||||
Agent: agentsdocker.AgentInfo{
|
||||
ID: "dual-mode-agent",
|
||||
Version: "6.1.1",
|
||||
IntervalSeconds: 30,
|
||||
},
|
||||
Host: agentsdocker.HostInfo{
|
||||
Hostname: "docker-lxc.local",
|
||||
MachineID: "dual-mode-machine",
|
||||
DockerVersion: "27.0.0",
|
||||
TotalCPU: 4,
|
||||
TotalMemoryBytes: 8 << 30,
|
||||
},
|
||||
Containers: []agentsdocker.Container{{
|
||||
ID: "workload-1",
|
||||
Name: "app",
|
||||
State: "running",
|
||||
}},
|
||||
Timestamp: now.Add(time.Second),
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
dockerFirst bool
|
||||
}{
|
||||
{name: "host report first"},
|
||||
{name: "Docker report first", dockerFirst: true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
monitor := newTestMonitor(t)
|
||||
adapter := unifiedresources.NewMonitorAdapter(unifiedresources.NewRegistry(nil))
|
||||
monitor.SetResourceStore(adapter)
|
||||
token := &config.APITokenRecord{ID: "dual-mode-token", Name: "Dual-mode Agent"}
|
||||
|
||||
applyHost := func() {
|
||||
t.Helper()
|
||||
if _, err := monitor.ApplyHostReport(hostReport, token); err != nil {
|
||||
t.Fatalf("ApplyHostReport: %v", err)
|
||||
}
|
||||
}
|
||||
applyDocker := func() {
|
||||
t.Helper()
|
||||
if _, err := monitor.ApplyDockerReport(dockerReport, token); err != nil {
|
||||
t.Fatalf("ApplyDockerReport: %v", err)
|
||||
}
|
||||
}
|
||||
if tc.dockerFirst {
|
||||
applyDocker()
|
||||
applyHost()
|
||||
} else {
|
||||
applyHost()
|
||||
applyDocker()
|
||||
}
|
||||
|
||||
resources := adapter.GetAll()
|
||||
if len(resources) != 2 {
|
||||
t.Fatalf("canonical resource count = %d, want one machine and one workload: %#v", len(resources), resources)
|
||||
}
|
||||
var machine *unifiedresources.Resource
|
||||
for i := range resources {
|
||||
if resources[i].Type == unifiedresources.ResourceTypeAgent {
|
||||
machine = &resources[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if machine == nil {
|
||||
t.Fatalf("canonical resources did not retain the machine: %#v", resources)
|
||||
}
|
||||
if machine.Agent == nil || machine.Docker == nil {
|
||||
t.Fatalf("machine facets = agent:%v docker:%v, want both", machine.Agent != nil, machine.Docker != nil)
|
||||
}
|
||||
if got := unifiedresources.ContractResourceType(*machine); got != unifiedresources.ResourceTypeAgent {
|
||||
t.Fatalf("machine contract type = %q, want agent", got)
|
||||
}
|
||||
|
||||
hostViews := adapter.Hosts()
|
||||
dockerViews := adapter.DockerHosts()
|
||||
if len(hostViews) != 1 || len(dockerViews) != 1 {
|
||||
t.Fatalf("typed views = hosts:%d docker:%d, want one in each", len(hostViews), len(dockerViews))
|
||||
}
|
||||
if hostViews[0].ID() != dockerViews[0].ID() || hostViews[0].ID() != machine.ID {
|
||||
t.Fatalf("typed view identities diverged: host=%q docker=%q machine=%q", hostViews[0].ID(), dockerViews[0].ID(), machine.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerOnlyAgentRemainsWorkloadOnly(t *testing.T) {
|
||||
monitor := newTestMonitor(t)
|
||||
adapter := unifiedresources.NewMonitorAdapter(unifiedresources.NewRegistry(nil))
|
||||
monitor.SetResourceStore(adapter)
|
||||
|
||||
_, err := monitor.ApplyDockerReport(agentsdocker.Report{
|
||||
Agent: agentsdocker.AgentInfo{
|
||||
ID: "workload-only-agent",
|
||||
Version: "6.1.1",
|
||||
IntervalSeconds: 30,
|
||||
},
|
||||
Host: agentsdocker.HostInfo{
|
||||
Hostname: "runtime-only.local",
|
||||
MachineID: "runtime-only-machine",
|
||||
DockerVersion: "27.0.0",
|
||||
TotalCPU: 2,
|
||||
TotalMemoryBytes: 4 << 30,
|
||||
},
|
||||
Timestamp: time.Now().UTC(),
|
||||
}, &config.APITokenRecord{ID: "workload-only-token"})
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyDockerReport: %v", err)
|
||||
}
|
||||
|
||||
resources := adapter.GetAll()
|
||||
if len(resources) != 1 {
|
||||
t.Fatalf("canonical resource count = %d, want one Docker runtime: %#v", len(resources), resources)
|
||||
}
|
||||
if resources[0].Agent != nil || resources[0].Docker == nil {
|
||||
t.Fatalf("workload-only facets = agent:%v docker:%v, want Docker only", resources[0].Agent != nil, resources[0].Docker != nil)
|
||||
}
|
||||
if got := unifiedresources.ContractResourceType(resources[0]); got != unifiedresources.ResourceType("docker-host") {
|
||||
t.Fatalf("workload-only contract type = %q, want docker-host", got)
|
||||
}
|
||||
if len(adapter.Hosts()) != 0 || len(adapter.DockerHosts()) != 1 {
|
||||
t.Fatalf("typed views = hosts:%d docker:%d, want workload-only runtime excluded from Hosts", len(adapter.Hosts()), len(adapter.DockerHosts()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyDockerReportMigratesAppContainerURLToStableNameAcceptedIngestProof(t *testing.T) {
|
||||
monitor := newTestMonitor(t)
|
||||
report := agentsdocker.Report{
|
||||
|
||||
@@ -24,7 +24,10 @@ Supported options:
|
||||
|
||||
Notes:
|
||||
This wrapper forwards to install.sh with:
|
||||
--enable-docker --disable-host
|
||||
--enable-docker --enable-host
|
||||
|
||||
Use install.sh directly with --enable-host=false for an intentional
|
||||
workload-only deployment.
|
||||
USAGE
|
||||
}
|
||||
|
||||
@@ -91,7 +94,7 @@ fi
|
||||
if [[ "$UNINSTALL" == "true" ]]; then
|
||||
forward_args+=(--uninstall)
|
||||
else
|
||||
forward_args+=(--enable-docker --disable-host)
|
||||
forward_args+=(--enable-docker --enable-host)
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
@@ -777,7 +777,7 @@ func TestDockerAndDemoBuildsUseCanonicalReleaseLdflags(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentRuntimeImagePersistsAgentIdentityByDefault(t *testing.T) {
|
||||
func TestAgentRuntimeImageDefaultsToUnifiedHostAndDockerMonitoring(t *testing.T) {
|
||||
dockerfileBytes, err := os.ReadFile(repoFile("Dockerfile"))
|
||||
if err != nil {
|
||||
t.Fatalf("read Dockerfile: %v", err)
|
||||
@@ -787,7 +787,7 @@ func TestAgentRuntimeImagePersistsAgentIdentityByDefault(t *testing.T) {
|
||||
required := []string{
|
||||
`mkdir -p /var/lib/pulse-agent`,
|
||||
`PULSE_DISABLE_AUTO_UPDATE=true`,
|
||||
`PULSE_ENABLE_HOST=false`,
|
||||
`PULSE_ENABLE_HOST=true`,
|
||||
`PULSE_ENABLE_DOCKER=true`,
|
||||
`PULSE_AGENT_ID_FILE=/var/lib/pulse-agent/agent-id`,
|
||||
`PULSE_STATE_DIR=/var/lib/pulse-agent`,
|
||||
@@ -796,9 +796,12 @@ func TestAgentRuntimeImagePersistsAgentIdentityByDefault(t *testing.T) {
|
||||
}
|
||||
for _, needle := range required {
|
||||
if !strings.Contains(dockerfile, needle) {
|
||||
t.Fatalf("Dockerfile agent_runtime missing persistent identity contract: %s", needle)
|
||||
t.Fatalf("Dockerfile agent_runtime missing unified host and Docker contract: %s", needle)
|
||||
}
|
||||
}
|
||||
if strings.Contains(dockerfile, `PULSE_ENABLE_HOST=false`) {
|
||||
t.Fatal("agent_runtime must not silently force every deployment into workload-only mode")
|
||||
}
|
||||
if strings.Contains(dockerfile, `ENTRYPOINT ["/usr/local/bin/pulse-agent", "--enable-docker", "--enable-host=false"]`) {
|
||||
t.Fatal("agent_runtime must not hard-code module flags in ENTRYPOINT; env defaults keep user args overridable")
|
||||
}
|
||||
|
||||
@@ -15,6 +15,24 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestContainerAgentCompatibilityWrapperDefaultsToUnifiedHostAndDockerMonitoring(t *testing.T) {
|
||||
wrapperBytes, err := os.ReadFile(repoFile("scripts", "install-container-agent.sh"))
|
||||
if err != nil {
|
||||
t.Fatalf("read container-agent compatibility wrapper: %v", err)
|
||||
}
|
||||
wrapper := string(wrapperBytes)
|
||||
|
||||
if !strings.Contains(wrapper, `forward_args+=(--enable-docker --enable-host)`) {
|
||||
t.Fatal("container-agent compatibility wrapper must preserve host telemetry while enabling Docker")
|
||||
}
|
||||
if strings.Contains(wrapper, `forward_args+=(--enable-docker --disable-host)`) {
|
||||
t.Fatal("container-agent compatibility wrapper must not silently force workload-only mode")
|
||||
}
|
||||
if !strings.Contains(wrapper, `--enable-host=false for an intentional`) {
|
||||
t.Fatal("container-agent compatibility wrapper must document the explicit workload-only path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallSHAllowsMissingTokenForOptionalAuth(t *testing.T) {
|
||||
content, err := os.ReadFile(repoFile("scripts", "install.sh"))
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user