diff --git a/docs/release-control/v6/internal/records/cloud-hosted-tier-runtime-readiness-storage-blocker-2026-04-23.md b/docs/release-control/v6/internal/records/cloud-hosted-tier-runtime-readiness-storage-blocker-2026-04-23.md new file mode 100644 index 000000000..32f3aeeac --- /dev/null +++ b/docs/release-control/v6/internal/records/cloud-hosted-tier-runtime-readiness-storage-blocker-2026-04-23.md @@ -0,0 +1,104 @@ +# Cloud Hosted Tier Runtime Readiness Storage Blocker + +- Date: `2026-04-23` +- Gate: `cloud-hosted-tier-runtime-readiness` +- Assertion: `RA11` +- Result: `blocked` +- Environment: + - Live control plane: `https://cloud.pulserelay.pro` + - Host: `root@pulse-cloud` + - Registry DB: `/data/control-plane/tenants.db` + - Tenant runtime root: `/data/tenants` + +## Blocking Facts + +1. A fresh live MSP rehearsal was intentionally stopped before creating the + canary account or workspaces because the production host root filesystem was + full: + - `/dev/vda1` mounted at `/` reported `154G` used of `154G` + - inode usage was only `6%`, so this is block exhaustion rather than inode + exhaustion +2. The space pressure is dominated by runtime/build retention, not tenant data: + - `/var/lib/containerd`: `109G` + - `/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs`: `99G` + - `/var/lib/containerd/io.containerd.content.v1.content`: `10G` + - `/var/lib/docker`: `8.5G` + - `/var/lib/docker/containers`: `8.1G` + - `/tmp`: `34G` + - `/data`: `846M` +3. Docker reported substantial reclaimable runtime/build state: + - images: `116.2GB` total, `109.9GB` reclaimable + - containers: `52.36GB` total, `6.828GB` reclaimable + - build cache: `21.28GB` total, `15.48GB` private/reclaimable +4. The live control-plane registry currently carries a standing hosted fleet + rather than an empty production system: + - tenant states: `77 active`, `3 canceled`, `4 deleted`, `1 suspended` + - accounts: `97` total (`93 individual`, `4 msp`) + - active plan distribution included `50` `v5_pro_annual_grandfathered`, + `13` `msp_starter`, `10` `v5_pro_monthly_grandfathered`, and `4` + `cloud_starter` +5. Docker runtime state showed `93` containers, `83` running, and `81` unhealthy. + Sampled tenant health checks were failing with: + - `OCI runtime exec failed: write /tmp/runc-process...: no space left on device` +6. Docker host-level retention policy is absent: + - `/etc/docker/daemon.json` was not present + - tenant containers use Docker's default `json-file` log driver with no + `max-size` or `max-file` + - no Docker/container prune timer was present in `systemctl list-timers` +7. Container JSON logs are already materially contributing to the issue: + individual tenant log files were sampled at roughly `140M`-`185M`, with no + configured rotation. +8. Historical build artifacts remain on the production host under `/tmp`, with + old Pulse build/source directories contributing tens of gigabytes, including + multi-gigabyte directories from March 2026. + +## Why The Gate Cannot Be Treated As Passed + +The previously recorded hosted production proofs remain valuable point-in-time +functional evidence, but the current live production environment no longer +meets the operational floor needed for GA. A customer-facing hosted tier cannot +be called ready while tenant health checks fail from disk exhaustion, old proof +tenants remain as a standing production fleet, and the host has no Docker +retention or log-bounding policy. + +This also blocks the requested fresh MSP production rehearsal. Running new +workspace provisioning into a full root filesystem would produce misleading +evidence and risk additional production damage. + +## Required Unblock Steps + +1. Immediate live-host containment, with explicit destructive-action approval: + - remove stale Pulse build/source directories from `/tmp` + - remove stopped pre-rollout tenant containers + - prune old build cache and unused images + - classify the active hosted proof/canary tenant fleet before deprovisioning + or retaining any tenant +2. Add a production retention policy: + - Docker log rotation for tenant containers + - scheduled Docker/containerd build-cache and unused-image cleanup + - documented canary/proof tenant lifecycle and cleanup ownership +3. Add production storage guardrails: + - disk pressure monitoring and alerting for `/`, `/var/lib/containerd`, + `/var/lib/docker`, `/tmp`, and `/data` + - provisioning admission should fail closed before creating a tenant when + required runtime storage is below the safe threshold +4. Move live release builds away from persistent production `/tmp` state and + favor digest-pinned images built outside the production runtime host. +5. After cleanup and guardrails are in place, rerun the fresh production MSP + rehearsal and hosted-runtime proof from a new canary. + +## Conclusion + +`cloud-hosted-tier-runtime-readiness` is blocked again as of `2026-04-23`. +The code-level MSP readiness fixes are still useful, but the live hosted +production environment is not GA-ready until this storage and retention issue is +fixed and re-proven. + +## Immediate Repo Containment + +The control-plane Docker manager now creates new tenant runtime containers with +bounded `json-file` logs (`CP_TENANT_LOG_MAX_SIZE`, default `10m`, and +`CP_TENANT_LOG_MAX_FILE`, default `3`). This prevents future tenant containers +from accumulating unbounded Docker JSON logs after the fix is deployed and +tenant containers are recreated, but it does not reclaim the existing production +host or replace the required live-host cleanup steps above. diff --git a/docs/release-control/v6/internal/status.json b/docs/release-control/v6/internal/status.json index 1c4ee6ddb..6ff904fee 100644 --- a/docs/release-control/v6/internal/status.json +++ b/docs/release-control/v6/internal/status.json @@ -915,6 +915,11 @@ "path": "docs/release-control/v6/internal/records/cloud-hosted-tier-runtime-readiness-production-recovered-2026-03-26.md", "kind": "file" }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/cloud-hosted-tier-runtime-readiness-storage-blocker-2026-04-23.md", + "kind": "file" + }, { "repo": "pulse", "path": "docs/release-control/v6/internal/subsystems/cloud-paid.md", @@ -3434,7 +3439,7 @@ "owner": "project-owner", "blocking_level": "rc-ready", "minimum_evidence_tier": "real-external-e2e", - "status": "passed", + "status": "blocked", "verification_doc": "docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md", "lane_ids": [ "L3", @@ -3478,6 +3483,12 @@ "path": "docs/release-control/v6/internal/records/cloud-hosted-tier-runtime-readiness-production-recovered-2026-03-26.md", "kind": "file", "evidence_tier": "real-external-e2e" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/cloud-hosted-tier-runtime-readiness-storage-blocker-2026-04-23.md", + "kind": "file", + "evidence_tier": "real-external-e2e" } ] }, diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index 89e4dff85..a90c03c35 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -137,6 +137,9 @@ cloud-specific enforcement rules. 7. `internal/cloudcp/auth/magiclink_store.go` shared with `security-privacy`: control-plane magic-link persistence is both a Pulse Cloud account-access boundary and a security/privacy storage-hardening boundary. 8. `internal/cloudcp/docker/labels.go` shared with `deployment-installability`: hosted tenant Docker labels are both a Pulse Cloud runtime contract boundary and a deployment-installability rollout boundary. 9. `internal/cloudcp/docker/manager.go` shared with `deployment-installability`: hosted tenant container management is both a Pulse Cloud runtime contract boundary and a deployment-installability rollout boundary. + Hosted tenant container creation must also bound Docker `json-file` logs + through the control-plane Docker manager so tenant runtime logging cannot + fill the live Pulse Cloud host independently of tenant data quotas. 10. `internal/cloudcp/tenant_runtime_rollout.go` shared with `deployment-installability`: hosted tenant runtime rollout is both a Pulse Cloud runtime contract boundary and a deployment-installability release-rollout boundary. The real `pulse-pro` license-server legacy checkout issuance, recurring @@ -168,7 +171,10 @@ Community limit enforcement. 2. Add or change hosted entitlement issuance through `internal/cloudcp/entitlements/service.go` 3. Add or change control-plane plan storage through `internal/cloudcp/registry/models.go` and `internal/cloudcp/registry/registry.go` 4. Add or change MSP account-scoped workspace provisioning entry handlers through `internal/cloudcp/account/tenant_handlers.go` -5. Add or change public cloud self-serve signup price configuration or checkout gating through `internal/cloudcp/config.go` and `internal/cloudcp/public_cloud_signup_handlers.go` +5. Add or change public cloud self-serve signup price configuration, + tenant-runtime capacity/log retention configuration, or checkout gating + through `internal/cloudcp/config.go` and + `internal/cloudcp/public_cloud_signup_handlers.go` 6. Add or change the hosted account portal API, Pulse Account access/auth/session handling, task-first browser shell, maintained portal frontend/bundle, or account-scoped workspace/access/billing handoff through `internal/cloudcp/account/audit.go`, `internal/cloudcp/account/handlers.go`, `internal/cloudcp/auth/handlers.go`, `internal/cloudcp/auth/session.go`, `internal/cloudcp/portal/`, and `internal/cloudcp/routes.go` That same customer-entry boundary owns the canonical hosted Cloud handoff: public Cloud entry, secure checkout return, and returning-customer sign-in diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 60bbdea95..0a37fb40d 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -100,6 +100,9 @@ server-side update execution surfaces. 2. `internal/api/updates.go` shared with `api-contracts`: update handlers are both a deployment-installability control surface and a canonical API payload contract boundary. 3. `internal/cloudcp/docker/labels.go` shared with `cloud-paid`: hosted tenant Docker labels are both a Pulse Cloud runtime contract boundary and a deployment-installability rollout boundary. 4. `internal/cloudcp/docker/manager.go` shared with `cloud-paid`: hosted tenant container management is both a Pulse Cloud runtime contract boundary and a deployment-installability rollout boundary. + Tenant runtime containers must be created with bounded Docker `json-file` + logging so rollout and canary fleets cannot consume unbounded production + host storage while they remain running. 5. `internal/cloudcp/tenant_runtime_rollout.go` shared with `cloud-paid`: hosted tenant runtime rollout is both a Pulse Cloud runtime contract boundary and a deployment-installability release-rollout boundary. 6. `scripts/install.ps1` shared with `agent-lifecycle`: the Windows installer is both a deployment installability entry point and a canonical agent lifecycle runtime continuity boundary. 7. `scripts/install.sh` shared with `agent-lifecycle`: the shell installer is both a deployment installability entry point and a canonical agent lifecycle runtime continuity boundary. @@ -107,7 +110,7 @@ server-side update execution surfaces. ## Extension Points 1. Add or change deployment-type detection, update planning, or apply behavior through `internal/updates/` -2. Add or change release-build metadata injection, Docker build-context allowlists, release artifact assembly, governed promotion metadata resolution, the canonical version file, operator-facing release packet content, prerelease feedback intake wording, historical published-release integrity backfill, or the canonical in-repo v6 upgrade guide through `scripts/build-release.sh`, `scripts/release_asset_common.sh`, `scripts/backfill-release-assets.sh`, `scripts/release_ldflags.sh`, `scripts/check-workflow-dispatch-inputs.py`, `scripts/release_control/render_release_body.py`, `scripts/release_control/resolve_release_promotion.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`, `.dockerignore`, `Dockerfile`, `.github/ISSUE_TEMPLATE/v6_rc_feedback.yml`, `docs/RELEASE_NOTES.md`, `docs/releases/`, `docs/UPGRADE_v6.md`, `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/validate-release.sh`, `scripts/validate-published-release.sh`, the operator dispatch helpers `scripts/trigger-release.sh` and `scripts/trigger-release-dry-run.sh`, and the governed release workflows `.github/workflows/backfill-release-assets.yml`, `.github/workflows/create-release.yml`, `.github/workflows/deploy-demo-server.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`, and `.github/workflows/update-demo-server.yml` +2. Add or change release-build metadata injection, Docker build-context allowlists, release artifact assembly, governed promotion metadata resolution, the canonical version file, operator-facing release packet content, prerelease feedback intake wording, historical published-release integrity backfill, download endpoint checksum/signature header proof, or the canonical in-repo v6 upgrade guide through `scripts/build-release.sh`, `scripts/release_asset_common.sh`, `scripts/backfill-release-assets.sh`, `scripts/release_ldflags.sh`, `scripts/check-workflow-dispatch-inputs.py`, `scripts/release_control/render_release_body.py`, `scripts/release_control/resolve_release_promotion.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`, `.dockerignore`, `Dockerfile`, `.github/ISSUE_TEMPLATE/v6_rc_feedback.yml`, `docs/RELEASE_NOTES.md`, `docs/releases/`, `docs/UPGRADE_v6.md`, `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/validate-release.sh`, `scripts/validate-published-release.sh`, the operator dispatch helpers `scripts/trigger-release.sh` and `scripts/trigger-release-dry-run.sh`, and the governed release workflows `.github/workflows/backfill-release-assets.yml`, `.github/workflows/create-release.yml`, `.github/workflows/deploy-demo-server.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`, and `.github/workflows/update-demo-server.yml` 3. Add or change shell installer, Docker bootstrap installer, Windows installer, container-agent installer, repo-root compose defaults, or auto-update script behavior through `scripts/install.sh`, `scripts/install-docker.sh`, `scripts/install.ps1`, `scripts/install-container-agent.sh`, `docker-compose.yml`, and `scripts/pulse-auto-update.sh` 4. Add or change server update transport through `internal/api/updates.go` and `frontend-modern/src/api/updates.ts` 5. Add or change local dev-runtime orchestration, managed ownership, browser-runtime proof wiring, frontend/backend coherence diagnostics, canonical developer entry wrappers, dependency manifest floors, frontend build chunking, or dev-runtime helper control surfaces through `scripts/hot-dev.sh`, `scripts/hot-dev-bg.sh`, `scripts/dev-deploy-agent.sh`, `Makefile`, `package.json`, `package-lock.json`, `frontend-modern/package.json`, `frontend-modern/package-lock.json`, `frontend-modern/vite.config.ts`, `go.mod`, `go.sum`, `scripts/dev-check.sh`, `scripts/toggle-mock.sh`, `scripts/clean-mock-alerts.sh`, `scripts/dev-launchd-setup.sh`, `scripts/dev-launchd-wrapper.sh`, `scripts/run_demo_public_browser_smoke.sh`, `scripts/demo_public_browser_smoke.cjs`, `scripts/com.pulse.hot-dev.plist.template`, `tests/integration/scripts/managed-dev-runtime.mjs`, `tests/integration/playwright.config.ts`, `tests/integration/tests/helpers.ts`, `tests/integration/tests/runtime-defaults.ts`, `tests/integration/README.md`, and `tests/integration/QUICK_START.md` @@ -175,12 +178,20 @@ server-side update execution surfaces. External helper binaries fetched by governed release workflows are part of the same supply-chain boundary and must be checksum-verified before they are executed. + Release validation must prove that installer script download endpoints return + signature headers, and unified-agent download endpoints must return checksum and signature headers whose checksum value matches the served binary. 8. Add or change the non-secret Pulse Cloud public signup route smoke through `scripts/run_cloud_public_signup_smoke.sh`. That smoke must prove either the open signup route contract or the intentionally closed redirect contract, and valid magic-link probes must remain opt-in so routine public checks do not send email accidentally. -9. Add or change operator-facing hosted tenant runtime canary rollout, batch runtime contract reconciliation, canonical hosted route/public URL generation, or control-plane runtime-registry reconciliation through `cmd/pulse-control-plane/main.go`, `internal/cloudcp/docker/manager.go`, `internal/cloudcp/docker/labels.go`, and `internal/cloudcp/tenant_runtime_rollout.go` +9. Add or change operator-facing hosted tenant runtime canary rollout, tenant + runtime container log-retention bounds, batch runtime contract + reconciliation, canonical hosted route/public URL generation, or + control-plane runtime-registry reconciliation through + `cmd/pulse-control-plane/main.go`, `internal/cloudcp/docker/manager.go`, + `internal/cloudcp/docker/labels.go`, and + `internal/cloudcp/tenant_runtime_rollout.go` 10. Add or change the canonical hosted staging smoke operator path through `scripts/run_hosted_staging_smoke.sh`, `tests/integration/scripts/bootstrap-hosted-mobile-onboarding.mjs`, `tests/integration/scripts/hosted-mobile-token-runtime.mjs`, `tests/integration/scripts/hosted-tenant-runtime.mjs`, and `tests/integration/scripts/relay-mobile-token-helper.go` ## Forbidden Paths diff --git a/internal/cloudcp/config.go b/internal/cloudcp/config.go index b84acf2c5..b8c359aa8 100644 --- a/internal/cloudcp/config.go +++ b/internal/cloudcp/config.go @@ -35,6 +35,8 @@ type CPConfig struct { TrustedProxyCIDRs []string TenantMemoryLimit int64 // bytes TenantCPUShares int64 + TenantLogMaxSize string + TenantLogMaxFile int AllowDockerlessProvisioning bool StripeWebhookSecret string StripeAPIKey string @@ -79,6 +81,10 @@ func LoadConfig() (*CPConfig, error) { if err != nil { return nil, err } + tenantLogMaxFile, err := envOrDefaultInt("CP_TENANT_LOG_MAX_FILE", 3) + if err != nil { + return nil, err + } webhookRPS, err := envOrDefaultInt("CP_RL_WEBHOOK_PER_MINUTE", 120) if err != nil { return nil, err @@ -124,6 +130,8 @@ func LoadConfig() (*CPConfig, error) { TrustedProxyCIDRs: parseTrustedProxyCIDRValues("CP_TRUSTED_PROXY_CIDRS", "PULSE_TRUSTED_PROXY_CIDRS"), TenantMemoryLimit: tenantMemoryLimit, TenantCPUShares: tenantCPUShares, + TenantLogMaxSize: envOrDefault("CP_TENANT_LOG_MAX_SIZE", "10m"), + TenantLogMaxFile: tenantLogMaxFile, AllowDockerlessProvisioning: envOrDefaultBool("CP_ALLOW_DOCKERLESS_PROVISIONING", false), StripeWebhookSecret: strings.TrimSpace(os.Getenv("STRIPE_WEBHOOK_SECRET")), StripeAPIKey: strings.TrimSpace(os.Getenv("STRIPE_API_KEY")), @@ -188,6 +196,12 @@ func (c *CPConfig) validate() error { if c.TenantCPUShares <= 0 { return fmt.Errorf("CP_TENANT_CPU_SHARES must be greater than 0, got %d", c.TenantCPUShares) } + if strings.TrimSpace(c.TenantLogMaxSize) == "" { + return fmt.Errorf("CP_TENANT_LOG_MAX_SIZE must not be empty") + } + if c.TenantLogMaxFile <= 0 { + return fmt.Errorf("CP_TENANT_LOG_MAX_FILE must be greater than 0") + } if c.WebhookRateLimitPerMinute <= 0 { return fmt.Errorf("CP_RL_WEBHOOK_PER_MINUTE must be greater than 0") } diff --git a/internal/cloudcp/config_test.go b/internal/cloudcp/config_test.go index 532ed31da..6096a871f 100644 --- a/internal/cloudcp/config_test.go +++ b/internal/cloudcp/config_test.go @@ -67,6 +67,12 @@ func TestLoadConfig_AllRequired(t *testing.T) { if cfg.BindAddress != "0.0.0.0" { t.Errorf("BindAddress = %q, want 0.0.0.0", cfg.BindAddress) } + if cfg.TenantLogMaxSize != "10m" { + t.Errorf("TenantLogMaxSize = %q, want 10m", cfg.TenantLogMaxSize) + } + if cfg.TenantLogMaxFile != 3 { + t.Errorf("TenantLogMaxFile = %d, want 3", cfg.TenantLogMaxFile) + } } func TestLoadConfig_CustomValues(t *testing.T) { @@ -78,6 +84,8 @@ func TestLoadConfig_CustomValues(t *testing.T) { t.Setenv("CP_PORT", "9000") t.Setenv("CP_DATA_DIR", "/custom/data") t.Setenv("CP_BIND_ADDRESS", "127.0.0.1") + t.Setenv("CP_TENANT_LOG_MAX_SIZE", "25m") + t.Setenv("CP_TENANT_LOG_MAX_FILE", "4") cfg, err := LoadConfig() if err != nil { @@ -92,6 +100,12 @@ func TestLoadConfig_CustomValues(t *testing.T) { if cfg.BindAddress != "127.0.0.1" { t.Errorf("BindAddress = %q", cfg.BindAddress) } + if cfg.TenantLogMaxSize != "25m" { + t.Errorf("TenantLogMaxSize = %q", cfg.TenantLogMaxSize) + } + if cfg.TenantLogMaxFile != 4 { + t.Errorf("TenantLogMaxFile = %d, want 4", cfg.TenantLogMaxFile) + } } func TestLoadConfig_DerivesTrialActivationPublicKey(t *testing.T) { diff --git a/internal/cloudcp/docker/manager.go b/internal/cloudcp/docker/manager.go index 060150c47..fe31113ff 100644 --- a/internal/cloudcp/docker/manager.go +++ b/internal/cloudcp/docker/manager.go @@ -29,6 +29,8 @@ type ManagerConfig struct { TrustedProxyCIDRs []string MemoryLimit int64 // bytes CPUShares int64 + TenantLogMaxSize string + TenantLogMaxFile int ContainerPort int // port inside the container (default 7655) } @@ -53,8 +55,10 @@ type RuntimeContainerInfo struct { const immutableOwnershipPathsEnv = "PULSE_IMMUTABLE_OWNERSHIP_PATHS" const ( - tenantRuntimeUID = 1000 - tenantRuntimeGID = 1000 + tenantRuntimeUID = 1000 + tenantRuntimeGID = 1000 + defaultTenantLogMaxSize = "10m" + defaultTenantLogMaxFile = 3 ) // NewManager creates a Docker manager connected to the local daemon. @@ -124,6 +128,7 @@ func (m *Manager) CreateAndStart(ctx context.Context, tenantID, tenantDataDir st }, HostConfig: &container.HostConfig{ RestartPolicy: container.RestartPolicy{Name: "unless-stopped"}, + LogConfig: tenantRuntimeLogConfig(m.cfg.TenantLogMaxSize, m.cfg.TenantLogMaxFile), Resources: container.Resources{ Memory: m.cfg.MemoryLimit, CPUShares: m.cfg.CPUShares, @@ -154,6 +159,23 @@ func (m *Manager) CreateAndStart(ctx context.Context, tenantID, tenantDataDir st return resp.ID, nil } +func tenantRuntimeLogConfig(maxSize string, maxFile int) container.LogConfig { + maxSize = strings.TrimSpace(maxSize) + if maxSize == "" { + maxSize = defaultTenantLogMaxSize + } + if maxFile <= 0 { + maxFile = defaultTenantLogMaxFile + } + return container.LogConfig{ + Type: "json-file", + Config: map[string]string{ + "max-size": maxSize, + "max-file": fmt.Sprintf("%d", maxFile), + }, + } +} + func tenantImmutableOwnershipPaths() []string { return []string{ "/etc/pulse/secrets/handoff.key", diff --git a/internal/cloudcp/docker/manager_test.go b/internal/cloudcp/docker/manager_test.go index 3edec836c..246c16d26 100644 --- a/internal/cloudcp/docker/manager_test.go +++ b/internal/cloudcp/docker/manager_test.go @@ -146,6 +146,29 @@ func TestTenantEnvOmitsPublicURLWithoutTenantContext(t *testing.T) { } } +func TestTenantRuntimeLogConfigBoundsJSONLogs(t *testing.T) { + t.Parallel() + + got := tenantRuntimeLogConfig("", 0) + if got.Type != "json-file" { + t.Fatalf("LogConfig.Type = %q, want json-file", got.Type) + } + if got.Config["max-size"] != defaultTenantLogMaxSize { + t.Fatalf("max-size = %q, want %q", got.Config["max-size"], defaultTenantLogMaxSize) + } + if got.Config["max-file"] != "3" { + t.Fatalf("max-file = %q, want 3", got.Config["max-file"]) + } + + custom := tenantRuntimeLogConfig("25m", 4) + if custom.Config["max-size"] != "25m" { + t.Fatalf("custom max-size = %q, want 25m", custom.Config["max-size"]) + } + if custom.Config["max-file"] != "4" { + t.Fatalf("custom max-file = %q, want 4", custom.Config["max-file"]) + } +} + func TestCanonicalTrustedProxyCIDR(t *testing.T) { t.Parallel() diff --git a/internal/cloudcp/server.go b/internal/cloudcp/server.go index 0570a834c..b8b1d4216 100644 --- a/internal/cloudcp/server.go +++ b/internal/cloudcp/server.go @@ -60,6 +60,8 @@ func Run(ctx context.Context, version string) error { TrustedProxyCIDRs: cfg.TrustedProxyCIDRs, MemoryLimit: cfg.TenantMemoryLimit, CPUShares: cfg.TenantCPUShares, + TenantLogMaxSize: cfg.TenantLogMaxSize, + TenantLogMaxFile: cfg.TenantLogMaxFile, }) if err != nil { log.Warn().Err(err).Msg("Docker unavailable — container management disabled") diff --git a/internal/cloudcp/tenant_runtime_rollout_test.go b/internal/cloudcp/tenant_runtime_rollout_test.go index 15274143c..082f99e22 100644 --- a/internal/cloudcp/tenant_runtime_rollout_test.go +++ b/internal/cloudcp/tenant_runtime_rollout_test.go @@ -121,6 +121,15 @@ func TestTenantRuntimeRollout_RollsForwardCanonically(t *testing.T) { if docker.renameCalls[0].newName != "pulse-t-ROLLFWD.pre-aliasfix" { t.Fatalf("rename target = %q, want pulse-t-ROLLFWD.pre-aliasfix", docker.renameCalls[0].newName) } + if len(docker.createCalls) != 1 { + t.Fatalf("create call count = %d, want 1", len(docker.createCalls)) + } + if docker.createCalls[0].tenantID != tenant.ID { + t.Fatalf("created tenant id = %q, want %q", docker.createCalls[0].tenantID, tenant.ID) + } + if docker.createCalls[0].tenantDataDir != filepath.Join(tTempDirForRolloutService(), tenant.ID) { + t.Fatalf("created tenant data dir = %q", docker.createCalls[0].tenantDataDir) + } } func TestTenantRuntimeRollout_RollsBackOnHealthFailure(t *testing.T) {