v1 fallback and fixes

Signed-off-by: Antonio Murdaca <runcom@redhat.com>
This commit is contained in:
Antonio Murdaca 2016-01-20 12:36:59 +01:00
parent 2431a49897
commit b59409255a
3 changed files with 266 additions and 74 deletions

View File

@ -1,6 +1,7 @@
package main
import (
"encoding/json"
"fmt"
"time"
@ -9,6 +10,8 @@ import (
"github.com/docker/distribution/digest"
"github.com/docker/docker/api"
"github.com/docker/docker/image"
"github.com/docker/docker/opts"
versionPkg "github.com/docker/docker/pkg/version"
"github.com/docker/docker/reference"
"github.com/docker/docker/registry"
types "github.com/docker/engine-api/types"
@ -59,16 +62,16 @@ func inspect(c *cli.Context) (*imageInspect, error) {
if err != nil {
return nil, err
}
authConfig, err := getAuthConfig(c, ref)
if err != nil {
return nil, err
}
var (
ii *imageInspect
)
// TODO(runcom): remove docker.io case cause unqualified images
// can be from additional registry below
// tweak the ParseNamed above so I can know if its unqualified
if ref.Hostname() != "" {
ii, err = getData(ref, authConfig)
if err != nil {
@ -77,7 +80,6 @@ func inspect(c *cli.Context) (*imageInspect, error) {
return ii, nil
}
_ = authConfig
// TODO(runcom): ...
// both authConfig and unqualified images
@ -93,16 +95,24 @@ func getData(ref reference.Named, authConfig types.AuthConfig) (*imageInspect, e
return nil, err
}
registryService := registry.NewService(nil)
// FATA[0000] open /etc/docker/certs.d/myreg.com:4000: permission denied
// need to be run as root, really? :(
// just pass tlsconfig via cli?!?!?!
//
// this happens only with private registry, docker.io works out of the box
// EDIT: this happens with v1 registries?! no
//
// TODO(runcom): do not assume docker is installed on the system!
// just fallback as for getAuthConfig
options := &registry.Options{}
options.InsecureRegistries = opts.NewListOpts(nil)
options.Mirrors = opts.NewListOpts(nil)
options.InsecureRegistries.Set("0.0.0.0/0")
registryService := registry.NewService(options)
for _, ic := range registryService.Config.IndexConfigs {
ic.Secure = false
}
endpoints, err := registryService.LookupPullEndpoints(repoInfo)
if err != nil {
return nil, err
@ -117,6 +127,21 @@ func getData(ref reference.Named, authConfig types.AuthConfig) (*imageInspect, e
)
for _, endpoint := range endpoints {
// TODO(runcom):
//
// always try to login first so the registry is pinged and we can return timeout
// instead of trying every v version (like push,pull and others do in docker)
//
//./skopeo --debug --username runcom --password 20121990cia0@! myreg.com:4000/rhel7
//DEBU[0000] hostDir: /etc/docker/certs.d/https:/index.docker.io/v1
//FATA[0000] open /etc/docker/certs.d/https:/index.docker.io/v1: permission denied
//
//status, err := registryService.Auth(&authConfig)
//if err != nil {
//return nil, err
//}
//logrus.Debug(status)
if confirmedV2 && endpoint.Version == registry.APIVersion1 {
logrus.Debugf("Skipping v1 endpoint %s because v2 registry was detected", endpoint.URL)
continue
@ -178,12 +203,13 @@ func newManifestFetcher(endpoint registry.APIEndpoint, repoInfo *registry.Reposi
service: registryService,
repoInfo: repoInfo,
}, nil
//case registry.APIVersion1:
//return &v1ManifestFetcher{
//endpoint: endpoint,
////config: config,
//repoInfo: repoInfo,
//}, nil
case registry.APIVersion1:
return &v1ManifestFetcher{
endpoint: endpoint,
authConfig: authConfig,
service: registryService,
repoInfo: repoInfo,
}, nil
}
return nil, fmt.Errorf("unknown version %d for registry %s", endpoint.Version, endpoint.URL)
}
@ -257,3 +283,45 @@ func makeImageInspect(repoInfo *registry.RepositoryInfo, img *image.Image, tag s
Registry: repoInfo.Index.Name,
}
}
func makeRawConfigFromV1Config(imageJSON []byte, rootfs *image.RootFS, history []image.History) (map[string]*json.RawMessage, error) {
var dver struct {
DockerVersion string `json:"docker_version"`
}
if err := json.Unmarshal(imageJSON, &dver); err != nil {
return nil, err
}
useFallback := versionPkg.Version(dver.DockerVersion).LessThan("1.8.3")
if useFallback {
var v1Image image.V1Image
err := json.Unmarshal(imageJSON, &v1Image)
if err != nil {
return nil, err
}
imageJSON, err = json.Marshal(v1Image)
if err != nil {
return nil, err
}
}
var c map[string]*json.RawMessage
if err := json.Unmarshal(imageJSON, &c); err != nil {
return nil, err
}
c["rootfs"] = rawJSON(rootfs)
c["history"] = rawJSON(history)
return c, nil
}
func rawJSON(value interface{}) *json.RawMessage {
jsonval, err := json.Marshal(value)
if err != nil {
return nil
}
return (*json.RawMessage)(&jsonval)
}

View File

@ -1 +1,171 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/Sirupsen/logrus"
"github.com/docker/distribution"
"github.com/docker/distribution/registry/client/transport"
"github.com/docker/docker/image"
"github.com/docker/docker/image/v1"
"github.com/docker/docker/reference"
"github.com/docker/docker/registry"
"github.com/docker/engine-api/types"
"golang.org/x/net/context"
)
type v1ManifestFetcher struct {
endpoint registry.APIEndpoint
repoInfo *registry.RepositoryInfo
repo distribution.Repository
confirmedV2 bool
// wrap in a config?
authConfig types.AuthConfig
service *registry.Service
session *registry.Session
}
func (mf *v1ManifestFetcher) Fetch(ctx context.Context, ref reference.Named) (*imageInspect, error) {
var (
imgInspect *imageInspect
)
if _, isCanonical := ref.(reference.Canonical); isCanonical {
// Allowing fallback, because HTTPS v1 is before HTTP v2
return nil, fallbackError{err: registry.ErrNoSupport{errors.New("Cannot pull by digest with v1 registry")}}
}
tag := ""
if tagged, isTagged := ref.(reference.NamedTagged); isTagged {
tag = tagged.Tag()
}
tlsConfig, err := mf.service.TLSConfig(mf.repoInfo.Index.Name)
if err != nil {
return nil, err
}
// Adds Docker-specific headers as well as user-specified headers (metaHeaders)
tr := transport.NewTransport(
registry.NewTransport(tlsConfig),
//registry.DockerHeaders(mf.config.MetaHeaders)...,
registry.DockerHeaders(nil)...,
)
client := registry.HTTPClient(tr)
//v1Endpoint, err := mf.endpoint.ToV1Endpoint(mf.config.MetaHeaders)
v1Endpoint, err := mf.endpoint.ToV1Endpoint(nil)
if err != nil {
logrus.Debugf("Could not get v1 endpoint: %v", err)
return nil, fallbackError{err: err}
}
mf.session, err = registry.NewSession(client, &mf.authConfig, v1Endpoint)
if err != nil {
logrus.Debugf("Fallback from error: %s", err)
return nil, fallbackError{err: err}
}
imgInspect, err = mf.fetchWithSession(ctx, tag)
if err != nil {
return nil, err
}
return imgInspect, nil
}
func (mf *v1ManifestFetcher) fetchWithSession(ctx context.Context, askedTag string) (*imageInspect, error) {
repoData, err := mf.session.GetRepositoryData(mf.repoInfo)
if err != nil {
if strings.Contains(err.Error(), "HTTP code: 404") {
return nil, fmt.Errorf("Error: image %s not found", mf.repoInfo.RemoteName)
}
// Unexpected HTTP error
return nil, err
}
logrus.Debugf("Retrieving the tag list from V1 endpoints")
tagsList, err := mf.session.GetRemoteTags(repoData.Endpoints, mf.repoInfo)
if err != nil {
logrus.Errorf("Unable to get remote tags: %s", err)
return nil, err
}
if len(tagsList) < 1 {
return nil, fmt.Errorf("No tags available for remote repository %s", mf.repoInfo.FullName())
}
for tag, id := range tagsList {
repoData.ImgList[id] = &registry.ImgData{
ID: id,
Tag: tag,
Checksum: "",
}
}
// If no tag has been specified, choose `latest` if it exists
if askedTag == "" {
if _, exists := tagsList[reference.DefaultTag]; exists {
askedTag = reference.DefaultTag
}
}
if askedTag == "" {
// fallback to any tag in the repository
for tag := range tagsList {
askedTag = tag
break
}
}
id, exists := tagsList[askedTag]
if !exists {
return nil, fmt.Errorf("Tag %s not found in repository %s", askedTag, mf.repoInfo.FullName())
}
img := repoData.ImgList[id]
var pulledImg *image.Image
for _, ep := range mf.repoInfo.Index.Mirrors {
if pulledImg, err = mf.pullImageJSON(img.ID, ep, repoData.Tokens); err != nil {
// Don't report errors when pulling from mirrors.
logrus.Debugf("Error pulling image json of %s:%s, mirror: %s, %s", mf.repoInfo.FullName(), img.Tag, ep, err)
continue
}
break
}
if pulledImg == nil {
for _, ep := range repoData.Endpoints {
if pulledImg, err = mf.pullImageJSON(img.ID, ep, repoData.Tokens); err != nil {
// It's not ideal that only the last error is returned, it would be better to concatenate the errors.
logrus.Infof("Error pulling image json of %s:%s, endpoint: %s, %v", mf.repoInfo.FullName(), img.Tag, ep, err)
continue
}
break
}
}
if err != nil {
return nil, fmt.Errorf("Error pulling image (%s) from %s, %v", img.Tag, mf.repoInfo.FullName(), err)
}
if pulledImg == nil {
return nil, fmt.Errorf("No such image %s:%s", mf.repoInfo.FullName(), askedTag)
}
return makeImageInspect(mf.repoInfo, pulledImg, askedTag, ""), nil
}
func (mf *v1ManifestFetcher) pullImageJSON(imgID, endpoint string, token []string) (*image.Image, error) {
imgJSON, _, err := mf.session.GetRemoteImageJSON(imgID, endpoint)
if err != nil {
return nil, err
}
h, err := v1.HistoryFromConfig(imgJSON, false)
if err != nil {
return nil, err
}
configRaw, err := makeRawConfigFromV1Config(imgJSON, image.NewRootFS(), []image.History{h})
if err != nil {
return nil, err
}
config, err := json.Marshal(configRaw)
if err != nil {
return nil, err
}
img, err := image.NewFromJSON(config)
if err != nil {
return nil, err
}
return img, nil
}

View File

@ -16,7 +16,6 @@ import (
dockerdistribution "github.com/docker/docker/distribution"
"github.com/docker/docker/image"
"github.com/docker/docker/image/v1"
versionPkg "github.com/docker/docker/pkg/version"
"github.com/docker/docker/reference"
"github.com/docker/docker/registry"
"github.com/docker/engine-api/types"
@ -28,8 +27,9 @@ type v2ManifestFetcher struct {
repoInfo *registry.RepositoryInfo
repo distribution.Repository
confirmedV2 bool
authConfig types.AuthConfig
service *registry.Service
// wrap in a config?
authConfig types.AuthConfig
service *registry.Service
}
func (mf *v2ManifestFetcher) Fetch(ctx context.Context, ref reference.Named) (*imageInspect, error) {
@ -45,21 +45,12 @@ func (mf *v2ManifestFetcher) Fetch(ctx context.Context, ref reference.Named) (*i
return nil, fallbackError{err: err, confirmedV2: mf.confirmedV2}
}
_, err = mf.service.Auth(&mf.authConfig)
if err != nil {
return nil, err
}
imgInspect, err = mf.fetchWithRepository(ctx, ref)
if err != nil {
switch t := err.(type) {
case errcode.Errors:
if len(t) == 1 {
err = t[0]
}
if _, ok := err.(fallbackError); ok {
return nil, err
}
if registry.ContinueOnError(err) {
logrus.Debugf("Error trying v2 registry: %v", err)
err = fallbackError{err: err, confirmedV2: mf.confirmedV2}
}
}
@ -92,8 +83,14 @@ func (mf *v2ManifestFetcher) fetchWithRepository(ctx context.Context, ref refere
} else {
tagList, err := mf.repo.Tags(ctx).All(ctx)
if err != nil {
return nil, err
return nil, allowV1Fallback(err)
}
// The v2 registry knows about this repository, so we will not
// allow fallback to the v1 protocol even if we encounter an
// error later on.
mf.confirmedV2 = true
for _, t := range tagList {
if t == reference.DefaultTag {
tag = t
@ -113,16 +110,16 @@ func (mf *v2ManifestFetcher) fetchWithRepository(ctx context.Context, ref refere
if err != nil {
return nil, allowV1Fallback(err)
}
// If manSvc.Get succeeded, we can be confident that the registry on
// the other side speaks the v2 protocol.
mf.confirmedV2 = true
}
if manifest == nil {
return nil, fmt.Errorf("image manifest does not exist for tag or digest %q", tagOrDigest)
}
// If manSvc.Get succeeded, we can be confident that the registry on
// the other side speaks the v2 protocol.
mf.confirmedV2 = true
var (
image *image.Image
manifestDigest digest.Digest
@ -202,48 +199,6 @@ func (mf *v2ManifestFetcher) pullSchema1(ctx context.Context, ref reference.Name
return img, manifestDigest, nil
}
func makeRawConfigFromV1Config(imageJSON []byte, rootfs *image.RootFS, history []image.History) (map[string]*json.RawMessage, error) {
var dver struct {
DockerVersion string `json:"docker_version"`
}
if err := json.Unmarshal(imageJSON, &dver); err != nil {
return nil, err
}
useFallback := versionPkg.Version(dver.DockerVersion).LessThan("1.8.3")
if useFallback {
var v1Image image.V1Image
err := json.Unmarshal(imageJSON, &v1Image)
if err != nil {
return nil, err
}
imageJSON, err = json.Marshal(v1Image)
if err != nil {
return nil, err
}
}
var c map[string]*json.RawMessage
if err := json.Unmarshal(imageJSON, &c); err != nil {
return nil, err
}
c["rootfs"] = rawJSON(rootfs)
c["history"] = rawJSON(history)
return c, nil
}
func rawJSON(value interface{}) *json.RawMessage {
jsonval, err := json.Marshal(value)
if err != nil {
return nil
}
return (*json.RawMessage)(&jsonval)
}
func verifySchema1Manifest(signedManifest *schema1.SignedManifest, ref reference.Named) (m *schema1.Manifest, err error) {
// If pull by digest, then verify the manifest digest. NOTE: It is
// important to do this first, before any other content validation. If the
@ -431,7 +386,6 @@ func allowV1Fallback(err error) error {
return fallbackError{err: err, confirmedV2: false}
}
}
return err
}