Skip to content

Commit a8fc483

Browse files
hellolijjallencloud
authored andcommitted
Signed-off-by: Junjun Li <[email protected]>
# Conflicts: # daemon/mgr/image.go
1 parent d2342d1 commit a8fc483

File tree

11 files changed

+393
-24
lines changed

11 files changed

+393
-24
lines changed

apis/server/image_bridge.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/alibaba/pouch/pkg/httputils"
1818
util_metrics "github.com/alibaba/pouch/pkg/utils/metrics"
1919

20+
"github.com/go-openapi/strfmt"
2021
"github.com/gorilla/mux"
2122
"github.com/opencontainers/go-digest"
2223
"github.com/sirupsen/logrus"
@@ -93,7 +94,20 @@ func (s *Server) searchImages(ctx context.Context, rw http.ResponseWriter, req *
9394
searchPattern := req.FormValue("term")
9495
registry := req.FormValue("registry")
9596

96-
searchResultItem, err := s.ImageMgr.SearchImages(ctx, searchPattern, registry)
97+
// get registry auth from Request header
98+
authStr := req.Header.Get("X-Registry-Auth")
99+
authConfig := types.AuthConfig{}
100+
if authStr != "" {
101+
data := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authStr))
102+
if err := json.NewDecoder(data).Decode(&authConfig); err != nil {
103+
return err
104+
}
105+
if err := authConfig.Validate(strfmt.NewFormats()); err != nil {
106+
return err
107+
}
108+
}
109+
110+
searchResultItem, err := s.ImageMgr.SearchImages(ctx, searchPattern, registry, &authConfig)
97111
if err != nil {
98112
logrus.Errorf("failed to search images from registry: %v", err)
99113
return err

apis/swagger.yml

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,17 @@ 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+
# TODO: add limit and filters
373384

374385
/images/{imageid}/tag:
375386
post:
@@ -3166,24 +3177,24 @@ definitions:
31663177
x-nullable: false
31673178

31683179
SearchResultItem:
3169-
type: "object"
3170-
description: "search result item in search results."
3171-
properties:
3172-
description:
3173-
type: "string"
3174-
description: "description just shows the description of this image"
3175-
is_official:
3176-
type: "boolean"
3177-
description: "is_official shows if this image is marked official."
3178-
is_automated:
3179-
type: "boolean"
3180-
description: "is_automated means whether this image is automated."
3181-
name:
3182-
type: "string"
3183-
description: "name represents the name of this image"
3184-
star_count:
3185-
type: "integer"
3186-
description: "star_count refers to the star count of this image."
3180+
type: "object"
3181+
description: "search result item in search results."
3182+
properties:
3183+
description:
3184+
type: "string"
3185+
description: "description just shows the description of this image"
3186+
is_official:
3187+
type: "boolean"
3188+
description: "is_official shows if this image is marked official."
3189+
is_automated:
3190+
type: "boolean"
3191+
description: "is_automated means whether this image is automated."
3192+
name:
3193+
type: "string"
3194+
description: "name represents the name of this image"
3195+
star_count:
3196+
type: "integer"
3197+
description: "star_count refers to the star count of this image."
31873198

31883199
VolumeInfo:
31893200
type: "object"

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: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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 search 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+
48+
// TODO: add flags --filter、--format、--limit、--no-trunc
49+
searchResults, err := apiClient.ImageSearch(ctx, term, s.registry, fetchRegistryAuth(s.registry))
50+
51+
if err != nil {
52+
return err
53+
}
54+
55+
display := s.cli.NewTableDisplay()
56+
display.AddRow([]string{"NAME", "DESCRIPTION", "STARS", "OFFICIAL", "AUTOMATED"})
57+
58+
for _, result := range searchResults {
59+
display.AddRow([]string{result.Name, result.Description, fmt.Sprint(result.StarCount), boolToOKOrNot(result.IsOfficial), boolToOKOrNot(result.IsAutomated)})
60+
}
61+
62+
display.Flush()
63+
return nil
64+
}
65+
66+
func boolToOKOrNot(isTrue bool) string {
67+
if isTrue {
68+
return "[OK]"
69+
}
70+
return ""
71+
}
72+
73+
func searchExample() string {
74+
return `$ pouch search nginx
75+
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
76+
nginx Official build of Nginx. 11403 [OK]
77+
jwilder/nginx-proxy Automated Nginx reverse proxy for docker con… 1600 [OK]
78+
richarvey/nginx-php-fpm Container running Nginx + PHP-FPM capable of… 712 [OK]
79+
jrcs/letsencrypt-nginx-proxy-companion LetsEncrypt container to use with nginx as p… 509 [OK]
80+
webdevops/php-nginx Nginx with PHP-FPM 127 [OK]
81+
zabbix/zabbix-web-nginx-mysql Zabbix frontend based on Nginx web-server wi… 101 [OK]
82+
bitnami/nginx Bitnami nginx Docker Image 66 [OK]
83+
linuxserver/nginx An Nginx container, brought to you by LinuxS… 61
84+
1and1internet/ubuntu-16-nginx-php-phpmyadmin-mysql-5 ubuntu-16-nginx-php-phpmyadmin-mysql-5 50 [OK]
85+
zabbix/zabbix-web-nginx-pgsql Zabbix frontend based on Nginx with PostgreS… 33 [OK]
86+
tobi312/rpi-nginx NGINX on Raspberry Pi / ARM 26 [OK]
87+
nginx/nginx-ingress NGINX Ingress Controller for Kubernetes 20
88+
schmunk42/nginx-redirect A very simple container to redirect HTTP tra… 15 [OK]
89+
nginxdemos/hello NGINX webserver that serves a simple page co… 14 [OK]
90+
blacklabelops/nginx Dockerized Nginx Reverse Proxy Server. 12 [OK]
91+
wodby/drupal-nginx Nginx for Drupal container image 12 [OK]
92+
centos/nginx-18-centos7 Platform for running nginx 1.8 or building n… 10
93+
centos/nginx-112-centos7 Platform for running nginx 1.12 or building … 9
94+
nginxinc/nginx-unprivileged Unprivileged NGINX Dockerfiles 4
95+
1science/nginx Nginx Docker images that include Consul Temp… 4 [OK]
96+
nginx/nginx-prometheus-exporter NGINX Prometheus Exporter 4
97+
mailu/nginx Mailu nginx frontend 3 [OK]
98+
toccoag/openshift-nginx Nginx reverse proxy for Nice running on same… 1 [OK]
99+
ansibleplaybookbundle/nginx-apb An APB to deploy NGINX 0 [OK]
100+
wodby/nginx Generic nginx 0 [OK]
101+
`
102+
}

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, registry, encodedAuth string) ([]types.SearchResultItem, error) {
13+
var results []types.SearchResultItem
14+
15+
q := url.Values{}
16+
q.Set("term", term)
17+
q.Set("registry", registry)
18+
19+
headers := map[string][]string{}
20+
if encodedAuth != "" {
21+
headers["X-Registry-Auth"] = []string{encodedAuth}
22+
}
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: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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+
15+
"github.com/stretchr/testify/assert"
16+
)
17+
18+
func TestImageSearchServerError(t *testing.T) {
19+
client := &APIClient{
20+
HTTPCli: newMockClient(errorMockResponse(http.StatusInternalServerError, "Server error")),
21+
}
22+
term, registry, auth := "", "nginx", ""
23+
_, err := client.ImageSearch(context.Background(), term, registry, auth)
24+
if err == nil || !strings.Contains(err.Error(), "Server error") {
25+
t.Fatalf("expected a Server Error, got %v", err)
26+
}
27+
}
28+
29+
func TestImageSearchOK(t *testing.T) {
30+
expectedURL := "/images/search"
31+
32+
httpClient := newMockClient(func(req *http.Request) (*http.Response, error) {
33+
if !strings.HasPrefix(req.URL.Path, expectedURL) {
34+
return nil, fmt.Errorf("expected URL '%s', got '%s'", expectedURL, req.URL)
35+
}
36+
if req.Method != "POST" {
37+
return nil, fmt.Errorf("expected POST method, got %s", req.Method)
38+
}
39+
40+
searchResultResp, err := json.Marshal([]types.SearchResultItem{
41+
{
42+
Description: "nginx info",
43+
IsAutomated: false,
44+
IsOfficial: true,
45+
Name: "nginx",
46+
StarCount: 1233,
47+
},
48+
})
49+
if err != nil {
50+
return nil, err
51+
}
52+
53+
return &http.Response{
54+
StatusCode: http.StatusOK,
55+
Body: ioutil.NopCloser(bytes.NewReader([]byte(searchResultResp))),
56+
}, nil
57+
})
58+
59+
client := &APIClient{
60+
HTTPCli: httpClient,
61+
}
62+
63+
searchResultResp, err := client.ImageSearch(context.Background(), "nginx", "", "")
64+
if err != nil {
65+
t.Fatal(err)
66+
}
67+
68+
assert.Equal(t, searchResultResp[0].StarCount, int64(1233))
69+
assert.Equal(t, searchResultResp[0].Name, "nginx")
70+
}
71+
72+
func TestImageSearchStatusUnauthorizedError(t *testing.T) {
73+
client := &APIClient{
74+
HTTPCli: newMockClient(errorMockResponse(http.StatusUnauthorized, "Unauthorized Error")),
75+
}
76+
term, registry, auth := "", "nginx", "some-auth-code"
77+
_, err := client.ImageSearch(context.Background(), term, registry, auth)
78+
if err == nil || !strings.Contains(err.Error(), "Unauthorized Error") {
79+
t.Fatalf("expected a Unauthorized Error, got %v", err)
80+
}
81+
}

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, registry, encodedAuth string) ([]types.SearchResultItem, error)
6263
}
6364

6465
// VolumeAPIClient defines methods of Volume client.

0 commit comments

Comments
 (0)