From e6a58c664dbec7277575a2ca4b9fcdbd4feb40f7 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:16:07 -0400 Subject: [PATCH 1/4] fix(run): the first-Init port guard excuses every container this service's agent kept, not just one named after the service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An agent may run SEVERAL containers for one service and names each after itself: service-object-storage publishes the service's own grpc port from `-gateway` and keeps `-minio` and `-network` beside it. The guard added in #1180 excused only a container named exactly `ContainerName()`, so it excused none of them — and `adoptableContainer` could therefore never match a keep-running agent that suffixes its container names, which is the fleet's actual convention. What that cost: the first flow of a job starts the service fine and leaves its gateway container up, publishing the service's deterministic port. A later flow in the same job builds a fresh Runner, so `everStarted` is false and the guard runs, finds the port bound by Docker's forwarder, fails to attribute it to this service's own container, and refuses. Deterministically, in every phase, which is what saas#1265 hit on both of its runs — the first run of that graph on a release carrying #1180. A container is now this service's when its logical name label and its Docker name are the service's name or that name with a suffix. Every other guard is unchanged: the codefly owner label, this flow's exact recovery scope, the namespace rule, running, non-ephemeral, and the container must publish the proposed port on that address's own loopback family. A sibling service is still not this one, and a squatter is still refused on either family. Diagnosed by reproduction rather than inference, with the RELEASED 0.1.219 in a repo unrelated to saas: `codefly run service documents/object-storage --init-only` twice in one workspace and scope leaves `…-object-storage-osproof-gateway` publishing 0.0.0.0:47103 and then refuses `port 47103 (grpc) held by pid … (OrbStack Helper)` — the Docker forwarder, so the holder is the kept container and neither a leaked process nor a port derivation. The same second flow passes with this fix. Restoring the old comparison makes the two new unit cases fail with that same refusal. Co-Authored-By: Claude Opus 5 --- pkg/orchestration/adopted_container_ports.go | 30 +++++++++++++++++-- .../adopted_container_ports_test.go | 23 ++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/pkg/orchestration/adopted_container_ports.go b/pkg/orchestration/adopted_container_ports.go index aa2b9d59..6fb17ae9 100644 --- a/pkg/orchestration/adopted_container_ports.go +++ b/pkg/orchestration/adopted_container_ports.go @@ -48,7 +48,14 @@ var listOwnedContainers = func(ctx context.Context, scopeID string) ([]container // The container is identified the way the agent's runner reuses it: the name // every Core service agent gives it (dockerrun.ContainerName over the // service's unique name in the run's workspace and naming scope, as -// Base.UniqueWithWorkspace spells it), owned by codefly, +// Base.UniqueWithWorkspace spells it) — or that name with a suffix, because an +// agent may run SEVERAL containers for one service and names each after +// itself: service-object-storage's runtime publishes the service's own grpc +// port from `-gateway` and keeps `-minio` and +// `-network` beside it. Requiring the bare name excused none of them, +// so the first Init of a second flow in one job found the port its own agent +// had left published and refused it — deterministically, which is what +// saas#1265 hit on every phase. Owned by codefly, // labelled with this flow's exact recovery scope and, when it carries one, its // namespace (the rule dockerrun applies before reusing a container), running, // and not an ephemeral one, which is never reused across sessions. An address @@ -86,13 +93,30 @@ func (runner *Runner) adoptedContainerAddresses(ctx context.Context) func(addres func adoptableContainer(c *container.Summary, name, scopeID, namespace string) bool { labels := c.Labels if labels[dockerrun.LabelCodeflyOwner] != "true" || labels[dockerrun.LabelCodeflyRecoveryScope] != scopeID || - labels[dockerrun.LabelCodeflyName] != name || labels[dockerrun.LabelCodeflyEphemeral] == "true" || c.State != "running" { + labels[dockerrun.LabelCodeflyEphemeral] == "true" || c.State != "running" { return false } if found := labels[dockerrun.LabelCodeflyRecoveryNamespace]; found != "" && found != namespace { return false } - return slices.Contains(c.Names, "/"+name) + // This service's container, or one its agent derived from it: Core's + // ContainerName only prefixes and replaces slashes, so an agent's + // `-minio` is this name plus that suffix. The logical name label + // and the Docker name must agree on it, and the caller still requires the + // container to publish the proposed port on its own loopback family — so + // a sibling service whose name merely EXTENDS this one is excused only if + // it published this service's port, which is a misconfiguration this + // guard was never the detector for. + return ownedByService(labels[dockerrun.LabelCodeflyName], name) && + slices.ContainsFunc(c.Names, func(dockerName string) bool { + return ownedByService(strings.TrimPrefix(dockerName, "/"), name) + }) +} + +// ownedByService reports whether a container name is the service's own or one +// its agent derived from it by suffix. +func ownedByService(candidate, name string) bool { + return candidate == name || strings.HasPrefix(candidate, name+"-") } // publishesAddress reports whether c publishes the loopback host:port address diff --git a/pkg/orchestration/adopted_container_ports_test.go b/pkg/orchestration/adopted_container_ports_test.go index 93768fef..56642191 100644 --- a/pkg/orchestration/adopted_container_ports_test.go +++ b/pkg/orchestration/adopted_container_ports_test.go @@ -53,6 +53,21 @@ func TestFirstInitAdoptsItsOwnKeptContainerAndRefusesAnyOtherHolder(t *testing.T c.Names = []string{"/codefly-other"} c.Labels[dockerrun.LabelCodeflyName] = "codefly-other" }, refused: true}, + // An agent may run several containers for one service and names each + // after itself: service-object-storage publishes the service's own + // grpc port from `-gateway`, beside `-minio` and + // `-network`. Each is this service's container. + {name: "its agent's gateway container", mutate: func(c *container.Summary) { + suffixed(c, "-gateway") + }, refused: false}, + {name: "its agent's minio container", mutate: func(c *container.Summary) { + suffixed(c, "-minio") + }, refused: false}, + // A sibling service is still not this one, suffix or no suffix. + {name: "a sibling service's container", mutate: func(c *container.Summary) { + c.Names = []string{"/codefly-other-web-gateway-minio"} + c.Labels[dockerrun.LabelCodeflyName] = "codefly-other-web-gateway-minio" + }, refused: true}, {name: "ephemeral", mutate: func(c *container.Summary) { c.Labels[dockerrun.LabelCodeflyEphemeral] = "true" }, refused: true}, {name: "not running", mutate: func(c *container.Summary) { c.State = "exited" }, refused: true}, {name: "published on another port", mutate: func(c *container.Summary) { c.Ports[0].PublicPort++ }, refused: true}, @@ -108,6 +123,14 @@ func TestAdoptedContainerAddressesExcuseNothingWithoutARecoveryIdentity(t *testi require.False(t, runner.adoptedContainerAddresses(context.Background())("127.0.0.1:5432")) } +// suffixed renames a kept container the way an agent that runs more than one +// container for a service does, through Core's own ContainerName. +func suffixed(c *container.Summary, suffix string) { + name := c.Labels[dockerrun.LabelCodeflyName] + suffix + c.Names = []string{"/" + name} + c.Labels[dockerrun.LabelCodeflyName] = name +} + // A publication covers only its own loopback family. func TestPublishesAddressIsPerLoopbackFamily(t *testing.T) { c := container.Summary{Ports: []container.Port{{IP: "0.0.0.0", PublicPort: 5432, Type: "tcp"}}} From 363b4727481a390ce86fa3d90431d35ab3b2f5fb Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 27 Oct 2026 16:34:48 -0400 Subject: [PATCH 2/4] fix(orchestration): require exact parent attribution for sidecars --- docs/orchestration.md | 15 ++++- go.mod | 8 +-- go.sum | 10 +++ pkg/orchestration/adopted_container_ports.go | 66 ++++++++----------- .../adopted_container_ports_test.go | 23 +++++++ 5 files changed, 77 insertions(+), 45 deletions(-) diff --git a/docs/orchestration.md b/docs/orchestration.md index b489e4f6..7272a0de 100644 --- a/docs/orchestration.md +++ b/docs/orchestration.md @@ -1252,9 +1252,18 @@ wildcard :P both bind. The one listener that is not a squatter is the service's own kept container: a Docker store with `keep-running` leaves its container up for the next run to adopt, still publishing the service's port. An address is let through only when the container listening there is the one -the service's agent will adopt — its name in this run's workspace and naming -scope, codefly-owned, labelled with this run's recovery scope, running, not -ephemeral, and publishing that port on that loopback family; any other holder, +the service owns: Core's exact parent-service label must match the loaded +module/service, and the logical name label must match an actual Docker name. +The container must also be codefly-owned, labelled with this run's recovery +scope and a compatible namespace, running, not ephemeral, and publishing that +port on that loopback family. Agents bind each Docker environment through +Core's `WithServiceIdentity`; CLI does not infer parent identity from suffixes. +For older unlabelled containers, the existing exact bare name in this run's +workspace and naming scope remains the only compatibility exception. A present +conflicting or malformed parent label refuses even that exception. Old suffixed +containers need owner-directed recovery preserving their data; the CLI does not +relabel or delete them. Parent attribution proves ownership, while the agent +owns actual reuse of its concrete containers. Any other holder, including one on the other family beside it, is refused. Not covered: a process that binds a loopback address on the port after that probe, and a routed module that crashes or is withdrawn mid-run, whose port the hop keeps diff --git a/go.mod b/go.mod index 513ccffe..1ed43ef7 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/codefly-dev/authority/libraries/delivery/go v0.0.0-20261026023425-000d44495d93 github.com/codefly-dev/authority/libraries/fixture-composition/go v0.0.0-20261027171336-5178f01696e3 github.com/codefly-dev/authority/libraries/hop/go v0.0.0-20261026175541-3afad7224855 - github.com/codefly-dev/core v0.19.7-0.20261026194550-a8599bd3991a + github.com/codefly-dev/core v0.19.8-0.20261027203111-6eadd92dd156 github.com/codefly-dev/golor v0.1.3 github.com/codefly-dev/llm v0.1.7 github.com/codefly-dev/sdk-go/workcontext v0.1.1-0.20261018052022-70dc7a546458 @@ -175,7 +175,7 @@ require ( github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/moby/buildkit v0.32.2 // indirect + github.com/moby/buildkit v0.34.1 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect @@ -195,7 +195,7 @@ require ( github.com/sergi/go-diff v1.4.0 // indirect github.com/shoenig/go-m1cpu v0.2.2 // indirect github.com/shopspring/decimal v1.4.0 // indirect - github.com/sirupsen/logrus v1.10.0 // indirect + github.com/sirupsen/logrus v1.10.2 // indirect github.com/skeema/knownhosts v1.3.2 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/standard-webhooks/standard-webhooks/libraries v0.0.1 // indirect @@ -243,7 +243,7 @@ require ( golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect golang.org/x/net v0.60.0 // indirect golang.org/x/text v0.42.0 // indirect - golang.org/x/time v0.15.0 // indirect + golang.org/x/time v0.16.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20261012220534-4341edf3f941 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect diff --git a/go.sum b/go.sum index 47505b10..b2dd79eb 100644 --- a/go.sum +++ b/go.sum @@ -117,6 +117,8 @@ github.com/codefly-dev/authority/libraries/resources/go v0.0.0-20261026023425-00 github.com/codefly-dev/authority/libraries/resources/go v0.0.0-20261026023425-000d44495d93/go.mod h1:1rmjrVyr0h7zOuwOHS9i+BoeS9dr3XNvtupLZFwY6As= github.com/codefly-dev/core v0.19.7-0.20261026194550-a8599bd3991a h1:ar8ERpN9n9ZL29LZ63mqBZNLPZ7OpcnQ1+D1g89DbPY= github.com/codefly-dev/core v0.19.7-0.20261026194550-a8599bd3991a/go.mod h1:1ina3kP+YSAL4tlZvnEYd4P8bFGnJECfkP86ojeeF1A= +github.com/codefly-dev/core v0.19.8-0.20261027203111-6eadd92dd156 h1:OLQ/LG4+KGRz3crTc8zgQlxTS/Ta3Yh7OUFu+12N5u4= +github.com/codefly-dev/core v0.19.8-0.20261027203111-6eadd92dd156/go.mod h1:NB7x1UZ01rYTfEZEl4vUFD1A+fgOXhsWbgA3M8x4ThA= github.com/codefly-dev/golor v0.1.3 h1:xmo+ceyJFRYZdvpWE2fNd0jeaadp/Ibm1BnganiGKOc= github.com/codefly-dev/golor v0.1.3/go.mod h1:sl/u/K1l7J0Pr3xyVZp8fOJYQItKKst1No9JqgzLLoY= github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ= @@ -133,6 +135,7 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/log v0.2.0 h1:BewD/umNgVnoczglOpX8eRMyEy5t5iPlu5AIpnWDONc= github.com/containerd/typeurl/v2 v2.3.0 h1:HZHPhRWo5XMy3QGQoPrUzbW/2ckwjfweHmOwlkIrPAQ= github.com/containerd/typeurl/v2 v2.3.0/go.mod h1:Qk+PAdUYArVj41TnGi6rJ+48RF0PkcTc4i/taoBcK0w= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -332,6 +335,8 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/buildkit v0.32.2 h1:Sfy7+u6dUv/2yuBc9KCoK70Re8atuV8aPZ5UOC068Vc= github.com/moby/buildkit v0.32.2/go.mod h1:0GB/EJ1d+4VIVqIAgy3asaoGkVXy7IrDfVy7mPhOvg8= +github.com/moby/buildkit v0.34.1 h1:jVdxsXX0UpU0q1Et+9lFezPmh9sGE6vF0TrKZaGlfy4= +github.com/moby/buildkit v0.34.1/go.mod h1:e+Za70y2a3nL1U2Y0P8TvhPyKp2Ok2NS+Q4siSArDiM= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= @@ -406,6 +411,8 @@ github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+D github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.10.0 h1:T8MxJJXVZkfcC5zSRMRAg2F8+lxjmUCGGWPzFxO+Msc= github.com/sirupsen/logrus v1.10.0/go.mod h1:FXZFonkDAnFozmO+5hGAFvB0Yg9/j2SIhA/QuIkP180= +github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo= +github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q= github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg= github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= @@ -514,6 +521,7 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0 h1:UFpxO go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0/go.mod h1:YBGjxe3lt0jtXoEXxGrGhv/jM7oUBUDVK7zhAvNyEjc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0 h1:QBajQ2SrwQijzHyZbQlPsuIzpl/ll8DY6wPWsajeGcI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0/go.mod h1:08ZQLjrPLQ6R4kAXvuOvODEer5Yh4CoFvll5qB2BCI8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.46.0 h1:KrC1YrQeSt46ITMWAbgQx1M1eV1/1TKzttrBzymPmss= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.47.0 h1:N3YQCxjxQ/bMjyc3heladfRm9t9RTksGQH8z4w6yU/0= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.47.0/go.mod h1:Mp8HOFqcaUyypCuGv9IhDdTHnJ56lSudSHMd+pVSCEA= go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI= @@ -581,6 +589,8 @@ golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE= +golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI= golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA= diff --git a/pkg/orchestration/adopted_container_ports.go b/pkg/orchestration/adopted_container_ports.go index 6fb17ae9..2d93db61 100644 --- a/pkg/orchestration/adopted_container_ports.go +++ b/pkg/orchestration/adopted_container_ports.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/codefly-dev/core/resources" "github.com/codefly-dev/core/runners/dockerrun" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" @@ -45,24 +46,11 @@ var listOwnedContainers = func(ctx context.Context, scopeID string) ([]container // name, and that container still publishes the service's deterministic port — // the one this Init proposes. // -// The container is identified the way the agent's runner reuses it: the name -// every Core service agent gives it (dockerrun.ContainerName over the -// service's unique name in the run's workspace and naming scope, as -// Base.UniqueWithWorkspace spells it) — or that name with a suffix, because an -// agent may run SEVERAL containers for one service and names each after -// itself: service-object-storage's runtime publishes the service's own grpc -// port from `-gateway` and keeps `-minio` and -// `-network` beside it. Requiring the bare name excused none of them, -// so the first Init of a second flow in one job found the port its own agent -// had left published and refused it — deterministically, which is what -// saas#1265 hit on every phase. Owned by codefly, -// labelled with this flow's exact recovery scope and, when it carries one, its -// namespace (the rule dockerrun applies before reusing a container), running, -// and not an ephemeral one, which is never reused across sessions. An address -// is excused only for the loopback family the container publishes on: a -// 127.0.0.1 or 0.0.0.0 publication covers 127.0.0.1, a ::1 or :: one covers -// ::1, so a squatter on the other family is still refused. Anything this -// cannot establish — no recovery identity, no Docker engine — excuses nothing. +// Core supplies exact parent-service attribution for containers with arbitrary +// agent-owned names. Scope, namespace, lifecycle, the concrete name and the +// published loopback family are checked separately. Unlabelled containers retain +// only the existing exact-bare-name compatibility path; suffixes prove nothing. +// A present conflicting or malformed parent label always refuses the exception. func (runner *Runner) adoptedContainerAddresses(ctx context.Context) func(address string) bool { scopeID, namespace, ok := strings.Cut(runner.containerRecoveryIdentity, ":") if !ok || scopeID == "" || runner.instance == nil || runner.instance.Identity == nil || @@ -85,12 +73,12 @@ func (runner *Runner) adoptedContainerAddresses(ctx context.Context) func(addres containers, _ = listOwnedContainers(ctx, scopeID) } return slices.ContainsFunc(containers, func(c container.Summary) bool { - return adoptableContainer(&c, name, scopeID, namespace) && publishesAddress(&c, address) + return adoptableContainer(&c, identity, name, scopeID, namespace) && publishesAddress(&c, address) }) } } -func adoptableContainer(c *container.Summary, name, scopeID, namespace string) bool { +func adoptableContainer(c *container.Summary, service *resources.ServiceIdentity, name, scopeID, namespace string) bool { labels := c.Labels if labels[dockerrun.LabelCodeflyOwner] != "true" || labels[dockerrun.LabelCodeflyRecoveryScope] != scopeID || labels[dockerrun.LabelCodeflyEphemeral] == "true" || c.State != "running" { @@ -99,24 +87,26 @@ func adoptableContainer(c *container.Summary, name, scopeID, namespace string) b if found := labels[dockerrun.LabelCodeflyRecoveryNamespace]; found != "" && found != namespace { return false } - // This service's container, or one its agent derived from it: Core's - // ContainerName only prefixes and replaces slashes, so an agent's - // `-minio` is this name plus that suffix. The logical name label - // and the Docker name must agree on it, and the caller still requires the - // container to publish the proposed port on its own loopback family — so - // a sibling service whose name merely EXTENDS this one is excused only if - // it published this service's port, which is a misconfiguration this - // guard was never the detector for. - return ownedByService(labels[dockerrun.LabelCodeflyName], name) && - slices.ContainsFunc(c.Names, func(dockerName string) bool { - return ownedByService(strings.TrimPrefix(dockerName, "/"), name) - }) -} - -// ownedByService reports whether a container name is the service's own or one -// its agent derived from it by suffix. -func ownedByService(candidate, name string) bool { - return candidate == name || strings.HasPrefix(candidate, name+"-") + parent, err := dockerrun.ContainerServiceIdentity(labels) + if err != nil || service == nil { + return false + } + logicalName := labels[dockerrun.LabelCodeflyName] + if logicalName == "" { + return false + } + if parent != nil { + if parent.Unique() != service.Unique() { + return false + } + } else if logicalName != name { + // Compatibility for the pre-existing exact-name guard only. An absent + // label cannot extend ownership to a suffixed container. + return false + } + return slices.ContainsFunc(c.Names, func(dockerName string) bool { + return strings.TrimPrefix(dockerName, "/") == logicalName + }) } // publishesAddress reports whether c publishes the loopback host:port address diff --git a/pkg/orchestration/adopted_container_ports_test.go b/pkg/orchestration/adopted_container_ports_test.go index 56642191..27dfad14 100644 --- a/pkg/orchestration/adopted_container_ports_test.go +++ b/pkg/orchestration/adopted_container_ports_test.go @@ -25,6 +25,7 @@ func keptContainer(name string, port uint16) container.Summary { State: "running", Labels: map[string]string{ dockerrun.LabelCodeflyOwner: "true", + dockerrun.LabelCodeflyService: "web/gateway", dockerrun.LabelCodeflyName: name, dockerrun.LabelCodeflyRecoveryScope: testRecoveryScope, dockerrun.LabelCodeflyRecoveryNamespace: testRecoveryNamespace, @@ -52,6 +53,7 @@ func TestFirstInitAdoptsItsOwnKeptContainerAndRefusesAnyOtherHolder(t *testing.T {name: "another service's name", mutate: func(c *container.Summary) { c.Names = []string{"/codefly-other"} c.Labels[dockerrun.LabelCodeflyName] = "codefly-other" + c.Labels[dockerrun.LabelCodeflyService] = "other/service" }, refused: true}, // An agent may run several containers for one service and names each // after itself: service-object-storage publishes the service's own @@ -67,6 +69,27 @@ func TestFirstInitAdoptsItsOwnKeptContainerAndRefusesAnyOtherHolder(t *testing.T {name: "a sibling service's container", mutate: func(c *container.Summary) { c.Names = []string{"/codefly-other-web-gateway-minio"} c.Labels[dockerrun.LabelCodeflyName] = "codefly-other-web-gateway-minio" + c.Labels[dockerrun.LabelCodeflyService] = "other/gateway" + }, refused: true}, + {name: "distinct sibling sharing the name prefix", mutate: func(c *container.Summary) { + suffixed(c, "-admin") + c.Labels[dockerrun.LabelCodeflyService] = "web/gateway-admin" + }, refused: true}, + {name: "label and concrete name disagree", mutate: func(c *container.Summary) { + c.Names[0] += "-other" + }, refused: true}, + {name: "legacy exact bare name", mutate: func(c *container.Summary) { + delete(c.Labels, dockerrun.LabelCodeflyService) + }}, + {name: "legacy suffix proves no parent", mutate: func(c *container.Summary) { + suffixed(c, "-gateway") + delete(c.Labels, dockerrun.LabelCodeflyService) + }, refused: true}, + {name: "malformed parent on exact bare name", mutate: func(c *container.Summary) { + c.Labels[dockerrun.LabelCodeflyService] = "web/gateway/other" + }, refused: true}, + {name: "conflicting parent on exact bare name", mutate: func(c *container.Summary) { + c.Labels[dockerrun.LabelCodeflyService] = "web/other" }, refused: true}, {name: "ephemeral", mutate: func(c *container.Summary) { c.Labels[dockerrun.LabelCodeflyEphemeral] = "true" }, refused: true}, {name: "not running", mutate: func(c *container.Summary) { c.State = "exited" }, refused: true}, From eb9f4b0011bd37444ad3d69603ea4a88a03ba417 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 27 Oct 2026 16:41:56 -0400 Subject: [PATCH 3/4] docs(orchestration): bound container attribution claims --- pkg/orchestration/adopted_container_ports.go | 6 +++--- pkg/orchestration/adopted_container_ports_test.go | 3 ++- pkg/orchestration/runner.go | 6 +++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/pkg/orchestration/adopted_container_ports.go b/pkg/orchestration/adopted_container_ports.go index 2d93db61..51faf250 100644 --- a/pkg/orchestration/adopted_container_ports.go +++ b/pkg/orchestration/adopted_container_ports.go @@ -41,10 +41,10 @@ var listOwnedContainers = func(ctx context.Context, scopeID string) ([]container // adoptedContainerAddresses answers which loopback addresses the first-Init // port guard must not treat as a squatter: the ones published by the -// container this service's agent will adopt. A Docker store with +// container attributed to this service. A Docker store with // keep-running leaves its container up so the next run reattaches to it by -// name, and that container still publishes the service's deterministic port — -// the one this Init proposes. +// name, and that container still publishes the service's deterministic port. +// The agent decides actual reuse during Init; this check establishes ownership. // // Core supplies exact parent-service attribution for containers with arbitrary // agent-owned names. Scope, namespace, lifecycle, the concrete name and the diff --git a/pkg/orchestration/adopted_container_ports_test.go b/pkg/orchestration/adopted_container_ports_test.go index 27dfad14..2bdb407c 100644 --- a/pkg/orchestration/adopted_container_ports_test.go +++ b/pkg/orchestration/adopted_container_ports_test.go @@ -58,7 +58,8 @@ func TestFirstInitAdoptsItsOwnKeptContainerAndRefusesAnyOtherHolder(t *testing.T // An agent may run several containers for one service and names each // after itself: service-object-storage publishes the service's own // grpc port from `-gateway`, beside `-minio` and - // `-network`. Each is this service's container. + // a separate `-network` Docker network. Each container belongs + // to this service. {name: "its agent's gateway container", mutate: func(c *container.Summary) { suffixed(c, "-gateway") }, refused: false}, diff --git a/pkg/orchestration/runner.go b/pkg/orchestration/runner.go index 5cdda09f..fe9c4be6 100644 --- a/pkg/orchestration/runner.go +++ b/pkg/orchestration/runner.go @@ -1126,8 +1126,8 @@ type portCollision struct { // of both families before the service binds sees either squatter. // // adopted excuses an address whose listener is the service's own: the -// container its agent will adopt (Runner.adoptedContainerAddresses). nil -// excuses nothing. +// container attributed by Runner.adoptedContainerAddresses. The agent decides +// actual reuse during Init. nil excuses nothing. func boundNativePorts(ctx context.Context, mappings []*basev0.NetworkMapping, adopted func(address string) bool) []string { var collisions []portCollision for _, mapping := range mappings { @@ -1282,7 +1282,7 @@ func (runner *Runner) checkInitialPortAvailability(ctx context.Context, proposed return nil } // A listener that is the service's own kept container is not a ghost: its - // agent adopts it (keep-running), and 'codefly clear' would delete it. + // agent decides its reuse (keep-running); this guard must not delete it. if held := boundNativePorts(ctx, proposed, runner.adoptedContainerAddresses(ctx)); len(held) > 0 { // The holder is whoever bound the port first — it may be a leftover // codefly run, but it may equally be the user's own server, so the From 100d30d81d1d4f7e05f2b190679ab8bf77985358 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint <312536060+antoinetoussaint-byte@users.noreply.github.com> Date: Tue, 27 Oct 2026 16:57:09 -0400 Subject: [PATCH 4/4] fix(conformance): track the selected Core release line --- docs/supported-matrix.md | 34 +++++++--------------------------- pkg/conformance/matrix.json | 2 +- 2 files changed, 8 insertions(+), 28 deletions(-) diff --git a/docs/supported-matrix.md b/docs/supported-matrix.md index 6f81c7c7..93646fbe 100644 --- a/docs/supported-matrix.md +++ b/docs/supported-matrix.md @@ -27,33 +27,13 @@ When `go.mod` moves to a new core release line, update the `core` field in `go test ./pkg/conformance/...` before the full coverage and race gates. `TestMatrixCoreMatchesGoMod` refuses a stale declared version. -The current pin is a `v0.19.7` pseudo-version of core: `257e6b23`, branch -`chore/envelope-on-0.19.6`, which merges `v0.19.6` (with the git source -declaration, codefly-dev/core#832) into `5522c4da`, the commit carrying the -module envelope that `pkg/delivery/signing` uses. `v0.19.6` carries -`b81d92e6`, the merge of codefly-dev/core#812 — the module manifest -(`solutionhost.ModuleManifest`, one signed document per module, whose -workload is `(service, container)` and which names no build) and -`workloadidentity`, which a native run attests its workloads with. Its -companion is `github.com/codefly-dev/sdk-go/workcontext` at `70dc7a54` -(codefly-dev/sdk-go#80, a credential carries no build seal). Releasing a CLI -on it requires core `v0.19.6` and a `workcontext` release past `v0.1.0` to -be tagged first. The previous pin, a `v0.19.5` pseudo-version, carried the -`runtime-started-artifact/v1` contract (codefly-dev/core#796, -codefly-dev/cli#989), which this CLI no longer reads. On top of `v0.19.2`, `v0.19.4` adds -the issuer's non-consuming `Authority.ReadBack` and moves kustomize back to -`v0.21.1`, whose kube-openapi does not pull `structured-merge-diff/v7` (the -`v0.21.3` that `v0.19.2` and `v0.19.3` carried broke every build compiling -client-go). `v0.19.2` added a -solution manifest's scoped `api.consumes` and its consumption contract (#784) -and an issuer-assigned Work Context session (#783). `v0.19.1` -verifies delivered carriers -against pinned platform keys (#773), projects a service account entirely on a -caller-supplied filesystem (#774, #777), and carries ToolProjection and slot-only authority (#755), ciguard (#750), caller tokens, -image-lock/publish evidence (#759), and configuration-origin evidence (#746), -in addition to #752's composition-provenance contract. This source matrix does -not qualify released agents for the new -`deployment-composition-provenance/v1` capability. +The current pin targets Core's `v0.19.8` release line. The exact source +revision is declared in `go.mod`; the CLI consumes Core's container parent +attribution and release acquisition APIs. These source gates qualify the CLI +against that selected revision. They do not qualify separately compiled agents +or imply that the target Core release has been published. Agents creating +sidecar containers must adopt the attribution API in their own source and +qualify their lifecycle before consumers select the resulting release. A missing receipt can be a consequence of an earlier failure: coverage and race run `go test -failfast`, so a failed package can stop the npm tests in diff --git a/pkg/conformance/matrix.json b/pkg/conformance/matrix.json index e17544d6..7ad1a155 100644 --- a/pkg/conformance/matrix.json +++ b/pkg/conformance/matrix.json @@ -1,7 +1,7 @@ { "schema_version": 1, "cli": "source", - "core": "v0.19.7", + "core": "v0.19.8", "rows": [ { "id": "linux-amd64-source",