Skip to content

Commit 17ebd7a

Browse files
committed
add pouch search cli
Signed-off-by: Junjun Li <[email protected]>
1 parent 5c253f9 commit 17ebd7a

File tree

11 files changed

+333
-17
lines changed

11 files changed

+333
-17
lines changed

apis/swagger.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,29 @@ paths:
370370
$ref: "#/definitions/SearchResultItem"
371371
500:
372372
$ref: "#/responses/500ErrorResponse"
373+
parameters:
374+
- name: "term"
375+
in: "query"
376+
description: "Term to search"
377+
type: "string"
378+
required: true
379+
- name: "registry"
380+
in: "query"
381+
description: "Search images from specified registry"
382+
type: "string"
383+
384+
- name: "limit"
385+
in: "query"
386+
description: "Maximum number of results to return"
387+
type: "integer"
388+
- name: "filters"
389+
in: "query"
390+
description: |
391+
A JSON encoded value of the filters (a `map[string][]string`) to process on the images list. Available filters:
392+
- `is-automated=(true|false)`
393+
- `is-official=(true|false)`
394+
- `stars=<number>` Matches images that has at least 'number' stars.
395+
type: "string"
373396

374397
/images/{imageid}/tag:
375398
post:

cli/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ func main() {
3535
cli.AddCommand(base, &LoadCommand{})
3636
cli.AddCommand(base, &SaveCommand{})
3737
cli.AddCommand(base, &HistoryCommand{})
38+
cli.AddCommand(base, &SearchCommand{})
3839

3940
cli.AddCommand(base, &InspectCommand{})
4041
cli.AddCommand(base, &RenameCommand{})

cli/search.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/spf13/cobra"
8+
)
9+
10+
var searchDescription = "\nSearch the images from specific registry."
11+
12+
// SearchCommand implements search images.
13+
type SearchCommand struct {
14+
baseCommand
15+
registry string
16+
}
17+
18+
// Init initialize start command.
19+
func (s *SearchCommand) Init(c *Cli) {
20+
s.cli = c
21+
22+
s.cmd = &cobra.Command{
23+
Use: "search [OPTIONS] TERM",
24+
Short: "Search the images from specific registry",
25+
Long: searchDescription,
26+
Args: cobra.MinimumNArgs(1),
27+
RunE: func(cmd *cobra.Command, args []string) error {
28+
return s.runSearch(args)
29+
},
30+
Example: searchExample(),
31+
}
32+
s.addFlags()
33+
}
34+
35+
// addFlags adds flags for specific command.
36+
func (s *SearchCommand) addFlags() {
37+
flagSet := s.cmd.Flags()
38+
39+
flagSet.StringVarP(&s.registry, "registry", "r", "", "set registry name")
40+
}
41+
42+
func (s *SearchCommand) runSearch(args []string) error {
43+
ctx := context.Background()
44+
apiClient := s.cli.Client()
45+
46+
term := args[0]
47+
searchResults, err := apiClient.ImageSearch(ctx, term, s.registry)
48+
49+
if err != nil {
50+
return err
51+
}
52+
53+
display := s.cli.NewTableDisplay()
54+
display.AddRow([]string{"NAME", "DESCRIPTION", "STARS", "OFFICIAL", "AUTOMATED"})
55+
56+
for _, result := range searchResults {
57+
display.AddRow([]string{result.Name, result.Description, fmt.Sprint(result.StarCount), boolToOKOrNot(result.IsOfficial), boolToOKOrNot(result.IsAutomated)})
58+
}
59+
60+
display.Flush()
61+
return nil
62+
}
63+
64+
// chang bool value to ok or "" bool => "[OK]" false => ""
65+
func boolToOKOrNot(isTrue bool) string {
66+
if isTrue {
67+
return "[OK]"
68+
}
69+
return ""
70+
}
71+
72+
func searchExample() string {
73+
return `$ pouch search nginx
74+
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
75+
nginx Official build of Nginx. 11403 [OK]
76+
jwilder/nginx-proxy Automated Nginx reverse proxy for docker con… 1600 [OK]
77+
richarvey/nginx-php-fpm Container running Nginx + PHP-FPM capable of… 712 [OK]
78+
jrcs/letsencrypt-nginx-proxy-companion LetsEncrypt container to use with nginx as p… 509 [OK]
79+
webdevops/php-nginx Nginx with PHP-FPM 127 [OK]
80+
zabbix/zabbix-web-nginx-mysql Zabbix frontend based on Nginx web-server wi… 101 [OK]
81+
bitnami/nginx Bitnami nginx Docker Image 66 [OK]
82+
linuxserver/nginx An Nginx container, brought to you by LinuxS… 61
83+
1and1internet/ubuntu-16-nginx-php-phpmyadmin-mysql-5 ubuntu-16-nginx-php-phpmyadmin-mysql-5 50 [OK]
84+
zabbix/zabbix-web-nginx-pgsql Zabbix frontend based on Nginx with PostgreS… 33 [OK]
85+
tobi312/rpi-nginx NGINX on Raspberry Pi / ARM 26 [OK]
86+
nginx/nginx-ingress NGINX Ingress Controller for Kubernetes 20
87+
schmunk42/nginx-redirect A very simple container to redirect HTTP tra… 15 [OK]
88+
nginxdemos/hello NGINX webserver that serves a simple page co… 14 [OK]
89+
blacklabelops/nginx Dockerized Nginx Reverse Proxy Server. 12 [OK]
90+
wodby/drupal-nginx Nginx for Drupal container image 12 [OK]
91+
centos/nginx-18-centos7 Platform for running nginx 1.8 or building n… 10
92+
centos/nginx-112-centos7 Platform for running nginx 1.12 or building … 9
93+
nginxinc/nginx-unprivileged Unprivileged NGINX Dockerfiles 4
94+
1science/nginx Nginx Docker images that include Consul Temp… 4 [OK]
95+
nginx/nginx-prometheus-exporter NGINX Prometheus Exporter 4
96+
mailu/nginx Mailu nginx frontend 3 [OK]
97+
toccoag/openshift-nginx Nginx reverse proxy for Nice running on same… 1 [OK]
98+
ansibleplaybookbundle/nginx-apb An APB to deploy NGINX 0 [OK]
99+
wodby/nginx Generic nginx 0 [OK]
100+
`
101+
}

