Skip to content

Commit

Permalink
Rewrite registry client to use docker distribution
Browse files Browse the repository at this point in the history
The package github.com/docker/distribution/registry/client (now) has
types and procedures for fetching image metadata.

This means we can get rid of a lot of the workarounds we were using to
patch `docker-registry-client` so that e.g., it works with quay.io.

I have changed the interfaces around a bit, since we usually need to
request image manifests "in a straight line", reusing the same
authorisation, and it makes sense to construct a client for each such
series of requests.

There are a few things we can keep track of across series of requests:
specifically, the challenges we've seen from each host. So it's still
useful to have a "factory" to hold that information, as well as other
commonalities like rate limiting.
  • Loading branch information
squaremo committed Dec 18, 2017
1 parent 3f7bad9 commit 0da0a8a
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 196 deletions.
17 changes: 5 additions & 12 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 4 additions & 5 deletions Gopkg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@
name = "k8s.io/client-go"
version = "4.0.0"

[[constraint]]
name = "github.com/heroku/docker-registry-client"
source = "github.com/weaveworks/docker-registry-client"
branch = "master"

[[override]]
name = "github.com/ugorji/go"
revision = "8c0409fcbb70099c748d71f714529204975f6c3f"

[[constraint]]
name = "github.com/docker/distribution"
branch = "master"
6 changes: 3 additions & 3 deletions registry/cache/warming.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func imageCredsToBacklog(imageCreds registry.ImageCreds) []backlogItem {
}

