Skip to content

Commit 17d4229

Browse files
authored
feat(desktop): add Docker Desktop detection and client skeleton (#11593)
1 parent 4efb897 commit 17d4229

File tree

11 files changed

+318
-36
lines changed

11 files changed

+318
-36
lines changed

Diff for: cmd/compose/compose.go

+27-6
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"github.com/docker/cli/cli-plugins/manager"
3838
"github.com/docker/cli/cli/command"
3939
"github.com/docker/compose/v2/cmd/formatter"
40+
"github.com/docker/compose/v2/internal/desktop"
4041
"github.com/docker/compose/v2/internal/tracing"
4142
"github.com/docker/compose/v2/pkg/api"
4243
"github.com/docker/compose/v2/pkg/compose"
@@ -365,11 +366,17 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { //
365366
}
366367
},
367368
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
369+
ctx := cmd.Context()
370+
371+
// (1) process env vars
368372
err := setEnvWithDotEnv(&opts)
369373
if err != nil {
370374
return err
371375
}
372376
parent := cmd.Root()
377+
378+
// (2) call parent pre-run
379+
// TODO(milas): this seems incorrect, remove or document
373380
if parent != nil {
374381
parentPrerun := parent.PersistentPreRunE
375382
if parentPrerun != nil {
@@ -379,21 +386,21 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { //
379386
}
380387
}
381388
}
389+
390+
// (3) set up display/output
391+
if verbose {
392+
logrus.SetLevel(logrus.TraceLevel)
393+
}
382394
if noAnsi {
383395
if ansi != "auto" {
384396
return errors.New(`cannot specify DEPRECATED "--no-ansi" and "--ansi". Please use only "--ansi"`)
385397
}
386398
ansi = "never"
387399
fmt.Fprint(os.Stderr, "option '--no-ansi' is DEPRECATED ! Please use '--ansi' instead.\n")
388400
}
389-
if verbose {
390-
logrus.SetLevel(logrus.TraceLevel)
391-
}
392-
393401
if v, ok := os.LookupEnv("COMPOSE_ANSI"); ok && !cmd.Flags().Changed("ansi") {
394402
ansi = v
395403
}
396-
397404
formatter.SetANSIMode(dockerCli, ansi)
398405