client/image_search.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package client
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net/url"
7+
8+
"github.com/alibaba/pouch/apis/types"
9+
)
10+
11+
// ImageSearch requests daemon to search an image from registry.
12+
func (client *APIClient) ImageSearch(ctx context.Context, term string, register string) ([]types.SearchResultItem, error) {
13+
var results []types.SearchResultItem
14+
15+
q := url.Values{}
16+
q.Set("term", term)
17+
if len(register) > 0 {
18+
q.Set("registry", register)
19+
}
20+
21+
// todo: add some auth info
22+
headers := map[string][]string{}
23+
24+
resp, err := client.post(ctx, "/images/search", q, nil, headers)
25+
26+
if err != nil {
27+
return nil, err
28+
}
29+
30+
err = json.NewDecoder(resp.Body).Decode(&results)
31+
return results, err
32+
}

client/image_search_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package client
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io/ioutil"
9+
"net/http"
10+
"strings"
11+
"testing"
12+
13+
"github.com/alibaba/pouch/apis/types"
14+
"github.com/stretchr/testify/assert"
15+
)
16+
17+
func TestImageSearchServerError(t *testing.T) {
18+
client := &APIClient{
19+
HTTPCli: newMockClient(errorMockResponse(http.StatusInternalServerError, "Server error")),
20+
}
21+
term, registry := "", "nginx"
22+
_, err := client.ImageSearch(context.Background(), term, registry)
23+
if err == nil || !strings.Contains(err.Error(), "Server error") {
24+
t.Fatalf("expected a Server Error, got %v", err)
25+
}
26+
}
27+
28+
func TestImageSearchOK(t *testing.T) {
29+
expectedURL := "/images/search"
30+
31+
httpClient := newMockClient(func(req *http.Request) (*http.Response, error) {
32+
if !strings.HasPrefix(req.URL.Path, expectedURL) {
33+
return nil, fmt.Errorf("expected URL '%s', got '%s'", expectedURL, req.URL)
34+
}
35+
if req.Method != "POST" {
36+
return nil, fmt.Errorf("expected POST method, got %s", req.Method)
37+
}
38+
39+
searchResults, err := json.Marshal([]types.SearchResultItem{
40+
{
41+
Description: "nginx info",
42+
IsAutomated: false,
43+
IsOfficial: true,
44+
Name: "nginx",
45+
StarCount: 1233,
46+
},
47+
})
48+
if err != nil {
49+
return nil, err
50+
}
51+
52+
return &http.Response{
53+
StatusCode: http.StatusOK,
54+
Body: ioutil.NopCloser(bytes.NewReader([]byte(searchResults))),
55+
}, nil
56+
})
57+
58+
client := &APIClient{
59+
HTTPCli: httpClient,
60+
}
61+
62+
searchResults, err := client.ImageSearch(context.Background(), "nginx", "")
63+
if err != nil {
64+
t.Fatal(err)
65+
}
66+
67+
assert.Equal(t, searchResults[0].StarCount, int64(1233))
68+
assert.Equal(t, searchResults[0].Name, "nginx")
69+
}

