Files
pulse/internal/cloudcp/docker/manager.go
T
rcourtman f338bd4765 Judge client health from Docker when status runs out of band
provider-msp status, which upgrade.sh and the install proof gate on,
runs in a one-off `docker compose run` container attached only to the
provider ingress network. Its health check dialled each client's
container on the client's isolated network, which that container cannot
reach, and an unreachable client counts as unhealthy. So on any install
with a real client, status failed with "unhealthy active workspaces" and
./upgrade.sh --dry-run exited 1: the backup-gated upgrade path refused to
run as soon as a provider onboarded anyone. CI proofs never saw it
because they only ever run against an empty install. Reproduced on a
fresh v6.4.1 provider bundle with one client whose container Docker
reported healthy.

The runtime image declares a HEALTHCHECK that probes the same
/api/health inside the container, and Docker records the result where
any caller with the socket can read it. Use that when it exists and keep
the HTTP probe for images without one. With the change the same install
reports healthy_tenants=1 and upgrade.sh --dry-run plans cleanly.
2026-09-23 22:46:47 +01:00

1115 lines
34 KiB
Go

package docker
import (
"context"
"fmt"
"io"
"net/http"
"net/netip"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/containerd/errdefs"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/mount"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/client"
"github.com/rs/zerolog/log"
pkglicensing "github.com/rcourtman/pulse-go-rewrite/pkg/licensing"
)
// ManagerConfig holds Docker manager settings.
type ManagerConfig struct {
Image string
Network string
IsolateTenantNetworks bool
TenantNetworkPrefix string
SupportContainerLabels []string
BaseDomain string
TrialActivationPublicKey string
// ControlPlaneBaseURL is the control plane's own external base URL
// (CP_BASE_URL). It is injected into client runtimes as the hosted
// commercial base URL so entitlement lease refresh targets this
// control plane instead of the built-in Pulse Cloud default.
ControlPlaneBaseURL string
TrustedProxyCIDRs []string
// TenantDisplayName resolves a tenant's workspace display name at
// container-create time; the value is injected as PULSE_TENANT_NAME so
// alert webhook payloads carry a human-readable workspace label instead
// of falling back to the tenant ID. Display-name changes after creation
// apply on the next runtime rollout, which recreates the container with
// freshly resolved env. May be nil.
TenantDisplayName func(tenantID string) string
TenantReportBrand TenantReportBrandConfig
TenantRuntimeUID int
TenantRuntimeGID int
MemoryLimit int64 // bytes
CPUShares int64
TenantLogMaxSize string
TenantLogMaxFile int
ContainerPort int // port inside the container (default 7655)
}
type TenantReportBrandConfig struct {
DisplayName string
LogoPath string
LogoBase64 string
LogoFormat string
}
// Manager orchestrates Docker containers for tenant lifecycle.
type Manager struct {
cli *client.Client
cfg ManagerConfig
}
// RuntimeContainerInfo is the canonical live-container snapshot used by
// operator rollout and reconciliation flows.
type RuntimeContainerInfo struct {
ID string
Name string
ImageRef string
ImageID string
Running bool
RouteHost string
PublicURL string
}
type DiskUsageClass struct {
ActiveCount int64
TotalCount int64
Reclaimable int64
TotalSize int64
}
type DiskUsageSnapshot struct {
Images DiskUsageClass
Containers DiskUsageClass
BuildCache DiskUsageClass
Volumes DiskUsageClass
}
type RuntimeContainerSummary struct {
ID string
Name string
Image string
ImageID string
State string
Status string
HealthStatus string
Created int64
}
type RuntimePrerequisiteOptions struct {
PullImage bool
}
type RuntimePrerequisiteReport struct {
OK bool
DockerReachable bool
NetworkName string
NetworkID string
NetworkOK bool
ImageRef string
ImageID string
ImageAvailable bool
ImagePulled bool
ImagePullRequired bool
Failures []string
}
const immutableOwnershipPathsEnv = "PULSE_IMMUTABLE_OWNERSHIP_PATHS"
const (
tenantRuntimeUID = 1000
tenantRuntimeGID = 1000
defaultTenantLogMaxSize = "10m"
defaultTenantLogMaxFile = 3
)
const (
providerSupportTraefikLabel = "pulse.provider-msp.role=traefik"
providerSupportControlPlaneLabel = "pulse.provider-msp.role=control-plane"
tenantRuntimeNetworkLabel = "pulse.provider-msp.network"
tenantRuntimeNetworkLabelValue = "tenant"
)
// NewManager creates a Docker manager connected to the local daemon.
func NewManager(cfg ManagerConfig) (*Manager, error) {
cli, err := client.New(client.FromEnv)
if err != nil {
return nil, fmt.Errorf("create docker client: %w", err)
}
if cfg.ContainerPort == 0 {
cfg.ContainerPort = 7655
}
cfg.Network = strings.TrimSpace(cfg.Network)
cfg.TenantNetworkPrefix = strings.TrimSpace(cfg.TenantNetworkPrefix)
if cfg.IsolateTenantNetworks {
if cfg.TenantNetworkPrefix == "" {
if cfg.Network != "" {
cfg.TenantNetworkPrefix = cfg.Network + "-tenant"
} else {
cfg.TenantNetworkPrefix = "pulse-tenant"
}
}
if len(cfg.SupportContainerLabels) == 0 {
cfg.SupportContainerLabels = []string{
providerSupportTraefikLabel,
providerSupportControlPlaneLabel,
}
}
}
return &Manager{cli: cli, cfg: cfg}, nil
}
// Close closes the Docker client.
func (m *Manager) Close() error {
if m.cli != nil {
return m.cli.Close()
}
return nil
}
// DesiredRuntimeRouting returns the canonical hosted route/public URL contract
// for a tenant on this manager's configured base domain.
func (m *Manager) DesiredRuntimeRouting(tenantID string) TenantRuntimeRoutingContract {
if m == nil {
return TenantRuntimeRoutingContract{}
}
return CanonicalTenantRuntimeRouting(tenantID, m.cfg.BaseDomain)
}
func (m *Manager) DiskUsage(ctx context.Context) (*DiskUsageSnapshot, error) {
if err := m.ensureDaemonReachable(ctx); err != nil {
return nil, err
}
usage, err := m.cli.DiskUsage(ctx, client.DiskUsageOptions{
Containers: true,
Images: true,
BuildCache: true,
Volumes: true,
})
if err != nil {
return nil, fmt.Errorf("read docker disk usage: %w", err)
}
return &DiskUsageSnapshot{
Images: DiskUsageClass{
ActiveCount: usage.Images.ActiveCount,
TotalCount: usage.Images.TotalCount,
Reclaimable: usage.Images.Reclaimable,
TotalSize: usage.Images.TotalSize,
},
Containers: DiskUsageClass{
ActiveCount: usage.Containers.ActiveCount,
TotalCount: usage.Containers.TotalCount,
Reclaimable: usage.Containers.Reclaimable,
TotalSize: usage.Containers.TotalSize,
},
BuildCache: DiskUsageClass{
ActiveCount: usage.BuildCache.ActiveCount,
TotalCount: usage.BuildCache.TotalCount,
Reclaimable: usage.BuildCache.Reclaimable,
TotalSize: usage.BuildCache.TotalSize,
},
Volumes: DiskUsageClass{
ActiveCount: usage.Volumes.ActiveCount,
TotalCount: usage.Volumes.TotalCount,
Reclaimable: usage.Volumes.Reclaimable,
TotalSize: usage.Volumes.TotalSize,
},
}, nil
}
func (m *Manager) ListManagedRuntimeContainers(ctx context.Context) ([]RuntimeContainerSummary, error) {
if err := m.ensureDaemonReachable(ctx); err != nil {
return nil, err
}
filters := client.Filters{}
filters = filters.Add("label", "pulse.managed=true")
result, err := m.cli.ContainerList(ctx, client.ContainerListOptions{
All: true,
Filters: filters,
})
if err != nil {
return nil, fmt.Errorf("list managed tenant containers: %w", err)
}
containers := make([]RuntimeContainerSummary, 0, len(result.Items))
for _, item := range result.Items {
name := ""
if len(item.Names) > 0 {
name = strings.TrimPrefix(strings.TrimSpace(item.Names[0]), "/")
}
healthStatus := string(container.NoHealthcheck)
if item.Health != nil {
healthStatus = string(item.Health.Status)
}
containers = append(containers, RuntimeContainerSummary{
ID: item.ID,
Name: name,
Image: item.Image,
ImageID: item.ImageID,
State: string(item.State),
Status: item.Status,
HealthStatus: healthStatus,
Created: item.Created,
})
}
return containers, nil
}
func (m *Manager) CheckRuntimePrerequisites(ctx context.Context, opts RuntimePrerequisiteOptions) (*RuntimePrerequisiteReport, error) {
if m == nil {
return &RuntimePrerequisiteReport{
OK: false,
Failures: []string{"docker manager is not initialized"},
}, nil
}
report := &RuntimePrerequisiteReport{
OK: true,
NetworkName: strings.TrimSpace(m.cfg.Network),
ImageRef: strings.TrimSpace(m.cfg.Image),
}
addFailure := func(format string, args ...any) {
report.OK = false
report.Failures = append(report.Failures, fmt.Sprintf(format, args...))
}
if err := m.ensureDaemonReachable(ctx); err != nil {
addFailure("%v", err)
return report, nil
}
report.DockerReachable = true
if report.NetworkName == "" {
addFailure("tenant Docker network is required")
} else {
inspect, err := m.cli.NetworkInspect(ctx, report.NetworkName, client.NetworkInspectOptions{})
if err != nil {
addFailure("inspect tenant Docker network %q: %v", report.NetworkName, err)
} else {
report.NetworkOK = true
report.NetworkID = strings.TrimSpace(inspect.Network.ID)
}
}
image, pulled, err := m.ensureRuntimeImageAvailable(ctx, opts.PullImage)
report.ImagePulled = pulled
report.ImageID = image
if err != nil {
if !opts.PullImage && errdefs.IsNotFound(err) {
report.ImagePullRequired = true
addFailure("tenant runtime image %q is not present locally; rerun preflight with image pulling enabled or pull it before creating a workspace", report.ImageRef)
} else {
addFailure("%v", err)
}
} else {
report.ImageAvailable = true
}
return report, nil
}
func (m *Manager) tenantNetworkName(tenantID string) string {
if m == nil {
return ""
}
prefix := strings.TrimSpace(m.cfg.TenantNetworkPrefix)
if prefix == "" {
prefix = strings.TrimSpace(m.cfg.Network)
}
if prefix == "" {
prefix = "pulse-tenant"
}
return sanitizeDockerNameFragment(prefix + "-" + strings.TrimSpace(tenantID))
}
func sanitizeDockerNameFragment(raw string) string {
raw = strings.ToLower(strings.TrimSpace(raw))
if raw == "" {
return "pulse-tenant"
}
var b strings.Builder
lastDash := false
for _, r := range raw {
allowed := (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_' || r == '.' || r == '-'
if !allowed {
if !lastDash {
b.WriteByte('-')
lastDash = true
}
continue
}
b.WriteRune(r)
lastDash = r == '-'
}
out := strings.Trim(b.String(), "-_.")
if out == "" {
return "pulse-tenant"
}
return out
}
func (m *Manager) ensureTenantNetwork(ctx context.Context, tenantID string) (string, error) {
if m == nil {
return "", fmt.Errorf("docker manager unavailable")
}
if !m.cfg.IsolateTenantNetworks {
networkName := strings.TrimSpace(m.cfg.Network)
if networkName == "" {
return "", fmt.Errorf("tenant Docker network is required")
}
return networkName, nil
}
networkName := m.tenantNetworkName(tenantID)
inspect, err := m.cli.NetworkInspect(ctx, networkName, client.NetworkInspectOptions{})
if err == nil {
if got := strings.TrimSpace(inspect.Network.Labels["pulse.tenant.id"]); got != "" && got != tenantID {
return "", fmt.Errorf("tenant network %q belongs to tenant %q, not %q", networkName, got, tenantID)
}
return networkName, nil
}
if !errdefs.IsNotFound(err) {
return "", fmt.Errorf("inspect tenant network %q: %w", networkName, err)
}
_, err = m.cli.NetworkCreate(ctx, networkName, client.NetworkCreateOptions{
Driver: "bridge",
Labels: map[string]string{
"pulse.managed": "true",
"pulse.tenant.id": tenantID,
tenantRuntimeNetworkLabel: tenantRuntimeNetworkLabelValue,
},
})
if err != nil {
return "", fmt.Errorf("create tenant network %q: %w", networkName, err)
}
return networkName, nil
}
func (m *Manager) connectSupportContainersToTenantNetwork(ctx context.Context, networkName string) error {
if m == nil || !m.cfg.IsolateTenantNetworks {
return nil
}
for _, label := range m.cfg.SupportContainerLabels {
label = strings.TrimSpace(label)
if label == "" {
continue
}
if err := m.connectSupportContainersByLabel(ctx, networkName, label); err != nil {
return err
}
}
return nil
}
func (m *Manager) connectSupportContainersByLabel(ctx context.Context, networkName, label string) error {
filters := client.Filters{}
filters = filters.Add("label", label)
result, err := m.cli.ContainerList(ctx, client.ContainerListOptions{
Filters: filters,
})
if err != nil {
return fmt.Errorf("list provider support containers for label %q: %w", label, err)
}
if len(result.Items) == 0 {
return fmt.Errorf("provider support container with label %q is required for isolated tenant network %q", label, networkName)
}
for _, item := range result.Items {
if item.ID == "" {
continue
}
if err := m.ensureContainerConnectedToNetwork(ctx, networkName, item.ID); err != nil {
return fmt.Errorf("connect provider support container %s to tenant network %q: %w", item.ID[:min(len(item.ID), 12)], networkName, err)
}
}
return nil
}
func (m *Manager) ensureContainerConnectedToNetwork(ctx context.Context, networkName, containerID string) error {
inspect, err := m.cli.NetworkInspect(ctx, networkName, client.NetworkInspectOptions{})
if err != nil {
return fmt.Errorf("inspect tenant network %q: %w", networkName, err)
}
for id := range inspect.Network.Containers {
if id == containerID || strings.HasPrefix(id, containerID) || strings.HasPrefix(containerID, id) {
return nil
}
}
_, err = m.cli.NetworkConnect(ctx, networkName, client.NetworkConnectOptions{
Container: containerID,
EndpointConfig: &network.EndpointSettings{},
})
return err
}
// IsNotFound reports whether Docker treated an identifier as missing.
func IsNotFound(err error) bool {
return errdefs.IsNotFound(err)
}
func (m *Manager) ensureDaemonReachable(ctx context.Context) error {
if m == nil || m.cli == nil {
return fmt.Errorf("docker client unavailable")
}
if _, err := m.cli.Ping(ctx, client.PingOptions{}); err != nil {
return fmt.Errorf("ping docker daemon: %w", err)
}
return nil
}
func (m *Manager) ensureRuntimeImageAvailable(ctx context.Context, pullIfMissing bool) (imageID string, pulled bool, err error) {
if err := m.ensureDaemonReachable(ctx); err != nil {
return "", false, err
}
imageRef := strings.TrimSpace(m.cfg.Image)
if imageRef == "" {
return "", false, fmt.Errorf("tenant runtime image is required")
}
inspect, err := m.cli.ImageInspect(ctx, imageRef)
if err == nil {
return strings.TrimSpace(inspect.ID), false, nil
}
if !errdefs.IsNotFound(err) {
return "", false, fmt.Errorf("inspect tenant runtime image %q: %w", imageRef, err)
}
if !pullIfMissing {
return "", false, err
}
rc, err := m.cli.ImagePull(ctx, imageRef, client.ImagePullOptions{})
if err != nil {
return "", false, fmt.Errorf("pull tenant runtime image %q: %w", imageRef, err)
}
defer func() {
if closeErr := rc.Close(); closeErr != nil && err == nil {
err = fmt.Errorf("close tenant runtime image pull stream %q: %w", imageRef, closeErr)
}
}()
if _, err := io.Copy(io.Discard, rc); err != nil {
return "", true, fmt.Errorf("read tenant runtime image pull output %q: %w", imageRef, err)
}
inspect, err = m.cli.ImageInspect(ctx, imageRef)
if err != nil {
return "", true, fmt.Errorf("inspect pulled tenant runtime image %q: %w", imageRef, err)
}
return strings.TrimSpace(inspect.ID), true, nil
}
// CreateAndStart creates and starts a tenant container.
// tenantDataDir is the host path that gets bind-mounted to /etc/pulse in the container.
func (m *Manager) CreateAndStart(ctx context.Context, tenantID, tenantDataDir string) (containerID string, err error) {
if err := m.ensureDaemonReachable(ctx); err != nil {
return "", err
}
if _, _, err := m.ensureRuntimeImageAvailable(ctx, true); err != nil {
return "", fmt.Errorf("prepare tenant runtime image: %w", err)
}
if err := prepareTenantRuntimeMountSources(tenantDataDir, tenantRuntimeUIDFor(m.cfg), tenantRuntimeGIDFor(m.cfg)); err != nil {
return "", fmt.Errorf("prepare tenant runtime mounts for %s: %w", tenantID, err)
}
tenantNetworkName, err := m.ensureTenantNetwork(ctx, tenantID)
if err != nil {
return "", err
}
labels := TraefikLabels(tenantID, m.cfg.BaseDomain, m.cfg.ContainerPort, tenantNetworkName)
labels["pulse.managed"] = "true"
containerName := "pulse-" + tenantID
resp, err := m.cli.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: tenantRuntimeContainerConfig(tenantID, m.cfg, labels, m.tenantTrustedProxyCIDRs(ctx, tenantNetworkName)),
HostConfig: tenantRuntimeHostConfig(tenantDataDir, m.cfg),
NetworkingConfig: &network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{
tenantNetworkName: {},
},
},
Name: containerName,
})
if err != nil {
return "", fmt.Errorf("create container for %s: %w", tenantID, err)
}
if err := m.connectSupportContainersToTenantNetwork(ctx, tenantNetworkName); err != nil {
_, _ = m.cli.ContainerRemove(ctx, resp.ID, client.ContainerRemoveOptions{Force: true})
return resp.ID, err
}
if _, err := m.cli.ContainerStart(ctx, resp.ID, client.ContainerStartOptions{}); err != nil {
return resp.ID, fmt.Errorf("start container for %s: %w", tenantID, err)
}
log.Info().
Str("tenant_id", tenantID).
Str("container_id", resp.ID[:12]).
Str("container_name", containerName).
Msg("Tenant container started")
return resp.ID, nil
}
// ReconcileTenantRuntimeMountSources restores the configured rootless runtime
// ownership after the provider control plane has deliberately mutated a live
// tenant's files on its behalf. CreateAndStart performs the same preparation
// before first boot; provider-hosted proof paths need this public boundary when
// they hand files back to an already-running tenant.
func (m *Manager) ReconcileTenantRuntimeMountSources(tenantDataDir string) error {
if m == nil {
return fmt.Errorf("docker manager is required")
}
if err := prepareTenantRuntimeMountSources(tenantDataDir, tenantRuntimeUIDFor(m.cfg), tenantRuntimeGIDFor(m.cfg)); err != nil {
return fmt.Errorf("reconcile tenant runtime mounts: %w", err)
}
return nil
}
func tenantRuntimeContainerConfig(tenantID string, cfg ManagerConfig, labels map[string]string, trustedProxyCIDRs []string) *container.Config {
return &container.Config{
Image: cfg.Image,
User: tenantRuntimeUserFor(cfg),
Labels: labels,
Env: tenantEnvForRuntime(tenantID, tenantDisplayNameFor(cfg, tenantID), cfg.BaseDomain, cfg.TrialActivationPublicKey, cfg.ControlPlaneBaseURL, trustedProxyCIDRs, tenantRuntimeUIDFor(cfg), tenantRuntimeGIDFor(cfg), cfg.TenantReportBrand),
}
}
func tenantDisplayNameFor(cfg ManagerConfig, tenantID string) string {
if cfg.TenantDisplayName == nil {
return ""
}
return strings.TrimSpace(cfg.TenantDisplayName(tenantID))
}
func tenantRuntimeUserFor(cfg ManagerConfig) string {
return fmt.Sprintf("%d:%d", tenantRuntimeUIDFor(cfg), tenantRuntimeGIDFor(cfg))
}
func tenantRuntimeUIDFor(cfg ManagerConfig) int {
if cfg.TenantRuntimeUID > 0 {
return cfg.TenantRuntimeUID
}
return tenantRuntimeUID
}
func tenantRuntimeGIDFor(cfg ManagerConfig) int {
if cfg.TenantRuntimeGID > 0 {
return cfg.TenantRuntimeGID
}
return tenantRuntimeGID
}
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 tenantRuntimeHostConfig(tenantDataDir string, cfg ManagerConfig) *container.HostConfig {
return &container.HostConfig{
RestartPolicy: container.RestartPolicy{Name: "unless-stopped"},
LogConfig: tenantRuntimeLogConfig(cfg.TenantLogMaxSize, cfg.TenantLogMaxFile),
Resources: container.Resources{
Memory: cfg.MemoryLimit,
CPUShares: cfg.CPUShares,
},
Mounts: tenantMounts(tenantDataDir),
SecurityOpt: tenantRuntimeSecurityOptions(),
CapDrop: []string{"ALL"},
ReadonlyRootfs: true,
Tmpfs: map[string]string{
"/run": "rw,noexec,nosuid,nodev,size=16m",
"/tmp": "rw,noexec,nosuid,nodev,size=64m",
},
}
}
func tenantRuntimeSecurityOptions() []string {
// Do not set seccomp=unconfined. Docker's default seccomp profile remains
// active unless the daemon has been explicitly weakened outside Pulse.
return []string{"no-new-privileges:true"}
}
func tenantImmutableOwnershipPaths() []string {
return []string{
"/etc/pulse/secrets/handoff.key",
"/etc/pulse/.cloud_handoff_key",
}
}
func tenantRuntimeOwnershipPaths() []string {
return []string{
"billing.json",
filepath.Join("secrets", "handoff.key"),
".cloud_handoff_key",
}
}
func tenantEnv(tenantID, tenantName, baseDomain, trialActivationPublicKey, controlPlaneBaseURL string, trustedProxyCIDRs []string, reportBrand TenantReportBrandConfig) []string {
return tenantEnvForRuntime(tenantID, tenantName, baseDomain, trialActivationPublicKey, controlPlaneBaseURL, trustedProxyCIDRs, tenantRuntimeUID, tenantRuntimeGID, reportBrand)
}
func tenantEnvForRuntime(tenantID, tenantName, baseDomain, trialActivationPublicKey, controlPlaneBaseURL string, trustedProxyCIDRs []string, runtimeUID, runtimeGID int, reportBrand TenantReportBrandConfig) []string {
routing := CanonicalTenantRuntimeRouting(tenantID, baseDomain)
tenantID = strings.TrimSpace(tenantID)
env := []string{
"PULSE_DATA_DIR=/etc/pulse",
"PULSE_HOSTED_MODE=true",
"PULSE_TENANT_ID=" + tenantID,
"PULSE_MULTI_TENANT_ENABLED=true",
fmt.Sprintf("PUID=%d", runtimeUID),
fmt.Sprintf("PGID=%d", runtimeGID),
fmt.Sprintf("%s=%s", immutableOwnershipPathsEnv, strings.Join(tenantImmutableOwnershipPaths(), ":")),
}
if name := strings.TrimSpace(tenantName); name != "" {
env = append(env, "PULSE_TENANT_NAME="+name)
}
if routing.PublicURL != "" {
env = append(env, "PULSE_PUBLIC_URL="+routing.PublicURL)
}
if strings.TrimSpace(trialActivationPublicKey) != "" {
env = append(env, "PULSE_TRIAL_ACTIVATION_PUBLIC_KEY="+strings.TrimSpace(trialActivationPublicKey))
}
if value := strings.TrimSpace(controlPlaneBaseURL); value != "" {
// Point hosted entitlement lease refresh at this control plane.
// Without it the runtime falls back to the built-in Pulse Cloud
// URL, which a provider-hosted control plane does not operate.
env = append(env, pkglicensing.ProTrialSignupURLEnvVar+"="+value)
}
if len(trustedProxyCIDRs) > 0 {
env = append(env, "PULSE_TRUSTED_PROXY_CIDRS="+strings.Join(trustedProxyCIDRs, ","))
}
if value := strings.TrimSpace(reportBrand.DisplayName); value != "" {
env = append(env, "PULSE_REPORT_PROVIDER_BRAND_DISPLAY_NAME="+value)
}
if value := strings.TrimSpace(reportBrand.LogoPath); value != "" {
env = append(env, "PULSE_REPORT_PROVIDER_BRAND_LOGO_PATH="+value)
}
if value := strings.TrimSpace(reportBrand.LogoBase64); value != "" {
env = append(env, "PULSE_REPORT_PROVIDER_BRAND_LOGO_BASE64="+value)
}
if value := strings.TrimSpace(reportBrand.LogoFormat); value != "" {
env = append(env, "PULSE_REPORT_PROVIDER_BRAND_LOGO_FORMAT="+value)
}
return env
}
func routeHostFromLabels(labels map[string]string) string {
for key, value := range labels {
if !strings.HasPrefix(key, "traefik.http.routers.") || !strings.HasSuffix(key, ".rule") {
continue
}
if host := parseTraefikHostRule(value); host != "" {
return host
}
}
return ""
}
func parseTraefikHostRule(rule string) string {
rule = strings.TrimSpace(rule)
if !strings.HasPrefix(rule, "Host(`") || !strings.HasSuffix(rule, "`)") {
return ""
}
return strings.TrimSuffix(strings.TrimPrefix(rule, "Host(`"), "`)")
}
func envValue(env []string, key string) string {
prefix := key + "="
for _, item := range env {
if !strings.HasPrefix(item, prefix) {
continue
}
return strings.TrimPrefix(item, prefix)
}
return ""
}
func (m *Manager) tenantTrustedProxyCIDRs(ctx context.Context, networkNames ...string) []string {
values := make([]string, 0, len(m.cfg.TrustedProxyCIDRs)+1)
seen := make(map[string]struct{})
appendCIDR := func(raw string) {
canonical := canonicalTrustedProxyCIDR(raw)
if canonical == "" {
return
}
if _, ok := seen[canonical]; ok {
return
}
seen[canonical] = struct{}{}
values = append(values, canonical)
}
for _, cidr := range m.cfg.TrustedProxyCIDRs {
appendCIDR(cidr)
}
if len(networkNames) == 0 && m != nil {
networkNames = []string{m.cfg.Network}
}
for _, networkName := range networkNames {
networkName = strings.TrimSpace(networkName)
if m == nil || m.cli == nil || networkName == "" {
continue
}
inspect, err := m.cli.NetworkInspect(ctx, networkName, client.NetworkInspectOptions{})
if err != nil {
log.Warn().Err(err).Str("network", networkName).Msg("Failed to inspect tenant network for trusted proxy CIDRs")
} else {
for _, cfg := range inspect.Network.IPAM.Config {
appendCIDR(cfg.Subnet.String())
}
}
}
sort.Strings(values)
return values
}
func canonicalTrustedProxyCIDR(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
}
if prefix, err := netip.ParsePrefix(raw); err == nil {
return prefix.Masked().String()
}
if addr, err := netip.ParseAddr(raw); err == nil {
bits := 32
if addr.Is6() {
bits = 128
}
return netip.PrefixFrom(addr, bits).Masked().String()
}
return ""
}
func tenantMounts(tenantDataDir string) []mount.Mount {
return []mount.Mount{
{
Type: mount.TypeBind,
Source: tenantDataDir,
Target: "/etc/pulse",
},
{
Type: mount.TypeBind,
Source: filepath.Join(tenantDataDir, "secrets", "handoff.key"),
Target: "/etc/pulse/secrets/handoff.key",
ReadOnly: true,
},
{
Type: mount.TypeBind,
Source: filepath.Join(tenantDataDir, ".cloud_handoff_key"),
Target: "/etc/pulse/.cloud_handoff_key",
ReadOnly: true,
},
}
}
func prepareTenantRuntimeMountSources(tenantDataDir string, uid, gid int) error {
if err := filepath.WalkDir(tenantDataDir, func(path string, entry os.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if err := os.Lchown(path, uid, gid); err != nil {
return fmt.Errorf("chown %s to %d:%d: %w", path, uid, gid, err)
}
return nil
}); err != nil {
return err
}
for _, relPath := range tenantRuntimeOwnershipPaths() {
path := filepath.Join(tenantDataDir, relPath)
info, err := os.Lstat(path)
if err != nil {
return fmt.Errorf("stat %s: %w", path, err)
}
if info.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("tenant runtime mount source %s must not be a symlink", path)
}
if err := os.Chmod(path, 0o600); err != nil {
return fmt.Errorf("chmod %s: %w", path, err)
}
if err := os.Chown(path, uid, gid); err != nil {
return fmt.Errorf("chown %s to %d:%d: %w", path, uid, gid, err)
}
}
return nil
}
// Stop stops a tenant container gracefully.
func (m *Manager) Stop(ctx context.Context, containerID string) error {
timeout := 30
_, err := m.cli.ContainerStop(ctx, containerID, client.ContainerStopOptions{Timeout: &timeout})
return err
}
// Start starts a stopped tenant container.
func (m *Manager) Start(ctx context.Context, containerID string) error {
_, err := m.cli.ContainerStart(ctx, containerID, client.ContainerStartOptions{})
return err
}
// Remove removes a stopped tenant container.
func (m *Manager) Remove(ctx context.Context, containerID string) error {
networkNames := m.tenantNetworkNamesForContainer(ctx, containerID)
_, err := m.cli.ContainerRemove(ctx, containerID, client.ContainerRemoveOptions{
Force: true,
})
if err == nil {
for _, networkName := range networkNames {
if removeErr := m.removeTenantNetwork(ctx, networkName); removeErr != nil {
log.Warn().Err(removeErr).Str("network", networkName).Msg("Failed to remove tenant runtime network")
}
}
}
return err
}
func (m *Manager) tenantNetworkNamesForContainer(ctx context.Context, containerID string) []string {
if m == nil || !m.cfg.IsolateTenantNetworks || strings.TrimSpace(containerID) == "" {
return nil
}
inspectResult, err := m.cli.ContainerInspect(ctx, containerID, client.ContainerInspectOptions{})
if err != nil {
return nil
}
inspect := inspectResult.Container
if inspect.NetworkSettings == nil {
return nil
}
var out []string
for networkName := range inspect.NetworkSettings.Networks {
if m.isTenantNetwork(ctx, networkName) {
out = append(out, networkName)
}
}
sort.Strings(out)
return out
}
func (m *Manager) isTenantNetwork(ctx context.Context, networkName string) bool {
networkName = strings.TrimSpace(networkName)
if m == nil || networkName == "" {
return false
}
inspect, err := m.cli.NetworkInspect(ctx, networkName, client.NetworkInspectOptions{})
if err != nil {
return false
}
return strings.TrimSpace(inspect.Network.Labels[tenantRuntimeNetworkLabel]) == tenantRuntimeNetworkLabelValue
}
func (m *Manager) removeTenantNetwork(ctx context.Context, networkName string) error {
if m == nil || strings.TrimSpace(networkName) == "" {
return nil
}
if err := m.disconnectSupportContainersFromTenantNetwork(ctx, networkName); err != nil {
return err
}
if _, err := m.cli.NetworkRemove(ctx, networkName, client.NetworkRemoveOptions{}); err != nil {
return fmt.Errorf("remove tenant network %q: %w", networkName, err)
}
return nil
}
func (m *Manager) disconnectSupportContainersFromTenantNetwork(ctx context.Context, networkName string) error {
if m == nil || !m.cfg.IsolateTenantNetworks {
return nil
}
for _, label := range m.cfg.SupportContainerLabels {
label = strings.TrimSpace(label)
if label == "" {
continue
}
filters := client.Filters{}
filters = filters.Add("label", label)
result, err := m.cli.ContainerList(ctx, client.ContainerListOptions{
All: true,
Filters: filters,
})
if err != nil {
return fmt.Errorf("list provider support containers for disconnect label %q: %w", label, err)
}
for _, item := range result.Items {
if item.ID == "" {
continue
}
_, err := m.cli.NetworkDisconnect(ctx, networkName, client.NetworkDisconnectOptions{
Container: item.ID,
Force: true,
})
if err != nil && !errdefs.IsNotFound(err) {
return fmt.Errorf("disconnect provider support container %s from tenant network %q: %w", item.ID[:min(len(item.ID), 12)], networkName, err)
}
}
}
return nil
}
// Rename renames a tenant container to the supplied Docker name.
func (m *Manager) Rename(ctx context.Context, containerIDOrName, newName string) error {
if err := m.ensureDaemonReachable(ctx); err != nil {
return err
}
if _, err := m.cli.ContainerRename(ctx, containerIDOrName, client.ContainerRenameOptions{NewName: newName}); err != nil {
return fmt.Errorf("rename container %s -> %s: %w", containerIDOrName, newName, err)
}
return nil
}
// Inspect returns the canonical live metadata for the supplied container
// identifier.
func (m *Manager) Inspect(ctx context.Context, containerIDOrName string) (*RuntimeContainerInfo, error) {
if err := m.ensureDaemonReachable(ctx); err != nil {
return nil, err
}
inspectResult, err := m.cli.ContainerInspect(ctx, containerIDOrName, client.ContainerInspectOptions{})
if err != nil {
return nil, fmt.Errorf("inspect container %s: %w", containerIDOrName, err)
}
inspect := inspectResult.Container
imageRef := ""
if inspect.Config != nil {
imageRef = strings.TrimSpace(inspect.Config.Image)
}
running := inspect.State != nil && inspect.State.Running
routeHost := ""
publicURL := ""
if inspect.Config != nil {
routeHost = routeHostFromLabels(inspect.Config.Labels)
publicURL = envValue(inspect.Config.Env, "PULSE_PUBLIC_URL")
}
return &RuntimeContainerInfo{
ID: inspect.ID,
Name: strings.TrimPrefix(strings.TrimSpace(inspect.Name), "/"),
ImageRef: imageRef,
ImageID: strings.TrimSpace(inspect.Image),
Running: running,
RouteHost: routeHost,
PublicURL: publicURL,
}, nil
}
// StopAndRemove stops then removes a tenant container.
func (m *Manager) StopAndRemove(ctx context.Context, containerID string) error {
if err := m.Stop(ctx, containerID); err != nil {
log.Warn().Err(err).Str("container_id", containerID).Msg("Failed to stop container, forcing remove")
}
return m.Remove(ctx, containerID)
}
// HealthCheck performs an HTTP health check against a running container.
// It connects to the container's published port via the Docker network.
func (m *Manager) HealthCheck(ctx context.Context, containerID string) (bool, error) {
inspectResult, err := m.cli.ContainerInspect(ctx, containerID, client.ContainerInspectOptions{})
if err != nil {
return false, fmt.Errorf("inspect container: %w", err)
}
inspect := inspectResult.Container
if !inspect.State.Running {
return false, nil
}
if healthy, recorded := dockerRecordedHealth(inspect); recorded {
return healthy, nil
}
networkNames := m.healthCheckNetworkCandidates(inspect)
var netSettings *network.EndpointSettings
for _, networkName := range networkNames {
candidate, ok := inspect.NetworkSettings.Networks[networkName]
if ok && candidate != nil && candidate.IPAddress.IsValid() {
netSettings = candidate
break
}
}
if netSettings == nil {
return false, fmt.Errorf("container not connected to tenant health network candidates %s", strings.Join(networkNames, ", "))
}
healthURL := fmt.Sprintf("http://%s:%d/api/health", netSettings.IPAddress.String(), m.cfg.ContainerPort)
httpClient := &http.Client{Timeout: 5 * time.Second}
resp, err := httpClient.Get(healthURL)
if err != nil {
return false, nil // unreachable, not an error condition
}
defer func() { _, _ = io.Copy(io.Discard, resp.Body); resp.Body.Close() }()
return resp.StatusCode == http.StatusOK, nil
}
// dockerRecordedHealth returns the result of the runtime image's own Docker
// HEALTHCHECK, which probes /api/health inside the container, when the
// container declares one. Docker records it on the container, so any caller
// with the socket reads the same answer. The HTTP fallback below only works
// from a process attached to the workspace's isolated network: the long-lived
// control plane is, but `provider-msp status` runs in a one-off container on
// the ingress network alone, so every real client read as unhealthy there and
// upgrade.sh and the install proof refused to run on any install with a
// client. A container still in its start period is not yet healthy.
func dockerRecordedHealth(inspect container.InspectResponse) (healthy bool, recorded bool) {
if inspect.State == nil || inspect.State.Health == nil {
return false, false
}
switch inspect.State.Health.Status {
case container.Healthy:
return true, true
case container.Unhealthy, container.Starting:
return false, true
default:
return false, false
}
}
func (m *Manager) healthCheckNetworkCandidates(inspect container.InspectResponse) []string {
var names []string
seen := make(map[string]struct{})
appendName := func(name string) {
name = strings.TrimSpace(name)
if name == "" {
return
}
if _, ok := seen[name]; ok {
return
}
seen[name] = struct{}{}
names = append(names, name)
}
if m != nil && m.cfg.IsolateTenantNetworks && inspect.Config != nil {
if tenantID := strings.TrimSpace(inspect.Config.Labels["pulse.tenant.id"]); tenantID != "" {
appendName(m.tenantNetworkName(tenantID))
}
}
if m != nil {
appendName(m.cfg.Network)
}
if inspect.NetworkSettings != nil {
for name := range inspect.NetworkSettings.Networks {
appendName(name)
}
}
return names
}