diff --git a/go.mod b/go.mod index cd69d80beb..c4aaf9bfe6 100644 --- a/go.mod +++ b/go.mod @@ -219,3 +219,5 @@ exclude ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 ) + +replace github.com/docker/cli => github.com/ndeloof/cli v0.0.0-20260812152003-b90d88ea3ad1 diff --git a/go.sum b/go.sum index 96ae733061..8f721098cd 100644 --- a/go.sum +++ b/go.sum @@ -177,8 +177,6 @@ github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxK github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/buildx v0.36.1 h1:U7JTUSSeooFXbG+0HcI+CMjEIkvuZgJegXRZ1q7K63Q= github.com/docker/buildx v0.36.1/go.mod h1:pkpx66TqJD4HOS65fORu9MYBEDbeiRxnGdN3VpH8SPM= -github.com/docker/cli v29.6.2+incompatible h1:/bjePvcbbFTnRrMfWJBY7AjfICdsiLVgHn6LwTVOcqw= -github.com/docker/cli v29.6.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli-docs-tool v0.11.0 h1:7d8QARFb7QEobizqxmEM7fOteZEHwH/zWgHQtHZEcfE= github.com/docker/cli-docs-tool v0.11.0/go.mod h1:ma8BKiisUo8D6W05XEYIh3oa1UbgrZhi1nowyKFJa8Q= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= @@ -443,6 +441,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= +github.com/ndeloof/cli v0.0.0-20260812152003-b90d88ea3ad1 h1:+piSMfZKPEtnnhalfeK4h7tNhcq5HdCBdDufN0TD6fs= +github.com/ndeloof/cli v0.0.0-20260812152003-b90d88ea3ad1/go.mod h1:wxmb4t3v3ZwISBr3B/B631+3VJe+erAxkl3vXQwXj2c= github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/open-policy-agent/opa v1.14.1 h1:MhurLB9mSbXmojYFCmGbiC1Uagu1+aFAV4XVotDA86M= diff --git a/internal/stackext/stackext.go b/internal/stackext/stackext.go new file mode 100644 index 0000000000..efc5a67474 --- /dev/null +++ b/internal/stackext/stackext.go @@ -0,0 +1,93 @@ +/* + Copyright 2026 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Package stackext is a proof of concept for publishing the resolved Compose +// project to the engine through the com.docker.compose.stack.v0 extension +// point of the moby extensions framework (moby/moby#53021): a gRPC service +// exposed by the daemon on the API socket itself (h2c). Best effort by +// design: an engine without the extension answers codes.Unimplemented, an +// engine without h2c support fails at transport level, and both are treated +// as "feature absent". +package stackext + +import ( + "context" + "time" + + "github.com/compose-spec/compose-go/v2/types" + cligrpc "github.com/docker/cli/cli/grpc" + "github.com/sirupsen/logrus" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/encoding/protowire" +) + +const publishMethod = "/com.docker.compose.stack.v0.Stack/Publish" + +// rawCodec sends pre-encoded protobuf wire bytes as-is, mirroring the +// passthrough codec the extensions gRPC proxy uses daemon-side. It reports +// "proto" so the request carries the default content-subtype. +type rawCodec struct{} + +func (rawCodec) Marshal(v any) ([]byte, error) { return v.([]byte), nil } +func (rawCodec) Unmarshal(data []byte, v any) error { *(v.(*[]byte)) = data; return nil } +func (rawCodec) Name() string { return "proto" } + +// Publish pushes the resolved project definition to the engine's compose +// stack extension point, when the engine exposes one. Never fails the caller. +// The connection goes through cli/grpc.Connect (docker/cli#7183), which hides +// the transport of the current context (unix/npipe socket, tcp with or +// without TLS, connection helpers) and the h2c/legacy-upgrade negotiation. +func Publish(ctx context.Context, dockerCli cligrpc.DockerCLI, project *types.Project, complete bool) { + model, err := project.MarshalYAML() + if err != nil { + logrus.Debugf("stackext: cannot marshal project: %v", err) + return + } + + // hand-encoded com.docker.compose.stack.v0.PublishRequest + var payload []byte + payload = protowire.AppendTag(payload, 1, protowire.BytesType) + payload = protowire.AppendString(payload, project.Name) + payload = protowire.AppendTag(payload, 2, protowire.BytesType) + payload = protowire.AppendBytes(payload, model) + if complete { + payload = protowire.AppendTag(payload, 3, protowire.VarintType) + payload = protowire.AppendVarint(payload, 1) + } + + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + conn, err := cligrpc.Connect(ctx, dockerCli) + if err != nil { + logrus.Debugf("stackext: cannot connect to the engine gRPC endpoint: %v", err) + return + } + defer func() { _ = conn.Close() }() + + var reply []byte + err = conn.Invoke(ctx, publishMethod, payload, &reply, grpc.ForceCodec(rawCodec{})) + switch status.Code(err) { + case codes.OK: + logrus.Infof("stackext: compose stack definition published to engine extension") + case codes.Unimplemented: + logrus.Debugf("stackext: engine does not expose the compose stack extension point") + default: + logrus.Debugf("stackext: publish failed: %v", err) + } +} diff --git a/pkg/compose/up.go b/pkg/compose/up.go index b2bcb3f7db..7473982696 100644 --- a/pkg/compose/up.go +++ b/pkg/compose/up.go @@ -37,11 +37,17 @@ import ( "github.com/docker/compose/v5/cmd/formatter" "github.com/docker/compose/v5/internal/desktop" + "github.com/docker/compose/v5/internal/stackext" "github.com/docker/compose/v5/internal/tracing" "github.com/docker/compose/v5/pkg/api" ) func (s *composeService) Up(ctx context.Context, project *types.Project, options api.UpOptions) error { //nolint:gocyclo + // PoC: publish the resolved stack definition to the engine's compose + // stack extension point (moby extensions framework) before anything else, + // best effort + stackext.Publish(ctx, s.dockerCli, project, len(options.Create.Services) == 0) + err := Run(ctx, tracing.SpanWrapFunc("project/up", tracing.ProjectOptions(ctx, project), func(ctx context.Context) error { err := s.create(ctx, project, options.Create) if err != nil {