diff --git a/cli/command/completion/functions.go b/cli/command/completion/functions.go index b217ec54b05d..917c84ff1f07 100644 --- a/cli/command/completion/functions.go +++ b/cli/command/completion/functions.go @@ -1,8 +1,12 @@ 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" @@ -10,6 +14,7 @@ import ( "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" ) @@ -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 Images(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) + 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.ShellCompDirectiveNoFileComp + } + 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.ShellCompDirectiveNoFileComp + } + + resp, err := c.Do(req) + if err != nil { + logrus.Errorf("Error sending hub image search request: %v", err) + return nil, cobra.ShellCompDirectiveNoFileComp + } + 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.ShellCompDirectiveNoFileComp + } + + names := make([]string, 0, len(images.Results)) + for _, i := range images.Results { + names = append(names, i.Name) + } + + return names, cobra.ShellCompDirectiveNoFileComp +} diff --git a/cli/command/container/run.go b/cli/command/container/run.go index a3fc5f983a3d..dc115df2ee8e 100644 --- a/cli/command/container/run.go +++ b/cli/command/container/run.go @@ -43,7 +43,33 @@ func NewRunCommand(dockerCli command.Cli) *cobra.Command { } 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) { + 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, fmt.Sprintf("%s\tlocal", img)) + } + } + + remoteImages, shellCompRemote := completion.Images(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, fmt.Sprintf("%s\tremote", img)) + } + } + } + + return all, cobra.ShellCompDirectiveKeepOrder | cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveDefault + }, Annotations: map[string]string{ "category-top": "1", "aliases": "docker container run, docker run", diff --git a/cli/command/image/pull.go b/cli/command/image/pull.go index 93388253f75a..928a3c0b81e0 100644 --- a/cli/command/image/pull.go +++ b/cli/command/image/pull.go @@ -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.Images(cmd, args, toComplete) + }, } flags := cmd.Flags() diff --git a/scripts/build/binary b/scripts/build/binary index 44cd4f14fdd8..33f37628c20d 100755 --- a/scripts/build/binary +++ b/scripts/build/binary @@ -24,6 +24,6 @@ if [ "$(go env GOOS)" = "windows" ]; then fi fi -(set -x ; go build -o "${TARGET}" -tags "${GO_BUILDTAGS}" -ldflags "${GO_LDFLAGS}" ${GO_BUILDMODE} "${SOURCE}") +(set -x ; go build -o "${TARGET}" -tags "${GO_BUILDTAGS}" -ldflags "${GO_LDFLAGS}" ${GO_BUILDMODE} -buildvcs=false "${SOURCE}") ln -sf "$(basename "${TARGET}")" "$(dirname "${TARGET}")/docker"