Skip to content

Commit

Permalink
feat(desktop): add Docker Desktop detection and client skeleton (#11593)
Browse files Browse the repository at this point in the history
  • Loading branch information
milas committed Mar 12, 2024
1 parent 4efb897 commit 17d4229
Show file tree
Hide file tree
Showing 11 changed files with 318 additions and 36 deletions.
33 changes: 27 additions & 6 deletions cmd/compose/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/cmd/formatter"
"github.com/docker/compose/v2/internal/desktop"
"github.com/docker/compose/v2/internal/tracing"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/compose"
Expand Down Expand Up @@ -365,11 +366,17 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { //
}
},
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

// (1) process env vars
err := setEnvWithDotEnv(&opts)
if err != nil {
return err
}
parent := cmd.Root()

// (2) call parent pre-run
// TODO(milas): this seems incorrect, remove or document
if parent != nil {
parentPrerun := parent.PersistentPreRunE
if parentPrerun != nil {
Expand All @@ -379,21 +386,21 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { //
}
}
}

// (3) set up display/output
if verbose {
logrus.SetLevel(logrus.TraceLevel)
}
if noAnsi {
if ansi != "auto" {
return errors.New(`cannot specify DEPRECATED "--no-ansi" and "--ansi". Please use only "--ansi"`)
}
ansi = "never"
fmt.Fprint(os.Stderr, "option '--no-ansi' is DEPRECATED ! Please use '--ansi' instead.\n")
}
if verbose {
logrus.SetLevel(logrus.TraceLevel)
}

if v, ok := os.LookupEnv("COMPOSE_ANSI"); ok && !cmd.Flags().Changed("ansi") {
ansi = v
}

formatter.SetANSIMode(dockerCli, ansi)

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

// (4) options validation / normalization
if opts.WorkDir != "" {
if opts.ProjectDir != "" {
return errors.New(`cannot specify DEPRECATED "--workdir" and "--project-directory". Please use only "--project-directory" instead`)
Expand Down Expand Up @@ -466,13 +474,26 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { //
parallel = i
}
if parallel > 0 {
logrus.Debugf("Limiting max concurrency to %d jobs", parallel)
backend.MaxConcurrency(parallel)
}
ctx, err := backend.DryRunMode(cmd.Context(), dryRun)

// (5) dry run detection
ctx, err = backend.DryRunMode(ctx, dryRun)
if err != nil {
return err
}
cmd.SetContext(ctx)

// (6) Desktop integration
if db, ok := backend.(desktop.IntegrationService); ok {
if err := db.MaybeEnableDesktopIntegration(ctx); err != nil {
// not fatal, Compose will still work but behave as though
// it's not running as part of Docker Desktop
logrus.Debugf("failed to enable Docker Desktop integration: %v", err)
}
}

return nil
},
}
Expand Down
13 changes: 7 additions & 6 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/compose/v2/cmd/cmdtrace"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"

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

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

if originalPreRun != nil {
return originalPreRun(cmd, args)
if originalPreRunE != nil {
return originalPreRunE(cmd, args)
}
return nil
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ require (
github.com/stretchr/testify v1.8.4
github.com/theupdateframework/notary v0.7.0
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0
go.opentelemetry.io/otel v1.19.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0
Expand Down Expand Up @@ -147,7 +148,6 @@ require (
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.45.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.45.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.42.0 // indirect
Expand Down
93 changes: 93 additions & 0 deletions internal/desktop/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
Copyright 2024 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 desktop

import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"strings"

"github.com/docker/compose/v2/internal/memnet"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

// Client for integration with Docker Desktop features.
type Client struct {
client *http.Client
}

// NewClient creates a Desktop integration client for the provided in-memory
// socket address (AF_UNIX or named pipe).
func NewClient(apiEndpoint string) *Client {
var transport http.RoundTripper = &http.Transport{
DisableCompression: true,
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return memnet.DialEndpoint(ctx, apiEndpoint)
},
}
transport = otelhttp.NewTransport(transport)

c := &Client{
client: &http.Client{Transport: transport},
}
return c
}

// Close releases any open connections.
func (c *Client) Close() error {
c.client.CloseIdleConnections()
return nil
}

type PingResponse struct {
ServerTime int64 `json:"serverTime"`
}

// Ping is a minimal API used to ensure that the server is available.
func (c *Client) Ping(ctx context.Context) (*PingResponse, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, backendURL("/ping"), http.NoBody)
if err != nil {
return nil, err
}
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}

var ret PingResponse
if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil {
return nil, err
}
return &ret, nil
}

