diff --git a/commands/history/import.go b/commands/history/import.go new file mode 100644 index 00000000..afd95608 --- /dev/null +++ b/commands/history/import.go @@ -0,0 +1,135 @@ +package history + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "os" + "strings" + + remoteutil "github.com/docker/buildx/driver/remote/util" + "github.com/docker/buildx/util/cobrautil/completion" + "github.com/docker/buildx/util/desktop" + "github.com/docker/cli/cli/command" + "github.com/pkg/browser" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +type importOptions struct { + file []string +} + +func runImport(ctx context.Context, dockerCli command.Cli, opts importOptions) error { + sock, err := desktop.BuildServerAddr() + if err != nil { + return err + } + + tr := http.DefaultTransport.(*http.Transport).Clone() + tr.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) { + network, addr, ok := strings.Cut(sock, "://") + if !ok { + return nil, errors.Errorf("invalid endpoint address: %s", sock) + } + return remoteutil.DialContext(ctx, network, addr) + } + + client := &http.Client{ + Transport: tr, + } + + var urls []string + + if len(opts.file) == 0 { + u, err := importFrom(ctx, client, os.Stdin) + if err != nil { + return err + } + urls = append(urls, u...) + } else { + for _, fn := range opts.file { + var f *os.File + var rdr io.Reader = os.Stdin + if fn != "-" { + f, err = os.Open(fn) + if err != nil { + return errors.Wrapf(err, "failed to open file %s", fn) + } + rdr = f + } + u, err := importFrom(ctx, client, rdr) + if err != nil { + return err + } + urls = append(urls, u...) + if f != nil { + f.Close() + } + } + } + + if len(urls) == 0 { + return errors.New("no build records found in the bundle") + } + + for i, url := range urls { + fmt.Fprintln(dockerCli.Err(), url) + if i == 0 { + err = browser.OpenURL(url) + } + } + return err +} + +func importFrom(ctx context.Context, c *http.Client, rdr io.Reader) ([]string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://docker-desktop/upload", rdr) + if err != nil { + return nil, errors.Wrap(err, "failed to create request") + } + + resp, err := c.Do(req) + if err != nil { + return nil, errors.Wrap(err, "failed to send request, check if Docker Desktop is running") + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, errors.Errorf("failed to import build: %s", string(body)) + } + + var refs []string + dec := json.NewDecoder(resp.Body) + if err := dec.Decode(&refs); err != nil { + return nil, errors.Wrap(err, "failed to decode response") + } + + var urls []string + for _, ref := range refs { + urls = append(urls, desktop.BuildURL(fmt.Sprintf(".imported/_/%s", ref))) + } + return urls, err +} + +func importCmd(dockerCli command.Cli, _ RootOptions) *cobra.Command { + var options importOptions + + cmd := &cobra.Command{ + Use: "import [OPTIONS] < bundle.dockerbuild", + Short: "Import a build into Docker Desktop", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runImport(cmd.Context(), dockerCli, options) + }, + ValidArgsFunction: completion.Disable, + } + + flags := cmd.Flags() + flags.StringArrayVarP(&options.file, "file", "f", nil, "Import from a file path") + + return cmd +} diff --git a/commands/history/root.go b/commands/history/root.go index 0b88545b..ae1f821e 100644 --- a/commands/history/root.go +++ b/commands/history/root.go @@ -25,6 +25,7 @@ func RootCmd(rootcmd *cobra.Command, dockerCli command.Cli, opts RootOptions) *c inspectCmd(dockerCli, opts), openCmd(dockerCli, opts), traceCmd(dockerCli, opts), + importCmd(dockerCli, opts), ) return cmd diff --git a/docs/reference/buildx_history.md b/docs/reference/buildx_history.md index 0935efb7..e2174453 100644 --- a/docs/reference/buildx_history.md +++ b/docs/reference/buildx_history.md @@ -7,6 +7,7 @@ Commands to work on build records | Name | Description | |:---------------------------------------|:-----------------------------------------------| +| [`import`](buildx_history_import.md) | Import a build into Docker Desktop | | [`inspect`](buildx_history_inspect.md) | Inspect a build | | [`logs`](buildx_history_logs.md) | Print the logs of a build | | [`ls`](buildx_history_ls.md) | List build records | diff --git a/docs/reference/buildx_history_import.md b/docs/reference/buildx_history_import.md new file mode 100644 index 00000000..618a485d --- /dev/null +++ b/docs/reference/buildx_history_import.md @@ -0,0 +1,16 @@ +# docker buildx history import + + +Import a build into Docker Desktop + +### Options + +| Name | Type | Default | Description | +|:----------------|:--------------|:--------|:-----------------------------------------| +| `--builder` | `string` | | Override the configured builder instance | +| `-D`, `--debug` | `bool` | | Enable debug logging | +| `-f`, `--file` | `stringArray` | | Import from a file path | + + + + diff --git a/util/desktop/paths_darwin.go b/util/desktop/paths_darwin.go new file mode 100644 index 00000000..735ec0db --- /dev/null +++ b/util/desktop/paths_darwin.go @@ -0,0 +1,21 @@ +package desktop + +import ( + "os" + "path/filepath" + + "github.com/pkg/errors" +) + +const ( + socketName = "docker-desktop-build.sock" + socketPath = "Library/Containers/com.docker.docker/Data" +) + +func BuildServerAddr() (string, error) { + dir, err := os.UserHomeDir() + if err != nil { + return "", errors.Wrap(err, "failed to get user home directory") + } + return "unix://" + filepath.Join(dir, socketPath, socketName), nil +} diff --git a/util/desktop/paths_linux.go b/util/desktop/paths_linux.go new file mode 100644 index 00000000..9b90bf67 --- /dev/null +++ b/util/desktop/paths_linux.go @@ -0,0 +1,29 @@ +package desktop + +import ( + "os" + "path/filepath" + + "github.com/pkg/errors" +) + +const ( + socketName = "docker-desktop-build.sock" + socketPath = ".docker/desktop" + wslSocketPath = "/mnt/wsl/docker-desktop/shared-sockets/host-services" +) + +func BuildServerAddr() (string, error) { + if os.Getenv("WSL_DISTRO_NAME") != "" { + socket := filepath.Join(wslSocketPath, socketName) + if _, err := os.Stat(socket); os.IsNotExist(err) { + return "", errors.New("Docker Desktop Build backend is not yet supported on WSL. Please run this command on Windows host instead.") //nolint:revive + } + return "unix://" + socket, nil + } + dir, err := os.UserHomeDir() + if err != nil { + return "", errors.Wrap(err, "failed to get user home directory") + } + return "unix://" + filepath.Join(dir, socketPath, socketName), nil +} diff --git a/util/desktop/paths_unsupported.go b/util/desktop/paths_unsupported.go new file mode 100644 index 00000000..0f6fffbb --- /dev/null +++ b/util/desktop/paths_unsupported.go @@ -0,0 +1,13 @@ +//go:build !windows && !darwin && !linux + +package desktop + +import ( + "runtime" + + "github.com/pkg/errors" +) + +func BuildServerAddr() (string, error) { + return "", errors.Errorf("Docker Desktop unsupported on %s", runtime.GOOS) +} diff --git a/util/desktop/paths_windows.go b/util/desktop/paths_windows.go new file mode 100644 index 00000000..fd8f9c6e --- /dev/null +++ b/util/desktop/paths_windows.go @@ -0,0 +1,5 @@ +package desktop + +func BuildServerAddr() (string, error) { + return "npipe:////./pipe/dockerDesktopBuildServer", nil +}