Skip to content

Commit

Permalink
add pouch search cli
Browse files Browse the repository at this point in the history
Signed-off-by: Junjun Li <junjunli666@gmail.com>
  • Loading branch information
hellolijj committed May 28, 2019
1 parent 7235f82 commit bc07a89
Show file tree
Hide file tree
Showing 11 changed files with 393 additions and 24 deletions.
16 changes: 15 additions & 1 deletion apis/server/image_bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/alibaba/pouch/pkg/httputils"
util_metrics "github.com/alibaba/pouch/pkg/utils/metrics"

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

searchResultItem, err := s.ImageMgr.SearchImages(ctx, searchPattern, registry)
// get registry auth from Request header
authStr := req.Header.Get("X-Registry-Auth")
authConfig := types.AuthConfig{}
if authStr != "" {
data := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authStr))
if err := json.NewDecoder(data).Decode(&authConfig); err != nil {
return err
}
if err := authConfig.Validate(strfmt.NewFormats()); err != nil {
return err
}
}

searchResultItem, err := s.ImageMgr.SearchImages(ctx, searchPattern, registry, &authConfig)
if err != nil {
logrus.Errorf("failed to search images from registry: %v", err)
return err
Expand Down
47 changes: 29 additions & 18 deletions apis/swagger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,17 @@ paths:
$ref: "#/definitions/SearchResultItem"
500:
$ref: "#/responses/500ErrorResponse"
parameters:
- name: "term"
in: "query"
description: "Term to search"
type: "string"
required: true
- name: "registry"
in: "query"
description: "Search images from specified registry"
type: "string"
# TODO: add limit and filters

/images/{imageid}/tag:
post:
Expand Down Expand Up @@ -3166,24 +3177,24 @@ definitions:
x-nullable: false

SearchResultItem:
type: "object"
description: "search result item in search results."
properties:
description:
type: "string"
description: "description just shows the description of this image"
is_official:
type: "boolean"
description: "is_official shows if this image is marked official."
is_automated:
type: "boolean"
description: "is_automated means whether this image is automated."
name:
type: "string"
description: "name represents the name of this image"
star_count:
type: "integer"
description: "star_count refers to the star count of this image."
type: "object"
description: "search result item in search results."
properties:
description:
type: "string"
description: "description just shows the description of this image"
is_official:
type: "boolean"
description: "is_official shows if this image is marked official."
is_automated:
type: "boolean"
description: "is_automated means whether this image is automated."
name:
type: "string"
description: "name represents the name of this image"
star_count:
type: "integer"
description: "star_count refers to the star count of this image."

VolumeInfo:
type: "object"
Expand Down
1 change: 1 addition & 0 deletions cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func main() {
cli.AddCommand(base, &LoadCommand{})
cli.AddCommand(base, &SaveCommand{})
cli.AddCommand(base, &HistoryCommand{})
cli.AddCommand(base, &SearchCommand{})

cli.AddCommand(base, &InspectCommand{})
cli.AddCommand(base, &RenameCommand{})
Expand Down
102 changes: 102 additions & 0 deletions cli/search.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package main

import (
"context"
"fmt"

"github.com/spf13/cobra"
)

var searchDescription = "\nSearch the images from specific registry."

// SearchCommand implements search images.
type SearchCommand struct {
baseCommand
registry string
}

// Init initialize search command.
func (s *SearchCommand) Init(c *Cli) {
s.cli = c

s.cmd = &cobra.Command{
Use: "search [OPTIONS] TERM",
Short: "Search the images from specific registry",
Long: searchDescription,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return s.runSearch(args)
},
Example: searchExample(),
}
s.addFlags()
}

// addFlags adds flags for specific command.
func (s *SearchCommand) addFlags() {
flagSet := s.cmd.Flags()

flagSet.StringVarP(&s.registry, "registry", "r", "", "set registry name")
}

func (s *SearchCommand) runSearch(args []string) error {
ctx := context.Background()
apiClient := s.cli.Client()

term := args[0]

// TODO: add flags --filter、--format、--limit、--no-trunc
searchResults, err := apiClient.ImageSearch(ctx, term, s.registry, fetchRegistryAuth(s.registry))

if err != nil {
return err
}

display := s.cli.NewTableDisplay()
display.AddRow([]string{"NAME", "DESCRIPTION", "STARS", "OFFICIAL", "AUTOMATED"})

for _, result := range searchResults {
display.AddRow([]string{result.Name, result.Description, fmt.Sprint(result.StarCount), boolToOKOrNot(result.IsOfficial), boolToOKOrNot(result.IsAutomated)})
}

display.Flush()
return nil
}

func boolToOKOrNot(isTrue bool) string {
if isTrue {
return "[OK]"
}
return ""
}

func searchExample() string {
return `$ pouch search nginx
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
nginx Official build of Nginx. 11403 [OK]
jwilder/nginx-proxy Automated Nginx reverse proxy for docker con… 1600 [OK]
richarvey/nginx-php-fpm Container running Nginx + PHP-FPM capable of… 712 [OK]
jrcs/letsencrypt-nginx-proxy-companion LetsEncrypt container to use with nginx as p… 509 [OK]
webdevops/php-nginx Nginx with PHP-FPM 127 [OK]
zabbix/zabbix-web-nginx-mysql Zabbix frontend based on Nginx web-server wi… 101 [OK]
bitnami/nginx Bitnami nginx Docker Image 66 [OK]
linuxserver/nginx An Nginx container, brought to you by LinuxS… 61
1and1internet/ubuntu-16-nginx-php-phpmyadmin-mysql-5 ubuntu-16-nginx-php-phpmyadmin-mysql-5 50 [OK]
zabbix/zabbix-web-nginx-pgsql Zabbix frontend based on Nginx with PostgreS… 33 [OK]
tobi312/rpi-nginx NGINX on Raspberry Pi / ARM 26 [OK]
nginx/nginx-ingress NGINX Ingress Controller for Kubernetes 20
schmunk42/nginx-redirect A very simple container to redirect HTTP tra… 15 [OK]
nginxdemos/hello NGINX webserver that serves a simple page co… 14 [OK]
blacklabelops/nginx Dockerized Nginx Reverse Proxy Server. 12 [OK]
wodby/drupal-nginx Nginx for Drupal container image 12 [OK]
centos/nginx-18-centos7 Platform for running nginx 1.8 or building n… 10
centos/nginx-112-centos7 Platform for running nginx 1.12 or building … 9
nginxinc/nginx-unprivileged Unprivileged NGINX Dockerfiles 4
1science/nginx Nginx Docker images that include Consul Temp… 4 [OK]
nginx/nginx-prometheus-exporter NGINX Prometheus Exporter 4
mailu/nginx Mailu nginx frontend 3 [OK]
toccoag/openshift-nginx Nginx reverse proxy for Nice running on same… 1 [OK]
ansibleplaybookbundle/nginx-apb An APB to deploy NGINX 0 [OK]
wodby/nginx Generic nginx 0 [OK]
`
}
32 changes: 32 additions & 0 deletions client/image_search.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package client

import (
"context"
"encoding/json"
"net/url"

"github.com/alibaba/pouch/apis/types"
)

// ImageSearch requests daemon to search an image from registry.
func (client *APIClient) ImageSearch(ctx context.Context, term, registry, encodedAuth string) ([]types.SearchResultItem, error) {
var results []types.SearchResultItem

q := url.Values{}
q.Set("term", term)
q.Set("registry", registry)

headers := map[string][]string{}
if encodedAuth != "" {
headers["X-Registry-Auth"] = []string{encodedAuth}
}

resp, err := client.post(ctx, "/images/search", q, nil, headers)

if err != nil {
return nil, err
}

err = json.NewDecoder(resp.Body).Decode(&results)
return results, err
}
81 changes: 81 additions & 0 deletions client/image_search_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package client

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"

"github.com/alibaba/pouch/apis/types"

"github.com/stretchr/testify/assert"
)