func (w *Warmer) warm(id image.Name, creds registry.Credentials) {
client, err := w.ClientFactory.ClientFor(id.Registry(), creds)
client, err := w.ClientFactory.ClientFor(id.CanonicalName(), creds)
if err != nil {
w.Logger.Log("err", err.Error())
return
Expand Down Expand Up @@ -136,7 +136,7 @@ func (w *Warmer) warm(id image.Name, creds registry.Credentials) {
}
}()

tags, err := client.Tags(id)
tags, err := client.Tags()
if err != nil {
if !strings.Contains(err.Error(), context.DeadlineExceeded.Error()) && !strings.Contains(err.Error(), "net/http: request canceled") {
w.Logger.Log("err", errors.Wrap(err, "requesting tags"))
Expand Down Expand Up @@ -188,7 +188,7 @@ func (w *Warmer) warm(id image.Name, creds registry.Credentials) {
go func(imageID image.Ref) {
defer func() { awaitFetchers.Done(); <-fetchers }()
// Get the image from the remote
img, err := client.Manifest(imageID)
img, err := client.Manifest(imageID.Tag)
if err != nil {
if err, ok := errors.Cause(err).(net.Error); ok && err.Timeout() {
// This was due to a context timeout, don't bother logging
Expand Down
165 changes: 102 additions & 63 deletions registry/client.go
Original file line number Diff line number Diff line change
@@ -1,100 +1,139 @@
package registry

import (
"context"
"encoding/json"
"fmt"
"errors"
"net/http"
"net/url"
"time"

dockerregistry "github.com/heroku/docker-registry-client/registry"
"github.com/pkg/errors"
"github.com/docker/distribution"
"github.com/docker/distribution/manifest/manifestlist"
"github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/manifest/schema2"
"github.com/docker/distribution/registry/client"
"github.com/opencontainers/go-digest"

"github.com/weaveworks/flux/image"
)

// An implementation of Client that represents a Remote registry.
// E.g. docker hub.
type Remote struct {
Registry *dockerregistry.Registry
transport http.RoundTripper
repo image.CanonicalName
}

// Return the tags for this repository.
func (a *Remote) Tags(id image.Name) ([]string, error) {
return a.Registry.Tags(id.Repository())
// Adapt to docker distribution reference.Named
type named struct {
image.CanonicalName
}

func (n named) Name() string {
return n.Image
}

func (n named) String() string {
return n.String()
}

// We need to do some adapting here to convert from the return values
// from dockerregistry to our domain types.
func (a *Remote) Manifest(id image.Ref) (image.Info, error) {
manifestV2, err := a.Registry.ManifestV2(id.Repository(), id.Tag)
// Return the tags for this repository.
func (a *Remote) Tags() ([]string, error) {
ctx := context.TODO()
repository, err := client.NewRepository(named{a.repo}, "https://"+a.repo.Domain, a.transport)
if err != nil {
if err, ok := err.(*url.Error); ok {
if err, ok := (err.Err).(*dockerregistry.HttpStatusError); ok {
if err.Response.StatusCode == http.StatusNotFound {
return a.ManifestFromV1(id)
}
}
}
return image.Info{}, err
}
// The above request will happily return a bogus, empty manifest
// if handed something other than a schema2 manifest.
if manifestV2.Config.Digest == "" {
return a.ManifestFromV1(id)
return nil, err
}
return repository.Tags(ctx).All(ctx)
}

// schema2 manifests have a reference to a blog that contains the
// image config. We have to fetch that in order to get the created
// datetime.
conf := manifestV2.Config
reader, err := a.Registry.DownloadLayer(id.Repository(), conf.Digest)
// Manifest fetches the metadata for an image reference; currently
// assumed to be in the same repo as that provided to `NewRemote(...)`
func (a *Remote) Manifest(ref string) (image.Info, error) {
ctx := context.TODO()
repository, err := client.NewRepository(named{a.repo}, "https://"+a.repo.Domain, a.transport)
if err != nil {
return image.Info{}, err
}
if reader == nil {
return image.Info{}, fmt.Errorf("nil reader from DownloadLayer")
manifests, err := repository.Manifests(ctx)
if err != nil {
return image.Info{}, err
}
manifest, fetchErr := manifests.Get(ctx, digest.Digest(ref), distribution.WithTagOption{ref})

type config struct {
Created time.Time `json:created`
interpret:
if fetchErr != nil {
return image.Info{}, err
}
var imageConf config

err = json.NewDecoder(reader).Decode(&imageConf)
mt, bytes, err := manifest.Payload()
if err != nil {
return image.Info{}, err
}
return image.Info{
ID: id,
CreatedAt: imageConf.Created,
}, nil
}

func (a *Remote) ManifestFromV1(id image.Ref) (image.Info, error) {
manifest, err := a.Registry.Manifest(id.Repository(), id.Tag)
if err != nil || manifest == nil {
return image.Info{}, errors.Wrap(err, "getting remote manifest")
}
info := image.Info{ID: a.repo.ToRef(ref)}

// the manifest includes some v1-backwards-compatibility data,
// oddly called "History", which are layer metadata as JSON
// strings; these appear most-recent (i.e., topmost layer) first,
// so happily we can just decode the first entry to get a created
// time.
type v1image struct {
// for decoding the v1-compatibility entry in schema1 manifests
var v1 struct {
ID string `json:"id"`
Created time.Time `json:"created"`
OS string `json:"os"`
Arch string `json:"architecture"`
}
var topmost v1image
var img image.Info
img.ID = id
if len(manifest.History) > 0 {
if err = json.Unmarshal([]byte(manifest.History[0].V1Compatibility), &topmost); err == nil {
if !topmost.Created.IsZero() {
img.CreatedAt = topmost.Created

// TODO(michael): can we type switch? Not sure how dependable the
// underlying types are.
switch mt {
case schema1.MediaTypeManifest:
// TODO: can this be fallthrough? Find something to check on...
var man schema1.Manifest
if err = json.Unmarshal(bytes, &man); err != nil {
return image.Info{}, err
}
if err = json.Unmarshal([]byte(man.History[0].V1Compatibility), &v1); err != nil {
return image.Info{}, err
}
info.CreatedAt = v1.Created
case schema1.MediaTypeSignedManifest:
var man schema1.SignedManifest
if err = json.Unmarshal(bytes, &man); err != nil {
return image.Info{}, err
}
if err = json.Unmarshal([]byte(man.History[0].V1Compatibility), &v1); err != nil {
return image.Info{}, err
}
info.CreatedAt = v1.Created
case schema2.MediaTypeManifest:
var man schema2.Manifest
if err = json.Unmarshal(bytes, &man); err != nil {
return image.Info{}, err
}

configBytes, err := repository.Blobs(ctx).Get(ctx, man.Config.Digest)
if err != nil {
return image.Info{}, err
}

var config struct {
Arch string `json:"architecture"`
Created time.Time `json:"created"`
OS string `json:"os"`
}
if err = json.Unmarshal(configBytes, &config); err != nil {
return image.Info{}, err
}
info.CreatedAt = config.Created
case manifestlist.MediaTypeManifestList:
var list manifestlist.ManifestList
if err = json.Unmarshal(bytes, &list); err != nil {
return image.Info{}, err
}
// TODO(michael): can we just pick the first one that matches?
for _, m := range list.Manifests {
if m.Platform.OS == "linux" && m.Platform.Architecture == "amd64" {
manifest, fetchErr = manifests.Get(ctx, m.Digest)
goto interpret
}
}
return image.Info{}, errors.New("no suitable manifest (linux amd64) in manifestlist")
}

return img, nil
return info, nil
}
Loading

0 comments on commit 0da0a8a

Please sign in to comment.