clair/cmd/clairctl/docker/dockerdist/dockerdist.go
Julien Garcia Gonzalez c52c952f0a add clairctl
2017-02-27 15:02:08 +01:00

272 lines
8.2 KiB
Go

// Copyright 2016 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package dockerdist provides helper methods for retrieving and parsing a
// information from a remote Docker repository.
package dockerdist
import (
"errors"
"net/url"
"reflect"
"strings"
"github.com/coreos/pkg/capnslog"
distlib "github.com/docker/distribution"
"github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/manifest/schema2"
"github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/registry/client"
"github.com/docker/docker/api/types"
"github.com/docker/docker/cli/config"
"github.com/docker/docker/distribution"
"github.com/docker/docker/dockerversion"
"github.com/docker/docker/reference"
"github.com/docker/docker/registry"
"github.com/opencontainers/go-digest"
"github.com/spf13/viper"
"golang.org/x/net/context"
)
var log = capnslog.NewPackageLogger("github.com/jgsqware/clairctl", "dockerdist")
var ErrTagNotFound = errors.New("this image or tag is not found")
func isInsecureRegistry(registryHostname string) bool {
for _, r := range viper.GetStringSlice("docker.insecure-registries") {
if r == registryHostname {
return true
}
}
return false
}
func getService() *registry.DefaultService {
serviceOptions := registry.ServiceOptions{
InsecureRegistries: viper.GetStringSlice("docker.insecure-registries"),
}
return registry.NewService(serviceOptions)
}
// getRepositoryClient returns a client for performing registry operations against the given named
// image.
func getRepositoryClient(image reference.Named, insecure bool, scopes ...string) (distlib.Repository, error) {
service := getService()
log.Debugf("Retrieving repository client")
ctx := context.Background()
authConfig, err := GetAuthCredentials(image.String())
if err != nil {
log.Debugf("GetAuthCredentials error: %v", err)
return nil, err
}
if (types.AuthConfig{}) != authConfig {
userAgent := dockerversion.DockerUserAgent(ctx)
_, _, err = service.Auth(ctx, &authConfig, userAgent)
if err != nil {
log.Debugf("Auth: err: %v", err)
return nil, err
}
}
repoInfo, err := service.ResolveRepository(image)
if err != nil {
log.Debugf("ResolveRepository err: %v", err)
return nil, err
}
metaHeaders := map[string][]string{}
endpoints, err := service.LookupPullEndpoints(image.Hostname())
if err != nil {
log.Debugf("registry.LookupPullEndpoints error: %v", err)
return nil, err
}
var confirmedV2 bool
var repository distlib.Repository
for _, endpoint := range endpoints {
if confirmedV2 && endpoint.Version == registry.APIVersion1 {
log.Debugf("Skipping v1 endpoint %s because v2 registry was detected", endpoint.URL)
continue
}
endpoint.TLSConfig.InsecureSkipVerify = viper.GetBool("auth.insecureSkipVerify")
if isInsecureRegistry(endpoint.URL.Host) {
endpoint.URL.Scheme = "http"
}
log.Debugf("endpoint.TLSConfig.InsecureSkipVerify: %v", endpoint.TLSConfig.InsecureSkipVerify)
repository, confirmedV2, err = distribution.NewV2Repository(ctx, repoInfo, endpoint, metaHeaders, &authConfig, scopes...)
if err != nil {
log.Debugf("cannot instanciate new v2 repository on %v", endpoint.URL)
return nil, err
}
if !confirmedV2 {
return nil, errors.New("Only V2 repository are supported")
}
break
}
return repository, nil
}
func GetPushURL(hostname string) (*url.URL, error) {
service := getService()
endpoints, err := service.LookupPushEndpoints(hostname)
if err != nil {
log.Debugf("registry.LookupPushEndpoints error: %v", err)
return nil, err
}
for _, endpoint := range endpoints {
endpoint.TLSConfig.InsecureSkipVerify = viper.GetBool("auth.insecureSkipVerify")
if isInsecureRegistry(endpoint.URL.Host) {
endpoint.URL.Scheme = "http"
}
return endpoint.URL, nil
}
return nil, errors.New("No endpoints found")
}
// getDigest returns the digest for the given image.
func getDigest(ctx context.Context, repo distlib.Repository, image reference.Named) (digest.Digest, error) {
if withDigest, ok := image.(reference.Canonical); ok {
return withDigest.Digest(), nil
}
// Get TagService.
tagSvc := repo.Tags(ctx)
// Get Tag name.
tag := "latest"
if withTag, ok := image.(reference.NamedTagged); ok {
tag = withTag.Tag()
}
// Get Tag's Descriptor.
descriptor, err := tagSvc.Get(ctx, tag)
if err != nil {
// Docker returns an UnexpectedHTTPResponseError if it cannot parse the JSON body of an
// unexpected error. Unfortunately, HEAD requests *by definition* don't have bodies, so
// Docker will return this error for non-200 HEAD requests. We therefore have to hack
// around it... *sigh*.
if _, ok := err.(*client.UnexpectedHTTPResponseError); ok {
return "", errors.New("Received error when trying to fetch the specified tag: it might not exist or you do not have access")
}
if strings.Contains(err.Error(), v2.ErrorCodeManifestUnknown.Message()) {
return "", ErrTagNotFound
}
return "", err
}
return descriptor.Digest, nil
}
// GetAuthCredentials returns the auth credentials (if any found) for the given repository, as found
// in the user's docker config.
func GetAuthCredentials(image string) (types.AuthConfig, error) {
// Lookup the index information for the name.
indexInfo, err := registry.ParseSearchIndexInfo(image)
if err != nil {
return types.AuthConfig{}, err
}
// Retrieve the user's Docker configuration file (if any).
configFile, err := config.Load(config.Dir())
if err != nil {
return types.AuthConfig{}, err
}
// Resolve the authentication information for the registry specified, via the config file.
return registry.ResolveAuthConfig(configFile.AuthConfigs, indexInfo), nil
}
// DownloadManifest the manifest for the given image, using the given credentials.
func DownloadManifest(image string, insecure bool) (reference.NamedTagged, distlib.Manifest, error) {
log.Debugf("Downloading manifest for %v", image)
// Parse the image name as a docker image reference.
n, err := reference.ParseNamed(image)
if err != nil {
return nil, nil, err
}
if reference.IsNameOnly(n) {
n, _ = reference.ParseNamed(image + ":" + reference.DefaultTag)
}
named := n.(reference.NamedTagged)
// Create a reference to a repository client for the repo.
repo, err := getRepositoryClient(named, insecure, "pull")
if err != nil {
return nil, nil, err
}
// Get the digest.
ctx := context.Background()
digest, err := getDigest(ctx, repo, named)
if err != nil {
return nil, nil, err
}
// Retrieve the manifest for the tag.
manSvc, err := repo.Manifests(ctx)
if err != nil {
return nil, nil, err
}
manifest, err := manSvc.Get(ctx, digest)
if err != nil {
return nil, nil, err
}
// Verify the manifest if it's signed.
log.Debugf("manifest type: %v", reflect.TypeOf(manifest))
switch manifest.(type) {
case *schema1.SignedManifest:
_, verr := schema1.Verify(manifest.(*schema1.SignedManifest))
if verr != nil {
return nil, nil, verr
}
case *schema2.DeserializedManifest:
log.Debugf("retrieved schema2 manifest, no verification")
default:
log.Printf("Could not verify manifest for image %v: not signed", image)
}
return named, manifest, nil
}
// DownloadV1Manifest the manifest for the given image in v1 schema format, using the given credentials.
func DownloadV1Manifest(imageName string, insecure bool) (reference.NamedTagged, schema1.SignedManifest, error) {
image, manifest, err := DownloadManifest(imageName, insecure)
if err != nil {
return nil, schema1.SignedManifest{}, err
}
// Ensure that the manifest type is supported.
switch manifest.(type) {
case *schema1.SignedManifest:
return image, *manifest.(*schema1.SignedManifest), nil
default:
return nil, schema1.SignedManifest{}, errors.New("only v1 manifests are currently supported")
}
}