399406
if noColor, ok := os.LookupEnv("NO_COLOR"); ok && noColor != "" {
@@ -430,6 +437,7 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { //
430437
return fmt.Errorf("unsupported --progress value %q", opts.Progress)
431438
}
432439

440+
// (4) options validation / normalization
433441
if opts.WorkDir != "" {
434442
if opts.ProjectDir != "" {
435443
return errors.New(`cannot specify DEPRECATED "--workdir" and "--project-directory". Please use only "--project-directory" instead`)
@@ -466,13 +474,26 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { //
466474
parallel = i
467475
}
468476
if parallel > 0 {
477+
logrus.Debugf("Limiting max concurrency to %d jobs", parallel)
469478
backend.MaxConcurrency(parallel)
470479
}
471-
ctx, err := backend.DryRunMode(cmd.Context(), dryRun)
480+
481+
// (5) dry run detection
482+
ctx, err = backend.DryRunMode(ctx, dryRun)
472483
if err != nil {
473484
return err
474485
}
475486
cmd.SetContext(ctx)
487+
488+
// (6) Desktop integration
489+
if db, ok := backend.(desktop.IntegrationService); ok {
490+
if err := db.MaybeEnableDesktopIntegration(ctx); err != nil {
491+
// not fatal, Compose will still work but behave as though
492+
// it's not running as part of Docker Desktop
493+
logrus.Debugf("failed to enable Docker Desktop integration: %v", err)
494+
}
495+
}
496+
476497
return nil
477498
},
478499
}

Diff for: cmd/main.go

+7-6
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/docker/cli/cli/command"
2626
"github.com/docker/compose/v2/cmd/cmdtrace"
2727
"github.com/docker/docker/client"
28+
"github.com/sirupsen/logrus"
2829
"github.com/spf13/cobra"
2930

3031
"github.com/docker/compose/v2/cmd/compatibility"
@@ -37,7 +38,7 @@ func pluginMain() {
3738
plugin.Run(func(dockerCli command.Cli) *cobra.Command {
3839
backend := compose.NewComposeService(dockerCli)
3940
cmd := commands.RootCommand(dockerCli, backend)
40-
originalPreRun := cmd.PersistentPreRunE
41+
originalPreRunE := cmd.PersistentPreRunE
4142
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
4243
// initialize the dockerCli instance
4344
if err := plugin.PersistentPreRunE(cmd, args); err != nil {
@@ -46,12 +47,12 @@ func pluginMain() {
4647
// compose-specific initialization
4748
dockerCliPostInitialize(dockerCli)
4849

49-
// TODO(milas): add an env var to enable logging from the
50-
// OTel components for debugging purposes
51-
_ = cmdtrace.Setup(cmd, dockerCli, os.Args[1:])
50+
if err := cmdtrace.Setup(cmd, dockerCli, os.Args[1:]); err != nil {
51+
logrus.Debugf("failed to enable tracing: %v", err)
52+
}
5253

53-
if originalPreRun != nil {
54-
return originalPreRun(cmd, args)
54+
if originalPreRunE != nil {
55+
return originalPreRunE(cmd, args)
5556
}
5657
return nil
5758
}

Diff for: go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ require (
3838
github.com/stretchr/testify v1.8.4
3939
github.com/theupdateframework/notary v0.7.0
4040
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375
41+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0
4142
go.opentelemetry.io/otel v1.19.0
4243
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0
4344
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0
@@ -147,7 +148,6 @@ require (
147148
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
148149
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.45.0 // indirect
149150
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.45.0 // indirect
150-
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect
151151
go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.42.0 // indirect
152152
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.42.0 // indirect
153153
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.42.0 // indirect

Diff for: internal/desktop/client.go

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
Copyright 2024 Docker Compose CLI authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package desktop
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
"fmt"
23+
"net"
24+
"net/http"
25+
"strings"
26+
27+
"github.com/docker/compose/v2/internal/memnet"
28+
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
29+
)
30+
31+
// Client for integration with Docker Desktop features.
32+
type Client struct {
33+
client *http.Client
34+
}
35+
36+
// NewClient creates a Desktop integration client for the provided in-memory
37+
// socket address (AF_UNIX or named pipe).
38+
func NewClient(apiEndpoint string) *Client {
39+
var transport http.RoundTripper = &http.Transport{
40+
DisableCompression: true,
41+
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
42+
return memnet.DialEndpoint(ctx, apiEndpoint)
43+
},
44+
}
45+
transport = otelhttp.NewTransport(transport)
46+
47+
c := &Client{
48+
client: &http.Client{Transport: transport},
49+
}
50+
return c
51+
}
52+
53+
// Close releases any open connections.
54+
func (c *Client) Close() error {
55+
c.client.CloseIdleConnections()
56+
return nil
57+
}
58+
59+
type PingResponse struct {
60+
ServerTime int64 `json:"serverTime"`
61+
}
62+
63+
// Ping is a minimal API used to ensure that the server is available.
64+
func (c *Client) Ping(ctx context.Context) (*PingResponse, error) {
65+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, backendURL("/ping"), http.NoBody)
66+
if err != nil {
67+
return nil, err
68+
}
69+
resp, err := c.client.Do(req)
70+
if err != nil {
71+
return nil, err
72+
}
73+
defer func() {
74+
_ = resp.Body.Close()
75+
}()
76+
if resp.StatusCode != http.StatusOK {
77+
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
78+
}
79+
80+
var ret PingResponse
81+
if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil {
82+
return nil, err
83+
}
84+
return &ret, nil
85+
}
86+
87+
// backendURL generates a URL for the given API path.
88+
//
89+
// NOTE: Custom transport handles communication. The host is to create a valid
90+
// URL for the Go http.Client that is also descriptive in error/logs.
91+
func backendURL(path string) string {
92+
return "http://docker-desktop/" + strings.TrimPrefix(path, "/")
93+
}

Diff for: internal/desktop/integration.go

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
Copyright 2024 Docker Compose CLI authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package desktop
18+
19+
import (
20+
"context"
21+
)
22+
23+
type IntegrationService interface {
24+
MaybeEnableDesktopIntegration(ctx context.Context) error
25+
}

Diff for: internal/memnet/conn.go

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
Copyright 2020 Docker Compose CLI authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package memnet
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"net"
23+
"strings"
24+
)
25+
26+
func DialEndpoint(ctx context.Context, endpoint string) (net.Conn, error) {
27+
if addr, ok := strings.CutPrefix(endpoint, "unix://"); ok {
28+
return Dial(ctx, "unix", addr)
29+
}
30+
if addr, ok := strings.CutPrefix(endpoint, "npipe://"); ok {
31+
return Dial(ctx, "npipe", addr)
32+
}
33+
return nil, fmt.Errorf("unsupported protocol for address: %s", endpoint)
34+
}
35+
36+
func Dial(ctx context.Context, network, addr string) (net.Conn, error) {
37+
var d net.Dialer
38+
switch network {
39+
case "unix":
40+
if err := validateSocketPath(addr); err != nil {
41+
return nil, err
42+
}
43+
return d.DialContext(ctx, "unix", addr)
44+
case "npipe":
45+
// N.B. this will return an error on non-Windows
46+
return dialNamedPipe(ctx, addr)
47+
default:
48+
return nil, fmt.Errorf("unsupported network: %s", network)
49+
}
50+
}

Diff for: internal/tracing/conn_unix.go renamed to internal/memnet/conn_unix.go

+7-12
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,24 @@
1616
limitations under the License.
1717
*/
1818

19-
package tracing
19+
package memnet
2020

2121
import (
2222
"context"
2323
"fmt"
2424
"net"
25-
"strings"
2625
"syscall"
2726
)
2827

2928
const maxUnixSocketPathSize = len(syscall.RawSockaddrUnix{}.Path)
3029

31-
func DialInMemory(ctx context.Context, addr string) (net.Conn, error) {
32-
if !strings.HasPrefix(addr, "unix://") {
33-
return nil, fmt.Errorf("not a Unix socket address: %s", addr)
34-
}
35-
addr = strings.TrimPrefix(addr, "unix://")
30+
func dialNamedPipe(_ context.Context, _ string) (net.Conn, error) {
31+
return nil, fmt.Errorf("named pipes are only available on Windows")
32+
}
3633

34+
func validateSocketPath(addr string) error {
3735
if len(addr) > maxUnixSocketPathSize {
38-
//goland:noinspection GoErrorStringFormat
39-
return nil, fmt.Errorf("Unix socket address is too long: %s", addr)
36+
return fmt.Errorf("socket address is too long: %s", addr)
4037
}
41-
42-
var d net.Dialer
43-
return d.DialContext(ctx, "unix", addr)
38+
return nil
4439
}

Diff for: internal/tracing/conn_windows.go renamed to internal/memnet/conn_windows.go

+7-9
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,20 @@
1414
limitations under the License.
1515
*/
1616

17-
package tracing
17+
package memnet
1818

1919
import (
2020
"context"
21-
"fmt"
2221
"net"
23-
"strings"
2422

2523
"github.com/Microsoft/go-winio"
2624
)
2725

28-
func DialInMemory(ctx context.Context, addr string) (net.Conn, error) {
29-
if !strings.HasPrefix(addr, "npipe://") {
30-
return nil, fmt.Errorf("not a named pipe address: %s", addr)
31-
}
32-
addr = strings.TrimPrefix(addr, "npipe://")
33-
26+
func dialNamedPipe(ctx context.Context, addr string) (net.Conn, error) {
3427
return winio.DialPipeContext(ctx, addr)
3528
}
29+
30+
func validateSocketPath(addr string) error {
31+
// AF_UNIX sockets do not have strict path limits on Windows
32+
return nil
33+
}

Diff for: internal/tracing/docker_context.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424

2525
"github.com/docker/cli/cli/command"
2626
"github.com/docker/cli/cli/context/store"
27+
"github.com/docker/compose/v2/internal/memnet"
2728
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
2829
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
2930
"google.golang.org/grpc"
@@ -67,7 +68,9 @@ func traceClientFromDockerContext(dockerCli command.Cli, otelEnv envMap) (otlptr
6768
conn, err := grpc.DialContext(
6869
dialCtx,
6970
cfg.Endpoint,
70-
grpc.WithContextDialer(DialInMemory),
71+
grpc.WithContextDialer(memnet.DialEndpoint),
72+
// this dial is restricted to using a local Unix socket / named pipe,
73+
// so there is no need for TLS
7174
grpc.WithTransportCredentials(insecure.NewCredentials()),
7275
)
7376
if err != nil {

0 commit comments

Comments
 (0)