// backendURL generates a URL for the given API path.
//
// NOTE: Custom transport handles communication. The host is to create a valid
// URL for the Go http.Client that is also descriptive in error/logs.
func backendURL(path string) string {
return "http://docker-desktop/" + strings.TrimPrefix(path, "/")
}
25 changes: 25 additions & 0 deletions internal/desktop/integration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
Copyright 2024 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 desktop

import (
"context"
)

type IntegrationService interface {
MaybeEnableDesktopIntegration(ctx context.Context) error
}
50 changes: 50 additions & 0 deletions internal/memnet/conn.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
Copyright 2020 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 memnet

import (
"context"
"fmt"
"net"
"strings"
)

func DialEndpoint(ctx context.Context, endpoint string) (net.Conn, error) {
if addr, ok := strings.CutPrefix(endpoint, "unix://"); ok {
return Dial(ctx, "unix", addr)
}
if addr, ok := strings.CutPrefix(endpoint, "npipe://"); ok {
return Dial(ctx, "npipe", addr)
}
return nil, fmt.Errorf("unsupported protocol for address: %s", endpoint)
}

func Dial(ctx context.Context, network, addr string) (net.Conn, error) {
var d net.Dialer
switch network {
case "unix":
if err := validateSocketPath(addr); err != nil {
return nil, err
}
return d.DialContext(ctx, "unix", addr)
case "npipe":
// N.B. this will return an error on non-Windows
return dialNamedPipe(ctx, addr)
default:
return nil, fmt.Errorf("unsupported network: %s", network)
}
}
19 changes: 7 additions & 12 deletions internal/tracing/conn_unix.go → internal/memnet/conn_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,24 @@
limitations under the License.
*/

package tracing
package memnet

import (
"context"
"fmt"
"net"
"strings"
"syscall"
)

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

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

func validateSocketPath(addr string) error {
if len(addr) > maxUnixSocketPathSize {
//goland:noinspection GoErrorStringFormat
return nil, fmt.Errorf("Unix socket address is too long: %s", addr)
return fmt.Errorf("socket address is too long: %s", addr)
}

var d net.Dialer
return d.DialContext(ctx, "unix", addr)
return nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,20 @@
limitations under the License.
*/

package tracing
package memnet

import (
"context"
"fmt"
"net"
"strings"

"github.com/Microsoft/go-winio"
)

func DialInMemory(ctx context.Context, addr string) (net.Conn, error) {
if !strings.HasPrefix(addr, "npipe://") {
return nil, fmt.Errorf("not a named pipe address: %s", addr)
}
addr = strings.TrimPrefix(addr, "npipe://")

func dialNamedPipe(ctx context.Context, addr string) (net.Conn, error) {
return winio.DialPipeContext(ctx, addr)
}

func validateSocketPath(addr string) error {
// AF_UNIX sockets do not have strict path limits on Windows
return nil
}
5 changes: 4 additions & 1 deletion internal/tracing/docker_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (

"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/context/store"
"github.com/docker/compose/v2/internal/memnet"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"google.golang.org/grpc"
Expand Down Expand Up @@ -67,7 +68,9 @@ func traceClientFromDockerContext(dockerCli command.Cli, otelEnv envMap) (otlptr
conn, err := grpc.DialContext(
dialCtx,
cfg.Endpoint,
grpc.WithContextDialer(DialInMemory),
grpc.WithContextDialer(memnet.DialEndpoint),
// this dial is restricted to using a local Unix socket / named pipe,
// so there is no need for TLS
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
Expand Down
Loading

0 comments on commit 17d4229

Please sign in to comment.