Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

history: add history import command #3039

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
109 changes: 109 additions & 0 deletions commands/history/import.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
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 rdr io.Reader = os.Stdin
if opts.file != "" {
f, err := os.Open(opts.file)
if err != nil {
return errors.Wrap(err, "failed to open file")
}
defer f.Close()
rdr = f
}

req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://docker-desktop/upload", rdr)
if err != nil {
return errors.Wrap(err, "failed to create request")
}

resp, err := client.Do(req)
if err != nil {
return 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 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 errors.Wrap(err, "failed to decode response")
}

if len(refs) == 0 {
return errors.New("no build records found in the bundle")
}

for i, ref := range refs {
url := desktop.BuildURL(fmt.Sprintf(".imported/_/%s", ref))
fmt.Fprintln(dockerCli.Err(), url)
if i == 0 {
err = browser.OpenURL(url)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If multiple files are provided I think we should skip this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because who knows what user wants to see? Is it the last one or the first one? But I guess that's fine to open the very first imported record in Docker Desktop.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although I wonder if there are more than one record imported we could instead open the Builds view and filter by imported builds 🤔

}

return 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,
Copy link
Member

@crazy-max crazy-max Mar 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should use args as file input instead of a flag and require at least one arg. Multiple files should be supported like Docker Desktop does atm imo:

docker buildx history import FILE...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not usually the convention I think when the file is optional and defaults to stdin. If file is arg then for stdin, the user should write import -

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Importing multiple files together, I'm ok with.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah indeed I missed it was reading from stdin. Sounds good to support multiple --file then

RunE: func(cmd *cobra.Command, args []string) error {
return runImport(cmd.Context(), dockerCli, options)
},
ValidArgsFunction: completion.Disable,
}

flags := cmd.Flags()
flags.StringVarP(&options.file, "file", "f", "", "Import from a file path")

return cmd
}
1 change: 1 addition & 0 deletions commands/history/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/reference/buildx_history.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
16 changes: 16 additions & 0 deletions docs/reference/buildx_history_import.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# docker buildx history import

<!---MARKER_GEN_START-->
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` | `string` | | Import from a file path |


<!---MARKER_GEN_END-->

21 changes: 21 additions & 0 deletions util/desktop/paths_darwin.go
Original file line number Diff line number Diff line change
@@ -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
}
21 changes: 21 additions & 0 deletions util/desktop/paths_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package desktop

import (
"os"
"path/filepath"

"github.com/pkg/errors"
)

const (
socketName = "docker-desktop-build.sock"
socketPath = ".docker/desktop"
)

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
}
13 changes: 13 additions & 0 deletions util/desktop/paths_unsupported.go
Original file line number Diff line number Diff line change
@@ -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)
}
5 changes: 5 additions & 0 deletions util/desktop/paths_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package desktop

func BuildServerAddr() (string, error) {
return "npipe:////./pipe/dockerDesktopBuildServer", nil
}
Loading