client/interface.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ type ImageAPIClient interface {
5959
ImageSave(ctx context.Context, imageName string) (io.ReadCloser, error)
6060
ImageHistory(ctx context.Context, name string) ([]types.HistoryResultItem, error)
6161
ImagePush(ctx context.Context, ref, encodedAuth string) (io.ReadCloser, error)
62+
ImageSearch(ctx context.Context, term string, registry string) ([]types.SearchResultItem, error)
6263
}
6364

6465
// VolumeAPIClient defines methods of Volume client.

cri/stream/portforward/httpstream.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ func handleHTTPStreams(ctx context.Context, w http.ResponseWriter, req *http.Req
7474
streamChan: streamChan,
7575
streamPairs: collect.NewSafeMap(),
7676
streamCreationTimeout: streamCreationTimeout,
77-
pod: podName,
78-
forwarder: portForwarder,
77+
pod: podName,
78+
forwarder: portForwarder,
7979
}
8080
h.run(ctx)
8181

cri/v1alpha2/cri_utils_test.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -985,10 +985,10 @@ func Test_toCriContainer(t *testing.T) {
985985
Config: &apitypes.ContainerConfig{
986986
Image: "image",
987987
Labels: map[string]string{
988-
containerTypeLabelKey: "b",
989-
sandboxIDLabelKey: "sid",
990-
"aa": "bb",
991-
"cc": "dd",
988+
containerTypeLabelKey: "b",
989+
sandboxIDLabelKey: "sid",
990+
"aa": "bb",
991+
"cc": "dd",
992992
annotationPrefix + "aaa": "bbb",
993993
annotationPrefix + "ccc": "ddd",
994994
},
@@ -1031,10 +1031,10 @@ func Test_toCriContainer(t *testing.T) {
10311031
Config: &apitypes.ContainerConfig{
10321032
Image: "image",
10331033
Labels: map[string]string{
1034-
containerTypeLabelKey: "b",
1035-
sandboxIDLabelKey: "sid",
1036-
"aa": "bb",
1037-
"cc": "dd",
1034+
containerTypeLabelKey: "b",
1035+
sandboxIDLabelKey: "sid",
1036+
"aa": "bb",
1037+
"cc": "dd",
10381038
annotationPrefix + "aaa": "bbb",
10391039
annotationPrefix + "ccc": "ddd",
10401040
},
@@ -1058,10 +1058,10 @@ func Test_toCriContainer(t *testing.T) {
10581058
Config: &apitypes.ContainerConfig{
10591059
Image: "image",
10601060
Labels: map[string]string{
1061-
containerTypeLabelKey: "b",
1062-
sandboxIDLabelKey: "sid",
1063-
"aa": "bb",
1064-
"cc": "dd",
1061+
containerTypeLabelKey: "b",
1062+
sandboxIDLabelKey: "sid",
1063+
"aa": "bb",
1064+
"cc": "dd",
10651065
annotationPrefix + "aaa": "bbb",
10661066
annotationPrefix + "ccc": "ddd",
10671067
},

daemon/mgr/image.go

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ package mgr
22

33
import (
44
"context"
5+
"encoding/json"
56
"errors"
67
"fmt"
78
"io"
9+
"io/ioutil"
810
"net/http"
11+
"net/url"
912
"strings"
1013
"time"
1114

@@ -326,7 +329,40 @@ func (mgr *ImageManager) ListImages(ctx context.Context, filter filters.Args) ([
326329
// SearchImages searches imaged from specified registry.
327330
func (mgr *ImageManager) SearchImages(ctx context.Context, name string, registry string) ([]types.SearchResultItem, error) {
328331
// Directly send API calls towards specified registry
329-
return nil, errtypes.ErrNotImplemented
332+
if len(registry) == 0 {
333+
registry = "https://index.docker.io/v1/"
334+
}
335+
336+
u := registry + "search?q=" + url.QueryEscape(name)
337+
res, err := http.Get(u)
338+
if err != nil {
339+
return nil, err
340+
}
341+
defer res.Body.Close()
342+
if res.StatusCode != 200 {
343+
return nil, errtypes.ErrTimeout
344+
}
345+
346+
rawData, err := ioutil.ReadAll(res.Body)
347+
if err != nil {
348+
return nil, err
349+
}
350+
351+
// todo: to move where it should be
352+
type SearchResults struct {
353+
Query string `json:"query"`
354+
NumResults int `json:"num_results"`
355+
Results []types.SearchResultItem `json:"results"`
356+
}
357+
358+
searchResults := new(SearchResults)
359+
err = json.Unmarshal(rawData, searchResults)
360+
361+
return searchResults.Results, err
362+
363+
// todo: whether this code rebuild in ctrd ?
364+
// todo: to add some session code and log info?
365+
//return mgr.client.SearchImage(ctx, name, registry)
330366
}
331367

332368
// RemoveImage deletes a reference.

0 commit comments

Comments
 (0)