func TestImageSearchServerError(t *testing.T) {
client := &APIClient{
HTTPCli: newMockClient(errorMockResponse(http.StatusInternalServerError, "Server error")),
}
term, registry, auth := "", "nginx", ""
_, err := client.ImageSearch(context.Background(), term, registry, auth)
if err == nil || !strings.Contains(err.Error(), "Server error") {
t.Fatalf("expected a Server Error, got %v", err)
}
}

func TestImageSearchOK(t *testing.T) {
expectedURL := "/images/search"

httpClient := newMockClient(func(req *http.Request) (*http.Response, error) {
if !strings.HasPrefix(req.URL.Path, expectedURL) {
return nil, fmt.Errorf("expected URL '%s', got '%s'", expectedURL, req.URL)
}
if req.Method != "POST" {
return nil, fmt.Errorf("expected POST method, got %s", req.Method)
}

searchResultResp, err := json.Marshal([]types.SearchResultItem{
{
Description: "nginx info",
IsAutomated: false,
IsOfficial: true,
Name: "nginx",
StarCount: 1233,
},
})
if err != nil {
return nil, err
}

return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader([]byte(searchResultResp))),
}, nil
})

client := &APIClient{
HTTPCli: httpClient,
}

searchResultResp, err := client.ImageSearch(context.Background(), "nginx", "", "")
if err != nil {
t.Fatal(err)
}

assert.Equal(t, searchResultResp[0].StarCount, int64(1233))
assert.Equal(t, searchResultResp[0].Name, "nginx")
}

func TestImageSearchStatusUnauthorizedError(t *testing.T) {
client := &APIClient{
HTTPCli: newMockClient(errorMockResponse(http.StatusUnauthorized, "Unauthorized Error")),
}
term, registry, auth := "", "nginx", "some-auth-code"
_, err := client.ImageSearch(context.Background(), term, registry, auth)
if err == nil || !strings.Contains(err.Error(), "Unauthorized Error") {
t.Fatalf("expected a Unauthorized Error, got %v", err)
}
}
1 change: 1 addition & 0 deletions client/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type ImageAPIClient interface {
ImageSave(ctx context.Context, imageName string) (io.ReadCloser, error)
ImageHistory(ctx context.Context, name string) ([]types.HistoryResultItem, error)
ImagePush(ctx context.Context, ref, encodedAuth string) (io.ReadCloser, error)
ImageSearch(ctx context.Context, term, registry, encodedAuth string) ([]types.SearchResultItem, error)
}

// VolumeAPIClient defines methods of Volume client.
Expand Down
Loading

0 comments on commit bc07a89

Please sign in to comment.