diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 136d22529..c2ab3936d 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -799,6 +799,13 @@ profile and assignment columns, but embedded table framing must route through the host, auto-detection and remote config must not start the Docker / Podman module. 8. Add or change installer flags, persisted service arguments, or upgrade-safe re-entry behavior through `scripts/install.sh` and `scripts/install.ps1`. + On Unix systemd hosts, `scripts/install.sh` must keep the default agent unit + hardened, but command-enabled Proxmox VE agents selected with + `--enable-commands --enable-proxmox` and type `pve` or `all` must explicitly + allow the minimum `pct exec` / `lxc-attach` service posture by setting + `NoNewPrivileges=false` and `RestrictSUIDSGID=false`. That exception is + limited to the PVE command-agent path; it must not become the default for + Docker / Podman, host-only, PBS-only, or ordinary non-command agents. Persistence-sensitive NAS targets must keep one canonical continuity model here: installer-owned bootstraps may use flash-backed or immutable-root launch hooks only as thin trampolines, while the durable wrapper, state, and reboot-surviving binary copy stay in the governed persistent state directory that updater continuity also refreshes. Approval-gated command execution must expose stable rejection reasons for invalid approval grants so fleet operators can distinguish missing, expired, @@ -2097,6 +2104,12 @@ server itself cannot use afterward. That same selection path must only persist selected host; if every candidate fingerprint probe fails, registration must fall back to the preferred normalized host with strict TLS disabled instead of pretending public-CA verification is now safe for a self-signed Proxmox node. +Cluster-member Proxmox agents that match a non-primary endpoint of an existing +cluster source are already registered for lifecycle purposes, even when the +cluster source's primary connection is currently disconnected. Only the primary +configured endpoint may drive disconnected-source repair and token rotation; a +covered member endpoint must not rotate the cluster token just because it can +reach `/api/auto-register`. That same canonical behavior also includes one auth transport for Proxmox completion: runtime-side Unified Agent and script callers must send `/api/auto-register` authentication through a one-time setup token in the request-body diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 13fa23501..4bdd3ac88 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -719,6 +719,13 @@ payload shape change when the portal presents compact client rows. 36. `internal/api/config_setup_handlers.go` shared with `agent-lifecycle`: auto-register and setup handlers are both an agent lifecycle control surface and a canonical API payload contract boundary. That same shared boundary also owns reachable-host selection truth for canonical Proxmox registration: runtime callers may propose ordered `candidateHosts`, but the API contract must persist and echo the first candidate Pulse can actually reach instead of freezing the caller's rejected first preference into the stored node endpoint. That same canonical payload contract also owns strict-TLS truth for that selected host: `/api/auto-register` may only persist `VerifySSL=true` when Pulse actually captured a certificate fingerprint for the selected candidate, and it must not pretend public-CA verification is safe after every candidate fingerprint probe failed. + For PVE cluster sources, that same contract must distinguish primary + configured endpoints from covered cluster-member endpoints. A caller whose + candidate hosts match a non-primary `clusterEndpoints` entry is already + registered and must not rotate the shared Proxmox token, even if the + primary connection is currently disconnected. A caller whose candidates + match the primary configured endpoint may still drive disconnected-source + repair. That same contract now owns stale-marker verification as well: setup-token-authenticated `checkRegistration` requests may omit token completion fields and must answer `{registered:boolean}` from canonical candidate-host matching so runtime repair can distinguish real registrations from stale local marker files without rotating tokens first. That same contract owns auto-register WebSocket event intent: a successful completion that creates a new PVE/PBS node may broadcast diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 1d5c6db60..67b5e6766 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -277,6 +277,12 @@ TLS floor in the dynamic config. When enabled, the terminal summary must also state that Proxmox LXC Docker inventory still requires explicit server-side `PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY=true`. + For command-enabled PVE agents, the generated systemd unit must keep the + normal service hardening except for the two flags that block host-side + `pct exec` / `lxc-attach`: `NoNewPrivileges=false` and + `RestrictSUIDSGID=false`. That exception is deployment-owned operator truth + for the Proxmox LXC Docker inventory path and must not leak into non-PVE or + non-command agent installs. ## Extension Points @@ -397,6 +403,13 @@ TLS floor in the dynamic config. first-run reset removes the runtime `.env`; otherwise the Pro backend fails closed before binding the API port and the supervisor loops without ever reaching browser-verifiable health. + Hot-dev backend launches, supervisor child launches, and takeover restarts + must also preserve `LOG_LEVEL` and the Proxmox guest-Docker opt-in + environment (`PULSE_ENABLE_PROXMOX_GUEST_DOCKER_DETECTION`, + `PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY`, and + `PULSE_PROXMOX_GUEST_DOCKER_INVENTORY_VMIDS`) so live dev verification of + host-side LXC Docker inventory does not silently restart into default-off + monitoring. 6. Add or change governed release-promotion workflow inputs, operator-facing promotion metadata, the canonical version file, prerelease feedback intake prompts, artifact publication lineage enforcement, release note or changelog packet composition, or stable-promotion rehearsal summaries through `.github/workflows/create-release.yml`, `.github/workflows/helm-pages.yml`, `.github/workflows/publish-docker.yml`, `.github/workflows/publish-helm-chart.yml`, `.github/workflows/promote-floating-tags.yml`, `.github/workflows/release-dry-run.yml`, `.github/workflows/update-demo-server.yml`, `.github/ISSUE_TEMPLATE/v6_rc_feedback.yml`, `docs/RELEASE_NOTES.md`, `docs/releases/`, `docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md`, `docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md`, `docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md`, `scripts/check-workflow-dispatch-inputs.py`, `scripts/release_control/render_release_body.py`, `scripts/release_control/record_rc_to_ga_rehearsal.py`, `scripts/release_control/internal/record_rc_to_ga_rehearsal.py`, `scripts/release_control/release_promotion_policy_support.py`, `scripts/trigger-release.sh`, and `scripts/trigger-release-dry-run.sh` That release-promotion boundary also owns prerelease note packet lineage: shipped RC notes must remain historically accurate, the top-level diff --git a/docs/release-control/v6/internal/subsystems/monitoring.md b/docs/release-control/v6/internal/subsystems/monitoring.md index fed4a20c3..88db61f5f 100644 --- a/docs/release-control/v6/internal/subsystems/monitoring.md +++ b/docs/release-control/v6/internal/subsystems/monitoring.md @@ -167,7 +167,12 @@ truth for live infrastructure data. server opt-in. LXC Docker inventory may only emit Docker / Podman module-compatible reports into `ApplyDockerReport`, must skip guests with a linked online guest-local host agent, and must keep the command set to - minimal read-only Docker summary and aggregate stats collection. + minimal read-only Docker summary and aggregate stats collection. The socket + probe must run its yes/no marker inside the target LXC through `pct exec`; + host-side `pct` / `lxc-attach` failures are probe errors and must not be + converted into cached `HasDocker=false` results. Negative Docker detections + may be rechecked on a short cadence so command enrollment, daemon startup, + or transient Proxmox access failures do not hide later inventory. 15. Add or change mock-mode Discovery context through the canonical mock fixture graph. Mock Discovery records must be derived from the same authored state graph as mock nodes, guests, Docker hosts, containers, and Kubernetes diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 4da971b83..03ec5239e 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -356,6 +356,11 @@ recovery scope, or a storage/recovery-owned secret source. for host-side Docker-in-LXC collection must not be reinterpreted as backup freshness, restore coverage, storage protection evidence, or remediation authority for the LXC guest. + Proxmox auto-registration dedupe for cluster-member endpoints is likewise + adjacent API/lifecycle continuity: treating a non-primary cluster endpoint + as already registered prevents accidental shared-token rotation, but it does + not create backup visibility, recovery readiness, restore scope, or + storage-local authority for that endpoint. Global resource timeline routing may also pass through shared `internal/api/` handlers. Storage and recovery surfaces may read canonical timeline records as adjacent evidence, but they must not reinterpret an diff --git a/internal/api/config_handlers_auto_register_test.go b/internal/api/config_handlers_auto_register_test.go index 250e979ff..a38c2a920 100644 --- a/internal/api/config_handlers_auto_register_test.go +++ b/internal/api/config_handlers_auto_register_test.go @@ -446,6 +446,56 @@ func TestHandleAutoRegisterCheckRegistrationReturnsFalseForMissingNode(t *testin } } +func TestAutoRegisteredPVEInstanceExistsTreatsClusterEndpointAsRegisteredWhenPrimaryDisconnected(t *testing.T) { + instances := []config.PVEInstance{ + { + Name: "delly", + Host: "https://delly:8006", + ClusterName: "homelab", + IsCluster: true, + ClusterEndpoints: []config.ClusterEndpoint{ + { + NodeName: "minipc", + Host: "https://minipc:8006", + IP: "192.168.0.134", + }, + }, + }, + } + connStatuses := map[string]bool{ + "delly": false, + } + + if !autoRegisteredPVEInstanceExists(instances, connStatuses, []string{"https://192.168.0.134:8006"}) { + t.Fatal("expected covered cluster endpoint to remain registered even while primary connection is disconnected") + } +} + +func TestAutoRegisteredPVEInstanceExistsAllowsPrimaryDisconnectedRepair(t *testing.T) { + instances := []config.PVEInstance{ + { + Name: "delly", + Host: "https://delly:8006", + ClusterName: "homelab", + IsCluster: true, + ClusterEndpoints: []config.ClusterEndpoint{ + { + NodeName: "minipc", + Host: "https://minipc:8006", + IP: "192.168.0.134", + }, + }, + }, + } + connStatuses := map[string]bool{ + "delly": false, + } + + if autoRegisteredPVEInstanceExists(instances, connStatuses, []string{"https://delly:8006"}) { + t.Fatal("expected primary configured connection to be repairable when monitor confirms it is disconnected") + } +} + func TestHandleAutoRegisterRejectsMissingSource(t *testing.T) { stubAutoRegisterNetworkDeps(t) diff --git a/internal/api/config_setup_handlers.go b/internal/api/config_setup_handlers.go index 591d14b84..84ac011e7 100644 --- a/internal/api/config_setup_handlers.go +++ b/internal/api/config_setup_handlers.go @@ -705,6 +705,52 @@ func autoRegisterHostMatchesCandidates(existingHost string, candidates []string) return false } +func autoRegisterClusterEndpointMatchesCandidates(endpoint config.ClusterEndpoint, candidates []string) bool { + for _, endpointHost := range []string{ + endpoint.Host, + endpoint.GuestURL, + endpoint.IPOverride, + endpoint.IP, + } { + if autoRegisterHostMatchesCandidates(endpointHost, candidates) { + return true + } + if normalized, err := normalizeNodeHost(endpointHost, "pve"); err == nil && autoRegisterHostMatchesCandidates(normalized, candidates) { + return true + } + } + return false +} + +func autoRegisterPVEInstanceMatchesCandidates(node config.PVEInstance, candidates []string) (matched bool, primary bool) { + if autoRegisterHostMatchesCandidates(node.Host, candidates) { + return true, true + } + for _, endpoint := range node.ClusterEndpoints { + if autoRegisterClusterEndpointMatchesCandidates(endpoint, candidates) { + return true, false + } + } + return false, false +} + +func autoRegisteredPVEInstanceExists(instances []config.PVEInstance, connStatuses map[string]bool, candidates []string) bool { + for _, node := range instances { + matched, primary := autoRegisterPVEInstanceMatchesCandidates(node, candidates) + if !matched { + continue + } + // A non-primary cluster endpoint is already covered by the cluster + // connection. Letting endpoint agents rotate the shared PVE token can + // invalidate the primary configured connection. + if primary && isKnownDisconnected(connStatuses, "pve", node.Name, node.Host) { + return false + } + return true + } + return false +} + func (h *ConfigHandlers) autoRegisteredNodeExists(ctx context.Context, req *AutoRegisterRequest, candidates []string) bool { if req == nil { return false @@ -718,18 +764,7 @@ func (h *ConfigHandlers) autoRegisteredNodeExists(ctx context.Context, req *Auto switch req.Type { case "pve": - for _, node := range h.getConfig(ctx).PVEInstances { - if autoRegisterHostMatchesCandidates(node.Host, candidates) { - // Node exists in config. If the monitor has definitively confirmed - // it is disconnected (stale token), allow re-registration so the - // agent can update the token. Default to "registered" when there - // is no monitor status yet (e.g. startup race). - if isKnownDisconnected(connStatuses, "pve", node.Name, node.Host) { - return false - } - return true - } - } + return autoRegisteredPVEInstanceExists(h.getConfig(ctx).PVEInstances, connStatuses, candidates) case "pbs": for _, node := range h.getConfig(ctx).PBSInstances { if autoRegisterHostMatchesCandidates(node.Host, candidates) { diff --git a/internal/monitoring/canonical_guardrails_test.go b/internal/monitoring/canonical_guardrails_test.go index 9fafa10cc..5ffa6c534 100644 --- a/internal/monitoring/canonical_guardrails_test.go +++ b/internal/monitoring/canonical_guardrails_test.go @@ -884,9 +884,13 @@ func TestProxmoxGuestDockerInventoryUsesCanonicalReportIngestPath(t *testing.T) requiredSnippets := map[string][]string{ "docker_detection.go": { "type DockerInventoryCollector interface {", + "proxmoxGuestDockerSocketMarker", + "proxmoxGuestDockerNegativeRecheckAfter", "func (m *Monitor) CollectProxmoxGuestDockerInventory(ctx context.Context, containers []models.Container) {", "m.hasOnlineHostAgentForContainer(ct.ID)", "m.ApplyDockerReport(report, nil)", + "pct exec %d -- sh -c", + "container docker socket probe failed", `docker ps -a --no-trunc --format 'CONTAINER\\t{{json .ID}}`, `docker stats --no-stream --no-trunc --format 'STAT\\t{{json .ID}}`, "ParseProxmoxGuestDockerInventoryVMIDs", @@ -908,6 +912,9 @@ func TestProxmoxGuestDockerInventoryUsesCanonicalReportIngestPath(t *testing.T) t.Fatalf("%s must contain %q", file, snippet) } } + if file == "docker_detection.go" && strings.Contains(source, "&& echo yes || echo no") { + t.Fatalf("%s must not mask host-side pct exec failures with a host-shell echo fallback", file) + } } } diff --git a/internal/monitoring/docker_detection.go b/internal/monitoring/docker_detection.go index 2d80daade..962ce7ea5 100644 --- a/internal/monitoring/docker_detection.go +++ b/internal/monitoring/docker_detection.go @@ -15,7 +15,11 @@ import ( "github.com/rs/zerolog/log" ) -const proxmoxGuestDockerInventoryMarker = "PULSE_DOCKER_GUEST_INVENTORY_V1" +const ( + proxmoxGuestDockerInventoryMarker = "PULSE_DOCKER_GUEST_INVENTORY_V1" + proxmoxGuestDockerSocketMarker = "PULSE_DOCKER_SOCKET" + proxmoxGuestDockerNegativeRecheckAfter = 5 * time.Minute +) // DockerChecker provides the ability to check for Docker inside LXC containers. // This is typically implemented by wrapping the agentexec.Server. @@ -157,6 +161,14 @@ func (m *Monitor) containerNeedsDockerCheck(ct models.Container, previousContain return "first_check" } + // Negative Docker checks can be transient during agent command enrollment, + // Proxmox token rotation, or Docker daemon startup. Recheck them on a short + // cadence so a single false result does not hide Docker-in-LXC inventory + // until the guest restarts. + if !prev.HasDocker && time.Since(prev.DockerCheckedAt) >= proxmoxGuestDockerNegativeRecheckAfter { + return "negative_cache_expired" + } + // Container restarted - uptime is less than before // (This catches containers that were stopped and started again) if ct.Uptime < prev.Uptime && prev.Uptime > 0 { @@ -402,36 +414,39 @@ func NewAgentDockerChecker(executeCommand func(ctx context.Context, hostname str } // CheckDockerInContainer checks if Docker is installed inside an LXC container -// by running `pct exec -- test -S /var/run/docker.sock` on the Proxmox node. +// by running a socket probe inside the guest through the owning Proxmox node. func (c *AgentDockerChecker) CheckDockerInContainer(ctx context.Context, node string, vmid int) (bool, error) { if c.executeCommand == nil { return false, fmt.Errorf("no command executor configured") } - // Check for Docker socket - this is the most reliable indicator - // We use test -S which checks if the file exists and is a socket - cmd := fmt.Sprintf("pct exec %d -- test -S /var/run/docker.sock && echo yes || echo no", vmid) + probe := fmt.Sprintf("if test -S /var/run/docker.sock; then printf '%s\\tyes\\n'; else printf '%s\\tno\\n'; fi", proxmoxGuestDockerSocketMarker, proxmoxGuestDockerSocketMarker) + cmd := fmt.Sprintf("pct exec %d -- sh -c %s", vmid, shellSingleQuote(probe)) stdout, exitCode, err := c.executeCommand(ctx, node, cmd, 10) // 10 second timeout if err != nil { return false, fmt.Errorf("command execution failed: %w", err) } - // The test command itself might fail if container is not accessible - // but our echo fallback should always give us output - stdout = strings.TrimSpace(stdout) - - // If we got "yes", Docker socket exists - if strings.Contains(stdout, "yes") { - return true, nil + output := strings.TrimSpace(stdout) + if exitCode != 0 { + return false, fmt.Errorf("container docker socket probe failed (exit code %d): %s", exitCode, output) } - // If exit code is non-zero and we didn't get "no", the container might not be accessible - if exitCode != 0 && !strings.Contains(stdout, "no") { - return false, fmt.Errorf("container not accessible (exit code %d): %s", exitCode, stdout) + for _, rawLine := range strings.Split(output, "\n") { + fields := strings.Split(strings.TrimSpace(rawLine), "\t") + if len(fields) != 2 || fields[0] != proxmoxGuestDockerSocketMarker { + continue + } + switch strings.ToLower(strings.TrimSpace(fields[1])) { + case "yes": + return true, nil + case "no": + return false, nil + } } - return false, nil + return false, fmt.Errorf("docker socket probe output missing %s marker: %s", proxmoxGuestDockerSocketMarker, output) } // AgentDockerInventoryCollector implements DockerInventoryCollector using the diff --git a/internal/monitoring/docker_detection_test.go b/internal/monitoring/docker_detection_test.go index 57dd1ffc3..5a852a25f 100644 --- a/internal/monitoring/docker_detection_test.go +++ b/internal/monitoring/docker_detection_test.go @@ -277,15 +277,15 @@ func TestAgentDockerChecker_ParsesOutput(t *testing.T) { }{ { name: "docker socket exists", - stdout: "yes\n", + stdout: proxmoxGuestDockerSocketMarker + "\tyes\n", exitCode: 0, wantDocker: true, wantErr: false, }, { name: "docker socket does not exist", - stdout: "no\n", - exitCode: 1, + stdout: proxmoxGuestDockerSocketMarker + "\tno\n", + exitCode: 0, wantDocker: false, wantErr: false, }, @@ -296,6 +296,20 @@ func TestAgentDockerChecker_ParsesOutput(t *testing.T) { wantDocker: false, wantErr: true, }, + { + name: "host-side pct failure is not cached as no docker", + stdout: "no\npct: CT 101 is locked", + exitCode: 255, + wantDocker: false, + wantErr: true, + }, + { + name: "missing marker is an error", + stdout: "no\n", + exitCode: 0, + wantDocker: false, + wantErr: true, + }, } for _, tt := range tests { @@ -319,6 +333,28 @@ func TestAgentDockerChecker_ParsesOutput(t *testing.T) { } } +func TestAgentDockerChecker_ProbeRunsInsideContainer(t *testing.T) { + var gotCommand string + checker := NewAgentDockerChecker(func(ctx context.Context, hostname string, command string, timeout int) (string, int, error) { + gotCommand = command + return proxmoxGuestDockerSocketMarker + "\tyes\n", 0, nil + }) + + hasDocker, err := checker.CheckDockerInContainer(context.Background(), "node1", 101) + if err != nil { + t.Fatalf("CheckDockerInContainer returned error: %v", err) + } + if !hasDocker { + t.Fatal("expected Docker to be detected") + } + if !strings.Contains(gotCommand, "pct exec 101 -- sh -c") { + t.Fatalf("expected probe to execute inside the LXC, got %q", gotCommand) + } + if strings.Contains(gotCommand, "&& echo yes || echo no") { + t.Fatalf("host-side shell fallback must not mask pct exec failures: %q", gotCommand) + } +} + func TestAgentDockerInventoryCollector_MinimalCommandAndReport(t *testing.T) { var gotCommand string collector := NewAgentDockerInventoryCollector(func(ctx context.Context, hostname string, command string, timeout int) (string, int, error) { @@ -820,6 +856,48 @@ func TestContainerNeedsDockerCheck(t *testing.T) { }, wantReason: "started", }, + { + name: "recheck expired negative docker result", + container: models.Container{ID: "ct-1", Status: "running", Uptime: 7200}, + previous: map[string]models.Container{ + "ct-1": { + ID: "ct-1", + Status: "running", + Uptime: 3600, + HasDocker: false, + DockerCheckedAt: time.Now().Add(-proxmoxGuestDockerNegativeRecheckAfter - time.Second), + }, + }, + wantReason: "negative_cache_expired", + }, + { + name: "keep fresh negative docker result", + container: models.Container{ID: "ct-1", Status: "running", Uptime: 7200}, + previous: map[string]models.Container{ + "ct-1": { + ID: "ct-1", + Status: "running", + Uptime: 3600, + HasDocker: false, + DockerCheckedAt: time.Now().Add(-proxmoxGuestDockerNegativeRecheckAfter + time.Second), + }, + }, + wantReason: "", + }, + { + name: "keep positive docker result", + container: models.Container{ID: "ct-1", Status: "running", Uptime: 7200}, + previous: map[string]models.Container{ + "ct-1": { + ID: "ct-1", + Status: "running", + Uptime: 3600, + HasDocker: true, + DockerCheckedAt: time.Now().Add(-proxmoxGuestDockerNegativeRecheckAfter - time.Hour), + }, + }, + wantReason: "", + }, { name: "no check needed - same state", container: models.Container{ID: "ct-1", Status: "running", Uptime: 7200}, diff --git a/scripts/hot-dev-bg.sh b/scripts/hot-dev-bg.sh index 3aeb47f01..183e99959 100755 --- a/scripts/hot-dev-bg.sh +++ b/scripts/hot-dev-bg.sh @@ -568,6 +568,10 @@ start_hot_dev_child() { PULSE_DEV_API_PORT="${PULSE_DEV_API_PORT}" \ PULSE_DEV_API_URL="${PULSE_DEV_API_URL}" \ PULSE_DEV_WS_URL="${PULSE_DEV_WS_URL}" \ + LOG_LEVEL="${LOG_LEVEL:-}" \ + PULSE_ENABLE_PROXMOX_GUEST_DOCKER_DETECTION="${PULSE_ENABLE_PROXMOX_GUEST_DOCKER_DETECTION:-}" \ + PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY="${PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY:-}" \ + PULSE_PROXMOX_GUEST_DOCKER_INVENTORY_VMIDS="${PULSE_PROXMOX_GUEST_DOCKER_INVENTORY_VMIDS:-}" \ HOT_DEV_SKIP_NPM_CLEANUP=true \ ROOT_DIR="${ROOT_DIR}" \ python3 - <<'PY' & @@ -648,6 +652,10 @@ spawn_detached_supervisor() { PULSE_DEV_API_PORT="${PULSE_DEV_API_PORT}" \ PULSE_DEV_API_URL="${PULSE_DEV_API_URL}" \ PULSE_DEV_WS_URL="${PULSE_DEV_WS_URL}" \ + LOG_LEVEL="${LOG_LEVEL:-}" \ + PULSE_ENABLE_PROXMOX_GUEST_DOCKER_DETECTION="${PULSE_ENABLE_PROXMOX_GUEST_DOCKER_DETECTION:-}" \ + PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY="${PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY:-}" \ + PULSE_PROXMOX_GUEST_DOCKER_INVENTORY_VMIDS="${PULSE_PROXMOX_GUEST_DOCKER_INVENTORY_VMIDS:-}" \ ROOT_DIR="${ROOT_DIR}" \ LOG_FILE="${LOG_FILE}" \ HOT_DEV_BG_SCRIPT="${ROOT_DIR}/scripts/hot-dev-bg.sh" \ @@ -1143,6 +1151,9 @@ main() { ;; stop) stop_bg ;; restart) + if [[ "${takeover}" == "true" ]]; then + stop_launchd_hot_dev_job + fi stop_bg start_bg "${takeover}" ;; diff --git a/scripts/hot-dev.sh b/scripts/hot-dev.sh index 9ddaa84f6..eff6e56b5 100755 --- a/scripts/hot-dev.sh +++ b/scripts/hot-dev.sh @@ -357,6 +357,7 @@ PULSE_DEV_DISABLE_BACKGROUND_AI="${PULSE_DEV_DISABLE_BACKGROUND_AI:-true}" PULSE_AUTH_USER="$(hot_dev_resolve_auth_user)" PULSE_AUTH_PASS="$(hot_dev_resolve_auth_pass)" export FRONTEND_PORT PULSE_DEV_API_PORT PORT PULSE_DEV ALLOW_ADMIN_BYPASS PULSE_AUTH_USER PULSE_AUTH_PASS LOG_LEVEL PULSE_DEV_DISABLE_BACKGROUND_AI +export PULSE_ENABLE_PROXMOX_GUEST_DOCKER_DETECTION PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY PULSE_PROXMOX_GUEST_DOCKER_INVENTORY_VMIDS # Data Directory Setup # Mock and real mode use SEPARATE data directories so switching between them @@ -496,6 +497,9 @@ start_backend_process() { PULSE_DEV="${PULSE_DEV:-true}" \ PULSE_AUTH_USER="${PULSE_AUTH_USER:-}" \ PULSE_AUTH_PASS="${PULSE_AUTH_PASS:-}" \ + PULSE_ENABLE_PROXMOX_GUEST_DOCKER_DETECTION="${PULSE_ENABLE_PROXMOX_GUEST_DOCKER_DETECTION:-}" \ + PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY="${PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY:-}" \ + PULSE_PROXMOX_GUEST_DOCKER_INVENTORY_VMIDS="${PULSE_PROXMOX_GUEST_DOCKER_INVENTORY_VMIDS:-}" \ ALLOWED_ORIGINS="${ALLOWED_ORIGINS:-}" \ LOG_FILE="${BACKEND_DEBUG_LOG}" \ LOG_MAX_SIZE="50" \ diff --git a/scripts/install.sh b/scripts/install.sh index b326284cb..3ffcacda8 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -701,30 +701,36 @@ ensure_freebsd_agent_enabled() { } render_systemd_agent_unit() { - local unit_path="$1" - local exec_path="$2" - local exec_args="$3" - local after_targets="$4" + local unit_path="$1" + local exec_path="$2" + local exec_args="$3" + local after_targets="$4" local wants_targets="$5" local run_as_user="$6" local log_target="$7" local env_line="" local wants_line="" - local user_line="" - local log_lines="" + local user_line="" + local log_lines="" + local no_new_privileges="true" + local restrict_suidsgid="true" - env_line="$SYSTEMD_ENV_LINES" - if [[ -n "$wants_targets" ]]; then - wants_line=$'\n'"Wants=${wants_targets}" - fi + env_line="$SYSTEMD_ENV_LINES" + if [[ -n "$wants_targets" ]]; then + wants_line=$'\n'"Wants=${wants_targets}" + fi if [[ -n "$run_as_user" ]]; then user_line=$'\n'"User=${run_as_user}" fi - if [[ -n "$log_target" ]]; then - log_lines=$'\n'"StandardOutput=append:${log_target}"$'\n'"StandardError=append:${log_target}" - fi + if [[ -n "$log_target" ]]; then + log_lines=$'\n'"StandardOutput=append:${log_target}"$'\n'"StandardError=append:${log_target}" + fi + if systemd_agent_requires_lxc_attach; then + no_new_privileges="false" + restrict_suidsgid="false" + fi - cat > "$unit_path" < "$unit_path" <