Restore Docker agents to Hosts inventory

This commit is contained in:
rcourtman
2026-07-23 21:40:41 +01:00
parent 6a527ba9f2
commit 615fa4418b
22 changed files with 360 additions and 37 deletions
+1 -1
View File
@@ -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
+6
View File
@@ -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
@@ -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');
});
@@ -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');
+22 -3
View File
@@ -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")
+15 -4
View File
@@ -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
View File
@@ -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{
+5 -2
View File
@@ -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")
}
+18
View File
@@ -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 {