Block hosted GA on production storage pressure

This commit is contained in:
rcourtman 2026-04-23 23:34:14 +01:00
parent 0f9437227f
commit 0a8d3586c6
10 changed files with 222 additions and 6 deletions

View file

@ -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.

View file

@ -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"
}
]
},

View file

@ -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

View file

@ -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

View file

@ -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")
}

View file

@ -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) {

View file

@ -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",

View file

@ -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()

View file

@ -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")

View file

@ -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) {