Skip to content

Commit

Permalink
Add ratelimit to dockerhub client (#16)
Browse files Browse the repository at this point in the history
* dockerhub returns uploadedAt

* dockerhub only set uploadedat

* add ratelimit to dockerhub

* fix for test

* add makefile and lint checks
  • Loading branch information
tomoyamachi authored Feb 25, 2020
1 parent bec2f61 commit 48f8c4d
Show file tree
Hide file tree
Showing 17 changed files with 111 additions and 57 deletions.
25 changes: 25 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.PHONY: \
lint \
vet \
fmt \
pretest \
test \

PKGS = $(shell go list ./... | grep -v /vendor/)
SRCS = $(shell git ls-files '*.go')
GO := GO111MODULE=on go
GO_OFF := GO111MODULE=off go

lint:
$(GO_OFF) get -u golang.org/x/lint/golint
$(foreach file,$(PKGS),golint -set_exit_status $(file) || exit;)
vet:
echo $(PKGS) | xargs env $(GO) vet || exit;

fmt:
$(foreach file,$(SRCS),gofmt -s -d $(file);)

pretest: lint vet fmt

test:
$(foreach file,$(PKGS),$(GO) test -v $(file) || exit;)
2 changes: 1 addition & 1 deletion cmd/dockertags/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ OPTIONS:
},
cli.StringFlag{
Name: "authurl, auth",
Usage: "Url when fetch authentication",
Usage: "GetURL when fetch authentication",
},
cli.DurationFlag{
Name: "timeout, t",
Expand Down
2 changes: 2 additions & 0 deletions internal/log/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import (
)

var (
// Logger is
Logger *zap.SugaredLogger
debugOption bool
)

// InitLogger set SugaredLogger to log.Logger
func InitLogger(debug bool) (err error) {
debugOption = debug
Logger, err = newLogger(debug)
Expand Down
6 changes: 4 additions & 2 deletions internal/report/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import (
"github.com/goodwithtech/dockertags/internal/types"
)

type JsonWriter struct {
// JSONWriter create json output
type JSONWriter struct {
Output io.Writer
}

func (jw JsonWriter) Write(tags types.ImageTags) (err error) {
// Write is
func (jw JSONWriter) Write(tags types.ImageTags) (err error) {
output, err := json.MarshalIndent(tags, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal json: %w", err)
Expand Down
2 changes: 2 additions & 0 deletions internal/report/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ import (
"github.com/goodwithtech/dockertags/internal/utils"
)

// TableWriter output table format
type TableWriter struct {
Output io.Writer
}

// Write is
func (w TableWriter) Write(tags types.ImageTags) (err error) {
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Tag", "Size", "Created At", "Uploaded At"})
Expand Down
1 change: 1 addition & 0 deletions internal/report/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package report

import "github.com/goodwithtech/dockertags/internal/types"

// Writer is
type Writer interface {
Write(types.ImageTags) error
}
1 change: 1 addition & 0 deletions internal/types/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
)

// errors
var (
ErrBasicAuth = errors.New("basic auth required")
ErrInvalidURL = errors.New("invalid url")
Expand Down
1 change: 1 addition & 0 deletions internal/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type ImageTag struct {
// ImageTags : tag information slice
type ImageTags []ImageTag

// Len interface method of sort
func (t ImageTags) Len() int { return len(t) }
func (t ImageTags) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
func (t ImageTags) Less(i, j int) bool {
Expand Down
2 changes: 1 addition & 1 deletion pkg/provider/dockerhub/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func getJSON(ctx context.Context, url string, auth types.AuthConfig, timeout tim

func new(auth types.AuthConfig, timeout time.Duration) (*http.Client, error) {
transport := http.DefaultTransport
tokenTransport := &DockerhubTokenTransport{
tokenTransport := &tokenTransport{
Transport: transport,
Username: auth.Username,
Password: auth.Password,
Expand Down
71 changes: 43 additions & 28 deletions pkg/provider/dockerhub/dockerhub.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,30 @@ import (
dockertypes "github.com/docker/docker/api/types"
)

const registryURL = "https://registry.hub.docker.com/"
const (
registryURL = "https://registry.hub.docker.com/"
rateLimit = 64
)

// DockerHub implements Run
type DockerHub struct {
filterOpt *types.FilterOption
filterOpt *types.FilterOption
requestOpt *types.RequestOption
authCfg dockertypes.AuthConfig
}

// Run returns tag list
func (p *DockerHub) Run(ctx context.Context, domain, repository string, reqOpt *types.RequestOption, filterOpt *types.FilterOption) (types.ImageTags, error) {
p.filterOpt = filterOpt
auth := dockertypes.AuthConfig{
p.requestOpt = reqOpt
p.authCfg = dockertypes.AuthConfig{
ServerAddress: "registry.hub.docker.com",
Username: reqOpt.UserName,
Password: reqOpt.Password,
}

// fetch page 1 for check max item count.
tagResp, err := getTagResponse(ctx, auth, reqOpt.Timeout, repository, 1)
tagResp, err := getTagResponse(ctx, p.authCfg, reqOpt.Timeout, repository, 1)
if err != nil {
return nil, err
}
Expand All @@ -42,29 +49,37 @@ func (p *DockerHub) Run(ctx context.Context, domain, repository string, reqOpt *
lastPage := calcMaxRequestPage(tagResp.Count, reqOpt.MaxCount, filterOpt)
// create ch (page - 1), already fetched first page,
tagsPerPage := make(chan []ImageSummary, lastPage-1)
eg := errgroup.Group{}
if err = p.controlGetTags(ctx, tagsPerPage, repository, 2, lastPage); err != nil {
return nil, err
}
for page := 2; page <= lastPage; page++ {
select {
case tags := <-tagsPerPage:
totalTagSummary = append(totalTagSummary, tags...)
}
}
close(tagsPerPage)
return p.convertResultToTag(totalTagSummary), nil
}

// rate limit for socket: too many open files
func (p *DockerHub) controlGetTags(ctx context.Context, tagsPerPage chan []ImageSummary, repository string, from, to int) error {
slots := make(chan struct{}, rateLimit)
eg := errgroup.Group{}
for page := from; page <= to; page++ {
page := page
slots <- struct{}{}
eg.Go(func() error {
tagResp, err := getTagResponse(ctx, auth, reqOpt.Timeout, repository, page)
defer func() { <-slots }()
tagResp, err := getTagResponse(ctx, p.authCfg, p.requestOpt.Timeout, repository, page)
if err != nil {
return err
}
tagsPerPage <- tagResp.Results
return nil
})
}
if err := eg.Wait(); err != nil {
return nil, err
}

for page := 2; page <= lastPage; page++ {
select {
case tags := <-tagsPerPage:
totalTagSummary = append(totalTagSummary, tags...)
}
}
return p.convertResultToTag(totalTagSummary), nil
return eg.Wait()
}

func summarizeByHash(summaries []ImageSummary) map[string]types.ImageTag {
Expand All @@ -81,16 +96,16 @@ func summarizeByHash(summaries []ImageSummary) map[string]types.ImageTag {
sort.Sort(imageSummary.Images)
firstHash := imageSummary.Images[0].Digest
target, ok := pools[firstHash]
// create first one if not exist
// create first hash key if not exist
if !ok {
pools[firstHash] = createImageTag(imageSummary)
pools[firstHash] = convertUploadImageTag(imageSummary)
continue
}
// set newer CreatedAt
// set newer uploaded at
target.Tags = append(target.Tags, imageSummary.Name)
createdAt, _ := time.Parse(time.RFC3339Nano, imageSummary.LastUpdated)
if createdAt.After(target.CreatedAt) {
target.CreatedAt = createdAt
uploadedAt, _ := time.Parse(time.RFC3339Nano, imageSummary.LastUpdated)
if uploadedAt.After(target.UploadedAt) {
target.UploadedAt = uploadedAt
}
pools[firstHash] = target
}
Expand All @@ -110,13 +125,13 @@ func (p *DockerHub) convertResultToTag(summaries []ImageSummary) types.ImageTags
return tags
}

func createImageTag(is ImageSummary) types.ImageTag {
createdAt, _ := time.Parse(time.RFC3339Nano, is.LastUpdated)
func convertUploadImageTag(is ImageSummary) types.ImageTag {
uploadedAt, _ := time.Parse(time.RFC3339Nano, is.LastUpdated)
tagNames := []string{is.Name}
return types.ImageTag{
Tags: tagNames,
Byte: is.FullSize,
CreatedAt: createdAt,
Tags: tagNames,
Byte: is.FullSize,
UploadedAt: uploadedAt,
}
}

Expand Down
18 changes: 9 additions & 9 deletions pkg/provider/dockerhub/dockerhub_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func TestScanImage(t *testing.T) {
sort.Strings(out)
return out
}),
cmpopts.IgnoreFields(types.ImageTag{}, "Byte", "CreatedAt"),
cmpopts.IgnoreFields(types.ImageTag{}, "Byte", "UploadedAt"),
}
sort.Sort(actual)
if diff := cmp.Diff(v.expected, actual, opts...); diff != "" {
Expand Down Expand Up @@ -128,12 +128,12 @@ func TestSummarizeByHash(t *testing.T) {
},
expected: map[string]types.ImageTag{
"400": {
Tags: []string{"a", "c"},
CreatedAt: time.Date(2019, time.December, 3, 0, 0, 0, 0, time.UTC),
Tags: []string{"a", "c"},
UploadedAt: time.Date(2019, time.December, 3, 0, 0, 0, 0, time.UTC),
},
"400b": {
Tags: []string{"b"},
CreatedAt: time.Date(2019, time.December, 1, 0, 0, 0, 0, time.UTC),
Tags: []string{"b"},
UploadedAt: time.Date(2019, time.December, 1, 0, 0, 0, 0, time.UTC),
},
},
},
Expand Down Expand Up @@ -175,12 +175,12 @@ func TestSummarizeByHash(t *testing.T) {
},
expected: map[string]types.ImageTag{
"400": {
Tags: []string{"a", "c"},
CreatedAt: time.Date(2019, time.December, 2, 0, 0, 0, 0, time.UTC),
Tags: []string{"a", "c"},
UploadedAt: time.Date(2019, time.December, 2, 0, 0, 0, 0, time.UTC),
},
"400b": {
Tags: []string{"b"},
CreatedAt: time.Date(2019, time.December, 1, 0, 0, 0, 0, time.UTC),
Tags: []string{"b"},
UploadedAt: time.Date(2019, time.December, 1, 0, 0, 0, 0, time.UTC),
},
},
},
Expand Down
10 changes: 5 additions & 5 deletions pkg/provider/dockerhub/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ type authToken struct {
Token string `json:"token"`
}

type DockerhubTokenTransport struct {
type tokenTransport struct {
Transport http.RoundTripper
Username string
Password string
}

// RoundTrip defines the round tripper for token transport.
func (t *DockerhubTokenTransport) RoundTrip(req *http.Request) (*http.Response, error) {
func (t *tokenTransport) RoundTrip(req *http.Request) (*http.Response, error) {
resp, err := t.Transport.RoundTrip(req)
if err != nil {
return resp, err
Expand All @@ -46,7 +46,7 @@ func isTokenDemand(resp *http.Response) bool {
return false
}

func (t *DockerhubTokenTransport) authAndRetry(req *http.Request) (*http.Response, error) {
func (t *tokenTransport) authAndRetry(req *http.Request) (*http.Response, error) {
token, authResp, err := t.auth(req.Context())
if err != nil {
return authResp, err
Expand All @@ -59,12 +59,12 @@ func (t *DockerhubTokenTransport) authAndRetry(req *http.Request) (*http.Respons
return response, err
}

func (t *DockerhubTokenTransport) retry(req *http.Request, token string) (*http.Response, error) {
func (t *tokenTransport) retry(req *http.Request, token string) (*http.Response, error) {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
return t.Transport.RoundTrip(req)
}

func (t *DockerhubTokenTransport) auth(ctx context.Context) (string, *http.Response, error) {
func (t *tokenTransport) auth(ctx context.Context) (string, *http.Response, error) {
jsonStr := []byte(fmt.Sprintf(`{"username": "%s","password": "%s"}`, t.Username, t.Password))
req, err := http.NewRequest("POST", authURL, bytes.NewBuffer(jsonStr))
req.Header.Set("Content-Type", "application/json")
Expand Down
3 changes: 2 additions & 1 deletion pkg/provider/ecr/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ import (
service "github.com/aws/aws-sdk-go/service/ecr"
)

// ECR :
type ECR struct{}

var _ time.Duration
var _ strings.Reader
var _ aws.Config

// Run : interface method
func (p *ECR) Run(ctx context.Context, domain, repository string, reqOpt *types.RequestOption, filterOpt *types.FilterOption) (types.ImageTags, error) {
sess, err := getSession(reqOpt)
if err != nil {
Expand Down Expand Up @@ -83,7 +85,6 @@ func (p *ECR) Run(ctx context.Context, domain, repository string, reqOpt *types.
imageTags = append(imageTags, types.ImageTag{
Tags: tags,
Byte: getIntByte(detail.ImageSizeInBytes),
CreatedAt: time.Time{},
UploadedAt: pushedAt,
})
}
Expand Down
10 changes: 6 additions & 4 deletions pkg/provider/gcr/gcr.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/goodwithtech/dockertags/pkg/registry"
)

// GCR :
type GCR struct {
registry *registry.Registry
domain string
Expand All @@ -30,16 +31,17 @@ type GCR struct {
type tagsResponse struct {
Next string `json:"next"`
Previous string `json:"previous"`
Manifest map[string]ManifestSummary `json:"manifest"`
Manifest map[string]manifestSummary `json:"manifest"`
}

type ManifestSummary struct {
type manifestSummary struct {
Tag []string `json:"tag"`
ImageSizeBytes string `json:"imageSizeBytes"`
CreatedMS string `json:"timeCreatedMs"`
UploadedMS string `json:"timeUploadedMs"`
}

// Run : interface method
func (p *GCR) Run(ctx context.Context, domain, repository string, reqOpt *types.RequestOption, filterOpt *types.FilterOption) (imageTags types.ImageTags, err error) {
p.domain = domain
p.reqOpt = reqOpt
Expand Down Expand Up @@ -93,8 +95,8 @@ func stringMStoTime(msstring string) (time.Time, error) {
}

// getTags returns the tags
func (p *GCR) getTags(ctx context.Context, repository string) (map[string]ManifestSummary, error) {
url := p.registry.Url("/v2/%s/tags/list", repository)
func (p *GCR) getTags(ctx context.Context, repository string) (map[string]manifestSummary, error) {
url := p.registry.GetURL("/v2/%s/tags/list", repository)
log.Logger.Debugf("url=%s,repository=%s", url, repository)

var response tagsResponse
Expand Down
Loading

0 comments on commit 48f8c4d

Please sign in to comment.