Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
93 changes: 93 additions & 0 deletions internal/stackext/stackext.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
6 changes: 6 additions & 0 deletions pkg/compose/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading