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

feat: run and pull image completion from hub #5528

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
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
124 changes: 124 additions & 0 deletions cli/command/completion/functions.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package completion

import (
"encoding/json"
"net/http"
"net/url"
"os"
"strings"
"time"

"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -189,3 +194,122 @@ var commonPlatforms = []string{
func Platforms(_ *cobra.Command, _ []string, _ string) (platforms []string, _ cobra.ShellCompDirective) {
return commonPlatforms, cobra.ShellCompDirectiveNoFileComp
}

type ImageSearchResult struct {
ID string `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
Type string `json:"type"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ShortDesc string `json:"short_description"`
Source string `json:"source"`
StarCount int `json:"star_count"`
}

type ImageSearch struct {
Totals int `json:"totals"`
Results []ImageSearchResult `json:"results"`
}

type Image struct {
ID int `json:"id"`
Name string `json:"name"`
TagStatus string `json:"tag_status"`
V2 bool `json:"v2"`
Digest string `json:"digest"`
LastUpdated time.Time `json:"last_updated"`
LastUpdater int `json:"last_updater"`
Creator int `json:"creator"`
Repository int `json:"repository"`
}

type ImageTags struct {
Count int `json:"count"`
Next string `json:"next"`
Prev string `json:"prev"`
Results []Image `json:"results"`
}

func RemoteImages(cmd *cobra.Command, arg []string, toComplete string) ([]string, cobra.ShellCompDirective) {
ctx := cmd.Context()
c := &http.Client{
Timeout: 2 * time.Second,
}

if imageName, imageTag, ok := strings.Cut(toComplete, ":"); ok {
u, err := url.Parse("https://hub.docker.com/v2/repositories/library/" + imageName + "/tags/")
if err != nil {
logrus.Errorf("Error parsing hub image tags URL: %v", err)
return nil, cobra.ShellCompDirectiveError
}
q := u.Query()
q.Set("ordering", "last_updated")
q.Set("page_size", "25")
q.Set("name", imageTag)
u.RawQuery = q.Encode()

req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
logrus.Errorf("Error creating hub image tags request: %v", err)
Copy link
Member Author

Choose a reason for hiding this comment

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

I honestly don't know what the best way would be for us to track errors for the completions. Maybe we should write debug logs to a file?

return nil, cobra.ShellCompDirectiveError
}

resp, err := c.Do(req)
if err != nil {
logrus.Errorf("Error sending hub image tags request: %v", err)
return nil, cobra.ShellCompDirectiveError
}
defer resp.Body.Close()

var tags *ImageTags
if err := json.NewDecoder(resp.Body).Decode(&tags); err != nil {
logrus.Errorf("Error decoding hub image tags response: %v", err)
return nil, cobra.ShellCompDirectiveError
}

names := make([]string, 0, len(tags.Results))
for _, i := range tags.Results {
names = append(names, imageName+":"+i.Name)
}
return names, cobra.ShellCompDirectiveNoFileComp
}

u, err := url.Parse("https://hub.docker.com/api/search/v3/catalog/search")
if err != nil {
logrus.Errorf("Error parsing hub image search URL: %v", err)
return nil, cobra.ShellCompDirectiveError
}
q := u.Query()
q.Set("query", toComplete)
q.Set("extension_reviewed", "")
q.Set("from", "0")
q.Set("size", "25")
u.RawQuery = q.Encode()

req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
logrus.Errorf("Error creating hub image search request: %v", err)
return nil, cobra.ShellCompDirectiveError
}

resp, err := c.Do(req)
if err != nil {
logrus.Errorf("Error sending hub image search request: %v", err)
return nil, cobra.ShellCompDirectiveError
}
defer resp.Body.Close()

var images *ImageSearch
if err := json.NewDecoder(resp.Body).Decode(&images); err != nil {
logrus.Errorf("Error decoding hub image search response: %v", err)
return nil, cobra.ShellCompDirectiveError
}

names := make([]string, 0, len(images.Results))
for _, i := range images.Results {
names = append(names, i.Name)
}

return names, cobra.ShellCompDirectiveNoFileComp
}
35 changes: 33 additions & 2 deletions cli/command/container/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,44 @@ func NewRunCommand(dockerCli command.Cli) *cobra.Command {
Short: "Create and run a new container from an image",
Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
copts.Image = args[0]
replacer := strings.NewReplacer("(local)", "", "(remote)", "")
copts.Image = replacer.Replace(args[0])
if len(args) > 1 {
copts.Args = args[1:]
}
return runRun(cmd.Context(), dockerCli, cmd.Flags(), &options, copts)
},
ValidArgsFunction: completion.ImageNames(dockerCli),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) > 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}

unique := map[string]struct{}{}
localImages, shellComp := completion.ImageNames(dockerCli)(cmd, args, toComplete)

var all []string
if shellComp != cobra.ShellCompDirectiveError {
all = make([]string, 0, len(localImages))
for _, img := range localImages {
unique[img] = struct{}{}
all = append(all, img+"\tlocal")
}
}

remoteImages, shellCompRemote := completion.RemoteImages(cmd, args, toComplete)
if shellCompRemote != cobra.ShellCompDirectiveError {
if len(all) == 0 {
all = make([]string, 0, len(remoteImages))
}
for _, img := range remoteImages {
if _, ok := unique[img]; !ok {
all = append(all, img+"\tremote")
}
}
}

return all, cobra.ShellCompDirectiveKeepOrder | cobra.ShellCompDirectiveNoFileComp
},
Annotations: map[string]string{
"category-top": "1",
"aliases": "docker container run, docker run",
Expand Down
7 changes: 6 additions & 1 deletion cli/command/image/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@ func NewPullCommand(dockerCli command.Cli) *cobra.Command {
"category-top": "5",
"aliases": "docker image pull, docker pull",
},
ValidArgsFunction: completion.NoComplete,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) > 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return completion.RemoteImages(cmd, args, toComplete)
},
}

flags := cmd.Flags()
Expand Down
2 changes: 1 addition & 1 deletion cmd/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func newDockerCommand(dockerCli *command.DockerCli) *cli.TopLevelCommand {
CompletionOptions: cobra.CompletionOptions{
DisableDefaultCmd: false,
HiddenDefaultCmd: true,
DisableDescriptions: true,
DisableDescriptions: false,
},
}
cmd.SetIn(dockerCli.In())
Expand Down
Loading