Merge pull request #13375 from tiborvass/distribution-refactor
Vendor distribution client v2
This commit is contained in:
commit
5365e6b463
73 changed files with 6874 additions and 2379 deletions
|
@ -121,7 +121,7 @@ RUN git clone https://github.com/golang/tools.git /go/src/golang.org/x/tools \
|
|||
RUN gem install --no-rdoc --no-ri fpm --version 1.3.2
|
||||
|
||||
# Install registry
|
||||
ENV REGISTRY_COMMIT d957768537c5af40e4f4cd96871f7b2bde9e2923
|
||||
ENV REGISTRY_COMMIT 2317f721a3d8428215a2b65da4ae85212ed473b4
|
||||
RUN set -x \
|
||||
&& export GOPATH="$(mktemp -d)" \
|
||||
&& git clone https://github.com/docker/distribution.git "$GOPATH/src/github.com/docker/distribution" \
|
||||
|
|
|
@ -21,7 +21,7 @@ import (
|
|||
//
|
||||
// Usage: docker login SERVER
|
||||
func (cli *DockerCli) CmdLogin(args ...string) error {
|
||||
cmd := cli.Subcmd("login", []string{"[SERVER]"}, "Register or log in to a Docker registry server, if no server is\nspecified \""+registry.IndexServerAddress()+"\" is the default.", true)
|
||||
cmd := cli.Subcmd("login", []string{"[SERVER]"}, "Register or log in to a Docker registry server, if no server is\nspecified \""+registry.INDEXSERVER+"\" is the default.", true)
|
||||
cmd.Require(flag.Max, 1)
|
||||
|
||||
var username, password, email string
|
||||
|
@ -32,7 +32,7 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
|
|||
|
||||
cmd.ParseFlags(args, true)
|
||||
|
||||
serverAddress := registry.IndexServerAddress()
|
||||
serverAddress := registry.INDEXSERVER
|
||||
if len(cmd.Args()) > 0 {
|
||||
serverAddress = cmd.Arg(0)
|
||||
}
|
||||
|
|
|
@ -13,12 +13,12 @@ import (
|
|||
//
|
||||
// Usage: docker logout [SERVER]
|
||||
func (cli *DockerCli) CmdLogout(args ...string) error {
|
||||
cmd := cli.Subcmd("logout", []string{"[SERVER]"}, "Log out from a Docker registry, if no server is\nspecified \""+registry.IndexServerAddress()+"\" is the default.", true)
|
||||
cmd := cli.Subcmd("logout", []string{"[SERVER]"}, "Log out from a Docker registry, if no server is\nspecified \""+registry.INDEXSERVER+"\" is the default.", true)
|
||||
cmd.Require(flag.Max, 1)
|
||||
|
||||
cmd.ParseFlags(args, true)
|
||||
|
||||
serverAddress := registry.IndexServerAddress()
|
||||
serverAddress := registry.INDEXSERVER
|
||||
if len(cmd.Args()) > 0 {
|
||||
serverAddress = cmd.Arg(0)
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ func (daemon *Daemon) SystemInfo() (*types.Info, error) {
|
|||
NEventsListener: daemon.EventsService.SubscribersCount(),
|
||||
KernelVersion: kernelVersion,
|
||||
OperatingSystem: operatingSystem,
|
||||
IndexServerAddress: registry.IndexServerAddress(),
|
||||
IndexServerAddress: registry.INDEXSERVER,
|
||||
RegistryConfig: daemon.RegistryService.Config,
|
||||
InitSha1: dockerversion.INITSHA1,
|
||||
InitPath: initPath,
|
||||
|
|
|
@ -1,178 +0,0 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/trust"
|
||||
"github.com/docker/libtrust"
|
||||
)
|
||||
|
||||
// loadManifest loads a manifest from a byte array and verifies its content,
|
||||
// returning the local digest, the manifest itself, whether or not it was
|
||||
// verified. If ref is a digest, rather than a tag, this will be treated as
|
||||
// the local digest. An error will be returned if the signature verification
|
||||
// fails, local digest verification fails and, if provided, the remote digest
|
||||
// verification fails. The boolean return will only be false without error on
|
||||
// the failure of signatures trust check.
|
||||
func (s *TagStore) loadManifest(manifestBytes []byte, ref string, remoteDigest digest.Digest) (digest.Digest, *registry.ManifestData, bool, error) {
|
||||
payload, keys, err := unpackSignedManifest(manifestBytes)
|
||||
if err != nil {
|
||||
return "", nil, false, fmt.Errorf("error unpacking manifest: %v", err)
|
||||
}
|
||||
|
||||
// TODO(stevvooe): It would be a lot better here to build up a stack of
|
||||
// verifiers, then push the bytes one time for signatures and digests, but
|
||||
// the manifests are typically small, so this optimization is not worth
|
||||
// hacking this code without further refactoring.
|
||||
|
||||
var localDigest digest.Digest
|
||||
|
||||
// Verify the local digest, if present in ref. ParseDigest will validate
|
||||
// that the ref is a digest and verify against that if present. Otherwize
|
||||
// (on error), we simply compute the localDigest and proceed.
|
||||
if dgst, err := digest.ParseDigest(ref); err == nil {
|
||||
// verify the manifest against local ref
|
||||
if err := verifyDigest(dgst, payload); err != nil {
|
||||
return "", nil, false, fmt.Errorf("verifying local digest: %v", err)
|
||||
}
|
||||
|
||||
localDigest = dgst
|
||||
} else {
|
||||
// We don't have a local digest, since we are working from a tag.
|
||||
// Compute the digest of the payload and return that.
|
||||
logrus.Debugf("provided manifest reference %q is not a digest: %v", ref, err)
|
||||
localDigest, err = digest.FromBytes(payload)
|
||||
if err != nil {
|
||||
// near impossible
|
||||
logrus.Errorf("error calculating local digest during tag pull: %v", err)
|
||||
return "", nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
// verify against the remote digest, if available
|
||||
if remoteDigest != "" {
|
||||
if err := verifyDigest(remoteDigest, payload); err != nil {
|
||||
return "", nil, false, fmt.Errorf("verifying remote digest: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var manifest registry.ManifestData
|
||||
if err := json.Unmarshal(payload, &manifest); err != nil {
|
||||
return "", nil, false, fmt.Errorf("error unmarshalling manifest: %s", err)
|
||||
}
|
||||
|
||||
// validate the contents of the manifest
|
||||
if err := validateManifest(&manifest); err != nil {
|
||||
return "", nil, false, err
|
||||
}
|
||||
|
||||
var verified bool
|
||||
verified, err = s.verifyTrustedKeys(manifest.Name, keys)
|
||||
if err != nil {
|
||||
return "", nil, false, fmt.Errorf("error verifying trusted keys: %v", err)
|
||||
}
|
||||
|
||||
return localDigest, &manifest, verified, nil
|
||||
}
|
||||
|
||||
// unpackSignedManifest takes the raw, signed manifest bytes, unpacks the jws
|
||||
// and returns the payload and public keys used to signed the manifest.
|
||||
// Signatures are verified for authenticity but not against the trust store.
|
||||
func unpackSignedManifest(p []byte) ([]byte, []libtrust.PublicKey, error) {
|
||||
sig, err := libtrust.ParsePrettySignature(p, "signatures")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error parsing payload: %s", err)
|
||||
}
|
||||
|
||||
keys, err := sig.Verify()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error verifying payload: %s", err)
|
||||
}
|
||||
|
||||
payload, err := sig.Payload()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error retrieving payload: %s", err)
|
||||
}
|
||||
|
||||
return payload, keys, nil
|
||||
}
|
||||
|
||||
// verifyTrustedKeys checks the keys provided against the trust store,
|
||||
// ensuring that the provided keys are trusted for the namespace. The keys
|
||||
// provided from this method must come from the signatures provided as part of
|
||||
// the manifest JWS package, obtained from unpackSignedManifest or libtrust.
|
||||
func (s *TagStore) verifyTrustedKeys(namespace string, keys []libtrust.PublicKey) (verified bool, err error) {
|
||||
if namespace[0] != '/' {
|
||||
namespace = "/" + namespace
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
b, err := key.MarshalJSON()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error marshalling public key: %s", err)
|
||||
}
|
||||
// Check key has read/write permission (0x03)
|
||||
v, err := s.trustService.CheckKey(namespace, b, 0x03)
|
||||
if err != nil {
|
||||
vErr, ok := err.(trust.NotVerifiedError)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("error running key check: %s", err)
|
||||
}
|
||||
logrus.Debugf("Key check result: %v", vErr)
|
||||
}
|
||||
verified = v
|
||||
}
|
||||
|
||||
if verified {
|
||||
logrus.Debug("Key check result: verified")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// verifyDigest checks the contents of p against the provided digest. Note
|
||||
// that for manifests, this is the signed payload and not the raw bytes with
|
||||
// signatures.
|
||||
func verifyDigest(dgst digest.Digest, p []byte) error {
|
||||
if err := dgst.Validate(); err != nil {
|
||||
return fmt.Errorf("error validating digest %q: %v", dgst, err)
|
||||
}
|
||||
|
||||
verifier, err := digest.NewDigestVerifier(dgst)
|
||||
if err != nil {
|
||||
// There are not many ways this can go wrong: if it does, its
|
||||
// fatal. Likley, the cause would be poor validation of the
|
||||
// incoming reference.
|
||||
return fmt.Errorf("error creating verifier for digest %q: %v", dgst, err)
|
||||
}
|
||||
|
||||
if _, err := verifier.Write(p); err != nil {
|
||||
return fmt.Errorf("error writing payload to digest verifier (verifier target %q): %v", dgst, err)
|
||||
}
|
||||
|
||||
if !verifier.Verified() {
|
||||
return fmt.Errorf("verification against digest %q failed", dgst)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateManifest(manifest *registry.ManifestData) error {
|
||||
if manifest.SchemaVersion != 1 {
|
||||
return fmt.Errorf("unsupported schema version: %d", manifest.SchemaVersion)
|
||||
}
|
||||
|
||||
if len(manifest.FSLayers) != len(manifest.History) {
|
||||
return fmt.Errorf("length of history not equal to number of layers")
|
||||
}
|
||||
|
||||
if len(manifest.FSLayers) == 0 {
|
||||
return fmt.Errorf("no FSLayers in manifest")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,293 +0,0 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/runconfig"
|
||||
"github.com/docker/docker/utils"
|
||||
"github.com/docker/libtrust"
|
||||
)
|
||||
|
||||
const (
|
||||
testManifestImageName = "testapp"
|
||||
testManifestImageID = "d821b739e8834ec89ac4469266c3d11515da88fdcbcbdddcbcddb636f54fdde9"
|
||||
testManifestImageIDShort = "d821b739e883"
|
||||
testManifestTag = "manifesttest"
|
||||
)
|
||||
|
||||
func (s *TagStore) newManifest(localName, remoteName, tag string) ([]byte, error) {
|
||||
manifest := ®istry.ManifestData{
|
||||
Name: remoteName,
|
||||
Tag: tag,
|
||||
SchemaVersion: 1,
|
||||
}
|
||||
localRepo, err := s.Get(localName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if localRepo == nil {
|
||||
return nil, fmt.Errorf("Repo does not exist: %s", localName)
|
||||
}
|
||||
|
||||
// Get the top-most layer id which the tag points to
|
||||
layerId, exists := localRepo[tag]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("Tag does not exist for %s: %s", localName, tag)
|
||||
}
|
||||
layersSeen := make(map[string]bool)
|
||||
|
||||
layer, err := s.graph.Get(layerId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
manifest.Architecture = layer.Architecture
|
||||
manifest.FSLayers = make([]*registry.FSLayer, 0, 4)
|
||||
manifest.History = make([]*registry.ManifestHistory, 0, 4)
|
||||
var metadata runconfig.Config
|
||||
if layer.Config != nil {
|
||||
metadata = *layer.Config
|
||||
}
|
||||
|
||||
for ; layer != nil; layer, err = s.graph.GetParent(layer) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if layersSeen[layer.ID] {
|
||||
break
|
||||
}
|
||||
if layer.Config != nil && metadata.Image != layer.ID {
|
||||
err = runconfig.Merge(&metadata, layer.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
dgst, err := s.graph.GetDigest(layer.ID)
|
||||
if err == ErrDigestNotSet {
|
||||
archive, err := s.graph.TarLayer(layer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer archive.Close()
|
||||
|
||||
dgst, err = digest.FromReader(archive)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Save checksum value
|
||||
if err := s.graph.SetDigest(layer.ID, dgst); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("Error getting image checksum: %s", err)
|
||||
}
|
||||
|
||||
jsonData, err := s.graph.RawJSON(layer.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot retrieve the path for {%s}: %s", layer.ID, err)
|
||||
}
|
||||
|
||||
manifest.FSLayers = append(manifest.FSLayers, ®istry.FSLayer{BlobSum: dgst.String()})
|
||||
|
||||
layersSeen[layer.ID] = true
|
||||
|
||||
manifest.History = append(manifest.History, ®istry.ManifestHistory{V1Compatibility: string(jsonData)})
|
||||
}
|
||||
|
||||
manifestBytes, err := json.MarshalIndent(manifest, "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return manifestBytes, nil
|
||||
}
|
||||
|
||||
func TestManifestTarsumCache(t *testing.T) {
|
||||
tmp, err := utils.TestDirectory("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
store := mkTestTagStore(tmp, t)
|
||||
defer store.graph.driver.Cleanup()
|
||||
|
||||
archive, err := fakeTar()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
img := &Image{ID: testManifestImageID}
|
||||
if err := store.graph.Register(img, archive); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.Tag(testManifestImageName, testManifestTag, testManifestImageID, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := store.graph.GetDigest(testManifestImageID); err == nil {
|
||||
t.Fatalf("Non-empty checksum file after register")
|
||||
} else if err != ErrDigestNotSet {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Generate manifest
|
||||
payload, err := store.newManifest(testManifestImageName, testManifestImageName, testManifestTag)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
manifestChecksum, err := store.graph.GetDigest(testManifestImageID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var manifest registry.ManifestData
|
||||
if err := json.Unmarshal(payload, &manifest); err != nil {
|
||||
t.Fatalf("error unmarshalling manifest: %s", err)
|
||||
}
|
||||
|
||||
if len(manifest.FSLayers) != 1 {
|
||||
t.Fatalf("Unexpected number of layers, expecting 1: %d", len(manifest.FSLayers))
|
||||
}
|
||||
|
||||
if manifest.FSLayers[0].BlobSum != manifestChecksum.String() {
|
||||
t.Fatalf("Unexpected blob sum, expecting %q, got %q", manifestChecksum, manifest.FSLayers[0].BlobSum)
|
||||
}
|
||||
|
||||
if len(manifest.History) != 1 {
|
||||
t.Fatalf("Unexpected number of layer history, expecting 1: %d", len(manifest.History))
|
||||
}
|
||||
|
||||
v1compat, err := store.graph.RawJSON(img.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if manifest.History[0].V1Compatibility != string(v1compat) {
|
||||
t.Fatalf("Unexpected json value\nExpected:\n%s\nActual:\n%s", v1compat, manifest.History[0].V1Compatibility)
|
||||
}
|
||||
}
|
||||
|
||||
// TestManifestDigestCheck ensures that loadManifest properly verifies the
|
||||
// remote and local digest.
|
||||
func TestManifestDigestCheck(t *testing.T) {
|
||||
tmp, err := utils.TestDirectory("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
store := mkTestTagStore(tmp, t)
|
||||
defer store.graph.driver.Cleanup()
|
||||
|
||||
archive, err := fakeTar()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
img := &Image{ID: testManifestImageID}
|
||||
if err := store.graph.Register(img, archive); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.Tag(testManifestImageName, testManifestTag, testManifestImageID, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := store.graph.GetDigest(testManifestImageID); err == nil {
|
||||
t.Fatalf("Non-empty checksum file after register")
|
||||
} else if err != ErrDigestNotSet {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Generate manifest
|
||||
payload, err := store.newManifest(testManifestImageName, testManifestImageName, testManifestTag)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error generating test manifest: %v", err)
|
||||
}
|
||||
|
||||
pk, err := libtrust.GenerateECP256PrivateKey()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error generating private key: %v", err)
|
||||
}
|
||||
|
||||
sig, err := libtrust.NewJSONSignature(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("error creating signature: %v", err)
|
||||
}
|
||||
|
||||
if err := sig.Sign(pk); err != nil {
|
||||
t.Fatalf("error signing manifest bytes: %v", err)
|
||||
}
|
||||
|
||||
signedBytes, err := sig.PrettySignature("signatures")
|
||||
if err != nil {
|
||||
t.Fatalf("error getting signed bytes: %v", err)
|
||||
}
|
||||
|
||||
dgst, err := digest.FromBytes(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("error getting digest of manifest: %v", err)
|
||||
}
|
||||
|
||||
// use this as the "bad" digest
|
||||
zeroDigest, err := digest.FromBytes([]byte{})
|
||||
if err != nil {
|
||||
t.Fatalf("error making zero digest: %v", err)
|
||||
}
|
||||
|
||||
// Remote and local match, everything should look good
|
||||
local, _, _, err := store.loadManifest(signedBytes, dgst.String(), dgst)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error verifying local and remote digest: %v", err)
|
||||
}
|
||||
|
||||
if local != dgst {
|
||||
t.Fatalf("local digest not correctly calculated: %v", err)
|
||||
}
|
||||
|
||||
// remote and no local, since pulling by tag
|
||||
local, _, _, err = store.loadManifest(signedBytes, "tag", dgst)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error verifying tag pull and remote digest: %v", err)
|
||||
}
|
||||
|
||||
if local != dgst {
|
||||
t.Fatalf("local digest not correctly calculated: %v", err)
|
||||
}
|
||||
|
||||
// remote and differing local, this is the most important to fail
|
||||
local, _, _, err = store.loadManifest(signedBytes, zeroDigest.String(), dgst)
|
||||
if err == nil {
|
||||
t.Fatalf("error expected when verifying with differing local digest")
|
||||
}
|
||||
|
||||
// no remote, no local (by tag)
|
||||
local, _, _, err = store.loadManifest(signedBytes, "tag", "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error verifying manifest without remote digest: %v", err)
|
||||
}
|
||||
|
||||
if local != dgst {
|
||||
t.Fatalf("local digest not correctly calculated: %v", err)
|
||||
}
|
||||
|
||||
// no remote, with local
|
||||
local, _, _, err = store.loadManifest(signedBytes, dgst.String(), "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error verifying manifest without remote digest: %v", err)
|
||||
}
|
||||
|
||||
if local != dgst {
|
||||
t.Fatalf("local digest not correctly calculated: %v", err)
|
||||
}
|
||||
|
||||
// bad remote, we fail the check.
|
||||
local, _, _, err = store.loadManifest(signedBytes, dgst.String(), zeroDigest)
|
||||
if err == nil {
|
||||
t.Fatalf("error expected when verifying with differing remote digest")
|
||||
}
|
||||
}
|
744
graph/pull.go
744
graph/pull.go
|
@ -3,20 +3,10 @@ package graph
|
|||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/docker/cliconfig"
|
||||
"github.com/docker/docker/pkg/progressreader"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/pkg/transport"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/utils"
|
||||
)
|
||||
|
@ -27,10 +17,38 @@ type ImagePullConfig struct {
|
|||
OutStream io.Writer
|
||||
}
|
||||
|
||||
type Puller interface {
|
||||
// Pull tries to pull the image referenced by `tag`
|
||||
// Pull returns an error if any, as well as a boolean that determines whether to retry Pull on the next configured endpoint.
|
||||
//
|
||||
// TODO(tiborvass): have Pull() take a reference to repository + tag, so that the puller itself is repository-agnostic.
|
||||
Pull(tag string) (fallback bool, err error)
|
||||
}
|
||||
|
||||
func NewPuller(s *TagStore, endpoint registry.APIEndpoint, repoInfo *registry.RepositoryInfo, imagePullConfig *ImagePullConfig, sf *streamformatter.StreamFormatter) (Puller, error) {
|
||||
switch endpoint.Version {
|
||||
case registry.APIVersion2:
|
||||
return &v2Puller{
|
||||
TagStore: s,
|
||||
endpoint: endpoint,
|
||||
config: imagePullConfig,
|
||||
sf: sf,
|
||||
repoInfo: repoInfo,
|
||||
}, nil
|
||||
case registry.APIVersion1:
|
||||
return &v1Puller{
|
||||
TagStore: s,
|
||||
endpoint: endpoint,
|
||||
config: imagePullConfig,
|
||||
sf: sf,
|
||||
repoInfo: repoInfo,
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unknown version %d for registry %s", endpoint.Version, endpoint.URL)
|
||||
}
|
||||
|
||||
func (s *TagStore) Pull(image string, tag string, imagePullConfig *ImagePullConfig) error {
|
||||
var (
|
||||
sf = streamformatter.NewJSONStreamFormatter()
|
||||
)
|
||||
var sf = streamformatter.NewJSONStreamFormatter()
|
||||
|
||||
// Resolve the Repository name from fqn to RepositoryInfo
|
||||
repoInfo, err := s.registryService.ResolveRepository(image)
|
||||
|
@ -38,424 +56,74 @@ func (s *TagStore) Pull(image string, tag string, imagePullConfig *ImagePullConf
|
|||
return err
|
||||
}
|
||||
|
||||
// makes sure name is not empty or `scratch`
|
||||
if err := validateRepoName(repoInfo.LocalName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c, err := s.poolAdd("pull", utils.ImageReference(repoInfo.LocalName, tag))
|
||||
endpoints, err := s.registryService.LookupEndpoints(repoInfo.CanonicalName)
|
||||
if err != nil {
|
||||
if c != nil {
|
||||
// Another pull of the same repository is already taking place; just wait for it to finish
|
||||
imagePullConfig.OutStream.Write(sf.FormatStatus("", "Repository %s already being pulled by another client. Waiting.", repoInfo.LocalName))
|
||||
<-c
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer s.poolRemove("pull", utils.ImageReference(repoInfo.LocalName, tag))
|
||||
|
||||
logName := repoInfo.LocalName
|
||||
if tag != "" {
|
||||
logName = utils.ImageReference(logName, tag)
|
||||
}
|
||||
|
||||
// Attempt pulling official content from a provided v2 mirror
|
||||
if repoInfo.Index.Official {
|
||||
v2mirrorEndpoint, v2mirrorRepoInfo, err := configureV2Mirror(repoInfo, s.registryService)
|
||||
if err != nil {
|
||||
logrus.Errorf("Error configuring mirrors: %s", err)
|
||||
return err
|
||||
}
|
||||
var (
|
||||
lastErr error
|
||||
|
||||
if v2mirrorEndpoint != nil {
|
||||
logrus.Debugf("Attempting to pull from v2 mirror: %s", v2mirrorEndpoint.URL)
|
||||
return s.pullFromV2Mirror(v2mirrorEndpoint, v2mirrorRepoInfo, imagePullConfig, tag, sf, logName)
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Debugf("pulling image from host %q with remote name %q", repoInfo.Index.Name, repoInfo.RemoteName)
|
||||
|
||||
endpoint, err := repoInfo.GetEndpoint(imagePullConfig.MetaHeaders)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO(tiborvass): reuse client from endpoint?
|
||||
// Adds Docker-specific headers as well as user-specified headers (metaHeaders)
|
||||
tr := transport.NewTransport(
|
||||
registry.NewTransport(registry.ReceiveTimeout, endpoint.IsSecure),
|
||||
registry.DockerHeaders(imagePullConfig.MetaHeaders)...,
|
||||
// discardNoSupportErrors is used to track whether an endpoint encountered an error of type registry.ErrNoSupport
|
||||
// By default it is false, which means that if a ErrNoSupport error is encountered, it will be saved in lastErr.
|
||||
// As soon as another kind of error is encountered, discardNoSupportErrors is set to true, avoiding the saving of
|
||||
// any subsequent ErrNoSupport errors in lastErr.
|
||||
// It's needed for pull-by-digest on v1 endpoints: if there are only v1 endpoints configured, the error should be
|
||||
// returned and displayed, but if there was a v2 endpoint which supports pull-by-digest, then the last relevant
|
||||
// error is the ones from v2 endpoints not v1.
|
||||
discardNoSupportErrors bool
|
||||
)
|
||||
client := registry.HTTPClient(tr)
|
||||
r, err := registry.NewSession(client, imagePullConfig.AuthConfig, endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, endpoint := range endpoints {
|
||||
logrus.Debugf("Trying to pull %s from %s %s", repoInfo.LocalName, endpoint.URL, endpoint.Version)
|
||||
|
||||
if len(repoInfo.Index.Mirrors) == 0 && (repoInfo.Index.Official || endpoint.Version == registry.APIVersion2) {
|
||||
if repoInfo.Official {
|
||||
s.trustService.UpdateBase()
|
||||
if !endpoint.Mirror && (endpoint.Official || endpoint.Version == registry.APIVersion2) {
|
||||
if repoInfo.Official {
|
||||
s.trustService.UpdateBase()
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Debugf("pulling v2 repository with local name %q", repoInfo.LocalName)
|
||||
if err := s.pullV2Repository(r, imagePullConfig.OutStream, repoInfo, tag, sf); err == nil {
|
||||
s.eventsService.Log("pull", logName, "")
|
||||
return nil
|
||||
} else if err != registry.ErrDoesNotExist && err != ErrV2RegistryUnavailable {
|
||||
logrus.Errorf("Error from V2 registry: %s", err)
|
||||
}
|
||||
|
||||
logrus.Debug("image does not exist on v2 registry, falling back to v1")
|
||||
}
|
||||
|
||||
if utils.DigestReference(tag) {
|
||||
return fmt.Errorf("pulling with digest reference failed from v2 registry")
|
||||
}
|
||||
|
||||
logrus.Debugf("pulling v1 repository with local name %q", repoInfo.LocalName)
|
||||
if err = s.pullRepository(r, imagePullConfig.OutStream, repoInfo, tag, sf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.eventsService.Log("pull", logName, "")
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func makeMirrorRepoInfo(repoInfo *registry.RepositoryInfo, mirror string) *registry.RepositoryInfo {
|
||||
mirrorRepo := ®istry.RepositoryInfo{
|
||||
RemoteName: repoInfo.RemoteName,
|
||||
LocalName: repoInfo.LocalName,
|
||||
CanonicalName: repoInfo.CanonicalName,
|
||||
Official: false,
|
||||
|
||||
Index: ®istry.IndexInfo{
|
||||
Official: false,
|
||||
Secure: repoInfo.Index.Secure,
|
||||
Name: mirror,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
}
|
||||
return mirrorRepo
|
||||
}
|
||||
|
||||
func configureV2Mirror(repoInfo *registry.RepositoryInfo, s *registry.Service) (*registry.Endpoint, *registry.RepositoryInfo, error) {
|
||||
mirrors := repoInfo.Index.Mirrors
|
||||
if len(mirrors) == 0 {
|
||||
// no mirrors configured
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
v1MirrorCount := 0
|
||||
var v2MirrorEndpoint *registry.Endpoint
|
||||
var v2MirrorRepoInfo *registry.RepositoryInfo
|
||||
for _, mirror := range mirrors {
|
||||
mirrorRepoInfo := makeMirrorRepoInfo(repoInfo, mirror)
|
||||
endpoint, err := registry.NewEndpoint(mirrorRepoInfo.Index, nil)
|
||||
puller, err := NewPuller(s, endpoint, repoInfo, imagePullConfig, sf)
|
||||
if err != nil {
|
||||
logrus.Errorf("Unable to create endpoint for %s: %s", mirror, err)
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
if endpoint.Version == 2 {
|
||||
if v2MirrorEndpoint == nil {
|
||||
v2MirrorEndpoint = endpoint
|
||||
v2MirrorRepoInfo = mirrorRepoInfo
|
||||
} else {
|
||||
// > 1 v2 mirrors given
|
||||
return nil, nil, fmt.Errorf("multiple v2 mirrors configured")
|
||||
}
|
||||
} else {
|
||||
v1MirrorCount++
|
||||
}
|
||||
}
|
||||
|
||||
if v1MirrorCount == len(mirrors) {
|
||||
// OK, but mirrors are v1
|
||||
return nil, nil, nil
|
||||
}
|
||||
if v2MirrorEndpoint != nil && v1MirrorCount == 0 {
|
||||
// OK, 1 v2 mirror specified
|
||||
return v2MirrorEndpoint, v2MirrorRepoInfo, nil
|
||||
}
|
||||
if v2MirrorEndpoint != nil && v1MirrorCount > 0 {
|
||||
return nil, nil, fmt.Errorf("v1 and v2 mirrors configured")
|
||||
}
|
||||
// No endpoint could be established with the given mirror configurations
|
||||
// Fallback to pulling from the hub as per v1 behavior.
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (s *TagStore) pullFromV2Mirror(mirrorEndpoint *registry.Endpoint, repoInfo *registry.RepositoryInfo,
|
||||
imagePullConfig *ImagePullConfig, tag string, sf *streamformatter.StreamFormatter, logName string) error {
|
||||
|
||||
tr := transport.NewTransport(
|
||||
registry.NewTransport(registry.ReceiveTimeout, mirrorEndpoint.IsSecure),
|
||||
registry.DockerHeaders(imagePullConfig.MetaHeaders)...,
|
||||
)
|
||||
client := registry.HTTPClient(tr)
|
||||
mirrorSession, err := registry.NewSession(client, &cliconfig.AuthConfig{}, mirrorEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logrus.Debugf("Pulling v2 repository with local name %q from %s", repoInfo.LocalName, mirrorEndpoint.URL)
|
||||
if err := s.pullV2Repository(mirrorSession, imagePullConfig.OutStream, repoInfo, tag, sf); err != nil {
|
||||
return err
|
||||
}
|
||||
s.eventsService.Log("pull", logName, "")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *TagStore) pullRepository(r *registry.Session, out io.Writer, repoInfo *registry.RepositoryInfo, askedTag string, sf *streamformatter.StreamFormatter) error {
|
||||
out.Write(sf.FormatStatus("", "Pulling repository %s", repoInfo.CanonicalName))
|
||||
|
||||
repoData, err := r.GetRepositoryData(repoInfo.RemoteName)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "HTTP code: 404") {
|
||||
return fmt.Errorf("Error: image %s not found", utils.ImageReference(repoInfo.RemoteName, askedTag))
|
||||
}
|
||||
// Unexpected HTTP error
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Debugf("Retrieving the tag list")
|
||||
tagsList := make(map[string]string)
|
||||
if askedTag == "" {
|
||||
tagsList, err = r.GetRemoteTags(repoData.Endpoints, repoInfo.RemoteName)
|
||||
} else {
|
||||
var tagId string
|
||||
tagId, err = r.GetRemoteTag(repoData.Endpoints, repoInfo.RemoteName, askedTag)
|
||||
tagsList[askedTag] = tagId
|
||||
}
|
||||
if err != nil {
|
||||
if err == registry.ErrRepoNotFound && askedTag != "" {
|
||||
return fmt.Errorf("Tag %s not found in repository %s", askedTag, repoInfo.CanonicalName)
|
||||
}
|
||||
logrus.Errorf("unable to get remote tags: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
for tag, id := range tagsList {
|
||||
repoData.ImgList[id] = ®istry.ImgData{
|
||||
ID: id,
|
||||
Tag: tag,
|
||||
Checksum: "",
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Debugf("Registering tags")
|
||||
// If no tag has been specified, pull them all
|
||||
if askedTag == "" {
|
||||
for tag, id := range tagsList {
|
||||
repoData.ImgList[id].Tag = tag
|
||||
}
|
||||
} else {
|
||||
// Otherwise, check that the tag exists and use only that one
|
||||
id, exists := tagsList[askedTag]
|
||||
if !exists {
|
||||
return fmt.Errorf("Tag %s not found in repository %s", askedTag, repoInfo.CanonicalName)
|
||||
}
|
||||
repoData.ImgList[id].Tag = askedTag
|
||||
}
|
||||
|
||||
errors := make(chan error)
|
||||
|
||||
layersDownloaded := false
|
||||
for _, image := range repoData.ImgList {
|
||||
downloadImage := func(img *registry.ImgData) {
|
||||
if askedTag != "" && img.Tag != askedTag {
|
||||
errors <- nil
|
||||
return
|
||||
}
|
||||
|
||||
if img.Tag == "" {
|
||||
logrus.Debugf("Image (id: %s) present in this repository but untagged, skipping", img.ID)
|
||||
errors <- nil
|
||||
return
|
||||
}
|
||||
|
||||
// ensure no two downloads of the same image happen at the same time
|
||||
if c, err := s.poolAdd("pull", "img:"+img.ID); err != nil {
|
||||
if c != nil {
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(img.ID), "Layer already being pulled by another client. Waiting.", nil))
|
||||
<-c
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(img.ID), "Download complete", nil))
|
||||
} else {
|
||||
logrus.Debugf("Image (id: %s) pull is already running, skipping: %v", img.ID, err)
|
||||
if fallback, err := puller.Pull(tag); err != nil {
|
||||
if fallback {
|
||||
if _, ok := err.(registry.ErrNoSupport); !ok {
|
||||
// Because we found an error that's not ErrNoSupport, discard all subsequent ErrNoSupport errors.
|
||||
discardNoSupportErrors = true
|
||||
// save the current error
|
||||
lastErr = err
|
||||
} else if !discardNoSupportErrors {
|
||||
// Save the ErrNoSupport error, because it's either the first error or all encountered errors
|
||||
// were also ErrNoSupport errors.
|
||||
lastErr = err
|
||||
}
|
||||
errors <- nil
|
||||
return
|
||||
continue
|
||||
}
|
||||
defer s.poolRemove("pull", "img:"+img.ID)
|
||||
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s", img.Tag, repoInfo.CanonicalName), nil))
|
||||
success := false
|
||||
var lastErr, err error
|
||||
var isDownloaded bool
|
||||
for _, ep := range repoInfo.Index.Mirrors {
|
||||
// Ensure endpoint is v1
|
||||
ep = ep + "v1/"
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s, mirror: %s", img.Tag, repoInfo.CanonicalName, ep), nil))
|
||||
if isDownloaded, err = s.pullImage(r, out, img.ID, ep, repoData.Tokens, sf); err != nil {
|
||||
// Don't report errors when pulling from mirrors.
|
||||
logrus.Debugf("Error pulling image (%s) from %s, mirror: %s, %s", img.Tag, repoInfo.CanonicalName, ep, err)
|
||||
continue
|
||||
}
|
||||
layersDownloaded = layersDownloaded || isDownloaded
|
||||
success = true
|
||||
break
|
||||
}
|
||||
if !success {
|
||||
for _, ep := range repoData.Endpoints {
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s, endpoint: %s", img.Tag, repoInfo.CanonicalName, ep), nil))
|
||||
if isDownloaded, err = s.pullImage(r, out, img.ID, ep, repoData.Tokens, sf); err != nil {
|
||||
// It's not ideal that only the last error is returned, it would be better to concatenate the errors.
|
||||
// As the error is also given to the output stream the user will see the error.
|
||||
lastErr = err
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(img.ID), fmt.Sprintf("Error pulling image (%s) from %s, endpoint: %s, %s", img.Tag, repoInfo.CanonicalName, ep, err), nil))
|
||||
continue
|
||||
}
|
||||
layersDownloaded = layersDownloaded || isDownloaded
|
||||
success = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !success {
|
||||
err := fmt.Errorf("Error pulling image (%s) from %s, %v", img.Tag, repoInfo.CanonicalName, lastErr)
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(img.ID), err.Error(), nil))
|
||||
errors <- err
|
||||
return
|
||||
}
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(img.ID), "Download complete", nil))
|
||||
|
||||
errors <- nil
|
||||
}
|
||||
|
||||
go downloadImage(image)
|
||||
}
|
||||
|
||||
var lastError error
|
||||
for i := 0; i < len(repoData.ImgList); i++ {
|
||||
if err := <-errors; err != nil {
|
||||
lastError = err
|
||||
}
|
||||
}
|
||||
if lastError != nil {
|
||||
return lastError
|
||||
}
|
||||
|
||||
for tag, id := range tagsList {
|
||||
if askedTag != "" && tag != askedTag {
|
||||
continue
|
||||
}
|
||||
if err := s.Tag(repoInfo.LocalName, tag, id, true); err != nil {
|
||||
logrus.Debugf("Not continuing with error: %v", err)
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
s.eventsService.Log("pull", logName, "")
|
||||
return nil
|
||||
}
|
||||
|
||||
requestedTag := repoInfo.LocalName
|
||||
if len(askedTag) > 0 {
|
||||
requestedTag = utils.ImageReference(repoInfo.LocalName, askedTag)
|
||||
if lastErr == nil {
|
||||
lastErr = fmt.Errorf("no endpoints found for %s", image)
|
||||
}
|
||||
WriteStatus(requestedTag, out, sf, layersDownloaded)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *TagStore) pullImage(r *registry.Session, out io.Writer, imgID, endpoint string, token []string, sf *streamformatter.StreamFormatter) (bool, error) {
|
||||
history, err := r.GetRemoteHistory(imgID, endpoint)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(imgID), "Pulling dependent layers", nil))
|
||||
// FIXME: Try to stream the images?
|
||||
// FIXME: Launch the getRemoteImage() in goroutines
|
||||
|
||||
layersDownloaded := false
|
||||
for i := len(history) - 1; i >= 0; i-- {
|
||||
id := history[i]
|
||||
|
||||
// ensure no two downloads of the same layer happen at the same time
|
||||
if c, err := s.poolAdd("pull", "layer:"+id); err != nil {
|
||||
logrus.Debugf("Image (id: %s) pull is already running, skipping: %v", id, err)
|
||||
<-c
|
||||
}
|
||||
defer s.poolRemove("pull", "layer:"+id)
|
||||
|
||||
if !s.graph.Exists(id) {
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(id), "Pulling metadata", nil))
|
||||
var (
|
||||
imgJSON []byte
|
||||
imgSize int
|
||||
err error
|
||||
img *Image
|
||||
)
|
||||
retries := 5
|
||||
for j := 1; j <= retries; j++ {
|
||||
imgJSON, imgSize, err = r.GetRemoteImageJSON(id, endpoint)
|
||||
if err != nil && j == retries {
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(id), "Error pulling dependent layers", nil))
|
||||
return layersDownloaded, err
|
||||
} else if err != nil {
|
||||
time.Sleep(time.Duration(j) * 500 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
img, err = NewImgJSON(imgJSON)
|
||||
layersDownloaded = true
|
||||
if err != nil && j == retries {
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(id), "Error pulling dependent layers", nil))
|
||||
return layersDownloaded, fmt.Errorf("Failed to parse json: %s", err)
|
||||
} else if err != nil {
|
||||
time.Sleep(time.Duration(j) * 500 * time.Millisecond)
|
||||
continue
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for j := 1; j <= retries; j++ {
|
||||
// Get the layer
|
||||
status := "Pulling fs layer"
|
||||
if j > 1 {
|
||||
status = fmt.Sprintf("Pulling fs layer [retries: %d]", j)
|
||||
}
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(id), status, nil))
|
||||
layer, err := r.GetRemoteImageLayer(img.ID, endpoint, int64(imgSize))
|
||||
if uerr, ok := err.(*url.Error); ok {
|
||||
err = uerr.Err
|
||||
}
|
||||
if terr, ok := err.(net.Error); ok && terr.Timeout() && j < retries {
|
||||
time.Sleep(time.Duration(j) * 500 * time.Millisecond)
|
||||
continue
|
||||
} else if err != nil {
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(id), "Error pulling dependent layers", nil))
|
||||
return layersDownloaded, err
|
||||
}
|
||||
layersDownloaded = true
|
||||
defer layer.Close()
|
||||
|
||||
err = s.graph.Register(img,
|
||||
progressreader.New(progressreader.Config{
|
||||
In: layer,
|
||||
Out: out,
|
||||
Formatter: sf,
|
||||
Size: imgSize,
|
||||
NewLines: false,
|
||||
ID: stringid.TruncateID(id),
|
||||
Action: "Downloading",
|
||||
}))
|
||||
if terr, ok := err.(net.Error); ok && terr.Timeout() && j < retries {
|
||||
time.Sleep(time.Duration(j) * 500 * time.Millisecond)
|
||||
continue
|
||||
} else if err != nil {
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(id), "Error downloading dependent layers", nil))
|
||||
return layersDownloaded, err
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(id), "Download complete", nil))
|
||||
}
|
||||
return layersDownloaded, nil
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func WriteStatus(requestedTag string, out io.Writer, sf *streamformatter.StreamFormatter, layersDownloaded bool) {
|
||||
|
@ -465,273 +133,3 @@ func WriteStatus(requestedTag string, out io.Writer, sf *streamformatter.StreamF
|
|||
out.Write(sf.FormatStatus("", "Status: Image is up to date for %s", requestedTag))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TagStore) pullV2Repository(r *registry.Session, out io.Writer, repoInfo *registry.RepositoryInfo, tag string, sf *streamformatter.StreamFormatter) error {
|
||||
endpoint, err := r.V2RegistryEndpoint(repoInfo.Index)
|
||||
if err != nil {
|
||||
if repoInfo.Index.Official {
|
||||
logrus.Debugf("Unable to pull from V2 registry, falling back to v1: %s", err)
|
||||
return ErrV2RegistryUnavailable
|
||||
}
|
||||
return fmt.Errorf("error getting registry endpoint: %s", err)
|
||||
}
|
||||
auth, err := r.GetV2Authorization(endpoint, repoInfo.RemoteName, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting authorization: %s", err)
|
||||
}
|
||||
if !auth.CanAuthorizeV2() {
|
||||
return ErrV2RegistryUnavailable
|
||||
}
|
||||
|
||||
var layersDownloaded bool
|
||||
if tag == "" {
|
||||
logrus.Debugf("Pulling tag list from V2 registry for %s", repoInfo.CanonicalName)
|
||||
tags, err := r.GetV2RemoteTags(endpoint, repoInfo.RemoteName, auth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(tags) == 0 {
|
||||
return registry.ErrDoesNotExist
|
||||
}
|
||||
for _, t := range tags {
|
||||
if downloaded, err := s.pullV2Tag(r, out, endpoint, repoInfo, t, sf, auth); err != nil {
|
||||
return err
|
||||
} else if downloaded {
|
||||
layersDownloaded = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if downloaded, err := s.pullV2Tag(r, out, endpoint, repoInfo, tag, sf, auth); err != nil {
|
||||
return err
|
||||
} else if downloaded {
|
||||
layersDownloaded = true
|
||||
}
|
||||
}
|
||||
|
||||
requestedTag := repoInfo.LocalName
|
||||
if len(tag) > 0 {
|
||||
requestedTag = utils.ImageReference(repoInfo.LocalName, tag)
|
||||
}
|
||||
WriteStatus(requestedTag, out, sf, layersDownloaded)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *TagStore) pullV2Tag(r *registry.Session, out io.Writer, endpoint *registry.Endpoint, repoInfo *registry.RepositoryInfo, tag string, sf *streamformatter.StreamFormatter, auth *registry.RequestAuthorization) (bool, error) {
|
||||
logrus.Debugf("Pulling tag from V2 registry: %q", tag)
|
||||
|
||||
remoteDigest, manifestBytes, err := r.GetV2ImageManifest(endpoint, repoInfo.RemoteName, tag, auth)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// loadManifest ensures that the manifest payload has the expected digest
|
||||
// if the tag is a digest reference.
|
||||
localDigest, manifest, verified, err := s.loadManifest(manifestBytes, tag, remoteDigest)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error verifying manifest: %s", err)
|
||||
}
|
||||
|
||||
if verified {
|
||||
logrus.Printf("Image manifest for %s has been verified", utils.ImageReference(repoInfo.CanonicalName, tag))
|
||||
}
|
||||
out.Write(sf.FormatStatus(tag, "Pulling from %s", repoInfo.CanonicalName))
|
||||
|
||||
// downloadInfo is used to pass information from download to extractor
|
||||
type downloadInfo struct {
|
||||
imgJSON []byte
|
||||
img *Image
|
||||
digest digest.Digest
|
||||
tmpFile *os.File
|
||||
length int64
|
||||
downloaded bool
|
||||
err chan error
|
||||
}
|
||||
|
||||
downloads := make([]downloadInfo, len(manifest.FSLayers))
|
||||
|
||||
for i := len(manifest.FSLayers) - 1; i >= 0; i-- {
|
||||
var (
|
||||
sumStr = manifest.FSLayers[i].BlobSum
|
||||
imgJSON = []byte(manifest.History[i].V1Compatibility)
|
||||
)
|
||||
|
||||
img, err := NewImgJSON(imgJSON)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to parse json: %s", err)
|
||||
}
|
||||
downloads[i].img = img
|
||||
|
||||
// Check if exists
|
||||
if s.graph.Exists(img.ID) {
|
||||
logrus.Debugf("Image already exists: %s", img.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
dgst, err := digest.ParseDigest(sumStr)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
downloads[i].digest = dgst
|
||||
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(img.ID), "Pulling fs layer", nil))
|
||||
|
||||
downloadFunc := func(di *downloadInfo) error {
|
||||
logrus.Debugf("pulling blob %q to V1 img %s", sumStr, img.ID)
|
||||
|
||||
if c, err := s.poolAdd("pull", "img:"+img.ID); err != nil {
|
||||
if c != nil {
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(img.ID), "Layer already being pulled by another client. Waiting.", nil))
|
||||
<-c
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(img.ID), "Download complete", nil))
|
||||
} else {
|
||||
logrus.Debugf("Image (id: %s) pull is already running, skipping: %v", img.ID, err)
|
||||
}
|
||||
} else {
|
||||
defer s.poolRemove("pull", "img:"+img.ID)
|
||||
tmpFile, err := ioutil.TempFile("", "GetV2ImageBlob")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r, l, err := r.GetV2ImageBlobReader(endpoint, repoInfo.RemoteName, di.digest, auth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
verifier, err := digest.NewDigestVerifier(di.digest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(tmpFile, progressreader.New(progressreader.Config{
|
||||
In: ioutil.NopCloser(io.TeeReader(r, verifier)),
|
||||
Out: out,
|
||||
Formatter: sf,
|
||||
Size: int(l),
|
||||
NewLines: false,
|
||||
ID: stringid.TruncateID(img.ID),
|
||||
Action: "Downloading",
|
||||
})); err != nil {
|
||||
return fmt.Errorf("unable to copy v2 image blob data: %s", err)
|
||||
}
|
||||
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(img.ID), "Verifying Checksum", nil))
|
||||
|
||||
if !verifier.Verified() {
|
||||
return fmt.Errorf("image layer digest verification failed for %q", di.digest)
|
||||
}
|
||||
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(img.ID), "Download complete", nil))
|
||||
|
||||
logrus.Debugf("Downloaded %s to tempfile %s", img.ID, tmpFile.Name())
|
||||
di.tmpFile = tmpFile
|
||||
di.length = l
|
||||
di.downloaded = true
|
||||
}
|
||||
di.imgJSON = imgJSON
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
downloads[i].err = make(chan error)
|
||||
go func(di *downloadInfo) {
|
||||
di.err <- downloadFunc(di)
|
||||
}(&downloads[i])
|
||||
}
|
||||
|
||||
var tagUpdated bool
|
||||
for i := len(downloads) - 1; i >= 0; i-- {
|
||||
d := &downloads[i]
|
||||
if d.err != nil {
|
||||
if err := <-d.err; err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
if d.downloaded {
|
||||
// if tmpFile is empty assume download and extracted elsewhere
|
||||
defer os.Remove(d.tmpFile.Name())
|
||||
defer d.tmpFile.Close()
|
||||
d.tmpFile.Seek(0, 0)
|
||||
if d.tmpFile != nil {
|
||||
err = s.graph.Register(d.img,
|
||||
progressreader.New(progressreader.Config{
|
||||
In: d.tmpFile,
|
||||
Out: out,
|
||||
Formatter: sf,
|
||||
Size: int(d.length),
|
||||
ID: stringid.TruncateID(d.img.ID),
|
||||
Action: "Extracting",
|
||||
}))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := s.graph.SetDigest(d.img.ID, d.digest); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// FIXME: Pool release here for parallel tag pull (ensures any downloads block until fully extracted)
|
||||
}
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(d.img.ID), "Pull complete", nil))
|
||||
tagUpdated = true
|
||||
} else {
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(d.img.ID), "Already exists", nil))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Check for new tag if no layers downloaded
|
||||
if !tagUpdated {
|
||||
repo, err := s.Get(repoInfo.LocalName)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if repo != nil {
|
||||
if _, exists := repo[tag]; !exists {
|
||||
tagUpdated = true
|
||||
}
|
||||
} else {
|
||||
tagUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
if verified && tagUpdated {
|
||||
out.Write(sf.FormatStatus(utils.ImageReference(repoInfo.CanonicalName, tag), "The image you are pulling has been verified. Important: image verification is a tech preview feature and should not be relied on to provide security."))
|
||||
}
|
||||
|
||||
if localDigest != remoteDigest { // this is not a verification check.
|
||||
// NOTE(stevvooe): This is a very defensive branch and should never
|
||||
// happen, since all manifest digest implementations use the same
|
||||
// algorithm.
|
||||
logrus.WithFields(
|
||||
logrus.Fields{
|
||||
"local": localDigest,
|
||||
"remote": remoteDigest,
|
||||
}).Debugf("local digest does not match remote")
|
||||
|
||||
out.Write(sf.FormatStatus("", "Remote Digest: %s", remoteDigest))
|
||||
}
|
||||
|
||||
out.Write(sf.FormatStatus("", "Digest: %s", localDigest))
|
||||
|
||||
if tag == localDigest.String() {
|
||||
// TODO(stevvooe): Ideally, we should always set the digest so we can
|
||||
// use the digest whether we pull by it or not. Unfortunately, the tag
|
||||
// store treats the digest as a separate tag, meaning there may be an
|
||||
// untagged digest image that would seem to be dangling by a user.
|
||||
|
||||
if err = s.SetDigest(repoInfo.LocalName, localDigest.String(), downloads[0].img.ID); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if !utils.DigestReference(tag) {
|
||||
// only set the repository/tag -> image ID mapping when pulling by tag (i.e. not by digest)
|
||||
if err = s.Tag(repoInfo.LocalName, tag, downloads[0].img.ID, true); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return tagUpdated, nil
|
||||
}
|
||||
|
|
316
graph/pull_v1.go
Normal file
316
graph/pull_v1.go
Normal file
|
@ -0,0 +1,316 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
"github.com/docker/docker/pkg/progressreader"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/utils"
|
||||
)
|
||||
|
||||
type v1Puller struct {
|
||||
*TagStore
|
||||
endpoint registry.APIEndpoint
|
||||
config *ImagePullConfig
|
||||
sf *streamformatter.StreamFormatter
|
||||
repoInfo *registry.RepositoryInfo
|
||||
session *registry.Session
|
||||
}
|
||||
|
||||
func (p *v1Puller) Pull(tag string) (fallback bool, err error) {
|
||||
if utils.DigestReference(tag) {
|
||||
// Allowing fallback, because HTTPS v1 is before HTTP v2
|
||||
return true, registry.ErrNoSupport{errors.New("Cannot pull by digest with v1 registry")}
|
||||
}
|
||||
|
||||
tlsConfig, err := p.registryService.TlsConfig(p.repoInfo.Index.Name)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// Adds Docker-specific headers as well as user-specified headers (metaHeaders)
|
||||
tr := transport.NewTransport(
|
||||
// TODO(tiborvass): was ReceiveTimeout
|
||||
registry.NewTransport(tlsConfig),
|
||||
registry.DockerHeaders(p.config.MetaHeaders)...,
|
||||
)
|
||||
client := registry.HTTPClient(tr)
|
||||
v1Endpoint, err := p.endpoint.ToV1Endpoint(p.config.MetaHeaders)
|
||||
if err != nil {
|
||||
logrus.Debugf("Could not get v1 endpoint: %v", err)
|
||||
return true, err
|
||||
}
|
||||
p.session, err = registry.NewSession(client, p.config.AuthConfig, v1Endpoint)
|
||||
if err != nil {
|
||||
// TODO(dmcgowan): Check if should fallback
|
||||
logrus.Debugf("Fallback from error: %s", err)
|
||||
return true, err
|
||||
}
|
||||
if err := p.pullRepository(tag); err != nil {
|
||||
// TODO(dmcgowan): Check if should fallback
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (p *v1Puller) pullRepository(askedTag string) error {
|
||||
out := p.config.OutStream
|
||||
out.Write(p.sf.FormatStatus("", "Pulling repository %s", p.repoInfo.CanonicalName))
|
||||
|
||||
repoData, err := p.session.GetRepositoryData(p.repoInfo.RemoteName)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "HTTP code: 404") {
|
||||
return fmt.Errorf("Error: image %s not found", utils.ImageReference(p.repoInfo.RemoteName, askedTag))
|
||||
}
|
||||
// Unexpected HTTP error
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Debugf("Retrieving the tag list")
|
||||
tagsList := make(map[string]string)
|
||||
if askedTag == "" {
|
||||
tagsList, err = p.session.GetRemoteTags(repoData.Endpoints, p.repoInfo.RemoteName)
|
||||
} else {
|
||||
var tagId string
|
||||
tagId, err = p.session.GetRemoteTag(repoData.Endpoints, p.repoInfo.RemoteName, askedTag)
|
||||
tagsList[askedTag] = tagId
|
||||
}
|
||||
if err != nil {
|
||||
if err == registry.ErrRepoNotFound && askedTag != "" {
|
||||
return fmt.Errorf("Tag %s not found in repository %s", askedTag, p.repoInfo.CanonicalName)
|
||||
}
|
||||
logrus.Errorf("unable to get remote tags: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
for tag, id := range tagsList {
|
||||
repoData.ImgList[id] = ®istry.ImgData{
|
||||
ID: id,
|
||||
Tag: tag,
|
||||
Checksum: "",
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Debugf("Registering tags")
|
||||
// If no tag has been specified, pull them all
|
||||
if askedTag == "" {
|
||||
for tag, id := range tagsList {
|
||||
repoData.ImgList[id].Tag = tag
|
||||
}
|
||||
} else {
|
||||
// Otherwise, check that the tag exists and use only that one
|
||||
id, exists := tagsList[askedTag]
|
||||
if !exists {
|
||||
return fmt.Errorf("Tag %s not found in repository %s", askedTag, p.repoInfo.CanonicalName)
|
||||
}
|
||||
repoData.ImgList[id].Tag = askedTag
|
||||
}
|
||||
|
||||
errors := make(chan error)
|
||||
|
||||
layersDownloaded := false
|
||||
for _, image := range repoData.ImgList {
|
||||
downloadImage := func(img *registry.ImgData) {
|
||||
if askedTag != "" && img.Tag != askedTag {
|
||||
errors <- nil
|
||||
return
|
||||
}
|
||||
|
||||
if img.Tag == "" {
|
||||
logrus.Debugf("Image (id: %s) present in this repository but untagged, skipping", img.ID)
|
||||
errors <- nil
|
||||
return
|
||||
}
|
||||
|
||||
// ensure no two downloads of the same image happen at the same time
|
||||
if c, err := p.poolAdd("pull", "img:"+img.ID); err != nil {
|
||||
if c != nil {
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), "Layer already being pulled by another client. Waiting.", nil))
|
||||
<-c
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), "Download complete", nil))
|
||||
} else {
|
||||
logrus.Debugf("Image (id: %s) pull is already running, skipping: %v", img.ID, err)
|
||||
}
|
||||
errors <- nil
|
||||
return
|
||||
}
|
||||
defer p.poolRemove("pull", "img:"+img.ID)
|
||||
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s", img.Tag, p.repoInfo.CanonicalName), nil))
|
||||
success := false
|
||||
var lastErr, err error
|
||||
var isDownloaded bool
|
||||
for _, ep := range p.repoInfo.Index.Mirrors {
|
||||
ep += "v1/"
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s, mirror: %s", img.Tag, p.repoInfo.CanonicalName, ep), nil))
|
||||
if isDownloaded, err = p.pullImage(img.ID, ep, repoData.Tokens); err != nil {
|
||||
// Don't report errors when pulling from mirrors.
|
||||
logrus.Debugf("Error pulling image (%s) from %s, mirror: %s, %s", img.Tag, p.repoInfo.CanonicalName, ep, err)
|
||||
continue
|
||||
}
|
||||
layersDownloaded = layersDownloaded || isDownloaded
|
||||
success = true
|
||||
break
|
||||
}
|
||||
if !success {
|
||||
for _, ep := range repoData.Endpoints {
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), fmt.Sprintf("Pulling image (%s) from %s, endpoint: %s", img.Tag, p.repoInfo.CanonicalName, ep), nil))
|
||||
if isDownloaded, err = p.pullImage(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.
|
||||
// As the error is also given to the output stream the user will see the error.
|
||||
lastErr = err
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), fmt.Sprintf("Error pulling image (%s) from %s, endpoint: %s, %s", img.Tag, p.repoInfo.CanonicalName, ep, err), nil))
|
||||
continue
|
||||
}
|
||||
layersDownloaded = layersDownloaded || isDownloaded
|
||||
success = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !success {
|
||||
err := fmt.Errorf("Error pulling image (%s) from %s, %v", img.Tag, p.repoInfo.CanonicalName, lastErr)
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), err.Error(), nil))
|
||||
errors <- err
|
||||
return
|
||||
}
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), "Download complete", nil))
|
||||
|
||||
errors <- nil
|
||||
}
|
||||
|
||||
go downloadImage(image)
|
||||
}
|
||||
|
||||
var lastError error
|
||||
for i := 0; i < len(repoData.ImgList); i++ {
|
||||
if err := <-errors; err != nil {
|
||||
lastError = err
|
||||
}
|
||||
}
|
||||
if lastError != nil {
|
||||
return lastError
|
||||
}
|
||||
|
||||
for tag, id := range tagsList {
|
||||
if askedTag != "" && tag != askedTag {
|
||||
continue
|
||||
}
|
||||
if err := p.Tag(p.repoInfo.LocalName, tag, id, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
requestedTag := p.repoInfo.LocalName
|
||||
if len(askedTag) > 0 {
|
||||
requestedTag = utils.ImageReference(p.repoInfo.LocalName, askedTag)
|
||||
}
|
||||
WriteStatus(requestedTag, out, p.sf, layersDownloaded)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *v1Puller) pullImage(imgID, endpoint string, token []string) (bool, error) {
|
||||
history, err := p.session.GetRemoteHistory(imgID, endpoint)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
out := p.config.OutStream
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(imgID), "Pulling dependent layers", nil))
|
||||
// FIXME: Try to stream the images?
|
||||
// FIXME: Launch the getRemoteImage() in goroutines
|
||||
|
||||
layersDownloaded := false
|
||||
for i := len(history) - 1; i >= 0; i-- {
|
||||
id := history[i]
|
||||
|
||||
// ensure no two downloads of the same layer happen at the same time
|
||||
if c, err := p.poolAdd("pull", "layer:"+id); err != nil {
|
||||
logrus.Debugf("Image (id: %s) pull is already running, skipping: %v", id, err)
|
||||
<-c
|
||||
}
|
||||
defer p.poolRemove("pull", "layer:"+id)
|
||||
|
||||
if !p.graph.Exists(id) {
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(id), "Pulling metadata", nil))
|
||||
var (
|
||||
imgJSON []byte
|
||||
imgSize int
|
||||
err error
|
||||
img *Image
|
||||
)
|
||||
retries := 5
|
||||
for j := 1; j <= retries; j++ {
|
||||
imgJSON, imgSize, err = p.session.GetRemoteImageJSON(id, endpoint)
|
||||
if err != nil && j == retries {
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(id), "Error pulling dependent layers", nil))
|
||||
return layersDownloaded, err
|
||||
} else if err != nil {
|
||||
time.Sleep(time.Duration(j) * 500 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
img, err = NewImgJSON(imgJSON)
|
||||
layersDownloaded = true
|
||||
if err != nil && j == retries {
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(id), "Error pulling dependent layers", nil))
|
||||
return layersDownloaded, fmt.Errorf("Failed to parse json: %s", err)
|
||||
} else if err != nil {
|
||||
time.Sleep(time.Duration(j) * 500 * time.Millisecond)
|
||||
continue
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for j := 1; j <= retries; j++ {
|
||||
// Get the layer
|
||||
status := "Pulling fs layer"
|
||||
if j > 1 {
|
||||
status = fmt.Sprintf("Pulling fs layer [retries: %d]", j)
|
||||
}
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(id), status, nil))
|
||||
layer, err := p.session.GetRemoteImageLayer(img.ID, endpoint, int64(imgSize))
|
||||
if uerr, ok := err.(*url.Error); ok {
|
||||
err = uerr.Err
|
||||
}
|
||||
if terr, ok := err.(net.Error); ok && terr.Timeout() && j < retries {
|
||||
time.Sleep(time.Duration(j) * 500 * time.Millisecond)
|
||||
continue
|
||||
} else if err != nil {
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(id), "Error pulling dependent layers", nil))
|
||||
return layersDownloaded, err
|
||||
}
|
||||
layersDownloaded = true
|
||||
defer layer.Close()
|
||||
|
||||
err = p.graph.Register(img,
|
||||
progressreader.New(progressreader.Config{
|
||||
In: layer,
|
||||
Out: out,
|
||||
Formatter: p.sf,
|
||||
Size: imgSize,
|
||||
NewLines: false,
|
||||
ID: stringid.TruncateID(id),
|
||||
Action: "Downloading",
|
||||
}))
|
||||
if terr, ok := err.(net.Error); ok && terr.Timeout() && j < retries {
|
||||
time.Sleep(time.Duration(j) * 500 * time.Millisecond)
|
||||
continue
|
||||
} else if err != nil {
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(id), "Error downloading dependent layers", nil))
|
||||
return layersDownloaded, err
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(id), "Download complete", nil))
|
||||
}
|
||||
return layersDownloaded, nil
|
||||
}
|
384
graph/pull_v2.go
Normal file
384
graph/pull_v2.go
Normal file
|
@ -0,0 +1,384 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/manifest"
|
||||
"github.com/docker/docker/pkg/progressreader"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/trust"
|
||||
"github.com/docker/docker/utils"
|
||||
"github.com/docker/libtrust"
|
||||
)
|
||||
|
||||
type v2Puller struct {
|
||||
*TagStore
|
||||
endpoint registry.APIEndpoint
|
||||
config *ImagePullConfig
|
||||
sf *streamformatter.StreamFormatter
|
||||
repoInfo *registry.RepositoryInfo
|
||||
repo distribution.Repository
|
||||
}
|
||||
|
||||
func (p *v2Puller) Pull(tag string) (fallback bool, err error) {
|
||||
// TODO(tiborvass): was ReceiveTimeout
|
||||
p.repo, err = NewV2Repository(p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig)
|
||||
if err != nil {
|
||||
logrus.Debugf("Error getting v2 registry: %v", err)
|
||||
return true, err
|
||||
}
|
||||
|
||||
if err := p.pullV2Repository(tag); err != nil {
|
||||
if registry.ContinueOnError(err) {
|
||||
logrus.Debugf("Error trying v2 registry: %v", err)
|
||||
return true, err
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (p *v2Puller) pullV2Repository(tag string) (err error) {
|
||||
var tags []string
|
||||
taggedName := p.repoInfo.LocalName
|
||||
if len(tag) > 0 {
|
||||
tags = []string{tag}
|
||||
taggedName = utils.ImageReference(p.repoInfo.LocalName, tag)
|
||||
} else {
|
||||
var err error
|
||||
tags, err = p.repo.Manifests().Tags()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
c, err := p.poolAdd("pull", taggedName)
|
||||
if err != nil {
|
||||
if c != nil {
|
||||
// Another pull of the same repository is already taking place; just wait for it to finish
|
||||
p.sf.FormatStatus("", "Repository %s already being pulled by another client. Waiting.", p.repoInfo.CanonicalName)
|
||||
<-c
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer p.poolRemove("pull", taggedName)
|
||||
|
||||
var layersDownloaded bool
|
||||
for _, tag := range tags {
|
||||
// pulledNew is true if either new layers were downloaded OR if existing images were newly tagged
|
||||
// TODO(tiborvass): should we change the name of `layersDownload`? What about message in WriteStatus?
|
||||
pulledNew, err := p.pullV2Tag(tag, taggedName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
layersDownloaded = layersDownloaded || pulledNew
|
||||
}
|
||||
|
||||
WriteStatus(taggedName, p.config.OutStream, p.sf, layersDownloaded)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// downloadInfo is used to pass information from download to extractor
|
||||
type downloadInfo struct {
|
||||
img *Image
|
||||
tmpFile *os.File
|
||||
digest digest.Digest
|
||||
layer distribution.ReadSeekCloser
|
||||
size int64
|
||||
err chan error
|
||||
verified bool
|
||||
}
|
||||
|
||||
type errVerification struct{}
|
||||
|
||||
func (errVerification) Error() string { return "verification failed" }
|
||||
|
||||
func (p *v2Puller) download(di *downloadInfo) {
|
||||
logrus.Debugf("pulling blob %q to %s", di.digest, di.img.ID)
|
||||
|
||||
out := p.config.OutStream
|
||||
|
||||
if c, err := p.poolAdd("pull", "img:"+di.img.ID); err != nil {
|
||||
if c != nil {
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(di.img.ID), "Layer already being pulled by another client. Waiting.", nil))
|
||||
<-c
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(di.img.ID), "Download complete", nil))
|
||||
} else {
|
||||
logrus.Debugf("Image (id: %s) pull is already running, skipping: %v", di.img.ID, err)
|
||||
}
|
||||
di.err <- nil
|
||||
return
|
||||
}
|
||||
|
||||
defer p.poolRemove("pull", "img:"+di.img.ID)
|
||||
tmpFile, err := ioutil.TempFile("", "GetImageBlob")
|
||||
if err != nil {
|
||||
di.err <- err
|
||||
return
|
||||
}
|
||||
|
||||
blobs := p.repo.Blobs(nil)
|
||||
|
||||
desc, err := blobs.Stat(nil, di.digest)
|
||||
if err != nil {
|
||||
logrus.Debugf("Error statting layer: %v", err)
|
||||
di.err <- err
|
||||
return
|
||||
}
|
||||
di.size = desc.Length
|
||||
|
||||
layerDownload, err := blobs.Open(nil, di.digest)
|
||||
if err != nil {
|
||||
logrus.Debugf("Error fetching layer: %v", err)
|
||||
di.err <- err
|
||||
return
|
||||
}
|
||||
defer layerDownload.Close()
|
||||
|
||||
verifier, err := digest.NewDigestVerifier(di.digest)
|
||||
if err != nil {
|
||||
di.err <- err
|
||||
return
|
||||
}
|
||||
|
||||
reader := progressreader.New(progressreader.Config{
|
||||
In: ioutil.NopCloser(io.TeeReader(layerDownload, verifier)),
|
||||
Out: out,
|
||||
Formatter: p.sf,
|
||||
Size: int(di.size),
|
||||
NewLines: false,
|
||||
ID: stringid.TruncateID(di.img.ID),
|
||||
Action: "Downloading",
|
||||
})
|
||||
io.Copy(tmpFile, reader)
|
||||
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(di.img.ID), "Verifying Checksum", nil))
|
||||
|
||||
di.verified = verifier.Verified()
|
||||
if !di.verified {
|
||||
logrus.Infof("Image verification failed for layer %s", di.digest)
|
||||
}
|
||||
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(di.img.ID), "Download complete", nil))
|
||||
|
||||
logrus.Debugf("Downloaded %s to tempfile %s", di.img.ID, tmpFile.Name())
|
||||
di.tmpFile = tmpFile
|
||||
di.layer = layerDownload
|
||||
|
||||
di.err <- nil
|
||||
}
|
||||
|
||||
func (p *v2Puller) pullV2Tag(tag, taggedName string) (bool, error) {
|
||||
logrus.Debugf("Pulling tag from V2 registry: %q", tag)
|
||||
out := p.config.OutStream
|
||||
|
||||
manifest, err := p.repo.Manifests().GetByTag(tag)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
verified, err := p.validateManifest(manifest, tag)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if verified {
|
||||
logrus.Printf("Image manifest for %s has been verified", taggedName)
|
||||
}
|
||||
|
||||
out.Write(p.sf.FormatStatus(tag, "Pulling from %s", p.repo.Name()))
|
||||
|
||||
downloads := make([]downloadInfo, len(manifest.FSLayers))
|
||||
for i := len(manifest.FSLayers) - 1; i >= 0; i-- {
|
||||
img, err := NewImgJSON([]byte(manifest.History[i].V1Compatibility))
|
||||
if err != nil {
|
||||
logrus.Debugf("error getting image v1 json: %v", err)
|
||||
return false, err
|
||||
}
|
||||
downloads[i].img = img
|
||||
downloads[i].digest = manifest.FSLayers[i].BlobSum
|
||||
|
||||
// Check if exists
|
||||
if p.graph.Exists(img.ID) {
|
||||
logrus.Debugf("Image already exists: %s", img.ID)
|
||||
continue
|
||||
}
|
||||
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), "Pulling fs layer", nil))
|
||||
|
||||
downloads[i].err = make(chan error)
|
||||
go p.download(&downloads[i])
|
||||
}
|
||||
|
||||
var tagUpdated bool
|
||||
for i := len(downloads) - 1; i >= 0; i-- {
|
||||
d := &downloads[i]
|
||||
if d.err != nil {
|
||||
if err := <-d.err; err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
verified = verified && d.verified
|
||||
if d.layer != nil {
|
||||
// if tmpFile is empty assume download and extracted elsewhere
|
||||
defer os.Remove(d.tmpFile.Name())
|
||||
defer d.tmpFile.Close()
|
||||
d.tmpFile.Seek(0, 0)
|
||||
if d.tmpFile != nil {
|
||||
|
||||
reader := progressreader.New(progressreader.Config{
|
||||
In: d.tmpFile,
|
||||
Out: out,
|
||||
Formatter: p.sf,
|
||||
Size: int(d.size),
|
||||
NewLines: false,
|
||||
ID: stringid.TruncateID(d.img.ID),
|
||||
Action: "Extracting",
|
||||
})
|
||||
|
||||
err = p.graph.Register(d.img, reader)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := p.graph.SetDigest(d.img.ID, d.digest); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// FIXME: Pool release here for parallel tag pull (ensures any downloads block until fully extracted)
|
||||
}
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(d.img.ID), "Pull complete", nil))
|
||||
tagUpdated = true
|
||||
} else {
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(d.img.ID), "Already exists", nil))
|
||||
}
|
||||
}
|
||||
|
||||
manifestDigest, err := digestFromManifest(manifest, p.repoInfo.LocalName)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Check for new tag if no layers downloaded
|
||||
if !tagUpdated {
|
||||
repo, err := p.Get(p.repoInfo.LocalName)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if repo != nil {
|
||||
if _, exists := repo[tag]; !exists {
|
||||
tagUpdated = true
|
||||
}
|
||||
} else {
|
||||
tagUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
if verified && tagUpdated {
|
||||
out.Write(p.sf.FormatStatus(p.repo.Name()+":"+tag, "The image you are pulling has been verified. Important: image verification is a tech preview feature and should not be relied on to provide security."))
|
||||
}
|
||||
|
||||
if utils.DigestReference(tag) {
|
||||
// TODO(stevvooe): Ideally, we should always set the digest so we can
|
||||
// use the digest whether we pull by it or not. Unfortunately, the tag
|
||||
// store treats the digest as a separate tag, meaning there may be an
|
||||
// untagged digest image that would seem to be dangling by a user.
|
||||
if err = p.SetDigest(p.repoInfo.LocalName, tag, downloads[0].img.ID); err != nil {
|
||||
return false, err
|
||||
}
|
||||
} else {
|
||||
// only set the repository/tag -> image ID mapping when pulling by tag (i.e. not by digest)
|
||||
if err = p.Tag(p.repoInfo.LocalName, tag, downloads[0].img.ID, true); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if manifestDigest != "" {
|
||||
out.Write(p.sf.FormatStatus("", "Digest: %s", manifestDigest))
|
||||
}
|
||||
|
||||
return tagUpdated, nil
|
||||
}
|
||||
|
||||
// verifyTrustedKeys checks the keys provided against the trust store,
|
||||
// ensuring that the provided keys are trusted for the namespace. The keys
|
||||
// provided from this method must come from the signatures provided as part of
|
||||
// the manifest JWS package, obtained from unpackSignedManifest or libtrust.
|
||||
func (p *v2Puller) verifyTrustedKeys(namespace string, keys []libtrust.PublicKey) (verified bool, err error) {
|
||||
if namespace[0] != '/' {
|
||||
namespace = "/" + namespace
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
b, err := key.MarshalJSON()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error marshalling public key: %s", err)
|
||||
}
|
||||
// Check key has read/write permission (0x03)
|
||||
v, err := p.trustService.CheckKey(namespace, b, 0x03)
|
||||
if err != nil {
|
||||
vErr, ok := err.(trust.NotVerifiedError)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("error running key check: %s", err)
|
||||
}
|
||||
logrus.Debugf("Key check result: %v", vErr)
|
||||
}
|
||||
verified = v
|
||||
}
|
||||
|
||||
if verified {
|
||||
logrus.Debug("Key check result: verified")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (p *v2Puller) validateManifest(m *manifest.SignedManifest, tag string) (verified bool, err error) {
|
||||
// TODO(tiborvass): what's the usecase for having manifest == nil and err == nil ? Shouldn't be the error be "DoesNotExist" ?
|
||||
if m == nil {
|
||||
return false, fmt.Errorf("image manifest does not exist for tag %q", tag)
|
||||
}
|
||||
if m.SchemaVersion != 1 {
|
||||
return false, fmt.Errorf("unsupported schema version %d for tag %q", m.SchemaVersion, tag)
|
||||
}
|
||||
if len(m.FSLayers) != len(m.History) {
|
||||
return false, fmt.Errorf("length of history not equal to number of layers for tag %q", tag)
|
||||
}
|
||||
if len(m.FSLayers) == 0 {
|
||||
return false, fmt.Errorf("no FSLayers in manifest for tag %q", tag)
|
||||
}
|
||||
keys, err := manifest.Verify(m)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error verifying manifest for tag %q: %v", tag, err)
|
||||
}
|
||||
verified, err = p.verifyTrustedKeys(m.Name, keys)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error verifying manifest keys: %v", err)
|
||||
}
|
||||
localDigest, err := digest.ParseDigest(tag)
|
||||
// if pull by digest, then verify
|
||||
if err == nil {
|
||||
verifier, err := digest.NewDigestVerifier(localDigest)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
payload, err := m.Payload()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if _, err := verifier.Write(payload); err != nil {
|
||||
return false, err
|
||||
}
|
||||
verified = verified && verifier.Verified()
|
||||
}
|
||||
return verified, nil
|
||||
}
|
551
graph/push.go
551
graph/push.go
|
@ -1,29 +1,15 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/docker/cliconfig"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/docker/pkg/progressreader"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/pkg/transport"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/runconfig"
|
||||
"github.com/docker/docker/utils"
|
||||
"github.com/docker/libtrust"
|
||||
)
|
||||
|
||||
var ErrV2RegistryUnavailable = errors.New("error v2 registry unavailable")
|
||||
|
||||
type ImagePushConfig struct {
|
||||
MetaHeaders map[string][]string
|
||||
AuthConfig *cliconfig.AuthConfig
|
||||
|
@ -31,468 +17,41 @@ type ImagePushConfig struct {
|
|||
OutStream io.Writer
|
||||
}
|
||||
|
||||
// Retrieve the all the images to be uploaded in the correct order
|
||||
func (s *TagStore) getImageList(localRepo map[string]string, requestedTag string) ([]string, map[string][]string, error) {
|
||||
var (
|
||||
imageList []string
|
||||
imagesSeen = make(map[string]bool)
|
||||
tagsByImage = make(map[string][]string)
|
||||
)
|
||||
|
||||
for tag, id := range localRepo {
|
||||
if requestedTag != "" && requestedTag != tag {
|
||||
// Include only the requested tag.
|
||||
continue
|
||||
}
|
||||
|
||||
if utils.DigestReference(tag) {
|
||||
// Ignore digest references.
|
||||
continue
|
||||
}
|
||||
|
||||
var imageListForThisTag []string
|
||||
|
||||
tagsByImage[id] = append(tagsByImage[id], tag)
|
||||
|
||||
for img, err := s.graph.Get(id); img != nil; img, err = s.graph.GetParent(img) {
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if imagesSeen[img.ID] {
|
||||
// This image is already on the list, we can ignore it and all its parents
|
||||
break
|
||||
}
|
||||
|
||||
imagesSeen[img.ID] = true
|
||||
imageListForThisTag = append(imageListForThisTag, img.ID)
|
||||
}
|
||||
|
||||
// reverse the image list for this tag (so the "most"-parent image is first)
|
||||
for i, j := 0, len(imageListForThisTag)-1; i < j; i, j = i+1, j-1 {
|
||||
imageListForThisTag[i], imageListForThisTag[j] = imageListForThisTag[j], imageListForThisTag[i]
|
||||
}
|
||||
|
||||
// append to main image list
|
||||
imageList = append(imageList, imageListForThisTag...)
|
||||
}
|
||||
if len(imageList) == 0 {
|
||||
return nil, nil, fmt.Errorf("No images found for the requested repository / tag")
|
||||
}
|
||||
logrus.Debugf("Image list: %v", imageList)
|
||||
logrus.Debugf("Tags by image: %v", tagsByImage)
|
||||
|
||||
return imageList, tagsByImage, nil
|
||||
type Pusher interface {
|
||||
// Push tries to push the image configured at the creation of Pusher.
|
||||
// Push returns an error if any, as well as a boolean that determines whether to retry Push on the next configured endpoint.
|
||||
//
|
||||
// TODO(tiborvass): have Push() take a reference to repository + tag, so that the pusher itself is repository-agnostic.
|
||||
Push() (fallback bool, err error)
|
||||
}
|
||||
|
||||
func (s *TagStore) getImageTags(localRepo map[string]string, askedTag string) ([]string, error) {
|
||||
logrus.Debugf("Checking %s against %#v", askedTag, localRepo)
|
||||
if len(askedTag) > 0 {
|
||||
if _, ok := localRepo[askedTag]; !ok || utils.DigestReference(askedTag) {
|
||||
return nil, fmt.Errorf("Tag does not exist: %s", askedTag)
|
||||
}
|
||||
return []string{askedTag}, nil
|
||||
func (s *TagStore) NewPusher(endpoint registry.APIEndpoint, localRepo Repository, repoInfo *registry.RepositoryInfo, imagePushConfig *ImagePushConfig, sf *streamformatter.StreamFormatter) (Pusher, error) {
|
||||
switch endpoint.Version {
|
||||
case registry.APIVersion2:
|
||||
return &v2Pusher{
|
||||
TagStore: s,
|
||||
endpoint: endpoint,
|
||||
localRepo: localRepo,
|
||||
repoInfo: repoInfo,
|
||||
config: imagePushConfig,
|
||||
sf: sf,
|
||||
}, nil
|
||||
case registry.APIVersion1:
|
||||
return &v1Pusher{
|
||||
TagStore: s,
|
||||
endpoint: endpoint,
|
||||
localRepo: localRepo,
|
||||
repoInfo: repoInfo,
|
||||
config: imagePushConfig,
|
||||
sf: sf,
|
||||
}, nil
|
||||
}
|
||||
var tags []string
|
||||
for tag := range localRepo {
|
||||
if !utils.DigestReference(tag) {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// createImageIndex returns an index of an image's layer IDs and tags.
|
||||
func (s *TagStore) createImageIndex(images []string, tags map[string][]string) []*registry.ImgData {
|
||||
var imageIndex []*registry.ImgData
|
||||
for _, id := range images {
|
||||
if tags, hasTags := tags[id]; hasTags {
|
||||
// If an image has tags you must add an entry in the image index
|
||||
// for each tag
|
||||
for _, tag := range tags {
|
||||
imageIndex = append(imageIndex, ®istry.ImgData{
|
||||
ID: id,
|
||||
Tag: tag,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
// If the image does not have a tag it still needs to be sent to the
|
||||
// registry with an empty tag so that it is accociated with the repository
|
||||
imageIndex = append(imageIndex, ®istry.ImgData{
|
||||
ID: id,
|
||||
Tag: "",
|
||||
})
|
||||
}
|
||||
return imageIndex
|
||||
}
|
||||
|
||||
type imagePushData struct {
|
||||
id string
|
||||
endpoint string
|
||||
tokens []string
|
||||
}
|
||||
|
||||
// lookupImageOnEndpoint checks the specified endpoint to see if an image exists
|
||||
// and if it is absent then it sends the image id to the channel to be pushed.
|
||||
func lookupImageOnEndpoint(wg *sync.WaitGroup, r *registry.Session, out io.Writer, sf *streamformatter.StreamFormatter,
|
||||
images chan imagePushData, imagesToPush chan string) {
|
||||
defer wg.Done()
|
||||
for image := range images {
|
||||
if err := r.LookupRemoteImage(image.id, image.endpoint); err != nil {
|
||||
logrus.Errorf("Error in LookupRemoteImage: %s", err)
|
||||
imagesToPush <- image.id
|
||||
continue
|
||||
}
|
||||
out.Write(sf.FormatStatus("", "Image %s already pushed, skipping", stringid.TruncateID(image.id)))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TagStore) pushImageToEndpoint(endpoint string, out io.Writer, remoteName string, imageIDs []string,
|
||||
tags map[string][]string, repo *registry.RepositoryData, sf *streamformatter.StreamFormatter, r *registry.Session) error {
|
||||
workerCount := len(imageIDs)
|
||||
// start a maximum of 5 workers to check if images exist on the specified endpoint.
|
||||
if workerCount > 5 {
|
||||
workerCount = 5
|
||||
}
|
||||
var (
|
||||
wg = &sync.WaitGroup{}
|
||||
imageData = make(chan imagePushData, workerCount*2)
|
||||
imagesToPush = make(chan string, workerCount*2)
|
||||
pushes = make(chan map[string]struct{}, 1)
|
||||
)
|
||||
for i := 0; i < workerCount; i++ {
|
||||
wg.Add(1)
|
||||
go lookupImageOnEndpoint(wg, r, out, sf, imageData, imagesToPush)
|
||||
}
|
||||
// start a go routine that consumes the images to push
|
||||
go func() {
|
||||
shouldPush := make(map[string]struct{})
|
||||
for id := range imagesToPush {
|
||||
shouldPush[id] = struct{}{}
|
||||
}
|
||||
pushes <- shouldPush
|
||||
}()
|
||||
for _, id := range imageIDs {
|
||||
imageData <- imagePushData{
|
||||
id: id,
|
||||
endpoint: endpoint,
|
||||
tokens: repo.Tokens,
|
||||
}
|
||||
}
|
||||
// close the channel to notify the workers that there will be no more images to check.
|
||||
close(imageData)
|
||||
wg.Wait()
|
||||
close(imagesToPush)
|
||||
// wait for all the images that require pushes to be collected into a consumable map.
|
||||
shouldPush := <-pushes
|
||||
// finish by pushing any images and tags to the endpoint. The order that the images are pushed
|
||||
// is very important that is why we are still iterating over the ordered list of imageIDs.
|
||||
for _, id := range imageIDs {
|
||||
if _, push := shouldPush[id]; push {
|
||||
if _, err := s.pushImage(r, out, id, endpoint, repo.Tokens, sf); err != nil {
|
||||
// FIXME: Continue on error?
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, tag := range tags[id] {
|
||||
out.Write(sf.FormatStatus("", "Pushing tag for rev [%s] on {%s}", stringid.TruncateID(id), endpoint+"repositories/"+remoteName+"/tags/"+tag))
|
||||
if err := r.PushRegistryTag(remoteName, id, tag, endpoint); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pushRepository pushes layers that do not already exist on the registry.
|
||||
func (s *TagStore) pushRepository(r *registry.Session, out io.Writer,
|
||||
repoInfo *registry.RepositoryInfo, localRepo map[string]string,
|
||||
tag string, sf *streamformatter.StreamFormatter) error {
|
||||
logrus.Debugf("Local repo: %s", localRepo)
|
||||
out = ioutils.NewWriteFlusher(out)
|
||||
imgList, tags, err := s.getImageList(localRepo, tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out.Write(sf.FormatStatus("", "Sending image list"))
|
||||
|
||||
imageIndex := s.createImageIndex(imgList, tags)
|
||||
logrus.Debugf("Preparing to push %s with the following images and tags", localRepo)
|
||||
for _, data := range imageIndex {
|
||||
logrus.Debugf("Pushing ID: %s with Tag: %s", data.ID, data.Tag)
|
||||
}
|
||||
// Register all the images in a repository with the registry
|
||||
// If an image is not in this list it will not be associated with the repository
|
||||
repoData, err := r.PushImageJSONIndex(repoInfo.RemoteName, imageIndex, false, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nTag := 1
|
||||
if tag == "" {
|
||||
nTag = len(localRepo)
|
||||
}
|
||||
out.Write(sf.FormatStatus("", "Pushing repository %s (%d tags)", repoInfo.CanonicalName, nTag))
|
||||
// push the repository to each of the endpoints only if it does not exist.
|
||||
for _, endpoint := range repoData.Endpoints {
|
||||
if err := s.pushImageToEndpoint(endpoint, out, repoInfo.RemoteName, imgList, tags, repoData, sf, r); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = r.PushImageJSONIndex(repoInfo.RemoteName, imageIndex, true, repoData.Endpoints)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TagStore) pushImage(r *registry.Session, out io.Writer, imgID, ep string, token []string, sf *streamformatter.StreamFormatter) (checksum string, err error) {
|
||||
out = ioutils.NewWriteFlusher(out)
|
||||
jsonRaw, err := s.graph.RawJSON(imgID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Cannot retrieve the path for {%s}: %s", imgID, err)
|
||||
}
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(imgID), "Pushing", nil))
|
||||
|
||||
imgData := ®istry.ImgData{
|
||||
ID: imgID,
|
||||
}
|
||||
|
||||
// Send the json
|
||||
if err := r.PushImageJSONRegistry(imgData, jsonRaw, ep); err != nil {
|
||||
if err == registry.ErrAlreadyExists {
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(imgData.ID), "Image already pushed, skipping", nil))
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
layerData, err := s.graph.TempLayerArchive(imgID, sf, out)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to generate layer archive: %s", err)
|
||||
}
|
||||
defer os.RemoveAll(layerData.Name())
|
||||
|
||||
// Send the layer
|
||||
logrus.Debugf("rendered layer for %s of [%d] size", imgData.ID, layerData.Size)
|
||||
|
||||
checksum, checksumPayload, err := r.PushImageLayerRegistry(imgData.ID,
|
||||
progressreader.New(progressreader.Config{
|
||||
In: layerData,
|
||||
Out: out,
|
||||
Formatter: sf,
|
||||
Size: int(layerData.Size),
|
||||
NewLines: false,
|
||||
ID: stringid.TruncateID(imgData.ID),
|
||||
Action: "Pushing",
|
||||
}), ep, jsonRaw)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
imgData.Checksum = checksum
|
||||
imgData.ChecksumPayload = checksumPayload
|
||||
// Send the checksum
|
||||
if err := r.PushImageChecksumRegistry(imgData, ep); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(imgData.ID), "Image successfully pushed", nil))
|
||||
return imgData.Checksum, nil
|
||||
}
|
||||
|
||||
func (s *TagStore) pushV2Repository(r *registry.Session, localRepo Repository, out io.Writer, repoInfo *registry.RepositoryInfo, tag string, sf *streamformatter.StreamFormatter) error {
|
||||
endpoint, err := r.V2RegistryEndpoint(repoInfo.Index)
|
||||
if err != nil {
|
||||
if repoInfo.Index.Official {
|
||||
logrus.Debugf("Unable to push to V2 registry, falling back to v1: %s", err)
|
||||
return ErrV2RegistryUnavailable
|
||||
}
|
||||
return fmt.Errorf("error getting registry endpoint: %s", err)
|
||||
}
|
||||
|
||||
tags, err := s.getImageTags(localRepo, tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(tags) == 0 {
|
||||
return fmt.Errorf("No tags to push for %s", repoInfo.LocalName)
|
||||
}
|
||||
|
||||
auth, err := r.GetV2Authorization(endpoint, repoInfo.RemoteName, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting authorization: %s", err)
|
||||
}
|
||||
if !auth.CanAuthorizeV2() {
|
||||
return ErrV2RegistryUnavailable
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
logrus.Debugf("Pushing repository: %s:%s", repoInfo.CanonicalName, tag)
|
||||
|
||||
layerId, exists := localRepo[tag]
|
||||
if !exists {
|
||||
return fmt.Errorf("tag does not exist: %s", tag)
|
||||
}
|
||||
|
||||
layer, err := s.graph.Get(layerId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m := ®istry.ManifestData{
|
||||
SchemaVersion: 1,
|
||||
Name: repoInfo.RemoteName,
|
||||
Tag: tag,
|
||||
Architecture: layer.Architecture,
|
||||
}
|
||||
var metadata runconfig.Config
|
||||
if layer.Config != nil {
|
||||
metadata = *layer.Config
|
||||
}
|
||||
|
||||
layersSeen := make(map[string]bool)
|
||||
layers := []*Image{}
|
||||
for ; layer != nil; layer, err = s.graph.GetParent(layer) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if layersSeen[layer.ID] {
|
||||
break
|
||||
}
|
||||
layers = append(layers, layer)
|
||||
layersSeen[layer.ID] = true
|
||||
}
|
||||
m.FSLayers = make([]*registry.FSLayer, len(layers))
|
||||
m.History = make([]*registry.ManifestHistory, len(layers))
|
||||
|
||||
// Schema version 1 requires layer ordering from top to root
|
||||
for i, layer := range layers {
|
||||
logrus.Debugf("Pushing layer: %s", layer.ID)
|
||||
|
||||
if layer.Config != nil && metadata.Image != layer.ID {
|
||||
if err := runconfig.Merge(&metadata, layer.Config); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
jsonData, err := s.graph.RawJSON(layer.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot retrieve the path for %s: %s", layer.ID, err)
|
||||
}
|
||||
|
||||
var exists bool
|
||||
dgst, err := s.graph.GetDigest(layer.ID)
|
||||
if err != nil {
|
||||
if err != ErrDigestNotSet {
|
||||
return fmt.Errorf("error getting image checksum: %s", err)
|
||||
}
|
||||
} else {
|
||||
// Call mount blob
|
||||
exists, err = r.HeadV2ImageBlob(endpoint, repoInfo.RemoteName, dgst, auth)
|
||||
if err != nil {
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(layer.ID), "Image push failed", nil))
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
if pushDigest, err := s.pushV2Image(r, layer, endpoint, repoInfo.RemoteName, sf, out, auth); err != nil {
|
||||
return err
|
||||
} else if pushDigest != dgst {
|
||||
// Cache new checksum
|
||||
if err := s.graph.SetDigest(layer.ID, pushDigest); err != nil {
|
||||
return err
|
||||
}
|
||||
dgst = pushDigest
|
||||
}
|
||||
} else {
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(layer.ID), "Image already exists", nil))
|
||||
}
|
||||
m.FSLayers[i] = ®istry.FSLayer{BlobSum: dgst.String()}
|
||||
m.History[i] = ®istry.ManifestHistory{V1Compatibility: string(jsonData)}
|
||||
}
|
||||
|
||||
if err := validateManifest(m); err != nil {
|
||||
return fmt.Errorf("invalid manifest: %s", err)
|
||||
}
|
||||
|
||||
logrus.Debugf("Pushing %s:%s to v2 repository", repoInfo.LocalName, tag)
|
||||
mBytes, err := json.MarshalIndent(m, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
js, err := libtrust.NewJSONSignature(mBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = js.Sign(s.trustKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signedBody, err := js.PrettySignature("signatures")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logrus.Infof("Signed manifest for %s:%s using daemon's key: %s", repoInfo.LocalName, tag, s.trustKey.KeyID())
|
||||
|
||||
// push the manifest
|
||||
digest, err := r.PutV2ImageManifest(endpoint, repoInfo.RemoteName, tag, signedBody, mBytes, auth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out.Write(sf.FormatStatus("", "Digest: %s", digest))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PushV2Image pushes the image content to the v2 registry, first buffering the contents to disk
|
||||
func (s *TagStore) pushV2Image(r *registry.Session, img *Image, endpoint *registry.Endpoint, imageName string, sf *streamformatter.StreamFormatter, out io.Writer, auth *registry.RequestAuthorization) (digest.Digest, error) {
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(img.ID), "Buffering to Disk", nil))
|
||||
|
||||
image, err := s.graph.Get(img.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
arch, err := s.graph.TarLayer(image)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer arch.Close()
|
||||
|
||||
tf, err := s.graph.newTempFile()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() {
|
||||
tf.Close()
|
||||
os.Remove(tf.Name())
|
||||
}()
|
||||
|
||||
size, dgst, err := bufferToFile(tf, arch)
|
||||
|
||||
// Send the layer
|
||||
logrus.Debugf("rendered layer for %s of [%d] size", img.ID, size)
|
||||
|
||||
if err := r.PutV2ImageBlob(endpoint, imageName, dgst,
|
||||
progressreader.New(progressreader.Config{
|
||||
In: tf,
|
||||
Out: out,
|
||||
Formatter: sf,
|
||||
Size: int(size),
|
||||
NewLines: false,
|
||||
ID: stringid.TruncateID(img.ID),
|
||||
Action: "Pushing",
|
||||
}), auth); err != nil {
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(img.ID), "Image push failed", nil))
|
||||
return "", err
|
||||
}
|
||||
out.Write(sf.FormatProgress(stringid.TruncateID(img.ID), "Image successfully pushed", nil))
|
||||
return dgst, nil
|
||||
return nil, fmt.Errorf("unknown version %d for registry %s", endpoint.Version, endpoint.URL)
|
||||
}
|
||||
|
||||
// FIXME: Allow to interrupt current push when new push of same image is done.
|
||||
func (s *TagStore) Push(localName string, imagePushConfig *ImagePushConfig) error {
|
||||
var (
|
||||
sf = streamformatter.NewJSONStreamFormatter()
|
||||
)
|
||||
var sf = streamformatter.NewJSONStreamFormatter()
|
||||
|
||||
// Resolve the Repository name from fqn to RepositoryInfo
|
||||
repoInfo, err := s.registryService.ResolveRepository(localName)
|
||||
|
@ -500,23 +59,7 @@ func (s *TagStore) Push(localName string, imagePushConfig *ImagePushConfig) erro
|
|||
return err
|
||||
}
|
||||
|
||||
if _, err := s.poolAdd("push", repoInfo.LocalName); err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.poolRemove("push", repoInfo.LocalName)
|
||||
|
||||
endpoint, err := repoInfo.GetEndpoint(imagePushConfig.MetaHeaders)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO(tiborvass): reuse client from endpoint?
|
||||
// Adds Docker-specific headers as well as user-specified headers (metaHeaders)
|
||||
tr := transport.NewTransport(
|
||||
registry.NewTransport(registry.NoTimeout, endpoint.IsSecure),
|
||||
registry.DockerHeaders(imagePushConfig.MetaHeaders)...,
|
||||
)
|
||||
client := registry.HTTPClient(tr)
|
||||
r, err := registry.NewSession(client, imagePushConfig.AuthConfig, endpoint)
|
||||
endpoints, err := s.registryService.LookupEndpoints(repoInfo.CanonicalName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -534,23 +77,31 @@ func (s *TagStore) Push(localName string, imagePushConfig *ImagePushConfig) erro
|
|||
return fmt.Errorf("Repository does not exist: %s", repoInfo.LocalName)
|
||||
}
|
||||
|
||||
if repoInfo.Index.Official || endpoint.Version == registry.APIVersion2 {
|
||||
err := s.pushV2Repository(r, localRepo, imagePushConfig.OutStream, repoInfo, imagePushConfig.Tag, sf)
|
||||
if err == nil {
|
||||
s.eventsService.Log("push", repoInfo.LocalName, "")
|
||||
return nil
|
||||
var lastErr error
|
||||
for _, endpoint := range endpoints {
|
||||
logrus.Debugf("Trying to push %s to %s %s", repoInfo.CanonicalName, endpoint.URL, endpoint.Version)
|
||||
|
||||
pusher, err := s.NewPusher(endpoint, localRepo, repoInfo, imagePushConfig, sf)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
if fallback, err := pusher.Push(); err != nil {
|
||||
if fallback {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
logrus.Debugf("Not continuing with error: %v", err)
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
if err != ErrV2RegistryUnavailable {
|
||||
return fmt.Errorf("Error pushing to registry: %s", err)
|
||||
}
|
||||
logrus.Debug("V2 registry is unavailable, falling back on V1")
|
||||
s.eventsService.Log("push", repoInfo.LocalName, "")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := s.pushRepository(r, imagePushConfig.OutStream, repoInfo, localRepo, imagePushConfig.Tag, sf); err != nil {
|
||||
return err
|
||||
if lastErr == nil {
|
||||
lastErr = fmt.Errorf("no endpoints found for %s", repoInfo.CanonicalName)
|
||||
}
|
||||
s.eventsService.Log("push", repoInfo.LocalName, "")
|
||||
return nil
|
||||
|
||||
return lastErr
|
||||
}
|
||||
|
|
309
graph/push_v1.go
Normal file
309
graph/push_v1.go
Normal file
|
@ -0,0 +1,309 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/docker/pkg/progressreader"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/utils"
|
||||
)
|
||||
|
||||
type v1Pusher struct {
|
||||
*TagStore
|
||||
endpoint registry.APIEndpoint
|
||||
localRepo Repository
|
||||
repoInfo *registry.RepositoryInfo
|
||||
config *ImagePushConfig
|
||||
sf *streamformatter.StreamFormatter
|
||||
session *registry.Session
|
||||
|
||||
out io.Writer
|
||||
}
|
||||
|
||||
func (p *v1Pusher) Push() (fallback bool, err error) {
|
||||
tlsConfig, err := p.registryService.TlsConfig(p.repoInfo.Index.Name)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// Adds Docker-specific headers as well as user-specified headers (metaHeaders)
|
||||
tr := transport.NewTransport(
|
||||
// TODO(tiborvass): was NoTimeout
|
||||
registry.NewTransport(tlsConfig),
|
||||
registry.DockerHeaders(p.config.MetaHeaders)...,
|
||||
)
|
||||
client := registry.HTTPClient(tr)
|
||||
v1Endpoint, err := p.endpoint.ToV1Endpoint(p.config.MetaHeaders)
|
||||
if err != nil {
|
||||
logrus.Debugf("Could not get v1 endpoint: %v", err)
|
||||
return true, err
|
||||
}
|
||||
p.session, err = registry.NewSession(client, p.config.AuthConfig, v1Endpoint)
|
||||
if err != nil {
|
||||
// TODO(dmcgowan): Check if should fallback
|
||||
return true, err
|
||||
}
|
||||
if err := p.pushRepository(p.config.Tag); err != nil {
|
||||
// TODO(dmcgowan): Check if should fallback
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Retrieve the all the images to be uploaded in the correct order
|
||||
func (p *v1Pusher) getImageList(requestedTag string) ([]string, map[string][]string, error) {
|
||||
var (
|
||||
imageList []string
|
||||
imagesSeen = make(map[string]bool)
|
||||
tagsByImage = make(map[string][]string)
|
||||
)
|
||||
|
||||
for tag, id := range p.localRepo {
|
||||
if requestedTag != "" && requestedTag != tag {
|
||||
// Include only the requested tag.
|
||||
continue
|
||||
}
|
||||
|
||||
if utils.DigestReference(tag) {
|
||||
// Ignore digest references.
|
||||
continue
|
||||
}
|
||||
|
||||
var imageListForThisTag []string
|
||||
|
||||
tagsByImage[id] = append(tagsByImage[id], tag)
|
||||
|
||||
for img, err := p.graph.Get(id); img != nil; img, err = p.graph.GetParent(img) {
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if imagesSeen[img.ID] {
|
||||
// This image is already on the list, we can ignore it and all its parents
|
||||
break
|
||||
}
|
||||
|
||||
imagesSeen[img.ID] = true
|
||||
imageListForThisTag = append(imageListForThisTag, img.ID)
|
||||
}
|
||||
|
||||
// reverse the image list for this tag (so the "most"-parent image is first)
|
||||
for i, j := 0, len(imageListForThisTag)-1; i < j; i, j = i+1, j-1 {
|
||||
imageListForThisTag[i], imageListForThisTag[j] = imageListForThisTag[j], imageListForThisTag[i]
|
||||
}
|
||||
|
||||
// append to main image list
|
||||
imageList = append(imageList, imageListForThisTag...)
|
||||
}
|
||||
if len(imageList) == 0 {
|
||||
return nil, nil, fmt.Errorf("No images found for the requested repository / tag")
|
||||
}
|
||||
logrus.Debugf("Image list: %v", imageList)
|
||||
logrus.Debugf("Tags by image: %v", tagsByImage)
|
||||
|
||||
return imageList, tagsByImage, nil
|
||||
}
|
||||
|
||||
// createImageIndex returns an index of an image's layer IDs and tags.
|
||||
func (s *TagStore) createImageIndex(images []string, tags map[string][]string) []*registry.ImgData {
|
||||
var imageIndex []*registry.ImgData
|
||||
for _, id := range images {
|
||||
if tags, hasTags := tags[id]; hasTags {
|
||||
// If an image has tags you must add an entry in the image index
|
||||
// for each tag
|
||||
for _, tag := range tags {
|
||||
imageIndex = append(imageIndex, ®istry.ImgData{
|
||||
ID: id,
|
||||
Tag: tag,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
// If the image does not have a tag it still needs to be sent to the
|
||||
// registry with an empty tag so that it is accociated with the repository
|
||||
imageIndex = append(imageIndex, ®istry.ImgData{
|
||||
ID: id,
|
||||
Tag: "",
|
||||
})
|
||||
}
|
||||
return imageIndex
|
||||
}
|
||||
|
||||
type imagePushData struct {
|
||||
id string
|
||||
endpoint string
|
||||
tokens []string
|
||||
}
|
||||
|
||||
// lookupImageOnEndpoint checks the specified endpoint to see if an image exists
|
||||
// and if it is absent then it sends the image id to the channel to be pushed.
|
||||
func (p *v1Pusher) lookupImageOnEndpoint(wg *sync.WaitGroup, images chan imagePushData, imagesToPush chan string) {
|
||||
defer wg.Done()
|
||||
for image := range images {
|
||||
if err := p.session.LookupRemoteImage(image.id, image.endpoint); err != nil {
|
||||
logrus.Errorf("Error in LookupRemoteImage: %s", err)
|
||||
imagesToPush <- image.id
|
||||
continue
|
||||
}
|
||||
p.out.Write(p.sf.FormatStatus("", "Image %s already pushed, skipping", stringid.TruncateID(image.id)))
|
||||
}
|
||||
}
|
||||
|
||||
func (p *v1Pusher) pushImageToEndpoint(endpoint string, imageIDs []string, tags map[string][]string, repo *registry.RepositoryData) error {
|
||||
workerCount := len(imageIDs)
|
||||
// start a maximum of 5 workers to check if images exist on the specified endpoint.
|
||||
if workerCount > 5 {
|
||||
workerCount = 5
|
||||
}
|
||||
var (
|
||||
wg = &sync.WaitGroup{}
|
||||
imageData = make(chan imagePushData, workerCount*2)
|
||||
imagesToPush = make(chan string, workerCount*2)
|
||||
pushes = make(chan map[string]struct{}, 1)
|
||||
)
|
||||
for i := 0; i < workerCount; i++ {
|
||||
wg.Add(1)
|
||||
go p.lookupImageOnEndpoint(wg, imageData, imagesToPush)
|
||||
}
|
||||
// start a go routine that consumes the images to push
|
||||
go func() {
|
||||
shouldPush := make(map[string]struct{})
|
||||
for id := range imagesToPush {
|
||||
shouldPush[id] = struct{}{}
|
||||
}
|
||||
pushes <- shouldPush
|
||||
}()
|
||||
for _, id := range imageIDs {
|
||||
imageData <- imagePushData{
|
||||
id: id,
|
||||
endpoint: endpoint,
|
||||
tokens: repo.Tokens,
|
||||
}
|
||||
}
|
||||
// close the channel to notify the workers that there will be no more images to check.
|
||||
close(imageData)
|
||||
wg.Wait()
|
||||
close(imagesToPush)
|
||||
// wait for all the images that require pushes to be collected into a consumable map.
|
||||
shouldPush := <-pushes
|
||||
// finish by pushing any images and tags to the endpoint. The order that the images are pushed
|
||||
// is very important that is why we are still iterating over the ordered list of imageIDs.
|
||||
for _, id := range imageIDs {
|
||||
if _, push := shouldPush[id]; push {
|
||||
if _, err := p.pushImage(id, endpoint, repo.Tokens); err != nil {
|
||||
// FIXME: Continue on error?
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, tag := range tags[id] {
|
||||
p.out.Write(p.sf.FormatStatus("", "Pushing tag for rev [%s] on {%s}", stringid.TruncateID(id), endpoint+"repositories/"+p.repoInfo.RemoteName+"/tags/"+tag))
|
||||
if err := p.session.PushRegistryTag(p.repoInfo.RemoteName, id, tag, endpoint); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pushRepository pushes layers that do not already exist on the registry.
|
||||
func (p *v1Pusher) pushRepository(tag string) error {
|
||||
|
||||
logrus.Debugf("Local repo: %s", p.localRepo)
|
||||
p.out = ioutils.NewWriteFlusher(p.config.OutStream)
|
||||
imgList, tags, err := p.getImageList(tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.out.Write(p.sf.FormatStatus("", "Sending image list"))
|
||||
|
||||
imageIndex := p.createImageIndex(imgList, tags)
|
||||
logrus.Debugf("Preparing to push %s with the following images and tags", p.localRepo)
|
||||
for _, data := range imageIndex {
|
||||
logrus.Debugf("Pushing ID: %s with Tag: %s", data.ID, data.Tag)
|
||||
}
|
||||
|
||||
if _, err := p.poolAdd("push", p.repoInfo.LocalName); err != nil {
|
||||
return err
|
||||
}
|
||||
defer p.poolRemove("push", p.repoInfo.LocalName)
|
||||
|
||||
// Register all the images in a repository with the registry
|
||||
// If an image is not in this list it will not be associated with the repository
|
||||
repoData, err := p.session.PushImageJSONIndex(p.repoInfo.RemoteName, imageIndex, false, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nTag := 1
|
||||
if tag == "" {
|
||||
nTag = len(p.localRepo)
|
||||
}
|
||||
p.out.Write(p.sf.FormatStatus("", "Pushing repository %s (%d tags)", p.repoInfo.CanonicalName, nTag))
|
||||
// push the repository to each of the endpoints only if it does not exist.
|
||||
for _, endpoint := range repoData.Endpoints {
|
||||
if err := p.pushImageToEndpoint(endpoint, imgList, tags, repoData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = p.session.PushImageJSONIndex(p.repoInfo.RemoteName, imageIndex, true, repoData.Endpoints)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *v1Pusher) pushImage(imgID, ep string, token []string) (checksum string, err error) {
|
||||
jsonRaw, err := p.graph.RawJSON(imgID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Cannot retrieve the path for {%s}: %s", imgID, err)
|
||||
}
|
||||
p.out.Write(p.sf.FormatProgress(stringid.TruncateID(imgID), "Pushing", nil))
|
||||
|
||||
imgData := ®istry.ImgData{
|
||||
ID: imgID,
|
||||
}
|
||||
|
||||
// Send the json
|
||||
if err := p.session.PushImageJSONRegistry(imgData, jsonRaw, ep); err != nil {
|
||||
if err == registry.ErrAlreadyExists {
|
||||
p.out.Write(p.sf.FormatProgress(stringid.TruncateID(imgData.ID), "Image already pushed, skipping", nil))
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
layerData, err := p.graph.TempLayerArchive(imgID, p.sf, p.out)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to generate layer archive: %s", err)
|
||||
}
|
||||
defer os.RemoveAll(layerData.Name())
|
||||
|
||||
// Send the layer
|
||||
logrus.Debugf("rendered layer for %s of [%d] size", imgData.ID, layerData.Size)
|
||||
|
||||
checksum, checksumPayload, err := p.session.PushImageLayerRegistry(imgData.ID,
|
||||
progressreader.New(progressreader.Config{
|
||||
In: layerData,
|
||||
Out: p.out,
|
||||
Formatter: p.sf,
|
||||
Size: int(layerData.Size),
|
||||
NewLines: false,
|
||||
ID: stringid.TruncateID(imgData.ID),
|
||||
Action: "Pushing",
|
||||
}), ep, jsonRaw)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
imgData.Checksum = checksum
|
||||
imgData.ChecksumPayload = checksumPayload
|
||||
// Send the checksum
|
||||
if err := p.session.PushImageChecksumRegistry(imgData, ep); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
p.out.Write(p.sf.FormatProgress(stringid.TruncateID(imgData.ID), "Image successfully pushed", nil))
|
||||
return imgData.Checksum, nil
|
||||
}
|
254
graph/push_v2.go
Normal file
254
graph/push_v2.go
Normal file
|
@ -0,0 +1,254 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/manifest"
|
||||
"github.com/docker/docker/pkg/progressreader"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/docker/runconfig"
|
||||
"github.com/docker/docker/utils"
|
||||
)
|
||||
|
||||
type v2Pusher struct {
|
||||
*TagStore
|
||||
endpoint registry.APIEndpoint
|
||||
localRepo Repository
|
||||
repoInfo *registry.RepositoryInfo
|
||||
config *ImagePushConfig
|
||||
sf *streamformatter.StreamFormatter
|
||||
repo distribution.Repository
|
||||
}
|
||||
|
||||
func (p *v2Pusher) Push() (fallback bool, err error) {
|
||||
p.repo, err = NewV2Repository(p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig)
|
||||
if err != nil {
|
||||
logrus.Debugf("Error getting v2 registry: %v", err)
|
||||
return true, err
|
||||
}
|
||||
return false, p.pushV2Repository(p.config.Tag)
|
||||
}
|
||||
|
||||
func (p *v2Pusher) getImageTags(askedTag string) ([]string, error) {
|
||||
logrus.Debugf("Checking %q against %#v", askedTag, p.localRepo)
|
||||
if len(askedTag) > 0 {
|
||||
if _, ok := p.localRepo[askedTag]; !ok || utils.DigestReference(askedTag) {
|
||||
return nil, fmt.Errorf("Tag does not exist for %s", askedTag)
|
||||
}
|
||||
return []string{askedTag}, nil
|
||||
}
|
||||
var tags []string
|
||||
for tag := range p.localRepo {
|
||||
if !utils.DigestReference(tag) {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (p *v2Pusher) pushV2Repository(tag string) error {
|
||||
localName := p.repoInfo.LocalName
|
||||
if _, err := p.poolAdd("push", localName); err != nil {
|
||||
return err
|
||||
}
|
||||
defer p.poolRemove("push", localName)
|
||||
|
||||
tags, err := p.getImageTags(tag)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting tags for %s: %s", localName, err)
|
||||
}
|
||||
if len(tags) == 0 {
|
||||
return fmt.Errorf("no tags to push for %s", localName)
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
if err := p.pushV2Tag(tag); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *v2Pusher) pushV2Tag(tag string) error {
|
||||
logrus.Debugf("Pushing repository: %s:%s", p.repo.Name(), tag)
|
||||
|
||||
layerId, exists := p.localRepo[tag]
|
||||
if !exists {
|
||||
return fmt.Errorf("tag does not exist: %s", tag)
|
||||
}
|
||||
|
||||
layersSeen := make(map[string]bool)
|
||||
|
||||
layer, err := p.graph.Get(layerId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m := &manifest.Manifest{
|
||||
Versioned: manifest.Versioned{
|
||||
SchemaVersion: 1,
|
||||
},
|
||||
Name: p.repo.Name(),
|
||||
Tag: tag,
|
||||
Architecture: layer.Architecture,
|
||||
FSLayers: []manifest.FSLayer{},
|
||||
History: []manifest.History{},
|
||||
}
|
||||
|
||||
var metadata runconfig.Config
|
||||
if layer != nil && layer.Config != nil {
|
||||
metadata = *layer.Config
|
||||
}
|
||||
|
||||
out := p.config.OutStream
|
||||
|
||||
for ; layer != nil; layer, err = p.graph.GetParent(layer) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if layersSeen[layer.ID] {
|
||||
break
|
||||
}
|
||||
|
||||
logrus.Debugf("Pushing layer: %s", layer.ID)
|
||||
|
||||
if layer.Config != nil && metadata.Image != layer.ID {
|
||||
if err := runconfig.Merge(&metadata, layer.Config); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
jsonData, err := p.graph.RawJSON(layer.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot retrieve the path for %s: %s", layer.ID, err)
|
||||
}
|
||||
|
||||
var exists bool
|
||||
dgst, err := p.graph.GetDigest(layer.ID)
|
||||
switch err {
|
||||
case nil:
|
||||
_, err := p.repo.Blobs(nil).Stat(nil, dgst)
|
||||
switch err {
|
||||
case nil:
|
||||
exists = true
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(layer.ID), "Image already exists", nil))
|
||||
case distribution.ErrBlobUnknown:
|
||||
// nop
|
||||
default:
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(layer.ID), "Image push failed", nil))
|
||||
return err
|
||||
}
|
||||
case ErrDigestNotSet:
|
||||
// nop
|
||||
case digest.ErrDigestInvalidFormat, digest.ErrDigestUnsupported:
|
||||
return fmt.Errorf("error getting image checksum: %v", err)
|
||||
}
|
||||
|
||||
// if digest was empty or not saved, or if blob does not exist on the remote repository,
|
||||
// then fetch it.
|
||||
if !exists {
|
||||
if pushDigest, err := p.pushV2Image(p.repo.Blobs(nil), layer); err != nil {
|
||||
return err
|
||||
} else if pushDigest != dgst {
|
||||
// Cache new checksum
|
||||
if err := p.graph.SetDigest(layer.ID, pushDigest); err != nil {
|
||||
return err
|
||||
}
|
||||
dgst = pushDigest
|
||||
}
|
||||
}
|
||||
|
||||
m.FSLayers = append(m.FSLayers, manifest.FSLayer{BlobSum: dgst})
|
||||
m.History = append(m.History, manifest.History{V1Compatibility: string(jsonData)})
|
||||
|
||||
layersSeen[layer.ID] = true
|
||||
}
|
||||
|
||||
logrus.Infof("Signed manifest for %s:%s using daemon's key: %s", p.repo.Name(), tag, p.trustKey.KeyID())
|
||||
signed, err := manifest.Sign(m, p.trustKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifestDigest, err := digestFromManifest(signed, p.repo.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if manifestDigest != "" {
|
||||
out.Write(p.sf.FormatStatus("", "Digest: %s", manifestDigest))
|
||||
}
|
||||
|
||||
return p.repo.Manifests().Put(signed)
|
||||
}
|
||||
|
||||
func (p *v2Pusher) pushV2Image(bs distribution.BlobService, img *Image) (digest.Digest, error) {
|
||||
out := p.config.OutStream
|
||||
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), "Buffering to Disk", nil))
|
||||
|
||||
image, err := p.graph.Get(img.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
arch, err := p.graph.TarLayer(image)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tf, err := p.graph.newTempFile()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() {
|
||||
tf.Close()
|
||||
os.Remove(tf.Name())
|
||||
}()
|
||||
|
||||
size, dgst, err := bufferToFile(tf, arch)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Send the layer
|
||||
logrus.Debugf("rendered layer for %s of [%d] size", img.ID, size)
|
||||
layerUpload, err := bs.Create(nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer layerUpload.Close()
|
||||
|
||||
reader := progressreader.New(progressreader.Config{
|
||||
In: ioutil.NopCloser(tf),
|
||||
Out: out,
|
||||
Formatter: p.sf,
|
||||
Size: int(size),
|
||||
NewLines: false,
|
||||
ID: stringid.TruncateID(img.ID),
|
||||
Action: "Pushing",
|
||||
})
|
||||
n, err := layerUpload.ReadFrom(reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if n != size {
|
||||
return "", fmt.Errorf("short upload: only wrote %d of %d", n, size)
|
||||
}
|
||||
|
||||
desc := distribution.Descriptor{Digest: dgst}
|
||||
if _, err := layerUpload.Commit(nil, desc); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
out.Write(p.sf.FormatProgress(stringid.TruncateID(img.ID), "Image successfully pushed", nil))
|
||||
|
||||
return dgst, nil
|
||||
}
|
111
graph/registry.go
Normal file
111
graph/registry.go
Normal file
|
@ -0,0 +1,111 @@
|
|||
package graph
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/manifest"
|
||||
"github.com/docker/distribution/registry/client"
|
||||
"github.com/docker/distribution/registry/client/auth"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
"github.com/docker/docker/cliconfig"
|
||||
"github.com/docker/docker/registry"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type dumbCredentialStore struct {
|
||||
auth *cliconfig.AuthConfig
|
||||
}
|
||||
|
||||
func (dcs dumbCredentialStore) Basic(*url.URL) (string, string) {
|
||||
return dcs.auth.Username, dcs.auth.Password
|
||||
}
|
||||
|
||||
// v2 only
|
||||
func NewV2Repository(repoInfo *registry.RepositoryInfo, endpoint registry.APIEndpoint, metaHeaders http.Header, authConfig *cliconfig.AuthConfig) (distribution.Repository, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
repoName := repoInfo.CanonicalName
|
||||
// If endpoint does not support CanonicalName, use the RemoteName instead
|
||||
if endpoint.TrimHostname {
|
||||
repoName = repoInfo.RemoteName
|
||||
}
|
||||
|
||||
// TODO(dmcgowan): Call close idle connections when complete, use keep alive
|
||||
base := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
DualStack: true,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
TLSClientConfig: endpoint.TLSConfig,
|
||||
// TODO(dmcgowan): Call close idle connections when complete and use keep alive
|
||||
DisableKeepAlives: true,
|
||||
}
|
||||
|
||||
modifiers := registry.DockerHeaders(metaHeaders)
|
||||
authTransport := transport.NewTransport(base, modifiers...)
|
||||
pingClient := &http.Client{
|
||||
Transport: authTransport,
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
endpointStr := endpoint.URL + "/v2/"
|
||||
req, err := http.NewRequest("GET", endpointStr, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := pingClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
versions := auth.APIVersions(resp, endpoint.VersionHeader)
|
||||
if endpoint.VersionHeader != "" && len(endpoint.Versions) > 0 {
|
||||
var foundVersion bool
|
||||
for _, version := range endpoint.Versions {
|
||||
for _, pingVersion := range versions {
|
||||
if version == pingVersion {
|
||||
foundVersion = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundVersion {
|
||||
return nil, errors.New("endpoint does not support v2 API")
|
||||
}
|
||||
}
|
||||
|
||||
challengeManager := auth.NewSimpleChallengeManager()
|
||||
if err := challengeManager.AddResponse(resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
creds := dumbCredentialStore{auth: authConfig}
|
||||
tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName, "push", "pull")
|
||||
basicHandler := auth.NewBasicHandler(creds)
|
||||
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
|
||||
tr := transport.NewTransport(base, modifiers...)
|
||||
|
||||
return client.NewRepository(ctx, repoName, endpoint.URL, tr)
|
||||
}
|
||||
|
||||
func digestFromManifest(m *manifest.SignedManifest, localName string) (digest.Digest, error) {
|
||||
payload, err := m.Payload()
|
||||
if err != nil {
|
||||
logrus.Debugf("could not retrieve manifest payload: %v", err)
|
||||
return "", err
|
||||
}
|
||||
manifestDigest, err := digest.FromBytes(payload)
|
||||
if err != nil {
|
||||
logrus.Infof("Could not compute manifest digest for %s:%s : %v", localName, m.Tag, err)
|
||||
}
|
||||
return manifestDigest, nil
|
||||
}
|
|
@ -7,7 +7,7 @@ source 'hack/.vendor-helpers.sh'
|
|||
|
||||
# the following lines are in sorted order, FYI
|
||||
clone git github.com/Sirupsen/logrus v0.8.2 # logrus is a common dependency among multiple deps
|
||||
clone git github.com/docker/libtrust 230dfd18c232
|
||||
clone git github.com/docker/libtrust 9cbd2a1374f46905c68a4eb3694a130610adc62a
|
||||
clone git github.com/go-check/check 64131543e7896d5bcc6bd5a76287eb75ea96c673
|
||||
clone git github.com/gorilla/context 14f550f51a
|
||||
clone git github.com/gorilla/mux e444e69cbd
|
||||
|
|
|
@ -104,7 +104,7 @@ func (s *DockerRegistrySuite) TestPullByDigestNoFallback(c *check.C) {
|
|||
// pull from the registry using the <name>@<digest> reference
|
||||
imageReference := fmt.Sprintf("%s@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", repoName)
|
||||
out, _, err := dockerCmdWithError(c, "pull", imageReference)
|
||||
if err == nil || !strings.Contains(out, "pulling with digest reference failed from v2 registry") {
|
||||
if err == nil || !strings.Contains(out, "manifest unknown") {
|
||||
c.Fatalf("expected non-zero exit status and correct error message when pulling non-existing image: %s", out)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -89,8 +89,6 @@ func (s *DockerSuite) TestPullImageOfficialNames(c *check.C) {
|
|||
testRequires(c, Network)
|
||||
|
||||
names := []string{
|
||||
"docker.io/hello-world",
|
||||
"index.docker.io/hello-world",
|
||||
"library/hello-world",
|
||||
"docker.io/library/hello-world",
|
||||
"index.docker.io/library/hello-world",
|
||||
|
|
|
@ -225,3 +225,29 @@ func HashData(src io.Reader) (string, error) {
|
|||
}
|
||||
return "sha256:" + hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
type OnEOFReader struct {
|
||||
Rc io.ReadCloser
|
||||
Fn func()
|
||||
}
|
||||
|
||||
func (r *OnEOFReader) Read(p []byte) (n int, err error) {
|
||||
n, err = r.Rc.Read(p)
|
||||
if err == io.EOF {
|
||||
r.runFunc()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *OnEOFReader) Close() error {
|
||||
err := r.Rc.Close()
|
||||
r.runFunc()
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *OnEOFReader) runFunc() {
|
||||
if fn := r.Fn; fn != nil {
|
||||
fn()
|
||||
r.Fn = nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
Copyright (c) 2009 The oauth2 Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -125,7 +125,7 @@ func loginV1(authConfig *cliconfig.AuthConfig, registryEndpoint *Endpoint) (stri
|
|||
return "", fmt.Errorf("Server Error: Server Address not set.")
|
||||
}
|
||||
|
||||
loginAgainstOfficialIndex := serverAddress == IndexServerAddress()
|
||||
loginAgainstOfficialIndex := serverAddress == INDEXSERVER
|
||||
|
||||
// to avoid sending the server address to the server it should be removed before being marshalled
|
||||
authCopy := *authConfig
|
||||
|
|
|
@ -37,7 +37,7 @@ func setupTempConfigFile() (*cliconfig.ConfigFile, error) {
|
|||
root = filepath.Join(root, cliconfig.CONFIGFILE)
|
||||
configFile := cliconfig.NewConfigFile(root)
|
||||
|
||||
for _, registry := range []string{"testIndex", IndexServerAddress()} {
|
||||
for _, registry := range []string{"testIndex", INDEXSERVER} {
|
||||
configFile.AuthConfigs[registry] = cliconfig.AuthConfig{
|
||||
Username: "docker-user",
|
||||
Password: "docker-pass",
|
||||
|
@ -82,7 +82,7 @@ func TestResolveAuthConfigIndexServer(t *testing.T) {
|
|||
}
|
||||
defer os.RemoveAll(configFile.Filename())
|
||||
|
||||
indexConfig := configFile.AuthConfigs[IndexServerAddress()]
|
||||
indexConfig := configFile.AuthConfigs[INDEXSERVER]
|
||||
|
||||
officialIndex := &IndexInfo{
|
||||
Official: true,
|
||||
|
@ -92,10 +92,10 @@ func TestResolveAuthConfigIndexServer(t *testing.T) {
|
|||
}
|
||||
|
||||
resolved := ResolveAuthConfig(configFile, officialIndex)
|
||||
assertEqual(t, resolved, indexConfig, "Expected ResolveAuthConfig to return IndexServerAddress()")
|
||||
assertEqual(t, resolved, indexConfig, "Expected ResolveAuthConfig to return INDEXSERVER")
|
||||
|
||||
resolved = ResolveAuthConfig(configFile, privateIndex)
|
||||
assertNotEqual(t, resolved, indexConfig, "Expected ResolveAuthConfig to not return IndexServerAddress()")
|
||||
assertNotEqual(t, resolved, indexConfig, "Expected ResolveAuthConfig to not return INDEXSERVER")
|
||||
}
|
||||
|
||||
func TestResolveAuthConfigFullURL(t *testing.T) {
|
||||
|
@ -120,7 +120,7 @@ func TestResolveAuthConfigFullURL(t *testing.T) {
|
|||
Password: "baz-pass",
|
||||
Email: "baz@example.com",
|
||||
}
|
||||
configFile.AuthConfigs[IndexServerAddress()] = officialAuth
|
||||
configFile.AuthConfigs[INDEXSERVER] = officialAuth
|
||||
|
||||
expectedAuths := map[string]cliconfig.AuthConfig{
|
||||
"registry.example.com": registryAuth,
|
||||
|
|
|
@ -21,9 +21,16 @@ type Options struct {
|
|||
}
|
||||
|
||||
const (
|
||||
DEFAULT_NAMESPACE = "docker.io"
|
||||
DEFAULT_V2_REGISTRY = "https://registry-1.docker.io"
|
||||
DEFAULT_REGISTRY_VERSION_HEADER = "Docker-Distribution-Api-Version"
|
||||
DEFAULT_V1_REGISTRY = "https://index.docker.io"
|
||||
|
||||
CERTS_DIR = "/etc/docker/certs.d"
|
||||
|
||||
// Only used for user auth + account creation
|
||||
INDEXSERVER = "https://index.docker.io/v1/"
|
||||
REGISTRYSERVER = "https://registry-1.docker.io/v2/"
|
||||
REGISTRYSERVER = DEFAULT_V2_REGISTRY
|
||||
INDEXSERVER = DEFAULT_V1_REGISTRY + "/v1/"
|
||||
INDEXNAME = "docker.io"
|
||||
|
||||
// INDEXSERVER = "https://registry-stage.hub.docker.com/v1/"
|
||||
|
@ -34,14 +41,6 @@ var (
|
|||
emptyServiceConfig = NewServiceConfig(nil)
|
||||
)
|
||||
|
||||
func IndexServerAddress() string {
|
||||
return INDEXSERVER
|
||||
}
|
||||
|
||||
func IndexServerName() string {
|
||||
return INDEXNAME
|
||||
}
|
||||
|
||||
// InstallFlags adds command-line options to the top-level flag parser for
|
||||
// the current process.
|
||||
func (options *Options) InstallFlags() {
|
||||
|
@ -72,6 +71,7 @@ func (ipnet *netIPNet) UnmarshalJSON(b []byte) (err error) {
|
|||
type ServiceConfig struct {
|
||||
InsecureRegistryCIDRs []*netIPNet `json:"InsecureRegistryCIDRs"`
|
||||
IndexConfigs map[string]*IndexInfo `json:"IndexConfigs"`
|
||||
Mirrors []string
|
||||
}
|
||||
|
||||
// NewServiceConfig returns a new instance of ServiceConfig
|
||||
|
@ -93,6 +93,9 @@ func NewServiceConfig(options *Options) *ServiceConfig {
|
|||
config := &ServiceConfig{
|
||||
InsecureRegistryCIDRs: make([]*netIPNet, 0),
|
||||
IndexConfigs: make(map[string]*IndexInfo, 0),
|
||||
// Hack: Bypass setting the mirrors to IndexConfigs since they are going away
|
||||
// and Mirrors are only for the official registry anyways.
|
||||
Mirrors: options.Mirrors.GetAll(),
|
||||
}
|
||||
// Split --insecure-registry into CIDR and registry-specific settings.
|
||||
for _, r := range options.InsecureRegistries.GetAll() {
|
||||
|
@ -113,9 +116,9 @@ func NewServiceConfig(options *Options) *ServiceConfig {
|
|||
}
|
||||
|
||||
// Configure public registry.
|
||||
config.IndexConfigs[IndexServerName()] = &IndexInfo{
|
||||
Name: IndexServerName(),
|
||||
Mirrors: options.Mirrors.GetAll(),
|
||||
config.IndexConfigs[INDEXNAME] = &IndexInfo{
|
||||
Name: INDEXNAME,
|
||||
Mirrors: config.Mirrors,
|
||||
Secure: true,
|
||||
Official: true,
|
||||
}
|
||||
|
@ -193,8 +196,8 @@ func ValidateMirror(val string) (string, error) {
|
|||
// ValidateIndexName validates an index name.
|
||||
func ValidateIndexName(val string) (string, error) {
|
||||
// 'index.docker.io' => 'docker.io'
|
||||
if val == "index."+IndexServerName() {
|
||||
val = IndexServerName()
|
||||
if val == "index."+INDEXNAME {
|
||||
val = INDEXNAME
|
||||
}
|
||||
if strings.HasPrefix(val, "-") || strings.HasSuffix(val, "-") {
|
||||
return "", fmt.Errorf("Invalid index name (%s). Cannot begin or end with a hyphen.", val)
|
||||
|
@ -264,7 +267,7 @@ func (config *ServiceConfig) NewIndexInfo(indexName string) (*IndexInfo, error)
|
|||
// index as the AuthConfig key, and uses the (host)name[:port] for private indexes.
|
||||
func (index *IndexInfo) GetAuthConfigKey() string {
|
||||
if index.Official {
|
||||
return IndexServerAddress()
|
||||
return INDEXSERVER
|
||||
}
|
||||
return index.Name
|
||||
}
|
||||
|
@ -277,7 +280,7 @@ func splitReposName(reposName string) (string, string) {
|
|||
!strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") {
|
||||
// This is a Docker Index repos (ex: samalba/hipache or ubuntu)
|
||||
// 'docker.io'
|
||||
indexName = IndexServerName()
|
||||
indexName = INDEXNAME
|
||||
remoteName = reposName
|
||||
} else {
|
||||
indexName = nameParts[0]
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
@ -11,7 +12,8 @@ import (
|
|||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/registry/api/v2"
|
||||
"github.com/docker/docker/pkg/transport"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
"github.com/docker/docker/pkg/tlsconfig"
|
||||
)
|
||||
|
||||
// for mocking in unit tests
|
||||
|
@ -44,7 +46,9 @@ func scanForAPIVersion(address string) (string, APIVersion) {
|
|||
// NewEndpoint parses the given address to return a registry endpoint.
|
||||
func NewEndpoint(index *IndexInfo, metaHeaders http.Header) (*Endpoint, error) {
|
||||
// *TODO: Allow per-registry configuration of endpoints.
|
||||
endpoint, err := newEndpoint(index.GetAuthConfigKey(), index.Secure, metaHeaders)
|
||||
tlsConfig := tlsconfig.ServerDefault
|
||||
tlsConfig.InsecureSkipVerify = !index.Secure
|
||||
endpoint, err := newEndpoint(index.GetAuthConfigKey(), &tlsConfig, metaHeaders)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -82,7 +86,7 @@ func validateEndpoint(endpoint *Endpoint) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func newEndpoint(address string, secure bool, metaHeaders http.Header) (*Endpoint, error) {
|
||||
func newEndpoint(address string, tlsConfig *tls.Config, metaHeaders http.Header) (*Endpoint, error) {
|
||||
var (
|
||||
endpoint = new(Endpoint)
|
||||
trimmedAddress string
|
||||
|
@ -93,13 +97,16 @@ func newEndpoint(address string, secure bool, metaHeaders http.Header) (*Endpoin
|
|||
address = "https://" + address
|
||||
}
|
||||
|
||||
endpoint.IsSecure = (tlsConfig == nil || !tlsConfig.InsecureSkipVerify)
|
||||
|
||||
trimmedAddress, endpoint.Version = scanForAPIVersion(address)
|
||||
|
||||
if endpoint.URL, err = url.Parse(trimmedAddress); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
endpoint.IsSecure = secure
|
||||
tr := NewTransport(ConnectTimeout, endpoint.IsSecure)
|
||||
|
||||
// TODO(tiborvass): make sure a ConnectTimeout transport is used
|
||||
tr := NewTransport(tlsConfig)
|
||||
endpoint.client = HTTPClient(transport.NewTransport(tr, DockerHeaders(metaHeaders)...))
|
||||
return endpoint, nil
|
||||
}
|
||||
|
@ -166,7 +173,7 @@ func (e *Endpoint) Ping() (RegistryInfo, error) {
|
|||
func (e *Endpoint) pingV1() (RegistryInfo, error) {
|
||||
logrus.Debugf("attempting v1 ping for registry endpoint %s", e)
|
||||
|
||||
if e.String() == IndexServerAddress() {
|
||||
if e.String() == INDEXSERVER {
|
||||
// Skip the check, we know this one is valid
|
||||
// (and we never want to fallback to http in case of error)
|
||||
return RegistryInfo{Standalone: false}, nil
|
||||
|
|
|
@ -12,14 +12,14 @@ func TestEndpointParse(t *testing.T) {
|
|||
str string
|
||||
expected string
|
||||
}{
|
||||
{IndexServerAddress(), IndexServerAddress()},
|
||||
{INDEXSERVER, INDEXSERVER},
|
||||
{"http://0.0.0.0:5000/v1/", "http://0.0.0.0:5000/v1/"},
|
||||
{"http://0.0.0.0:5000/v2/", "http://0.0.0.0:5000/v2/"},
|
||||
{"http://0.0.0.0:5000", "http://0.0.0.0:5000/v0/"},
|
||||
{"0.0.0.0:5000", "https://0.0.0.0:5000/v0/"},
|
||||
}
|
||||
for _, td := range testData {
|
||||
e, err := newEndpoint(td.str, false, nil)
|
||||
e, err := newEndpoint(td.str, nil, nil)
|
||||
if err != nil {
|
||||
t.Errorf("%q: %s", td.str, err)
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ func TestValidateEndpointAmbiguousAPIVersion(t *testing.T) {
|
|||
testEndpoint := Endpoint{
|
||||
URL: testServerURL,
|
||||
Version: APIVersionUnknown,
|
||||
client: HTTPClient(NewTransport(ConnectTimeout, false)),
|
||||
client: HTTPClient(NewTransport(nil)),
|
||||
}
|
||||
|
||||
if err = validateEndpoint(&testEndpoint); err != nil {
|
||||
|
|
|
@ -2,26 +2,21 @@ package registry
|
|||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/registry/api/errcode"
|
||||
"github.com/docker/distribution/registry/api/v2"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
"github.com/docker/docker/autogen/dockerversion"
|
||||
"github.com/docker/docker/pkg/parsers/kernel"
|
||||
"github.com/docker/docker/pkg/timeoutconn"
|
||||
"github.com/docker/docker/pkg/tlsconfig"
|
||||
"github.com/docker/docker/pkg/transport"
|
||||
"github.com/docker/docker/pkg/useragent"
|
||||
)
|
||||
|
||||
|
@ -57,135 +52,13 @@ func init() {
|
|||
dockerUserAgent = useragent.AppendVersions("", httpVersion...)
|
||||
}
|
||||
|
||||
type httpsRequestModifier struct {
|
||||
mu sync.Mutex
|
||||
tlsConfig *tls.Config
|
||||
}
|
||||
|
||||
// DRAGONS(tiborvass): If someone wonders why do we set tlsconfig in a roundtrip,
|
||||
// it's because it's so as to match the current behavior in master: we generate the
|
||||
// certpool on every-goddam-request. It's not great, but it allows people to just put
|
||||
// the certs in /etc/docker/certs.d/.../ and let docker "pick it up" immediately. Would
|
||||
// prefer an fsnotify implementation, but that was out of scope of my refactoring.
|
||||
func (m *httpsRequestModifier) ModifyRequest(req *http.Request) error {
|
||||
var (
|
||||
roots *x509.CertPool
|
||||
certs []tls.Certificate
|
||||
hostDir string
|
||||
)
|
||||
|
||||
if req.URL.Scheme == "https" {
|
||||
hasFile := func(files []os.FileInfo, name string) bool {
|
||||
for _, f := range files {
|
||||
if f.Name() == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
hostDir = path.Join(os.TempDir(), "/docker/certs.d", req.URL.Host)
|
||||
} else {
|
||||
hostDir = path.Join("/etc/docker/certs.d", req.URL.Host)
|
||||
}
|
||||
logrus.Debugf("hostDir: %s", hostDir)
|
||||
fs, err := ioutil.ReadDir(hostDir)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, f := range fs {
|
||||
if strings.HasSuffix(f.Name(), ".crt") {
|
||||
if roots == nil {
|
||||
roots = x509.NewCertPool()
|
||||
}
|
||||
logrus.Debugf("crt: %s", hostDir+"/"+f.Name())
|
||||
data, err := ioutil.ReadFile(filepath.Join(hostDir, f.Name()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
roots.AppendCertsFromPEM(data)
|
||||
}
|
||||
if strings.HasSuffix(f.Name(), ".cert") {
|
||||
certName := f.Name()
|
||||
keyName := certName[:len(certName)-5] + ".key"
|
||||
logrus.Debugf("cert: %s", hostDir+"/"+f.Name())
|
||||
if !hasFile(fs, keyName) {
|
||||
return fmt.Errorf("Missing key %s for certificate %s", keyName, certName)
|
||||
}
|
||||
cert, err := tls.LoadX509KeyPair(filepath.Join(hostDir, certName), path.Join(hostDir, keyName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
certs = append(certs, cert)
|
||||
}
|
||||
if strings.HasSuffix(f.Name(), ".key") {
|
||||
keyName := f.Name()
|
||||
certName := keyName[:len(keyName)-4] + ".cert"
|
||||
logrus.Debugf("key: %s", hostDir+"/"+f.Name())
|
||||
if !hasFile(fs, certName) {
|
||||
return fmt.Errorf("Missing certificate %s for key %s", certName, keyName)
|
||||
}
|
||||
}
|
||||
}
|
||||
m.mu.Lock()
|
||||
m.tlsConfig.RootCAs = roots
|
||||
m.tlsConfig.Certificates = certs
|
||||
m.mu.Unlock()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewTransport(timeout TimeoutType, secure bool) http.RoundTripper {
|
||||
tlsConfig := &tls.Config{
|
||||
// Avoid fallback to SSL protocols < TLS1.0
|
||||
MinVersion: tls.VersionTLS10,
|
||||
InsecureSkipVerify: !secure,
|
||||
CipherSuites: tlsconfig.DefaultServerAcceptedCiphers,
|
||||
}
|
||||
|
||||
tr := &http.Transport{
|
||||
DisableKeepAlives: true,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
TLSClientConfig: tlsConfig,
|
||||
}
|
||||
|
||||
switch timeout {
|
||||
case ConnectTimeout:
|
||||
tr.Dial = func(proto string, addr string) (net.Conn, error) {
|
||||
// Set the connect timeout to 30 seconds to allow for slower connection
|
||||
// times...
|
||||
d := net.Dialer{Timeout: 30 * time.Second, DualStack: true}
|
||||
|
||||
conn, err := d.Dial(proto, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Set the recv timeout to 10 seconds
|
||||
conn.SetDeadline(time.Now().Add(10 * time.Second))
|
||||
return conn, nil
|
||||
}
|
||||
case ReceiveTimeout:
|
||||
tr.Dial = func(proto string, addr string) (net.Conn, error) {
|
||||
d := net.Dialer{DualStack: true}
|
||||
|
||||
conn, err := d.Dial(proto, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn = timeoutconn.New(conn, 1*time.Minute)
|
||||
return conn, nil
|
||||
func hasFile(files []os.FileInfo, name string) bool {
|
||||
for _, f := range files {
|
||||
if f.Name() == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if secure {
|
||||
// note: httpsTransport also handles http transport
|
||||
// but for HTTPS, it sets up the certs
|
||||
return transport.NewTransport(tr, &httpsRequestModifier{tlsConfig: tlsConfig})
|
||||
}
|
||||
|
||||
return tr
|
||||
return false
|
||||
}
|
||||
|
||||
// DockerHeaders returns request modifiers that ensure requests have
|
||||
|
@ -202,10 +75,6 @@ func DockerHeaders(metaHeaders http.Header) []transport.RequestModifier {
|
|||
}
|
||||
|
||||
func HTTPClient(transport http.RoundTripper) *http.Client {
|
||||
if transport == nil {
|
||||
transport = NewTransport(ConnectTimeout, true)
|
||||
}
|
||||
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
CheckRedirect: AddRequiredHeadersToRedirectedRequests,
|
||||
|
@ -245,3 +114,52 @@ func AddRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Reque
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func shouldV2Fallback(err errcode.Error) bool {
|
||||
logrus.Debugf("v2 error: %T %v", err, err)
|
||||
switch err.Code {
|
||||
case v2.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ErrNoSupport struct{ Err error }
|
||||
|
||||
func (e ErrNoSupport) Error() string {
|
||||
if e.Err == nil {
|
||||
return "not supported"
|
||||
}
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
func ContinueOnError(err error) bool {
|
||||
switch v := err.(type) {
|
||||
case errcode.Errors:
|
||||
return ContinueOnError(v[0])
|
||||
case ErrNoSupport:
|
||||
return ContinueOnError(v.Err)
|
||||
case errcode.Error:
|
||||
return shouldV2Fallback(v)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func NewTransport(tlsConfig *tls.Config) *http.Transport {
|
||||
if tlsConfig == nil {
|
||||
var cfg = tlsconfig.ServerDefault
|
||||
tlsConfig = &cfg
|
||||
}
|
||||
return &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
DualStack: true,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
TLSClientConfig: tlsConfig,
|
||||
// TODO(dmcgowan): Call close idle connections when complete and use keep alive
|
||||
DisableKeepAlives: true,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -165,7 +165,7 @@ func makeHttpsIndex(req string) *IndexInfo {
|
|||
|
||||
func makePublicIndex() *IndexInfo {
|
||||
index := &IndexInfo{
|
||||
Name: IndexServerAddress(),
|
||||
Name: INDEXSERVER,
|
||||
Secure: true,
|
||||
Official: true,
|
||||
}
|
||||
|
|
|
@ -8,8 +8,8 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
"github.com/docker/docker/cliconfig"
|
||||
"github.com/docker/docker/pkg/transport"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -27,7 +27,7 @@ func spawnTestRegistrySession(t *testing.T) *Session {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var tr http.RoundTripper = debugTransport{NewTransport(ReceiveTimeout, endpoint.IsSecure), t.Log}
|
||||
var tr http.RoundTripper = debugTransport{NewTransport(nil), t.Log}
|
||||
tr = transport.NewTransport(AuthTransport(tr, authConfig, false), DockerHeaders(nil)...)
|
||||
client := HTTPClient(tr)
|
||||
r, err := NewSession(client, authConfig, endpoint)
|
||||
|
@ -332,7 +332,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
|||
expectedRepoInfos := map[string]RepositoryInfo{
|
||||
"fooo/bar": {
|
||||
Index: &IndexInfo{
|
||||
Name: IndexServerName(),
|
||||
Name: INDEXNAME,
|
||||
Official: true,
|
||||
},
|
||||
RemoteName: "fooo/bar",
|
||||
|
@ -342,7 +342,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
|||
},
|
||||
"library/ubuntu": {
|
||||
Index: &IndexInfo{
|
||||
Name: IndexServerName(),
|
||||
Name: INDEXNAME,
|
||||
Official: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu",
|
||||
|
@ -352,7 +352,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
|||
},
|
||||
"nonlibrary/ubuntu": {
|
||||
Index: &IndexInfo{
|
||||
Name: IndexServerName(),
|
||||
Name: INDEXNAME,
|
||||
Official: true,
|
||||
},
|
||||
RemoteName: "nonlibrary/ubuntu",
|
||||
|
@ -362,7 +362,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
|||
},
|
||||
"ubuntu": {
|
||||
Index: &IndexInfo{
|
||||
Name: IndexServerName(),
|
||||
Name: INDEXNAME,
|
||||
Official: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu",
|
||||
|
@ -372,7 +372,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
|||
},
|
||||
"other/library": {
|
||||
Index: &IndexInfo{
|
||||
Name: IndexServerName(),
|
||||
Name: INDEXNAME,
|
||||
Official: true,
|
||||
},
|
||||
RemoteName: "other/library",
|
||||
|
@ -480,9 +480,9 @@ func TestParseRepositoryInfo(t *testing.T) {
|
|||
CanonicalName: "localhost/privatebase",
|
||||
Official: false,
|
||||
},
|
||||
IndexServerName() + "/public/moonbase": {
|
||||
INDEXNAME + "/public/moonbase": {
|
||||
Index: &IndexInfo{
|
||||
Name: IndexServerName(),
|
||||
Name: INDEXNAME,
|
||||
Official: true,
|
||||
},
|
||||
RemoteName: "public/moonbase",
|
||||
|
@ -490,19 +490,9 @@ func TestParseRepositoryInfo(t *testing.T) {
|
|||
CanonicalName: "docker.io/public/moonbase",
|
||||
Official: false,
|
||||
},
|
||||
"index." + IndexServerName() + "/public/moonbase": {
|
||||
"index." + INDEXNAME + "/public/moonbase": {
|
||||
Index: &IndexInfo{
|
||||
Name: IndexServerName(),
|
||||
Official: true,
|
||||
},
|
||||
RemoteName: "public/moonbase",
|
||||
LocalName: "public/moonbase",
|
||||
CanonicalName: "docker.io/public/moonbase",
|
||||
Official: false,
|
||||
},
|
||||
IndexServerName() + "/public/moonbase": {
|
||||
Index: &IndexInfo{
|
||||
Name: IndexServerName(),
|
||||
Name: INDEXNAME,
|
||||
Official: true,
|
||||
},
|
||||
RemoteName: "public/moonbase",
|
||||
|
@ -512,7 +502,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
|||
},
|
||||
"ubuntu-12.04-base": {
|
||||
Index: &IndexInfo{
|
||||
Name: IndexServerName(),
|
||||
Name: INDEXNAME,
|
||||
Official: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu-12.04-base",
|
||||
|
@ -520,9 +510,9 @@ func TestParseRepositoryInfo(t *testing.T) {
|
|||
CanonicalName: "docker.io/library/ubuntu-12.04-base",
|
||||
Official: true,
|
||||
},
|
||||
IndexServerName() + "/ubuntu-12.04-base": {
|
||||
INDEXNAME + "/ubuntu-12.04-base": {
|
||||
Index: &IndexInfo{
|
||||
Name: IndexServerName(),
|
||||
Name: INDEXNAME,
|
||||
Official: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu-12.04-base",
|
||||
|
@ -530,19 +520,9 @@ func TestParseRepositoryInfo(t *testing.T) {
|
|||
CanonicalName: "docker.io/library/ubuntu-12.04-base",
|
||||
Official: true,
|
||||
},
|
||||
IndexServerName() + "/ubuntu-12.04-base": {
|
||||
"index." + INDEXNAME + "/ubuntu-12.04-base": {
|
||||
Index: &IndexInfo{
|
||||
Name: IndexServerName(),
|
||||
Official: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu-12.04-base",
|
||||
LocalName: "ubuntu-12.04-base",
|
||||
CanonicalName: "docker.io/library/ubuntu-12.04-base",
|
||||
Official: true,
|
||||
},
|
||||
"index." + IndexServerName() + "/ubuntu-12.04-base": {
|
||||
Index: &IndexInfo{
|
||||
Name: IndexServerName(),
|
||||
Name: INDEXNAME,
|
||||
Official: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu-12.04-base",
|
||||
|
@ -585,14 +565,14 @@ func TestNewIndexInfo(t *testing.T) {
|
|||
config := NewServiceConfig(nil)
|
||||
noMirrors := make([]string, 0)
|
||||
expectedIndexInfos := map[string]*IndexInfo{
|
||||
IndexServerName(): {
|
||||
Name: IndexServerName(),
|
||||
INDEXNAME: {
|
||||
Name: INDEXNAME,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
Mirrors: noMirrors,
|
||||
},
|
||||
"index." + IndexServerName(): {
|
||||
Name: IndexServerName(),
|
||||
"index." + INDEXNAME: {
|
||||
Name: INDEXNAME,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
Mirrors: noMirrors,
|
||||
|
@ -616,14 +596,14 @@ func TestNewIndexInfo(t *testing.T) {
|
|||
config = makeServiceConfig(publicMirrors, []string{"example.com"})
|
||||
|
||||
expectedIndexInfos = map[string]*IndexInfo{
|
||||
IndexServerName(): {
|
||||
Name: IndexServerName(),
|
||||
INDEXNAME: {
|
||||
Name: INDEXNAME,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
Mirrors: publicMirrors,
|
||||
},
|
||||
"index." + IndexServerName(): {
|
||||
Name: IndexServerName(),
|
||||
"index." + INDEXNAME: {
|
||||
Name: INDEXNAME,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
Mirrors: publicMirrors,
|
||||
|
@ -880,7 +860,7 @@ func TestIsSecureIndex(t *testing.T) {
|
|||
insecureRegistries []string
|
||||
expected bool
|
||||
}{
|
||||
{IndexServerName(), nil, true},
|
||||
{INDEXNAME, nil, true},
|
||||
{"example.com", []string{}, true},
|
||||
{"example.com", []string{"example.com"}, false},
|
||||
{"localhost", []string{"localhost:5000"}, false},
|
||||
|
|
|
@ -1,9 +1,19 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/registry/client/auth"
|
||||
"github.com/docker/docker/cliconfig"
|
||||
"github.com/docker/docker/pkg/tlsconfig"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
|
@ -25,7 +35,7 @@ func (s *Service) Auth(authConfig *cliconfig.AuthConfig) (string, error) {
|
|||
addr := authConfig.ServerAddress
|
||||
if addr == "" {
|
||||
// Use the official registry address if not specified.
|
||||
addr = IndexServerAddress()
|
||||
addr = INDEXSERVER
|
||||
}
|
||||
index, err := s.ResolveIndex(addr)
|
||||
if err != nil {
|
||||
|
@ -69,3 +79,186 @@ func (s *Service) ResolveRepository(name string) (*RepositoryInfo, error) {
|
|||
func (s *Service) ResolveIndex(name string) (*IndexInfo, error) {
|
||||
return s.Config.NewIndexInfo(name)
|
||||
}
|
||||
|
||||
type APIEndpoint struct {
|
||||
Mirror bool
|
||||
URL string
|
||||
Version APIVersion
|
||||
Official bool
|
||||
TrimHostname bool
|
||||
TLSConfig *tls.Config
|
||||
VersionHeader string
|
||||
Versions []auth.APIVersion
|
||||
}
|
||||
|
||||
func (e APIEndpoint) ToV1Endpoint(metaHeaders http.Header) (*Endpoint, error) {
|
||||
return newEndpoint(e.URL, e.TLSConfig, metaHeaders)
|
||||
}
|
||||
|
||||
func (s *Service) TlsConfig(hostname string) (*tls.Config, error) {
|
||||
// we construct a client tls config from server defaults
|
||||
// PreferredServerCipherSuites should have no effect
|
||||
tlsConfig := tlsconfig.ServerDefault
|
||||
|
||||
isSecure := s.Config.isSecureIndex(hostname)
|
||||
|
||||
tlsConfig.InsecureSkipVerify = !isSecure
|
||||
|
||||
if isSecure {
|
||||
hasFile := func(files []os.FileInfo, name string) bool {
|
||||
for _, f := range files {
|
||||
if f.Name() == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
hostDir := filepath.Join(CERTS_DIR, hostname)
|
||||
logrus.Debugf("hostDir: %s", hostDir)
|
||||
fs, err := ioutil.ReadDir(hostDir)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, f := range fs {
|
||||
if strings.HasSuffix(f.Name(), ".crt") {
|
||||
if tlsConfig.RootCAs == nil {
|
||||
// TODO(dmcgowan): Copy system pool
|
||||
tlsConfig.RootCAs = x509.NewCertPool()
|
||||
}
|
||||
logrus.Debugf("crt: %s", filepath.Join(hostDir, f.Name()))
|
||||
data, err := ioutil.ReadFile(filepath.Join(hostDir, f.Name()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig.RootCAs.AppendCertsFromPEM(data)
|
||||
}
|
||||
if strings.HasSuffix(f.Name(), ".cert") {
|
||||
certName := f.Name()
|
||||
keyName := certName[:len(certName)-5] + ".key"
|
||||
logrus.Debugf("cert: %s", filepath.Join(hostDir, f.Name()))
|
||||
if !hasFile(fs, keyName) {
|
||||
return nil, fmt.Errorf("Missing key %s for certificate %s", keyName, certName)
|
||||
}
|
||||
cert, err := tls.LoadX509KeyPair(filepath.Join(hostDir, certName), filepath.Join(hostDir, keyName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig.Certificates = append(tlsConfig.Certificates, cert)
|
||||
}
|
||||
if strings.HasSuffix(f.Name(), ".key") {
|
||||
keyName := f.Name()
|
||||
certName := keyName[:len(keyName)-4] + ".cert"
|
||||
logrus.Debugf("key: %s", filepath.Join(hostDir, f.Name()))
|
||||
if !hasFile(fs, certName) {
|
||||
return nil, fmt.Errorf("Missing certificate %s for key %s", certName, keyName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &tlsConfig, nil
|
||||
}
|
||||
|
||||
func (s *Service) LookupEndpoints(repoName string) (endpoints []APIEndpoint, err error) {
|
||||
var cfg = tlsconfig.ServerDefault
|
||||
tlsConfig := &cfg
|
||||
if strings.HasPrefix(repoName, DEFAULT_NAMESPACE+"/") {
|
||||
// v2 mirrors
|
||||
for _, mirror := range s.Config.Mirrors {
|
||||
endpoints = append(endpoints, APIEndpoint{
|
||||
URL: mirror,
|
||||
// guess mirrors are v2
|
||||
Version: APIVersion2,
|
||||
Mirror: true,
|
||||
TrimHostname: true,
|
||||
TLSConfig: tlsConfig,
|
||||
})
|
||||
}
|
||||
// v2 registry
|
||||
endpoints = append(endpoints, APIEndpoint{
|
||||
URL: DEFAULT_V2_REGISTRY,
|
||||
Version: APIVersion2,
|
||||
Official: true,
|
||||
TrimHostname: true,
|
||||
TLSConfig: tlsConfig,
|
||||
})
|
||||
// v1 mirrors
|
||||
// TODO(tiborvass): shouldn't we remove v1 mirrors from here, since v1 mirrors are kinda special?
|
||||
for _, mirror := range s.Config.Mirrors {
|
||||
endpoints = append(endpoints, APIEndpoint{
|
||||
URL: mirror,
|
||||
// guess mirrors are v1
|
||||
Version: APIVersion1,
|
||||
Mirror: true,
|
||||
TrimHostname: true,
|
||||
TLSConfig: tlsConfig,
|
||||
})
|
||||
}
|
||||
// v1 registry
|
||||
endpoints = append(endpoints, APIEndpoint{
|
||||
URL: DEFAULT_V1_REGISTRY,
|
||||
Version: APIVersion1,
|
||||
Official: true,
|
||||
TrimHostname: true,
|
||||
TLSConfig: tlsConfig,
|
||||
})
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
slashIndex := strings.IndexRune(repoName, '/')
|
||||
if slashIndex <= 0 {
|
||||
return nil, fmt.Errorf("invalid repo name: missing '/': %s", repoName)
|
||||
}
|
||||
hostname := repoName[:slashIndex]
|
||||
|
||||
tlsConfig, err = s.TlsConfig(hostname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
isSecure := !tlsConfig.InsecureSkipVerify
|
||||
|
||||
v2Versions := []auth.APIVersion{
|
||||
{
|
||||
Type: "registry",
|
||||
Version: "2.0",
|
||||
},
|
||||
}
|
||||
endpoints = []APIEndpoint{
|
||||
{
|
||||
URL: "https://" + hostname,
|
||||
Version: APIVersion2,
|
||||
TrimHostname: true,
|
||||
TLSConfig: tlsConfig,
|
||||
VersionHeader: DEFAULT_REGISTRY_VERSION_HEADER,
|
||||
Versions: v2Versions,
|
||||
},
|
||||
{
|
||||
URL: "https://" + hostname,
|
||||
Version: APIVersion1,
|
||||
TrimHostname: true,
|
||||
TLSConfig: tlsConfig,
|
||||
},
|
||||
}
|
||||
|
||||
if !isSecure {
|
||||
endpoints = append(endpoints, APIEndpoint{
|
||||
URL: "http://" + hostname,
|
||||
Version: APIVersion2,
|
||||
TrimHostname: true,
|
||||
// used to check if supposed to be secure via InsecureSkipVerify
|
||||
TLSConfig: tlsConfig,
|
||||
VersionHeader: DEFAULT_REGISTRY_VERSION_HEADER,
|
||||
Versions: v2Versions,
|
||||
}, APIEndpoint{
|
||||
URL: "http://" + hostname,
|
||||
Version: APIVersion1,
|
||||
TrimHostname: true,
|
||||
// used to check if supposed to be secure via InsecureSkipVerify
|
||||
TLSConfig: tlsConfig,
|
||||
})
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
|
|
@ -22,8 +22,8 @@ import (
|
|||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/docker/cliconfig"
|
||||
"github.com/docker/docker/pkg/httputils"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/docker/pkg/tarsum"
|
||||
"github.com/docker/docker/pkg/transport"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -73,6 +73,21 @@ func AuthTransport(base http.RoundTripper, authConfig *cliconfig.AuthConfig, alw
|
|||
}
|
||||
}
|
||||
|
||||
// cloneRequest returns a clone of the provided *http.Request.
|
||||
// The clone is a shallow copy of the struct and its Header map.
|
||||
func cloneRequest(r *http.Request) *http.Request {
|
||||
// shallow copy of the struct
|
||||
r2 := new(http.Request)
|
||||
*r2 = *r
|
||||
// deep copy of the Header
|
||||
r2.Header = make(http.Header, len(r.Header))
|
||||
for k, s := range r.Header {
|
||||
r2.Header[k] = append([]string(nil), s...)
|
||||
}
|
||||
|
||||
return r2
|
||||
}
|
||||
|
||||
func (tr *authTransport) RoundTrip(orig *http.Request) (*http.Response, error) {
|
||||
// Authorization should not be set on 302 redirect for untrusted locations.
|
||||
// This logic mirrors the behavior in AddRequiredHeadersToRedirectedRequests.
|
||||
|
@ -83,7 +98,7 @@ func (tr *authTransport) RoundTrip(orig *http.Request) (*http.Response, error) {
|
|||
return tr.RoundTripper.RoundTrip(orig)
|
||||
}
|
||||
|
||||
req := transport.CloneRequest(orig)
|
||||
req := cloneRequest(orig)
|
||||
tr.mu.Lock()
|
||||
tr.modReq[orig] = req
|
||||
tr.mu.Unlock()
|
||||
|
@ -112,7 +127,7 @@ func (tr *authTransport) RoundTrip(orig *http.Request) (*http.Response, error) {
|
|||
if len(resp.Header["X-Docker-Token"]) > 0 {
|
||||
tr.token = resp.Header["X-Docker-Token"]
|
||||
}
|
||||
resp.Body = &transport.OnEOFReader{
|
||||
resp.Body = &ioutils.OnEOFReader{
|
||||
Rc: resp.Body,
|
||||
Fn: func() {
|
||||
tr.mu.Lock()
|
||||
|
@ -149,12 +164,11 @@ func NewSession(client *http.Client, authConfig *cliconfig.AuthConfig, endpoint
|
|||
|
||||
// If we're working with a standalone private registry over HTTPS, send Basic Auth headers
|
||||
// alongside all our requests.
|
||||
if endpoint.VersionString(1) != IndexServerAddress() && endpoint.URL.Scheme == "https" {
|
||||
if endpoint.VersionString(1) != INDEXSERVER && endpoint.URL.Scheme == "https" {
|
||||
info, err := endpoint.Ping()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if info.Standalone && authConfig != nil {
|
||||
logrus.Debugf("Endpoint %s is eligible for private registry. Enabling decorator.", endpoint.String())
|
||||
alwaysSetBasicAuth = true
|
||||
|
@ -250,7 +264,7 @@ func (r *Session) GetRemoteImageLayer(imgID, registry string, imgSize int64) (io
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("Error while getting from the server: %v", err)
|
||||
}
|
||||
// TODO: why are we doing retries at this level?
|
||||
// TODO(tiborvass): why are we doing retries at this level?
|
||||
// These retries should be generic to both v1 and v2
|
||||
for i := 1; i <= retries; i++ {
|
||||
statusCode = 0
|
||||
|
@ -417,7 +431,7 @@ func (r *Session) GetRepositoryData(remote string) (*RepositoryData, error) {
|
|||
}
|
||||
|
||||
// Forge a better object from the retrieved data
|
||||
imgsData := make(map[string]*ImgData)
|
||||
imgsData := make(map[string]*ImgData, len(remoteChecksums))
|
||||
for _, elem := range remoteChecksums {
|
||||
imgsData[elem.ID] = elem
|
||||
}
|
||||
|
|
|
@ -1,414 +0,0 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/registry/api/v2"
|
||||
"github.com/docker/docker/pkg/httputils"
|
||||
)
|
||||
|
||||
const DockerDigestHeader = "Docker-Content-Digest"
|
||||
|
||||
func getV2Builder(e *Endpoint) *v2.URLBuilder {
|
||||
if e.URLBuilder == nil {
|
||||
e.URLBuilder = v2.NewURLBuilder(e.URL)
|
||||
}
|
||||
return e.URLBuilder
|
||||
}
|
||||
|
||||
func (r *Session) V2RegistryEndpoint(index *IndexInfo) (ep *Endpoint, err error) {
|
||||
// TODO check if should use Mirror
|
||||
if index.Official {
|
||||
ep, err = newEndpoint(REGISTRYSERVER, true, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = validateEndpoint(ep)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else if r.indexEndpoint.String() == index.GetAuthConfigKey() {
|
||||
ep = r.indexEndpoint
|
||||
} else {
|
||||
ep, err = NewEndpoint(index, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ep.URLBuilder = v2.NewURLBuilder(ep.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// GetV2Authorization gets the authorization needed to the given image
|
||||
// If readonly access is requested, then the authorization may
|
||||
// only be used for Get operations.
|
||||
func (r *Session) GetV2Authorization(ep *Endpoint, imageName string, readOnly bool) (auth *RequestAuthorization, err error) {
|
||||
scopes := []string{"pull"}
|
||||
if !readOnly {
|
||||
scopes = append(scopes, "push")
|
||||
}
|
||||
|
||||
logrus.Debugf("Getting authorization for %s %s", imageName, scopes)
|
||||
return NewRequestAuthorization(r.GetAuthConfig(true), ep, "repository", imageName, scopes), nil
|
||||
}
|
||||
|
||||
//
|
||||
// 1) Check if TarSum of each layer exists /v2/
|
||||
// 1.a) if 200, continue
|
||||
// 1.b) if 300, then push the
|
||||
// 1.c) if anything else, err
|
||||
// 2) PUT the created/signed manifest
|
||||
//
|
||||
|
||||
// GetV2ImageManifest simply fetches the bytes of a manifest and the remote
|
||||
// digest, if available in the request. Note that the application shouldn't
|
||||
// rely on the untrusted remoteDigest, and should also verify against a
|
||||
// locally provided digest, if applicable.
|
||||
func (r *Session) GetV2ImageManifest(ep *Endpoint, imageName, tagName string, auth *RequestAuthorization) (remoteDigest digest.Digest, p []byte, err error) {
|
||||
routeURL, err := getV2Builder(ep).BuildManifestURL(imageName, tagName)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
method := "GET"
|
||||
logrus.Debugf("[registry] Calling %q %s", method, routeURL)
|
||||
|
||||
req, err := http.NewRequest(method, routeURL, nil)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
if err := auth.Authorize(req); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
res, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
if res.StatusCode == 401 {
|
||||
return "", nil, errLoginRequired
|
||||
} else if res.StatusCode == 404 {
|
||||
return "", nil, ErrDoesNotExist
|
||||
}
|
||||
return "", nil, httputils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to fetch for %s:%s", res.StatusCode, imageName, tagName), res)
|
||||
}
|
||||
|
||||
p, err = ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("Error while reading the http response: %s", err)
|
||||
}
|
||||
|
||||
dgstHdr := res.Header.Get(DockerDigestHeader)
|
||||
if dgstHdr != "" {
|
||||
remoteDigest, err = digest.ParseDigest(dgstHdr)
|
||||
if err != nil {
|
||||
// NOTE(stevvooe): Including the remote digest is optional. We
|
||||
// don't need to verify against it, but it is good practice.
|
||||
remoteDigest = ""
|
||||
logrus.Debugf("error parsing remote digest when fetching %v: %v", routeURL, err)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// - Succeeded to head image blob (already exists)
|
||||
// - Failed with no error (continue to Push the Blob)
|
||||
// - Failed with error
|
||||
func (r *Session) HeadV2ImageBlob(ep *Endpoint, imageName string, dgst digest.Digest, auth *RequestAuthorization) (bool, error) {
|
||||
routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, dgst)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
method := "HEAD"
|
||||
logrus.Debugf("[registry] Calling %q %s", method, routeURL)
|
||||
|
||||
req, err := http.NewRequest(method, routeURL, nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := auth.Authorize(req); err != nil {
|
||||
return false, err
|
||||
}
|
||||
res, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
res.Body.Close() // close early, since we're not needing a body on this call .. yet?
|
||||
switch {
|
||||
case res.StatusCode >= 200 && res.StatusCode < 400:
|
||||
// return something indicating no push needed
|
||||
return true, nil
|
||||
case res.StatusCode == 401:
|
||||
return false, errLoginRequired
|
||||
case res.StatusCode == 404:
|
||||
// return something indicating blob push needed
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, httputils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying head request for %s - %s", res.StatusCode, imageName, dgst), res)
|
||||
}
|
||||
|
||||
func (r *Session) GetV2ImageBlob(ep *Endpoint, imageName string, dgst digest.Digest, blobWrtr io.Writer, auth *RequestAuthorization) error {
|
||||
routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, dgst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
method := "GET"
|
||||
logrus.Debugf("[registry] Calling %q %s", method, routeURL)
|
||||
req, err := http.NewRequest(method, routeURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := auth.Authorize(req); err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
if res.StatusCode == 401 {
|
||||
return errLoginRequired
|
||||
}
|
||||
return httputils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to pull %s blob", res.StatusCode, imageName), res)
|
||||
}
|
||||
|
||||
_, err = io.Copy(blobWrtr, res.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Session) GetV2ImageBlobReader(ep *Endpoint, imageName string, dgst digest.Digest, auth *RequestAuthorization) (io.ReadCloser, int64, error) {
|
||||
routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, dgst)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
method := "GET"
|
||||
logrus.Debugf("[registry] Calling %q %s", method, routeURL)
|
||||
req, err := http.NewRequest(method, routeURL, nil)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err := auth.Authorize(req); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
res, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
if res.StatusCode == 401 {
|
||||
return nil, 0, errLoginRequired
|
||||
}
|
||||
return nil, 0, httputils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to pull %s blob - %s", res.StatusCode, imageName, dgst), res)
|
||||
}
|
||||
lenStr := res.Header.Get("Content-Length")
|
||||
l, err := strconv.ParseInt(lenStr, 10, 64)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return res.Body, l, err
|
||||
}
|
||||
|
||||
// Push the image to the server for storage.
|
||||
// 'layer' is an uncompressed reader of the blob to be pushed.
|
||||
// The server will generate it's own checksum calculation.
|
||||
func (r *Session) PutV2ImageBlob(ep *Endpoint, imageName string, dgst digest.Digest, blobRdr io.Reader, auth *RequestAuthorization) error {
|
||||
location, err := r.initiateBlobUpload(ep, imageName, auth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
method := "PUT"
|
||||
logrus.Debugf("[registry] Calling %q %s", method, location)
|
||||
req, err := http.NewRequest(method, location, ioutil.NopCloser(blobRdr))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
queryParams := req.URL.Query()
|
||||
queryParams.Add("digest", dgst.String())
|
||||
req.URL.RawQuery = queryParams.Encode()
|
||||
if err := auth.Authorize(req); err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != 201 {
|
||||
if res.StatusCode == 401 {
|
||||
return errLoginRequired
|
||||
}
|
||||
errBody, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logrus.Debugf("Unexpected response from server: %q %#v", errBody, res.Header)
|
||||
return httputils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to push %s blob - %s", res.StatusCode, imageName, dgst), res)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// initiateBlobUpload gets the blob upload location for the given image name.
|
||||
func (r *Session) initiateBlobUpload(ep *Endpoint, imageName string, auth *RequestAuthorization) (location string, err error) {
|
||||
routeURL, err := getV2Builder(ep).BuildBlobUploadURL(imageName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
logrus.Debugf("[registry] Calling %q %s", "POST", routeURL)
|
||||
req, err := http.NewRequest("POST", routeURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := auth.Authorize(req); err != nil {
|
||||
return "", err
|
||||
}
|
||||
res, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusAccepted {
|
||||
if res.StatusCode == http.StatusUnauthorized {
|
||||
return "", errLoginRequired
|
||||
}
|
||||
if res.StatusCode == http.StatusNotFound {
|
||||
return "", ErrDoesNotExist
|
||||
}
|
||||
|
||||
errBody, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
logrus.Debugf("Unexpected response from server: %q %#v", errBody, res.Header)
|
||||
return "", httputils.NewHTTPRequestError(fmt.Sprintf("Server error: unexpected %d response status trying to initiate upload of %s", res.StatusCode, imageName), res)
|
||||
}
|
||||
|
||||
if location = res.Header.Get("Location"); location == "" {
|
||||
return "", fmt.Errorf("registry did not return a Location header for resumable blob upload for image %s", imageName)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Finally Push the (signed) manifest of the blobs we've just pushed
|
||||
func (r *Session) PutV2ImageManifest(ep *Endpoint, imageName, tagName string, signedManifest, rawManifest []byte, auth *RequestAuthorization) (digest.Digest, error) {
|
||||
routeURL, err := getV2Builder(ep).BuildManifestURL(imageName, tagName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
method := "PUT"
|
||||
logrus.Debugf("[registry] Calling %q %s", method, routeURL)
|
||||
req, err := http.NewRequest(method, routeURL, bytes.NewReader(signedManifest))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := auth.Authorize(req); err != nil {
|
||||
return "", err
|
||||
}
|
||||
res, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
// All 2xx and 3xx responses can be accepted for a put.
|
||||
if res.StatusCode >= 400 {
|
||||
if res.StatusCode == 401 {
|
||||
return "", errLoginRequired
|
||||
}
|
||||
errBody, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
logrus.Debugf("Unexpected response from server: %q %#v", errBody, res.Header)
|
||||
return "", httputils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to push %s:%s manifest", res.StatusCode, imageName, tagName), res)
|
||||
}
|
||||
|
||||
hdrDigest, err := digest.ParseDigest(res.Header.Get(DockerDigestHeader))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid manifest digest from registry: %s", err)
|
||||
}
|
||||
|
||||
dgstVerifier, err := digest.NewDigestVerifier(hdrDigest)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid manifest digest from registry: %s", err)
|
||||
}
|
||||
|
||||
dgstVerifier.Write(rawManifest)
|
||||
|
||||
if !dgstVerifier.Verified() {
|
||||
computedDigest, _ := digest.FromBytes(rawManifest)
|
||||
return "", fmt.Errorf("unable to verify manifest digest: registry has %q, computed %q", hdrDigest, computedDigest)
|
||||
}
|
||||
|
||||
return hdrDigest, nil
|
||||
}
|
||||
|
||||
type remoteTags struct {
|
||||
Name string `json:"name"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
// Given a repository name, returns a json array of string tags
|
||||
func (r *Session) GetV2RemoteTags(ep *Endpoint, imageName string, auth *RequestAuthorization) ([]string, error) {
|
||||
routeURL, err := getV2Builder(ep).BuildTagsURL(imageName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
method := "GET"
|
||||
logrus.Debugf("[registry] Calling %q %s", method, routeURL)
|
||||
|
||||
req, err := http.NewRequest(method, routeURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := auth.Authorize(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
if res.StatusCode == 401 {
|
||||
return nil, errLoginRequired
|
||||
} else if res.StatusCode == 404 {
|
||||
return nil, ErrDoesNotExist
|
||||
}
|
||||
return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to fetch for %s", res.StatusCode, imageName), res)
|
||||
}
|
||||
|
||||
var remote remoteTags
|
||||
if err := json.NewDecoder(res.Body).Decode(&remote); err != nil {
|
||||
return nil, fmt.Errorf("Error while decoding the http response: %s", err)
|
||||
}
|
||||
return remote.Tags, nil
|
||||
}
|
38
vendor/src/github.com/docker/distribution/.drone.yml
vendored
Normal file
38
vendor/src/github.com/docker/distribution/.drone.yml
vendored
Normal file
|
@ -0,0 +1,38 @@
|
|||
image: dmp42/go:stable
|
||||
|
||||
script:
|
||||
# To be spoofed back into the test image
|
||||
- go get github.com/modocache/gover
|
||||
|
||||
- go get -t ./...
|
||||
|
||||
# Go fmt
|
||||
- test -z "$(gofmt -s -l -w . | tee /dev/stderr)"
|
||||
# Go lint
|
||||
- test -z "$(golint ./... | tee /dev/stderr)"
|
||||
# Go vet
|
||||
- go vet ./...
|
||||
# Go test
|
||||
- go test -v -race -cover ./...
|
||||
# Helper to concatenate reports
|
||||
- gover
|
||||
# Send to coverall
|
||||
- goveralls -service drone.io -coverprofile=gover.coverprofile -repotoken {{COVERALLS_TOKEN}}
|
||||
|
||||
# Do we want these as well?
|
||||
# - go get code.google.com/p/go.tools/cmd/goimports
|
||||
# - test -z "$(goimports -l -w ./... | tee /dev/stderr)"
|
||||
# http://labix.org/gocheck
|
||||
|
||||
notify:
|
||||
email:
|
||||
recipients:
|
||||
- distribution@docker.com
|
||||
|
||||
slack:
|
||||
team: docker
|
||||
channel: "#dt"
|
||||
username: mom
|
||||
token: {{SLACK_TOKEN}}
|
||||
on_success: true
|
||||
on_failure: true
|
37
vendor/src/github.com/docker/distribution/.gitignore
vendored
Normal file
37
vendor/src/github.com/docker/distribution/.gitignore
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
# never checkin from the bin file (for now)
|
||||
bin/*
|
||||
|
||||
# Test key files
|
||||
*.pem
|
||||
|
||||
# Cover profiles
|
||||
*.out
|
||||
|
||||
# Editor/IDE specific files.
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
6
vendor/src/github.com/docker/distribution/.mailmap
vendored
Normal file
6
vendor/src/github.com/docker/distribution/.mailmap
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
Stephen J Day <stephen.day@docker.com> Stephen Day <stevvooe@users.noreply.github.com>
|
||||
Stephen J Day <stephen.day@docker.com> Stephen Day <stevvooe@gmail.com>
|
||||
Olivier Gambier <olivier@docker.com> Olivier Gambier <dmp42@users.noreply.github.com>
|
||||
Brian Bland <brian.bland@docker.com> Brian Bland <r4nd0m1n4t0r@gmail.com>
|
||||
Josh Hawn <josh.hawn@docker.com> Josh Hawn <jlhawn@berkeley.edu>
|
||||
Richard Scothern <richard.scothern@docker.com> Richard <richard.scothern@gmail.com>
|
61
vendor/src/github.com/docker/distribution/AUTHORS
vendored
Normal file
61
vendor/src/github.com/docker/distribution/AUTHORS
vendored
Normal file
|
@ -0,0 +1,61 @@
|
|||
Adam Enger <adamenger@gmail.com>
|
||||
Adrian Mouat <adrian.mouat@gmail.com>
|
||||
Ahmet Alp Balkan <ahmetalpbalkan@gmail.com>
|
||||
Alex Elman <aelman@indeed.com>
|
||||
Amy Lindburg <amy.lindburg@docker.com>
|
||||
Andrey Kostov <kostov.andrey@gmail.com>
|
||||
Andy Goldstein <agoldste@redhat.com>
|
||||
Anton Tiurin <noxiouz@yandex.ru>
|
||||
Antonio Mercado <amercado@thinknode.com>
|
||||
Arnaud Porterie <arnaud.porterie@docker.com>
|
||||
BadZen <dave.trombley@gmail.com>
|
||||
Ben Firshman <ben@firshman.co.uk>
|
||||
bin liu <liubin0329@gmail.com>
|
||||
Brian Bland <brian.bland@docker.com>
|
||||
burnettk <burnettk@gmail.com>
|
||||
Daisuke Fujita <dtanshi45@gmail.com>
|
||||
Dave Trombley <dave.trombley@gmail.com>
|
||||
David Lawrence <david.lawrence@docker.com>
|
||||
David Xia <dxia@spotify.com>
|
||||
Derek McGowan <derek@mcgstyle.net>
|
||||
Diogo Mónica <diogo.monica@gmail.com>
|
||||
Donald Huang <don.hcd@gmail.com>
|
||||
Doug Davis <dug@us.ibm.com>
|
||||
Frederick F. Kautz IV <fkautz@alumni.cmu.edu>
|
||||
Henri Gomez <henri.gomez@gmail.com>
|
||||
Hu Keping <hukeping@huawei.com>
|
||||
Ian Babrou <ibobrik@gmail.com>
|
||||
Jeff Nickoloff <jeff@allingeek.com>
|
||||
Jessie Frazelle <jfrazelle@users.noreply.github.com>
|
||||
Jordan Liggitt <jliggitt@redhat.com>
|
||||
Josh Hawn <josh.hawn@docker.com>
|
||||
Julien Fernandez <julien.fernandez@gmail.com>
|
||||
Kelsey Hightower <kelsey.hightower@gmail.com>
|
||||
Kenneth Lim <kennethlimcp@gmail.com>
|
||||
Mary Anthony <mary@docker.com>
|
||||
Matt Robenolt <matt@ydekproductions.com>
|
||||
Michael Prokop <mika@grml.org>
|
||||
moxiegirl <mary@docker.com>
|
||||
Nathan Sullivan <nathan@nightsys.net>
|
||||
Nghia Tran <tcnghia@gmail.com>
|
||||
Oilbeater <liumengxinfly@gmail.com>
|
||||
Olivier Gambier <olivier@docker.com>
|
||||
Philip Misiowiec <philip@atlashealth.com>
|
||||
Richard Scothern <richard.scothern@docker.com>
|
||||
Richard Scothern <richard.scothern@gmail.com>
|
||||
Sebastiaan van Stijn <github@gone.nl>
|
||||
Shawn Falkner-Horine <dreadpirateshawn@gmail.com>
|
||||
Shreyas Karnik <karnik.shreyas@gmail.com>
|
||||
Simon Thulbourn <simon+github@thulbourn.com>
|
||||
Spencer Rinehart <anubis@overthemonkey.com>
|
||||
Stephen J Day <stephen.day@docker.com>
|
||||
Thomas Sjögren <konstruktoid@users.noreply.github.com>
|
||||
Tianon Gravi <admwiggin@gmail.com>
|
||||
Tibor Vass <teabee89@gmail.com>
|
||||
Vincent Batts <vbatts@redhat.com>
|
||||
Vincent Demeester <vincent@sbr.pm>
|
||||
Vincent Giersch <vincent.giersch@ovh.net>
|
||||
W. Trevor King <wking@tremily.us>
|
||||
xiekeyang <xiekeyang@huawei.com>
|
||||
Yann ROBERT <yann.robert@anantaplex.fr>
|
||||
yuzou <zouyu7@huawei.com>
|
92
vendor/src/github.com/docker/distribution/CONTRIBUTING.md
vendored
Normal file
92
vendor/src/github.com/docker/distribution/CONTRIBUTING.md
vendored
Normal file
|
@ -0,0 +1,92 @@
|
|||
# Contributing to the registry
|
||||
|
||||
## Before reporting an issue...
|
||||
|
||||
### If your problem is with...
|
||||
|
||||
- automated builds
|
||||
- your account on the [Docker Hub](https://hub.docker.com/)
|
||||
- any other [Docker Hub](https://hub.docker.com/) issue
|
||||
|
||||
Then please do not report your issue here - you should instead report it to [https://support.docker.com](https://support.docker.com)
|
||||
|
||||
### If you...
|
||||
|
||||
- need help setting up your registry
|
||||
- can't figure out something
|
||||
- are not sure what's going on or what your problem is
|
||||
|
||||
Then please do not open an issue here yet - you should first try one of the following support forums:
|
||||
|
||||
- irc: #docker-distribution on freenode
|
||||
- mailing-list: <distribution@dockerproject.org> or https://groups.google.com/a/dockerproject.org/forum/#!forum/distribution
|
||||
|
||||
## Reporting an issue properly
|
||||
|
||||
By following these simple rules you will get better and faster feedback on your issue.
|
||||
|
||||
- search the bugtracker for an already reported issue
|
||||
|
||||
### If you found an issue that describes your problem:
|
||||
|
||||
- please read other user comments first, and confirm this is the same issue: a given error condition might be indicative of different problems - you may also find a workaround in the comments
|
||||
- please refrain from adding "same thing here" or "+1" comments
|
||||
- you don't need to comment on an issue to get notified of updates: just hit the "subscribe" button
|
||||
- comment if you have some new, technical and relevant information to add to the case
|
||||
|
||||
### If you have not found an existing issue that describes your problem:
|
||||
|
||||
1. create a new issue, with a succinct title that describes your issue:
|
||||
- bad title: "It doesn't work with my docker"
|
||||
- good title: "Private registry push fail: 400 error with E_INVALID_DIGEST"
|
||||
2. copy the output of:
|
||||
- `docker version`
|
||||
- `docker info`
|
||||
- `docker exec <registry-container> registry -version`
|
||||
3. copy the command line you used to launch your Registry
|
||||
4. restart your docker daemon in debug mode (add `-D` to the daemon launch arguments)
|
||||
5. reproduce your problem and get your docker daemon logs showing the error
|
||||
6. if relevant, copy your registry logs that show the error
|
||||
7. provide any relevant detail about your specific Registry configuration (e.g., storage backend used)
|
||||
8. indicate if you are using an enterprise proxy, Nginx, or anything else between you and your Registry
|
||||
|
||||
## Contributing a patch for a known bug, or a small correction
|
||||
|
||||
You should follow the basic GitHub workflow:
|
||||
|
||||
1. fork
|
||||
2. commit a change
|
||||
3. make sure the tests pass
|
||||
4. PR
|
||||
|
||||
Additionally, you must [sign your commits](https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work). It's very simple:
|
||||
|
||||
- configure your name with git: `git config user.name "Real Name" && git config user.email mail@example.com`
|
||||
- sign your commits using `-s`: `git commit -s -m "My commit"`
|
||||
|
||||
Some simple rules to ensure quick merge:
|
||||
|
||||
- clearly point to the issue(s) you want to fix in your PR comment (e.g., `closes #12345`)
|
||||
- prefer multiple (smaller) PRs addressing individual issues over a big one trying to address multiple issues at once
|
||||
- if you need to amend your PR following comments, please squash instead of adding more commits
|
||||
|
||||
## Contributing new features
|
||||
|
||||
You are heavily encouraged to first discuss what you want to do. You can do so on the irc channel, or by opening an issue that clearly describes the use case you want to fulfill, or the problem you are trying to solve.
|
||||
|
||||
If this is a major new feature, you should then submit a proposal that describes your technical solution and reasoning.
|
||||
If you did discuss it first, this will likely be greenlighted very fast. It's advisable to address all feedback on this proposal before starting actual work.
|
||||
|
||||
Then you should submit your implementation, clearly linking to the issue (and possible proposal).
|
||||
|
||||
Your PR will be reviewed by the community, then ultimately by the project maintainers, before being merged.
|
||||
|
||||
It's mandatory to:
|
||||
|
||||
- interact respectfully with other community members and maintainers - more generally, you are expected to abide by the [Docker community rules](https://github.com/docker/docker/blob/master/CONTRIBUTING.md#docker-community-guidelines)
|
||||
- address maintainers' comments and modify your submission accordingly
|
||||
- write tests for any new code
|
||||
|
||||
Complying to these simple rules will greatly accelerate the review process, and will ensure you have a pleasant experience in contributing code to the Registry.
|
||||
|
||||
Have a look at a great, succesful contribution: the [Ceph driver PR](https://github.com/docker/distribution/pull/443)
|
18
vendor/src/github.com/docker/distribution/Dockerfile
vendored
Normal file
18
vendor/src/github.com/docker/distribution/Dockerfile
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
FROM golang:1.4
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y librados-dev apache2-utils && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV DISTRIBUTION_DIR /go/src/github.com/docker/distribution
|
||||
ENV GOPATH $DISTRIBUTION_DIR/Godeps/_workspace:$GOPATH
|
||||
ENV DOCKER_BUILDTAGS include_rados
|
||||
|
||||
WORKDIR $DISTRIBUTION_DIR
|
||||
COPY . $DISTRIBUTION_DIR
|
||||
RUN make PREFIX=/go clean binaries
|
||||
|
||||
VOLUME ["/var/lib/registry"]
|
||||
EXPOSE 5000
|
||||
ENTRYPOINT ["registry"]
|
||||
CMD ["cmd/registry/config.yml"]
|
202
vendor/src/github.com/docker/distribution/LICENSE
vendored
Normal file
202
vendor/src/github.com/docker/distribution/LICENSE
vendored
Normal file
|
@ -0,0 +1,202 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
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.
|
||||
|
4
vendor/src/github.com/docker/distribution/MAINTAINERS
vendored
Normal file
4
vendor/src/github.com/docker/distribution/MAINTAINERS
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
Solomon Hykes <solomon@docker.com> (@shykes)
|
||||
Olivier Gambier <olivier@docker.com> (@dmp42)
|
||||
Sam Alba <sam@docker.com> (@samalba)
|
||||
Stephen Day <stephen.day@docker.com> (@stevvooe)
|
74
vendor/src/github.com/docker/distribution/Makefile
vendored
Normal file
74
vendor/src/github.com/docker/distribution/Makefile
vendored
Normal file
|
@ -0,0 +1,74 @@
|
|||
# Set an output prefix, which is the local directory if not specified
|
||||
PREFIX?=$(shell pwd)
|
||||
|
||||
|
||||
# Used to populate version variable in main package.
|
||||
VERSION=$(shell git describe --match 'v[0-9]*' --dirty='.m' --always)
|
||||
|
||||
# Allow turning off function inlining and variable registerization
|
||||
ifeq (${DISABLE_OPTIMIZATION},true)
|
||||
GO_GCFLAGS=-gcflags "-N -l"
|
||||
VERSION:="$(VERSION)-noopt"
|
||||
endif
|
||||
|
||||
GO_LDFLAGS=-ldflags "-X `go list ./version`.Version $(VERSION)"
|
||||
|
||||
.PHONY: clean all fmt vet lint build test binaries
|
||||
.DEFAULT: default
|
||||
all: AUTHORS clean fmt vet fmt lint build test binaries
|
||||
|
||||
AUTHORS: .mailmap .git/HEAD
|
||||
git log --format='%aN <%aE>' | sort -fu > $@
|
||||
|
||||
# This only needs to be generated by hand when cutting full releases.
|
||||
version/version.go:
|
||||
./version/version.sh > $@
|
||||
|
||||
${PREFIX}/bin/registry: version/version.go $(shell find . -type f -name '*.go')
|
||||
@echo "+ $@"
|
||||
@go build -tags "${DOCKER_BUILDTAGS}" -o $@ ${GO_LDFLAGS} ${GO_GCFLAGS} ./cmd/registry
|
||||
|
||||
${PREFIX}/bin/registry-api-descriptor-template: version/version.go $(shell find . -type f -name '*.go')
|
||||
@echo "+ $@"
|
||||
@go build -o $@ ${GO_LDFLAGS} ${GO_GCFLAGS} ./cmd/registry-api-descriptor-template
|
||||
|
||||
${PREFIX}/bin/dist: version/version.go $(shell find . -type f -name '*.go')
|
||||
@echo "+ $@"
|
||||
@go build -o $@ ${GO_LDFLAGS} ${GO_GCFLAGS} ./cmd/dist
|
||||
|
||||
docs/spec/api.md: docs/spec/api.md.tmpl ${PREFIX}/bin/registry-api-descriptor-template
|
||||
./bin/registry-api-descriptor-template $< > $@
|
||||
|
||||
vet:
|
||||
@echo "+ $@"
|
||||
@go vet ./...
|
||||
|
||||
fmt:
|
||||
@echo "+ $@"
|
||||
@test -z "$$(gofmt -s -l . | grep -v Godeps/_workspace/src/ | tee /dev/stderr)" || \
|
||||
echo "+ please format Go code with 'gofmt -s'"
|
||||
|
||||
lint:
|
||||
@echo "+ $@"
|
||||
@test -z "$$(golint ./... | grep -v Godeps/_workspace/src/ | tee /dev/stderr)"
|
||||
|
||||
build:
|
||||
@echo "+ $@"
|
||||
@go build -tags "${DOCKER_BUILDTAGS}" -v ${GO_LDFLAGS} ./...
|
||||
|
||||
test:
|
||||
@echo "+ $@"
|
||||
@go test -test.short -tags "${DOCKER_BUILDTAGS}" ./...
|
||||
|
||||
test-full:
|
||||
@echo "+ $@"
|
||||
@go test ./...
|
||||
|
||||
binaries: ${PREFIX}/bin/registry ${PREFIX}/bin/registry-api-descriptor-template ${PREFIX}/bin/dist
|
||||
@echo "+ $@"
|
||||
|
||||
clean:
|
||||
@echo "+ $@"
|
||||
@rm -rf "${PREFIX}/bin/registry" "${PREFIX}/bin/registry-api-descriptor-template"
|
||||
|
||||
|
119
vendor/src/github.com/docker/distribution/README.md
vendored
Normal file
119
vendor/src/github.com/docker/distribution/README.md
vendored
Normal file
|
@ -0,0 +1,119 @@
|
|||
# Distribution
|
||||
|
||||
The Docker toolset to pack, ship, store, and deliver content.
|
||||
|
||||
This repository's main product is the Docker Registry 2.0 implementation
|
||||
for storing and distributing Docker images. It supersedes the [docker/docker-
|
||||
registry](https://github.com/docker/docker-registry) project with a new API
|
||||
design, focused around security and performance.
|
||||
|
||||
This repository contains the following components:
|
||||
|
||||
|**Component** |Description |
|
||||
|--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **registry** | An implementation of the [Docker Registry HTTP API V2](docs/spec/api.md) for use with docker 1.6+. |
|
||||
| **libraries** | A rich set of libraries for interacting with,distribution components. Please see [godoc](http://godoc.org/github.com/docker/distribution) for details. **Note**: These libraries are **unstable**. |
|
||||
| **dist** | An _experimental_ tool to provide distribution, oriented functionality without the `docker` daemon. |
|
||||
| **specifications** | _Distribution_ related specifications are available in [docs/spec](docs/spec) |
|
||||
| **documentation** | Docker's full documentation set is available at [docs.docker.com](http://docs.docker.com). This repository [contains the subset](docs/index.md) related just to the registry. |
|
||||
|
||||
### How does this integrate with Docker engine?
|
||||
|
||||
This project should provide an implementation to a V2 API for use in the [Docker
|
||||
core project](https://github.com/docker/docker). The API should be embeddable
|
||||
and simplify the process of securely pulling and pushing content from `docker`
|
||||
daemons.
|
||||
|
||||
### What are the long term goals of the Distribution project?
|
||||
|
||||
The _Distribution_ project has the further long term goal of providing a
|
||||
secure tool chain for distributing content. The specifications, APIs and tools
|
||||
should be as useful with Docker as they are without.
|
||||
|
||||
Our goal is to design a professional grade and extensible content distribution
|
||||
system that allow users to:
|
||||
|
||||
* Enjoy an efficient, secured and reliable way to store, manage, package and
|
||||
exchange content
|
||||
* Hack/roll their own on top of healthy open-source components
|
||||
* Implement their own home made solution through good specs, and solid
|
||||
extensions mechanism.
|
||||
|
||||
## More about Registry 2.0
|
||||
|
||||
The new registry implementation provides the following benefits:
|
||||
|
||||
- faster push and pull
|
||||
- new, more efficient implementation
|
||||
- simplified deployment
|
||||
- pluggable storage backend
|
||||
- webhook notifications
|
||||
|
||||
For information on upcoming functionality, please see [ROADMAP.md](ROADMAP.md).
|
||||
|
||||
### Who needs to deploy a registry?
|
||||
|
||||
By default, Docker users pull images from Docker's public registry instance.
|
||||
[Installing Docker](http://docs.docker.com/installation) gives users this
|
||||
ability. Users can also push images to a repository on Docker's public registry,
|
||||
if they have a [Docker Hub](https://hub.docker.com/) account.
|
||||
|
||||
For some users and even companies, this default behavior is sufficient. For
|
||||
others, it is not.
|
||||
|
||||
For example, users with their own software products may want to maintain a
|
||||
registry for private, company images. Also, you may wish to deploy your own
|
||||
image repository for images used to test or in continuous integration. For these
|
||||
use cases and others, [deploying your own registry instance](docs/deploying.md)
|
||||
may be the better choice.
|
||||
|
||||
## Contribute
|
||||
|
||||
Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute
|
||||
issues, fixes, and patches to this project. If you are contributing code, see
|
||||
the instructions for [building a development environment](docs/building.md).
|
||||
|
||||
## Support
|
||||
|
||||
If any issues are encountered while using the _Distribution_ project, several
|
||||
avenues are available for support:
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th align="left">
|
||||
IRC
|
||||
</th>
|
||||
<td>
|
||||
#docker-distribution on FreeNode
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th align="left">
|
||||
Issue Tracker
|
||||
</th>
|
||||
<td>
|
||||
github.com/docker/distribution/issues
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th align="left">
|
||||
Google Groups
|
||||
</th>
|
||||
<td>
|
||||
https://groups.google.com/a/dockerproject.org/forum/#!forum/distribution
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th align="left">
|
||||
Mailing List
|
||||
</th>
|
||||
<td>
|
||||
docker@dockerproject.org
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
## License
|
||||
|
||||
This project is distributed under [Apache License, Version 2.0](LICENSE.md).
|
92
vendor/src/github.com/docker/distribution/ROADMAP.md
vendored
Normal file
92
vendor/src/github.com/docker/distribution/ROADMAP.md
vendored
Normal file
|
@ -0,0 +1,92 @@
|
|||
# Roadmap
|
||||
|
||||
The Distribution Project consists of several components, some of which are still being defined. This document defines the high-level goals of the project, identifies the current components, and defines the release-relationship to the Docker Platform.
|
||||
|
||||
* [Distribution Goals](#distribution-goals)
|
||||
* [Distribution Components](#distribution-components)
|
||||
* [Project Planning](#project-planning): release-relationship to the Docker Platform.
|
||||
|
||||
## Distribution Goals
|
||||
|
||||
- Replace the existing [docker registry](github.com/docker/docker-registry)
|
||||
implementation as the primary implementation.
|
||||
- Replace the existing push and pull code in the docker engine with the
|
||||
distribution package.
|
||||
- Define a strong data model for distributing docker images
|
||||
- Provide a flexible distribution tool kit for use in the docker platform
|
||||
- Unlock new distribution models
|
||||
|
||||
## Distribution Components
|
||||
|
||||
Components of the Distribution Project are managed via github [milestones](https://github.com/docker/distribution/milestones). Upcoming
|
||||
features and bugfixes for a component will be added to the relevant milestone. If a feature or
|
||||
bugfix is not part of a milestone, it is currently unscheduled for
|
||||
implementation.
|
||||
|
||||
* [Registry](#registry)
|
||||
* [Distribution Package](#distribution-package)
|
||||
|
||||
***
|
||||
|
||||
### Registry
|
||||
|
||||
Registry 2.0 is the first release of the next-generation registry. This is primarily
|
||||
focused on implementing the [new registry
|
||||
API](https://github.com/docker/distribution/blob/master/docs/spec/api.md), with
|
||||
a focus on security and performance.
|
||||
|
||||
#### Registry 2.0
|
||||
|
||||
Features:
|
||||
|
||||
- Faster push and pull
|
||||
- New, more efficient implementation
|
||||
- Simplified deployment
|
||||
- Full API specification for V2 protocol
|
||||
- Pluggable storage system (s3, azure, filesystem and inmemory supported)
|
||||
- Immutable manifest references ([#46](https://github.com/docker/distribution/issues/46))
|
||||
- Webhook notification system ([#42](https://github.com/docker/distribution/issues/42))
|
||||
- Native TLS Support ([#132](https://github.com/docker/distribution/pull/132))
|
||||
- Pluggable authentication system
|
||||
- Health Checks ([#230](https://github.com/docker/distribution/pull/230))
|
||||
|
||||
#### Registry 2.1
|
||||
|
||||
Planned Features:
|
||||
|
||||
> **NOTE:** This feature list is incomplete at this time.
|
||||
|
||||
- Support for Manifest V2, Schema 2 and explicit tagging objects ([#62](https://github.com/docker/distribution/issues/62), [#173](https://github.com/docker/distribution/issues/173))
|
||||
- Mirroring ([#19](https://github.com/docker/distribution/issues/19))
|
||||
- Flexible client package based on distribution interfaces ([#193](https://github.com/docker/distribution/issues/193)
|
||||
|
||||
#### Registry 2.2
|
||||
|
||||
TBD
|
||||
|
||||
***
|
||||
|
||||
### Distribution Package
|
||||
|
||||
At its core, the Distribution Project is a set of Go packages that make up
|
||||
Distribution Components. At this time, most of these packages make up the
|
||||
Registry implementation.
|
||||
|
||||
The package itself is considered unstable. If you're using it, please take care to vendor the dependent version.
|
||||
|
||||
For feature additions, please see the Registry section. In the future, we may break out a
|
||||
separate Roadmap for distribution-specific features that apply to more than
|
||||
just the registry.
|
||||
|
||||
***
|
||||
|
||||
### Project Planning
|
||||
|
||||
Distribution Components map to Docker Platform Releases via the use of labels. Project Pages are used to define the set of features that are included in each Docker Platform Release.
|
||||
|
||||
| Platform Version | Label | Planning |
|
||||
|-----------|------|-----|
|
||||
| Docker 1.6 | [Docker/1.6](https://github.com/docker/distribution/labels/docker%2F1.6) | [Project Page](https://github.com/docker/distribution/wiki/docker-1.6-Project-Page) |
|
||||
| Docker 1.7| [Docker/1.7](https://github.com/docker/distribution/labels/docker%2F1.7) | [Project Page](https://github.com/docker/distribution/wiki/docker-1.7-Project-Page) |
|
||||
| Docker 1.8| [Docker/1.8](https://github.com/docker/distribution/labels/docker%2F1.8) | [Project Page](https://github.com/docker/distribution/wiki/docker-1.8-Project-Page) |
|
||||
|
190
vendor/src/github.com/docker/distribution/blobs.go
vendored
Normal file
190
vendor/src/github.com/docker/distribution/blobs.go
vendored
Normal file
|
@ -0,0 +1,190 @@
|
|||
package distribution
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution/context"
|
||||
"github.com/docker/distribution/digest"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrBlobExists returned when blob already exists
|
||||
ErrBlobExists = errors.New("blob exists")
|
||||
|
||||
// ErrBlobDigestUnsupported when blob digest is an unsupported version.
|
||||
ErrBlobDigestUnsupported = errors.New("unsupported blob digest")
|
||||
|
||||
// ErrBlobUnknown when blob is not found.
|
||||
ErrBlobUnknown = errors.New("unknown blob")
|
||||
|
||||
// ErrBlobUploadUnknown returned when upload is not found.
|
||||
ErrBlobUploadUnknown = errors.New("blob upload unknown")
|
||||
|
||||
// ErrBlobInvalidLength returned when the blob has an expected length on
|
||||
// commit, meaning mismatched with the descriptor or an invalid value.
|
||||
ErrBlobInvalidLength = errors.New("blob invalid length")
|
||||
)
|
||||
|
||||
// ErrBlobInvalidDigest returned when digest check fails.
|
||||
type ErrBlobInvalidDigest struct {
|
||||
Digest digest.Digest
|
||||
Reason error
|
||||
}
|
||||
|
||||
func (err ErrBlobInvalidDigest) Error() string {
|
||||
return fmt.Sprintf("invalid digest for referenced layer: %v, %v",
|
||||
err.Digest, err.Reason)
|
||||
}
|
||||
|
||||
// Descriptor describes targeted content. Used in conjunction with a blob
|
||||
// store, a descriptor can be used to fetch, store and target any kind of
|
||||
// blob. The struct also describes the wire protocol format. Fields should
|
||||
// only be added but never changed.
|
||||
type Descriptor struct {
|
||||
// MediaType describe the type of the content. All text based formats are
|
||||
// encoded as utf-8.
|
||||
MediaType string `json:"mediaType,omitempty"`
|
||||
|
||||
// Length in bytes of content.
|
||||
Length int64 `json:"length,omitempty"`
|
||||
|
||||
// Digest uniquely identifies the content. A byte stream can be verified
|
||||
// against against this digest.
|
||||
Digest digest.Digest `json:"digest,omitempty"`
|
||||
|
||||
// NOTE: Before adding a field here, please ensure that all
|
||||
// other options have been exhausted. Much of the type relationships
|
||||
// depend on the simplicity of this type.
|
||||
}
|
||||
|
||||
// BlobStatter makes blob descriptors available by digest. The service may
|
||||
// provide a descriptor of a different digest if the provided digest is not
|
||||
// canonical.
|
||||
type BlobStatter interface {
|
||||
// Stat provides metadata about a blob identified by the digest. If the
|
||||
// blob is unknown to the describer, ErrBlobUnknown will be returned.
|
||||
Stat(ctx context.Context, dgst digest.Digest) (Descriptor, error)
|
||||
}
|
||||
|
||||
// BlobDescriptorService manages metadata about a blob by digest. Most
|
||||
// implementations will not expose such an interface explicitly. Such mappings
|
||||
// should be maintained by interacting with the BlobIngester. Hence, this is
|
||||
// left off of BlobService and BlobStore.
|
||||
type BlobDescriptorService interface {
|
||||
BlobStatter
|
||||
|
||||
// SetDescriptor assigns the descriptor to the digest. The provided digest and
|
||||
// the digest in the descriptor must map to identical content but they may
|
||||
// differ on their algorithm. The descriptor must have the canonical
|
||||
// digest of the content and the digest algorithm must match the
|
||||
// annotators canonical algorithm.
|
||||
//
|
||||
// Such a facility can be used to map blobs between digest domains, with
|
||||
// the restriction that the algorithm of the descriptor must match the
|
||||
// canonical algorithm (ie sha256) of the annotator.
|
||||
SetDescriptor(ctx context.Context, dgst digest.Digest, desc Descriptor) error
|
||||
}
|
||||
|
||||
// ReadSeekCloser is the primary reader type for blob data, combining
|
||||
// io.ReadSeeker with io.Closer.
|
||||
type ReadSeekCloser interface {
|
||||
io.ReadSeeker
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// BlobProvider describes operations for getting blob data.
|
||||
type BlobProvider interface {
|
||||
// Get returns the entire blob identified by digest along with the descriptor.
|
||||
Get(ctx context.Context, dgst digest.Digest) ([]byte, error)
|
||||
|
||||
// Open provides a ReadSeekCloser to the blob identified by the provided
|
||||
// descriptor. If the blob is not known to the service, an error will be
|
||||
// returned.
|
||||
Open(ctx context.Context, dgst digest.Digest) (ReadSeekCloser, error)
|
||||
}
|
||||
|
||||
// BlobServer can serve blobs via http.
|
||||
type BlobServer interface {
|
||||
// ServeBlob attempts to serve the blob, identifed by dgst, via http. The
|
||||
// service may decide to redirect the client elsewhere or serve the data
|
||||
// directly.
|
||||
//
|
||||
// This handler only issues successful responses, such as 2xx or 3xx,
|
||||
// meaning it serves data or issues a redirect. If the blob is not
|
||||
// available, an error will be returned and the caller may still issue a
|
||||
// response.
|
||||
//
|
||||
// The implementation may serve the same blob from a different digest
|
||||
// domain. The appropriate headers will be set for the blob, unless they
|
||||
// have already been set by the caller.
|
||||
ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error
|
||||
}
|
||||
|
||||
// BlobIngester ingests blob data.
|
||||
type BlobIngester interface {
|
||||
// Put inserts the content p into the blob service, returning a descriptor
|
||||
// or an error.
|
||||
Put(ctx context.Context, mediaType string, p []byte) (Descriptor, error)
|
||||
|
||||
// Create allocates a new blob writer to add a blob to this service. The
|
||||
// returned handle can be written to and later resumed using an opaque
|
||||
// identifier. With this approach, one can Close and Resume a BlobWriter
|
||||
// multiple times until the BlobWriter is committed or cancelled.
|
||||
Create(ctx context.Context) (BlobWriter, error)
|
||||
|
||||
// Resume attempts to resume a write to a blob, identified by an id.
|
||||
Resume(ctx context.Context, id string) (BlobWriter, error)
|
||||
}
|
||||
|
||||
// BlobWriter provides a handle for inserting data into a blob store.
|
||||
// Instances should be obtained from BlobWriteService.Writer and
|
||||
// BlobWriteService.Resume. If supported by the store, a writer can be
|
||||
// recovered with the id.
|
||||
type BlobWriter interface {
|
||||
io.WriteSeeker
|
||||
io.ReaderFrom
|
||||
io.Closer
|
||||
|
||||
// ID returns the identifier for this writer. The ID can be used with the
|
||||
// Blob service to later resume the write.
|
||||
ID() string
|
||||
|
||||
// StartedAt returns the time this blob write was started.
|
||||
StartedAt() time.Time
|
||||
|
||||
// Commit completes the blob writer process. The content is verified
|
||||
// against the provided provisional descriptor, which may result in an
|
||||
// error. Depending on the implementation, written data may be validated
|
||||
// against the provisional descriptor fields. If MediaType is not present,
|
||||
// the implementation may reject the commit or assign "application/octet-
|
||||
// stream" to the blob. The returned descriptor may have a different
|
||||
// digest depending on the blob store, referred to as the canonical
|
||||
// descriptor.
|
||||
Commit(ctx context.Context, provisional Descriptor) (canonical Descriptor, err error)
|
||||
|
||||
// Cancel ends the blob write without storing any data and frees any
|
||||
// associated resources. Any data written thus far will be lost. Cancel
|
||||
// implementations should allow multiple calls even after a commit that
|
||||
// result in a no-op. This allows use of Cancel in a defer statement,
|
||||
// increasing the assurance that it is correctly called.
|
||||
Cancel(ctx context.Context) error
|
||||
}
|
||||
|
||||
// BlobService combines the operations to access, read and write blobs. This
|
||||
// can be used to describe remote blob services.
|
||||
type BlobService interface {
|
||||
BlobStatter
|
||||
BlobProvider
|
||||
BlobIngester
|
||||
}
|
||||
|
||||
// BlobStore represent the entire suite of blob related operations. Such an
|
||||
// implementation can access, read, write and serve blobs.
|
||||
type BlobStore interface {
|
||||
BlobService
|
||||
BlobServer
|
||||
}
|
122
vendor/src/github.com/docker/distribution/circle.yml
vendored
Normal file
122
vendor/src/github.com/docker/distribution/circle.yml
vendored
Normal file
|
@ -0,0 +1,122 @@
|
|||
# Pony-up!
|
||||
machine:
|
||||
pre:
|
||||
# Install gvm
|
||||
- bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/1.0.22/binscripts/gvm-installer)
|
||||
# Install ceph to test rados driver & create pool
|
||||
- sudo -i ~/distribution/contrib/ceph/ci-setup.sh
|
||||
- ceph osd pool create docker-distribution 1
|
||||
|
||||
post:
|
||||
# Install many go versions
|
||||
# - gvm install go1.3.3 -B --name=old
|
||||
- gvm install go1.4.2 -B --name=stable
|
||||
# - gvm install tip --name=bleed
|
||||
|
||||
environment:
|
||||
# Convenient shortcuts to "common" locations
|
||||
CHECKOUT: /home/ubuntu/$CIRCLE_PROJECT_REPONAME
|
||||
BASE_DIR: src/github.com/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME
|
||||
# Trick circle brainflat "no absolute path" behavior
|
||||
BASE_OLD: ../../../$HOME/.gvm/pkgsets/old/global/$BASE_DIR
|
||||
BASE_STABLE: ../../../$HOME/.gvm/pkgsets/stable/global/$BASE_DIR
|
||||
# BASE_BLEED: ../../../$HOME/.gvm/pkgsets/bleed/global/$BASE_DIR
|
||||
DOCKER_BUILDTAGS: "include_rados"
|
||||
# Workaround Circle parsing dumb bugs and/or YAML wonkyness
|
||||
CIRCLE_PAIN: "mode: set"
|
||||
# Ceph config
|
||||
RADOS_POOL: "docker-distribution"
|
||||
|
||||
hosts:
|
||||
# Not used yet
|
||||
fancy: 127.0.0.1
|
||||
|
||||
dependencies:
|
||||
pre:
|
||||
# Copy the code to the gopath of all go versions
|
||||
# - >
|
||||
# gvm use old &&
|
||||
# mkdir -p "$(dirname $BASE_OLD)" &&
|
||||
# cp -R "$CHECKOUT" "$BASE_OLD"
|
||||
|
||||
- >
|
||||
gvm use stable &&
|
||||
mkdir -p "$(dirname $BASE_STABLE)" &&
|
||||
cp -R "$CHECKOUT" "$BASE_STABLE"
|
||||
|
||||
# - >
|
||||
# gvm use bleed &&
|
||||
# mkdir -p "$(dirname $BASE_BLEED)" &&
|
||||
# cp -R "$CHECKOUT" "$BASE_BLEED"
|
||||
|
||||
override:
|
||||
# Install dependencies for every copied clone/go version
|
||||
# - gvm use old && go get github.com/tools/godep:
|
||||
# pwd: $BASE_OLD
|
||||
|
||||
- gvm use stable && go get github.com/tools/godep:
|
||||
pwd: $BASE_STABLE
|
||||
|
||||
# - gvm use bleed && go get github.com/tools/godep:
|
||||
# pwd: $BASE_BLEED
|
||||
|
||||
post:
|
||||
# For the stable go version, additionally install linting tools
|
||||
- >
|
||||
gvm use stable &&
|
||||
go get github.com/axw/gocov/gocov github.com/golang/lint/golint
|
||||
# Disabling goveralls for now
|
||||
# go get github.com/axw/gocov/gocov github.com/mattn/goveralls github.com/golang/lint/golint
|
||||
|
||||
test:
|
||||
pre:
|
||||
# Output the go versions we are going to test
|
||||
# - gvm use old && go version
|
||||
- gvm use stable && go version
|
||||
# - gvm use bleed && go version
|
||||
|
||||
# FMT
|
||||
- gvm use stable && test -z "$(gofmt -s -l . | grep -v Godeps/_workspace/src/ | tee /dev/stderr)":
|
||||
pwd: $BASE_STABLE
|
||||
|
||||
# VET
|
||||
- gvm use stable && go vet ./...:
|
||||
pwd: $BASE_STABLE
|
||||
|
||||
# LINT
|
||||
- gvm use stable && test -z "$(golint ./... | grep -v Godeps/_workspace/src/ | tee /dev/stderr)":
|
||||
pwd: $BASE_STABLE
|
||||
|
||||
override:
|
||||
# Test every version we have (but stable)
|
||||
# - gvm use old; godep go test -test.v -test.short ./...:
|
||||
# timeout: 600
|
||||
# pwd: $BASE_OLD
|
||||
|
||||
# - gvm use bleed; go test -test.v -test.short ./...:
|
||||
# timeout: 600
|
||||
# pwd: $BASE_BLEED
|
||||
|
||||
# Test stable, and report
|
||||
# Preset the goverall report file
|
||||
- echo "$CIRCLE_PAIN" > ~/goverage.report
|
||||
- gvm use stable; go list ./... | xargs -L 1 -I{} rm -f $GOPATH/src/{}/coverage.out:
|
||||
pwd: $BASE_STABLE
|
||||
- gvm use stable; go list -tags "$DOCKER_BUILDTAGS" ./... | xargs -L 1 -I{} godep go test -tags "$DOCKER_BUILDTAGS" -test.short -coverprofile=$GOPATH/src/{}/coverage.out {}:
|
||||
timeout: 600
|
||||
pwd: $BASE_STABLE
|
||||
|
||||
post:
|
||||
# Aggregate and report to coveralls
|
||||
- gvm use stable; go list ./... | xargs -L 1 -I{} cat "$GOPATH/src/{}/coverage.out" | grep -v "$CIRCLE_PAIN" >> ~/goverage.report:
|
||||
pwd: $BASE_STABLE
|
||||
# - gvm use stable; goveralls -service circleci -coverprofile=/home/ubuntu/goverage.report -repotoken $COVERALLS_TOKEN:
|
||||
# pwd: $BASE_STABLE
|
||||
|
||||
## Notes
|
||||
# Disabled coveralls reporting: build breaking sending coverage data to coveralls
|
||||
# Disabled the -race detector due to massive memory usage.
|
||||
# Do we want these as well?
|
||||
# - go get code.google.com/p/go.tools/cmd/goimports
|
||||
# - test -z "$(goimports -l -w ./... | tee /dev/stderr)"
|
||||
# http://labix.org/gocheck
|
76
vendor/src/github.com/docker/distribution/context/context.go
vendored
Normal file
76
vendor/src/github.com/docker/distribution/context/context.go
vendored
Normal file
|
@ -0,0 +1,76 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"github.com/docker/distribution/uuid"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
// Context is a copy of Context from the golang.org/x/net/context package.
|
||||
type Context interface {
|
||||
context.Context
|
||||
}
|
||||
|
||||
// instanceContext is a context that provides only an instance id. It is
|
||||
// provided as the main background context.
|
||||
type instanceContext struct {
|
||||
Context
|
||||
id string // id of context, logged as "instance.id"
|
||||
}
|
||||
|
||||
func (ic *instanceContext) Value(key interface{}) interface{} {
|
||||
if key == "instance.id" {
|
||||
return ic.id
|
||||
}
|
||||
|
||||
return ic.Context.Value(key)
|
||||
}
|
||||
|
||||
var background = &instanceContext{
|
||||
Context: context.Background(),
|
||||
id: uuid.Generate().String(),
|
||||
}
|
||||
|
||||
// Background returns a non-nil, empty Context. The background context
|
||||
// provides a single key, "instance.id" that is globally unique to the
|
||||
// process.
|
||||
func Background() Context {
|
||||
return background
|
||||
}
|
||||
|
||||
// WithValue returns a copy of parent in which the value associated with key is
|
||||
// val. Use context Values only for request-scoped data that transits processes
|
||||
// and APIs, not for passing optional parameters to functions.
|
||||
func WithValue(parent Context, key, val interface{}) Context {
|
||||
return context.WithValue(parent, key, val)
|
||||
}
|
||||
|
||||
// stringMapContext is a simple context implementation that checks a map for a
|
||||
// key, falling back to a parent if not present.
|
||||
type stringMapContext struct {
|
||||
context.Context
|
||||
m map[string]interface{}
|
||||
}
|
||||
|
||||
// WithValues returns a context that proxies lookups through a map. Only
|
||||
// supports string keys.
|
||||
func WithValues(ctx context.Context, m map[string]interface{}) context.Context {
|
||||
mo := make(map[string]interface{}, len(m)) // make our own copy.
|
||||
for k, v := range m {
|
||||
mo[k] = v
|
||||
}
|
||||
|
||||
return stringMapContext{
|
||||
Context: ctx,
|
||||
m: mo,
|
||||
}
|
||||
}
|
||||
|
||||
func (smc stringMapContext) Value(key interface{}) interface{} {
|
||||
if ks, ok := key.(string); ok {
|
||||
if v, ok := smc.m[ks]; ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
return smc.Context.Value(key)
|
||||
}
|
76
vendor/src/github.com/docker/distribution/context/doc.go
vendored
Normal file
76
vendor/src/github.com/docker/distribution/context/doc.go
vendored
Normal file
|
@ -0,0 +1,76 @@
|
|||
// Package context provides several utilities for working with
|
||||
// golang.org/x/net/context in http requests. Primarily, the focus is on
|
||||
// logging relevent request information but this package is not limited to
|
||||
// that purpose.
|
||||
//
|
||||
// Logging
|
||||
//
|
||||
// The most useful aspect of this package is GetLogger. This function takes
|
||||
// any context.Context interface and returns the current logger from the
|
||||
// context. Canonical usage looks like this:
|
||||
//
|
||||
// GetLogger(ctx).Infof("something interesting happened")
|
||||
//
|
||||
// GetLogger also takes optional key arguments. The keys will be looked up in
|
||||
// the context and reported with the logger. The following example would
|
||||
// return a logger that prints the version with each log message:
|
||||
//
|
||||
// ctx := context.Context(context.Background(), "version", version)
|
||||
// GetLogger(ctx, "version").Infof("this log message has a version field")
|
||||
//
|
||||
// The above would print out a log message like this:
|
||||
//
|
||||
// INFO[0000] this log message has a version field version=v2.0.0-alpha.2.m
|
||||
//
|
||||
// When used with WithLogger, we gain the ability to decorate the context with
|
||||
// loggers that have information from disparate parts of the call stack.
|
||||
// Following from the version example, we can build a new context with the
|
||||
// configured logger such that we always print the version field:
|
||||
//
|
||||
// ctx = WithLogger(ctx, GetLogger(ctx, "version"))
|
||||
//
|
||||
// Since the logger has been pushed to the context, we can now get the version
|
||||
// field for free with our log messages. Future calls to GetLogger on the new
|
||||
// context will have the version field:
|
||||
//
|
||||
// GetLogger(ctx).Infof("this log message has a version field")
|
||||
//
|
||||
// This becomes more powerful when we start stacking loggers. Let's say we
|
||||
// have the version logger from above but also want a request id. Using the
|
||||
// context above, in our request scoped function, we place another logger in
|
||||
// the context:
|
||||
//
|
||||
// ctx = context.WithValue(ctx, "http.request.id", "unique id") // called when building request context
|
||||
// ctx = WithLogger(ctx, GetLogger(ctx, "http.request.id"))
|
||||
//
|
||||
// When GetLogger is called on the new context, "http.request.id" will be
|
||||
// included as a logger field, along with the original "version" field:
|
||||
//
|
||||
// INFO[0000] this log message has a version field http.request.id=unique id version=v2.0.0-alpha.2.m
|
||||
//
|
||||
// Note that this only affects the new context, the previous context, with the
|
||||
// version field, can be used independently. Put another way, the new logger,
|
||||
// added to the request context, is unique to that context and can have
|
||||
// request scoped varaibles.
|
||||
//
|
||||
// HTTP Requests
|
||||
//
|
||||
// This package also contains several methods for working with http requests.
|
||||
// The concepts are very similar to those described above. We simply place the
|
||||
// request in the context using WithRequest. This makes the request variables
|
||||
// available. GetRequestLogger can then be called to get request specific
|
||||
// variables in a log line:
|
||||
//
|
||||
// ctx = WithRequest(ctx, req)
|
||||
// GetRequestLogger(ctx).Infof("request variables")
|
||||
//
|
||||
// Like above, if we want to include the request data in all log messages in
|
||||
// the context, we push the logger to a new context and use that one:
|
||||
//
|
||||
// ctx = WithLogger(ctx, GetRequestLogger(ctx))
|
||||
//
|
||||
// The concept is fairly powerful and ensures that calls throughout the stack
|
||||
// can be traced in log messages. Using the fields like "http.request.id", one
|
||||
// can analyze call flow for a particular request with a simple grep of the
|
||||
// logs.
|
||||
package context
|
336
vendor/src/github.com/docker/distribution/context/http.go
vendored
Normal file
336
vendor/src/github.com/docker/distribution/context/http.go
vendored
Normal file
|
@ -0,0 +1,336 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/docker/distribution/uuid"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Common errors used with this package.
|
||||
var (
|
||||
ErrNoRequestContext = errors.New("no http request in context")
|
||||
ErrNoResponseWriterContext = errors.New("no http response in context")
|
||||
)
|
||||
|
||||
func parseIP(ipStr string) net.IP {
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
log.Warnf("invalid remote IP address: %q", ipStr)
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
// RemoteAddr extracts the remote address of the request, taking into
|
||||
// account proxy headers.
|
||||
func RemoteAddr(r *http.Request) string {
|
||||
if prior := r.Header.Get("X-Forwarded-For"); prior != "" {
|
||||
proxies := strings.Split(prior, ",")
|
||||
if len(proxies) > 0 {
|
||||
remoteAddr := strings.Trim(proxies[0], " ")
|
||||
if parseIP(remoteAddr) != nil {
|
||||
return remoteAddr
|
||||
}
|
||||
}
|
||||
}
|
||||
// X-Real-Ip is less supported, but worth checking in the
|
||||
// absence of X-Forwarded-For
|
||||
if realIP := r.Header.Get("X-Real-Ip"); realIP != "" {
|
||||
if parseIP(realIP) != nil {
|
||||
return realIP
|
||||
}
|
||||
}
|
||||
|
||||
return r.RemoteAddr
|
||||
}
|
||||
|
||||
// RemoteIP extracts the remote IP of the request, taking into
|
||||
// account proxy headers.
|
||||
func RemoteIP(r *http.Request) string {
|
||||
addr := RemoteAddr(r)
|
||||
|
||||
// Try parsing it as "IP:port"
|
||||
if ip, _, err := net.SplitHostPort(addr); err == nil {
|
||||
return ip
|
||||
}
|
||||
|
||||
return addr
|
||||
}
|
||||
|
||||
// WithRequest places the request on the context. The context of the request
|
||||
// is assigned a unique id, available at "http.request.id". The request itself
|
||||
// is available at "http.request". Other common attributes are available under
|
||||
// the prefix "http.request.". If a request is already present on the context,
|
||||
// this method will panic.
|
||||
func WithRequest(ctx Context, r *http.Request) Context {
|
||||
if ctx.Value("http.request") != nil {
|
||||
// NOTE(stevvooe): This needs to be considered a programming error. It
|
||||
// is unlikely that we'd want to have more than one request in
|
||||
// context.
|
||||
panic("only one request per context")
|
||||
}
|
||||
|
||||
return &httpRequestContext{
|
||||
Context: ctx,
|
||||
startedAt: time.Now(),
|
||||
id: uuid.Generate().String(),
|
||||
r: r,
|
||||
}
|
||||
}
|
||||
|
||||
// GetRequest returns the http request in the given context. Returns
|
||||
// ErrNoRequestContext if the context does not have an http request associated
|
||||
// with it.
|
||||
func GetRequest(ctx Context) (*http.Request, error) {
|
||||
if r, ok := ctx.Value("http.request").(*http.Request); r != nil && ok {
|
||||
return r, nil
|
||||
}
|
||||
return nil, ErrNoRequestContext
|
||||
}
|
||||
|
||||
// GetRequestID attempts to resolve the current request id, if possible. An
|
||||
// error is return if it is not available on the context.
|
||||
func GetRequestID(ctx Context) string {
|
||||
return GetStringValue(ctx, "http.request.id")
|
||||
}
|
||||
|
||||
// WithResponseWriter returns a new context and response writer that makes
|
||||
// interesting response statistics available within the context.
|
||||
func WithResponseWriter(ctx Context, w http.ResponseWriter) (Context, http.ResponseWriter) {
|
||||
irw := &instrumentedResponseWriter{
|
||||
ResponseWriter: w,
|
||||
Context: ctx,
|
||||
}
|
||||
|
||||
return irw, irw
|
||||
}
|
||||
|
||||
// GetResponseWriter returns the http.ResponseWriter from the provided
|
||||
// context. If not present, ErrNoResponseWriterContext is returned. The
|
||||
// returned instance provides instrumentation in the context.
|
||||
func GetResponseWriter(ctx Context) (http.ResponseWriter, error) {
|
||||
v := ctx.Value("http.response")
|
||||
|
||||
rw, ok := v.(http.ResponseWriter)
|
||||
if !ok || rw == nil {
|
||||
return nil, ErrNoResponseWriterContext
|
||||
}
|
||||
|
||||
return rw, nil
|
||||
}
|
||||
|
||||
// getVarsFromRequest let's us change request vars implementation for testing
|
||||
// and maybe future changes.
|
||||
var getVarsFromRequest = mux.Vars
|
||||
|
||||
// WithVars extracts gorilla/mux vars and makes them available on the returned
|
||||
// context. Variables are available at keys with the prefix "vars.". For
|
||||
// example, if looking for the variable "name", it can be accessed as
|
||||
// "vars.name". Implementations that are accessing values need not know that
|
||||
// the underlying context is implemented with gorilla/mux vars.
|
||||
func WithVars(ctx Context, r *http.Request) Context {
|
||||
return &muxVarsContext{
|
||||
Context: ctx,
|
||||
vars: getVarsFromRequest(r),
|
||||
}
|
||||
}
|
||||
|
||||
// GetRequestLogger returns a logger that contains fields from the request in
|
||||
// the current context. If the request is not available in the context, no
|
||||
// fields will display. Request loggers can safely be pushed onto the context.
|
||||
func GetRequestLogger(ctx Context) Logger {
|
||||
return GetLogger(ctx,
|
||||
"http.request.id",
|
||||
"http.request.method",
|
||||
"http.request.host",
|
||||
"http.request.uri",
|
||||
"http.request.referer",
|
||||
"http.request.useragent",
|
||||
"http.request.remoteaddr",
|
||||
"http.request.contenttype")
|
||||
}
|
||||
|
||||
// GetResponseLogger reads the current response stats and builds a logger.
|
||||
// Because the values are read at call time, pushing a logger returned from
|
||||
// this function on the context will lead to missing or invalid data. Only
|
||||
// call this at the end of a request, after the response has been written.
|
||||
func GetResponseLogger(ctx Context) Logger {
|
||||
l := getLogrusLogger(ctx,
|
||||
"http.response.written",
|
||||
"http.response.status",
|
||||
"http.response.contenttype")
|
||||
|
||||
duration := Since(ctx, "http.request.startedat")
|
||||
|
||||
if duration > 0 {
|
||||
l = l.WithField("http.response.duration", duration.String())
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// httpRequestContext makes information about a request available to context.
|
||||
type httpRequestContext struct {
|
||||
Context
|
||||
|
||||
startedAt time.Time
|
||||
id string
|
||||
r *http.Request
|
||||
}
|
||||
|
||||
// Value returns a keyed element of the request for use in the context. To get
|
||||
// the request itself, query "request". For other components, access them as
|
||||
// "request.<component>". For example, r.RequestURI
|
||||
func (ctx *httpRequestContext) Value(key interface{}) interface{} {
|
||||
if keyStr, ok := key.(string); ok {
|
||||
if keyStr == "http.request" {
|
||||
return ctx.r
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(keyStr, "http.request.") {
|
||||
goto fallback
|
||||
}
|
||||
|
||||
parts := strings.Split(keyStr, ".")
|
||||
|
||||
if len(parts) != 3 {
|
||||
goto fallback
|
||||
}
|
||||
|
||||
switch parts[2] {
|
||||
case "uri":
|
||||
return ctx.r.RequestURI
|
||||
case "remoteaddr":
|
||||
return RemoteAddr(ctx.r)
|
||||
case "method":
|
||||
return ctx.r.Method
|
||||
case "host":
|
||||
return ctx.r.Host
|
||||
case "referer":
|
||||
referer := ctx.r.Referer()
|
||||
if referer != "" {
|
||||
return referer
|
||||
}
|
||||
case "useragent":
|
||||
return ctx.r.UserAgent()
|
||||
case "id":
|
||||
return ctx.id
|
||||
case "startedat":
|
||||
return ctx.startedAt
|
||||
case "contenttype":
|
||||
ct := ctx.r.Header.Get("Content-Type")
|
||||
if ct != "" {
|
||||
return ct
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fallback:
|
||||
return ctx.Context.Value(key)
|
||||
}
|
||||
|
||||
type muxVarsContext struct {
|
||||
Context
|
||||
vars map[string]string
|
||||
}
|
||||
|
||||
func (ctx *muxVarsContext) Value(key interface{}) interface{} {
|
||||
if keyStr, ok := key.(string); ok {
|
||||
if keyStr == "vars" {
|
||||
return ctx.vars
|
||||
}
|
||||
|
||||
if strings.HasPrefix(keyStr, "vars.") {
|
||||
keyStr = strings.TrimPrefix(keyStr, "vars.")
|
||||
}
|
||||
|
||||
if v, ok := ctx.vars[keyStr]; ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.Context.Value(key)
|
||||
}
|
||||
|
||||
// instrumentedResponseWriter provides response writer information in a
|
||||
// context.
|
||||
type instrumentedResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
Context
|
||||
|
||||
mu sync.Mutex
|
||||
status int
|
||||
written int64
|
||||
}
|
||||
|
||||
func (irw *instrumentedResponseWriter) Write(p []byte) (n int, err error) {
|
||||
n, err = irw.ResponseWriter.Write(p)
|
||||
|
||||
irw.mu.Lock()
|
||||
irw.written += int64(n)
|
||||
|
||||
// Guess the likely status if not set.
|
||||
if irw.status == 0 {
|
||||
irw.status = http.StatusOK
|
||||
}
|
||||
|
||||
irw.mu.Unlock()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (irw *instrumentedResponseWriter) WriteHeader(status int) {
|
||||
irw.ResponseWriter.WriteHeader(status)
|
||||
|
||||
irw.mu.Lock()
|
||||
irw.status = status
|
||||
irw.mu.Unlock()
|
||||
}
|
||||
|
||||
func (irw *instrumentedResponseWriter) Flush() {
|
||||
if flusher, ok := irw.ResponseWriter.(http.Flusher); ok {
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func (irw *instrumentedResponseWriter) Value(key interface{}) interface{} {
|
||||
if keyStr, ok := key.(string); ok {
|
||||
if keyStr == "http.response" {
|
||||
return irw
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(keyStr, "http.response.") {
|
||||
goto fallback
|
||||
}
|
||||
|
||||
parts := strings.Split(keyStr, ".")
|
||||
|
||||
if len(parts) != 3 {
|
||||
goto fallback
|
||||
}
|
||||
|
||||
irw.mu.Lock()
|
||||
defer irw.mu.Unlock()
|
||||
|
||||
switch parts[2] {
|
||||
case "written":
|
||||
return irw.written
|
||||
case "status":
|
||||
return irw.status
|
||||
case "contenttype":
|
||||
contentType := irw.Header().Get("Content-Type")
|
||||
if contentType != "" {
|
||||
return contentType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fallback:
|
||||
return irw.Context.Value(key)
|
||||
}
|
108
vendor/src/github.com/docker/distribution/context/logger.go
vendored
Normal file
108
vendor/src/github.com/docker/distribution/context/logger.go
vendored
Normal file
|
@ -0,0 +1,108 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/distribution/uuid"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Logger provides a leveled-logging interface.
|
||||
type Logger interface {
|
||||
// standard logger methods
|
||||
Print(args ...interface{})
|
||||
Printf(format string, args ...interface{})
|
||||
Println(args ...interface{})
|
||||
|
||||
Fatal(args ...interface{})
|
||||
Fatalf(format string, args ...interface{})
|
||||
Fatalln(args ...interface{})
|
||||
|
||||
Panic(args ...interface{})
|
||||
Panicf(format string, args ...interface{})
|
||||
Panicln(args ...interface{})
|
||||
|
||||
// Leveled methods, from logrus
|
||||
Debug(args ...interface{})
|
||||
Debugf(format string, args ...interface{})
|
||||
Debugln(args ...interface{})
|
||||
|
||||
Error(args ...interface{})
|
||||
Errorf(format string, args ...interface{})
|
||||
Errorln(args ...interface{})
|
||||
|
||||
Info(args ...interface{})
|
||||
Infof(format string, args ...interface{})
|
||||
Infoln(args ...interface{})
|
||||
|
||||
Warn(args ...interface{})
|
||||
Warnf(format string, args ...interface{})
|
||||
Warnln(args ...interface{})
|
||||
}
|
||||
|
||||
// WithLogger creates a new context with provided logger.
|
||||
func WithLogger(ctx Context, logger Logger) Context {
|
||||
return WithValue(ctx, "logger", logger)
|
||||
}
|
||||
|
||||
// GetLoggerWithField returns a logger instance with the specified field key
|
||||
// and value without affecting the context. Extra specified keys will be
|
||||
// resolved from the context.
|
||||
func GetLoggerWithField(ctx Context, key, value interface{}, keys ...interface{}) Logger {
|
||||
return getLogrusLogger(ctx, keys...).WithField(fmt.Sprint(key), value)
|
||||
}
|
||||
|
||||
// GetLoggerWithFields returns a logger instance with the specified fields
|
||||
// without affecting the context. Extra specified keys will be resolved from
|
||||
// the context.
|
||||
func GetLoggerWithFields(ctx Context, fields map[string]interface{}, keys ...interface{}) Logger {
|
||||
return getLogrusLogger(ctx, keys...).WithFields(logrus.Fields(fields))
|
||||
}
|
||||
|
||||
// GetLogger returns the logger from the current context, if present. If one
|
||||
// or more keys are provided, they will be resolved on the context and
|
||||
// included in the logger. While context.Value takes an interface, any key
|
||||
// argument passed to GetLogger will be passed to fmt.Sprint when expanded as
|
||||
// a logging key field. If context keys are integer constants, for example,
|
||||
// its recommended that a String method is implemented.
|
||||
func GetLogger(ctx Context, keys ...interface{}) Logger {
|
||||
return getLogrusLogger(ctx, keys...)
|
||||
}
|
||||
|
||||
// GetLogrusLogger returns the logrus logger for the context. If one more keys
|
||||
// are provided, they will be resolved on the context and included in the
|
||||
// logger. Only use this function if specific logrus functionality is
|
||||
// required.
|
||||
func getLogrusLogger(ctx Context, keys ...interface{}) *logrus.Entry {
|
||||
var logger *logrus.Entry
|
||||
|
||||
// Get a logger, if it is present.
|
||||
loggerInterface := ctx.Value("logger")
|
||||
if loggerInterface != nil {
|
||||
if lgr, ok := loggerInterface.(*logrus.Entry); ok {
|
||||
logger = lgr
|
||||
}
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
// If no logger is found, just return the standard logger.
|
||||
logger = logrus.NewEntry(logrus.StandardLogger())
|
||||
}
|
||||
|
||||
fields := logrus.Fields{}
|
||||
|
||||
for _, key := range keys {
|
||||
v := ctx.Value(key)
|
||||
if v != nil {
|
||||
fields[fmt.Sprint(key)] = v
|
||||
}
|
||||
}
|
||||
|
||||
return logger.WithFields(fields)
|
||||
}
|
||||
|
||||
func init() {
|
||||
// inject a logger into the uuid library.
|
||||
uuid.Loggerf = GetLogger(Background()).Warnf
|
||||
}
|
104
vendor/src/github.com/docker/distribution/context/trace.go
vendored
Normal file
104
vendor/src/github.com/docker/distribution/context/trace.go
vendored
Normal file
|
@ -0,0 +1,104 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution/uuid"
|
||||
)
|
||||
|
||||
// WithTrace allocates a traced timing span in a new context. This allows a
|
||||
// caller to track the time between calling WithTrace and the returned done
|
||||
// function. When the done function is called, a log message is emitted with a
|
||||
// "trace.duration" field, corresponding to the elapased time and a
|
||||
// "trace.func" field, corresponding to the function that called WithTrace.
|
||||
//
|
||||
// The logging keys "trace.id" and "trace.parent.id" are provided to implement
|
||||
// dapper-like tracing. This function should be complemented with a WithSpan
|
||||
// method that could be used for tracing distributed RPC calls.
|
||||
//
|
||||
// The main benefit of this function is to post-process log messages or
|
||||
// intercept them in a hook to provide timing data. Trace ids and parent ids
|
||||
// can also be linked to provide call tracing, if so required.
|
||||
//
|
||||
// Here is an example of the usage:
|
||||
//
|
||||
// func timedOperation(ctx Context) {
|
||||
// ctx, done := WithTrace(ctx)
|
||||
// defer done("this will be the log message")
|
||||
// // ... function body ...
|
||||
// }
|
||||
//
|
||||
// If the function ran for roughly 1s, such a usage would emit a log message
|
||||
// as follows:
|
||||
//
|
||||
// INFO[0001] this will be the log message trace.duration=1.004575763s trace.func=github.com/docker/distribution/context.traceOperation trace.id=<id> ...
|
||||
//
|
||||
// Notice that the function name is automatically resolved, along with the
|
||||
// package and a trace id is emitted that can be linked with parent ids.
|
||||
func WithTrace(ctx Context) (Context, func(format string, a ...interface{})) {
|
||||
if ctx == nil {
|
||||
ctx = Background()
|
||||
}
|
||||
|
||||
pc, file, line, _ := runtime.Caller(1)
|
||||
f := runtime.FuncForPC(pc)
|
||||
ctx = &traced{
|
||||
Context: ctx,
|
||||
id: uuid.Generate().String(),
|
||||
start: time.Now(),
|
||||
parent: GetStringValue(ctx, "trace.id"),
|
||||
fnname: f.Name(),
|
||||
file: file,
|
||||
line: line,
|
||||
}
|
||||
|
||||
return ctx, func(format string, a ...interface{}) {
|
||||
GetLogger(ctx,
|
||||
"trace.duration",
|
||||
"trace.id",
|
||||
"trace.parent.id",
|
||||
"trace.func",
|
||||
"trace.file",
|
||||
"trace.line").
|
||||
Debugf(format, a...)
|
||||
}
|
||||
}
|
||||
|
||||
// traced represents a context that is traced for function call timing. It
|
||||
// also provides fast lookup for the various attributes that are available on
|
||||
// the trace.
|
||||
type traced struct {
|
||||
Context
|
||||
id string
|
||||
parent string
|
||||
start time.Time
|
||||
fnname string
|
||||
file string
|
||||
line int
|
||||
}
|
||||
|
||||
func (ts *traced) Value(key interface{}) interface{} {
|
||||
switch key {
|
||||
case "trace.start":
|
||||
return ts.start
|
||||
case "trace.duration":
|
||||
return time.Since(ts.start)
|
||||
case "trace.id":
|
||||
return ts.id
|
||||
case "trace.parent.id":
|
||||
if ts.parent == "" {
|
||||
return nil // must return nil to signal no parent.
|
||||
}
|
||||
|
||||
return ts.parent
|
||||
case "trace.func":
|
||||
return ts.fnname
|
||||
case "trace.file":
|
||||
return ts.file
|
||||
case "trace.line":
|
||||
return ts.line
|
||||
}
|
||||
|
||||
return ts.Context.Value(key)
|
||||
}
|
32
vendor/src/github.com/docker/distribution/context/util.go
vendored
Normal file
32
vendor/src/github.com/docker/distribution/context/util.go
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
package context
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Since looks up key, which should be a time.Time, and returns the duration
|
||||
// since that time. If the key is not found, the value returned will be zero.
|
||||
// This is helpful when inferring metrics related to context execution times.
|
||||
func Since(ctx Context, key interface{}) time.Duration {
|
||||
startedAtI := ctx.Value(key)
|
||||
if startedAtI != nil {
|
||||
if startedAt, ok := startedAtI.(time.Time); ok {
|
||||
return time.Since(startedAt)
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// GetStringValue returns a string value from the context. The empty string
|
||||
// will be returned if not found.
|
||||
func GetStringValue(ctx Context, key string) (value string) {
|
||||
stringi := ctx.Value(key)
|
||||
if stringi != nil {
|
||||
if valuev, ok := stringi.(string); ok {
|
||||
value = valuev
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
7
vendor/src/github.com/docker/distribution/doc.go
vendored
Normal file
7
vendor/src/github.com/docker/distribution/doc.go
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
// Package distribution will define the interfaces for the components of
|
||||
// docker distribution. The goal is to allow users to reliably package, ship
|
||||
// and store content related to docker images.
|
||||
//
|
||||
// This is currently a work in progress. More details are available in the
|
||||
// README.md.
|
||||
package distribution
|
82
vendor/src/github.com/docker/distribution/errors.go
vendored
Normal file
82
vendor/src/github.com/docker/distribution/errors.go
vendored
Normal file
|
@ -0,0 +1,82 @@
|
|||
package distribution
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/distribution/digest"
|
||||
)
|
||||
|
||||
// ErrRepositoryUnknown is returned if the named repository is not known by
|
||||
// the registry.
|
||||
type ErrRepositoryUnknown struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (err ErrRepositoryUnknown) Error() string {
|
||||
return fmt.Sprintf("unknown repository name=%s", err.Name)
|
||||
}
|
||||
|
||||
// ErrRepositoryNameInvalid should be used to denote an invalid repository
|
||||
// name. Reason may set, indicating the cause of invalidity.
|
||||
type ErrRepositoryNameInvalid struct {
|
||||
Name string
|
||||
Reason error
|
||||
}
|
||||
|
||||
func (err ErrRepositoryNameInvalid) Error() string {
|
||||
return fmt.Sprintf("repository name %q invalid: %v", err.Name, err.Reason)
|
||||
}
|
||||
|
||||
// ErrManifestUnknown is returned if the manifest is not known by the
|
||||
// registry.
|
||||
type ErrManifestUnknown struct {
|
||||
Name string
|
||||
Tag string
|
||||
}
|
||||
|
||||
func (err ErrManifestUnknown) Error() string {
|
||||
return fmt.Sprintf("unknown manifest name=%s tag=%s", err.Name, err.Tag)
|
||||
}
|
||||
|
||||
// ErrManifestUnknownRevision is returned when a manifest cannot be found by
|
||||
// revision within a repository.
|
||||
type ErrManifestUnknownRevision struct {
|
||||
Name string
|
||||
Revision digest.Digest
|
||||
}
|
||||
|
||||
func (err ErrManifestUnknownRevision) Error() string {
|
||||
return fmt.Sprintf("unknown manifest name=%s revision=%s", err.Name, err.Revision)
|
||||
}
|
||||
|
||||
// ErrManifestUnverified is returned when the registry is unable to verify
|
||||
// the manifest.
|
||||
type ErrManifestUnverified struct{}
|
||||
|
||||
func (ErrManifestUnverified) Error() string {
|
||||
return fmt.Sprintf("unverified manifest")
|
||||
}
|
||||
|
||||
// ErrManifestVerification provides a type to collect errors encountered
|
||||
// during manifest verification. Currently, it accepts errors of all types,
|
||||
// but it may be narrowed to those involving manifest verification.
|
||||
type ErrManifestVerification []error
|
||||
|
||||
func (errs ErrManifestVerification) Error() string {
|
||||
var parts []string
|
||||
for _, err := range errs {
|
||||
parts = append(parts, err.Error())
|
||||
}
|
||||
|
||||
return fmt.Sprintf("errors verifying manifest: %v", strings.Join(parts, ","))
|
||||
}
|
||||
|
||||
// ErrManifestBlobUnknown returned when a referenced blob cannot be found.
|
||||
type ErrManifestBlobUnknown struct {
|
||||
Digest digest.Digest
|
||||
}
|
||||
|
||||
func (err ErrManifestBlobUnknown) Error() string {
|
||||
return fmt.Sprintf("unknown blob %v on manifest", err.Digest)
|
||||
}
|
124
vendor/src/github.com/docker/distribution/manifest/manifest.go
vendored
Normal file
124
vendor/src/github.com/docker/distribution/manifest/manifest.go
vendored
Normal file
|
@ -0,0 +1,124 @@
|
|||
package manifest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/libtrust"
|
||||
)
|
||||
|
||||
// TODO(stevvooe): When we rev the manifest format, the contents of this
|
||||
// package should be moved to manifest/v1.
|
||||
|
||||
const (
|
||||
// ManifestMediaType specifies the mediaType for the current version. Note
|
||||
// that for schema version 1, the the media is optionally
|
||||
// "application/json".
|
||||
ManifestMediaType = "application/vnd.docker.distribution.manifest.v1+json"
|
||||
)
|
||||
|
||||
// Versioned provides a struct with just the manifest schemaVersion. Incoming
|
||||
// content with unknown schema version can be decoded against this struct to
|
||||
// check the version.
|
||||
type Versioned struct {
|
||||
// SchemaVersion is the image manifest schema that this image follows
|
||||
SchemaVersion int `json:"schemaVersion"`
|
||||
}
|
||||
|
||||
// Manifest provides the base accessible fields for working with V2 image
|
||||
// format in the registry.
|
||||
type Manifest struct {
|
||||
Versioned
|
||||
|
||||
// Name is the name of the image's repository
|
||||
Name string `json:"name"`
|
||||
|
||||
// Tag is the tag of the image specified by this manifest
|
||||
Tag string `json:"tag"`
|
||||
|
||||
// Architecture is the host architecture on which this image is intended to
|
||||
// run
|
||||
Architecture string `json:"architecture"`
|
||||
|
||||
// FSLayers is a list of filesystem layer blobSums contained in this image
|
||||
FSLayers []FSLayer `json:"fsLayers"`
|
||||
|
||||
// History is a list of unstructured historical data for v1 compatibility
|
||||
History []History `json:"history"`
|
||||
}
|
||||
|
||||
// SignedManifest provides an envelope for a signed image manifest, including
|
||||
// the format sensitive raw bytes. It contains fields to
|
||||
type SignedManifest struct {
|
||||
Manifest
|
||||
|
||||
// Raw is the byte representation of the ImageManifest, used for signature
|
||||
// verification. The value of Raw must be used directly during
|
||||
// serialization, or the signature check will fail. The manifest byte
|
||||
// representation cannot change or it will have to be re-signed.
|
||||
Raw []byte `json:"-"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON populates a new ImageManifest struct from JSON data.
|
||||
func (sm *SignedManifest) UnmarshalJSON(b []byte) error {
|
||||
var manifest Manifest
|
||||
if err := json.Unmarshal(b, &manifest); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sm.Manifest = manifest
|
||||
sm.Raw = make([]byte, len(b), len(b))
|
||||
copy(sm.Raw, b)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Payload returns the raw, signed content of the signed manifest. The
|
||||
// contents can be used to calculate the content identifier.
|
||||
func (sm *SignedManifest) Payload() ([]byte, error) {
|
||||
jsig, err := libtrust.ParsePrettySignature(sm.Raw, "signatures")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Resolve the payload in the manifest.
|
||||
return jsig.Payload()
|
||||
}
|
||||
|
||||
// Signatures returns the signatures as provided by
|
||||
// (*libtrust.JSONSignature).Signatures. The byte slices are opaque jws
|
||||
// signatures.
|
||||
func (sm *SignedManifest) Signatures() ([][]byte, error) {
|
||||
jsig, err := libtrust.ParsePrettySignature(sm.Raw, "signatures")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Resolve the payload in the manifest.
|
||||
return jsig.Signatures()
|
||||
}
|
||||
|
||||
// MarshalJSON returns the contents of raw. If Raw is nil, marshals the inner
|
||||
// contents. Applications requiring a marshaled signed manifest should simply
|
||||
// use Raw directly, since the the content produced by json.Marshal will be
|
||||
// compacted and will fail signature checks.
|
||||
func (sm *SignedManifest) MarshalJSON() ([]byte, error) {
|
||||
if len(sm.Raw) > 0 {
|
||||
return sm.Raw, nil
|
||||
}
|
||||
|
||||
// If the raw data is not available, just dump the inner content.
|
||||
return json.Marshal(&sm.Manifest)
|
||||
}
|
||||
|
||||
// FSLayer is a container struct for BlobSums defined in an image manifest
|
||||
type FSLayer struct {
|
||||
// BlobSum is the tarsum of the referenced filesystem image layer
|
||||
BlobSum digest.Digest `json:"blobSum"`
|
||||
}
|
||||
|
||||
// History stores unstructured v1 compatibility information
|
||||
type History struct {
|
||||
// V1Compatibility is the raw v1 compatibility information
|
||||
V1Compatibility string `json:"v1Compatibility"`
|
||||
}
|
66
vendor/src/github.com/docker/distribution/manifest/sign.go
vendored
Normal file
66
vendor/src/github.com/docker/distribution/manifest/sign.go
vendored
Normal file
|
@ -0,0 +1,66 @@
|
|||
package manifest
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/docker/libtrust"
|
||||
)
|
||||
|
||||
// Sign signs the manifest with the provided private key, returning a
|
||||
// SignedManifest. This typically won't be used within the registry, except
|
||||
// for testing.
|
||||
func Sign(m *Manifest, pk libtrust.PrivateKey) (*SignedManifest, error) {
|
||||
p, err := json.MarshalIndent(m, "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
js, err := libtrust.NewJSONSignature(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := js.Sign(pk); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pretty, err := js.PrettySignature("signatures")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &SignedManifest{
|
||||
Manifest: *m,
|
||||
Raw: pretty,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SignWithChain signs the manifest with the given private key and x509 chain.
|
||||
// The public key of the first element in the chain must be the public key
|
||||
// corresponding with the sign key.
|
||||
func SignWithChain(m *Manifest, key libtrust.PrivateKey, chain []*x509.Certificate) (*SignedManifest, error) {
|
||||
p, err := json.MarshalIndent(m, "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
js, err := libtrust.NewJSONSignature(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := js.SignWithChain(key, chain); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pretty, err := js.PrettySignature("signatures")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &SignedManifest{
|
||||
Manifest: *m,
|
||||
Raw: pretty,
|
||||
}, nil
|
||||
}
|
32
vendor/src/github.com/docker/distribution/manifest/verify.go
vendored
Normal file
32
vendor/src/github.com/docker/distribution/manifest/verify.go
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
package manifest
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/libtrust"
|
||||
)
|
||||
|
||||
// Verify verifies the signature of the signed manifest returning the public
|
||||
// keys used during signing.
|
||||
func Verify(sm *SignedManifest) ([]libtrust.PublicKey, error) {
|
||||
js, err := libtrust.ParsePrettySignature(sm.Raw, "signatures")
|
||||
if err != nil {
|
||||
logrus.WithField("err", err).Debugf("(*SignedManifest).Verify")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return js.Verify()
|
||||
}
|
||||
|
||||
// VerifyChains verifies the signature of the signed manifest against the
|
||||
// certificate pool returning the list of verified chains. Signatures without
|
||||
// an x509 chain are not checked.
|
||||
func VerifyChains(sm *SignedManifest, ca *x509.CertPool) ([][]*x509.Certificate, error) {
|
||||
js, err := libtrust.ParsePrettySignature(sm.Raw, "signatures")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return js.VerifyChains(ca)
|
||||
}
|
110
vendor/src/github.com/docker/distribution/registry.go
vendored
Normal file
110
vendor/src/github.com/docker/distribution/registry.go
vendored
Normal file
|
@ -0,0 +1,110 @@
|
|||
package distribution
|
||||
|
||||
import (
|
||||
"github.com/docker/distribution/context"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/manifest"
|
||||
)
|
||||
|
||||
// Scope defines the set of items that match a namespace.
|
||||
type Scope interface {
|
||||
// Contains returns true if the name belongs to the namespace.
|
||||
Contains(name string) bool
|
||||
}
|
||||
|
||||
type fullScope struct{}
|
||||
|
||||
func (f fullScope) Contains(string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GlobalScope represents the full namespace scope which contains
|
||||
// all other scopes.
|
||||
var GlobalScope = Scope(fullScope{})
|
||||
|
||||
// Namespace represents a collection of repositories, addressable by name.
|
||||
// Generally, a namespace is backed by a set of one or more services,
|
||||
// providing facilities such as registry access, trust, and indexing.
|
||||
type Namespace interface {
|
||||
// Scope describes the names that can be used with this Namespace. The
|
||||
// global namespace will have a scope that matches all names. The scope
|
||||
// effectively provides an identity for the namespace.
|
||||
Scope() Scope
|
||||
|
||||
// Repository should return a reference to the named repository. The
|
||||
// registry may or may not have the repository but should always return a
|
||||
// reference.
|
||||
Repository(ctx context.Context, name string) (Repository, error)
|
||||
}
|
||||
|
||||
// Repository is a named collection of manifests and layers.
|
||||
type Repository interface {
|
||||
// Name returns the name of the repository.
|
||||
Name() string
|
||||
|
||||
// Manifests returns a reference to this repository's manifest service.
|
||||
Manifests() ManifestService
|
||||
|
||||
// Blobs returns a reference to this repository's blob service.
|
||||
Blobs(ctx context.Context) BlobStore
|
||||
|
||||
// TODO(stevvooe): The above BlobStore return can probably be relaxed to
|
||||
// be a BlobService for use with clients. This will allow such
|
||||
// implementations to avoid implementing ServeBlob.
|
||||
|
||||
// Signatures returns a reference to this repository's signatures service.
|
||||
Signatures() SignatureService
|
||||
}
|
||||
|
||||
// TODO(stevvooe): Must add close methods to all these. May want to change the
|
||||
// way instances are created to better reflect internal dependency
|
||||
// relationships.
|
||||
|
||||
// ManifestService provides operations on image manifests.
|
||||
type ManifestService interface {
|
||||
// Exists returns true if the manifest exists.
|
||||
Exists(dgst digest.Digest) (bool, error)
|
||||
|
||||
// Get retrieves the identified by the digest, if it exists.
|
||||
Get(dgst digest.Digest) (*manifest.SignedManifest, error)
|
||||
|
||||
// Delete removes the manifest, if it exists.
|
||||
Delete(dgst digest.Digest) error
|
||||
|
||||
// Put creates or updates the manifest.
|
||||
Put(manifest *manifest.SignedManifest) error
|
||||
|
||||
// TODO(stevvooe): The methods after this message should be moved to a
|
||||
// discrete TagService, per active proposals.
|
||||
|
||||
// Tags lists the tags under the named repository.
|
||||
Tags() ([]string, error)
|
||||
|
||||
// ExistsByTag returns true if the manifest exists.
|
||||
ExistsByTag(tag string) (bool, error)
|
||||
|
||||
// GetByTag retrieves the named manifest, if it exists.
|
||||
GetByTag(tag string) (*manifest.SignedManifest, error)
|
||||
|
||||
// TODO(stevvooe): There are several changes that need to be done to this
|
||||
// interface:
|
||||
//
|
||||
// 1. Allow explicit tagging with Tag(digest digest.Digest, tag string)
|
||||
// 2. Support reading tags with a re-entrant reader to avoid large
|
||||
// allocations in the registry.
|
||||
// 3. Long-term: Provide All() method that lets one scroll through all of
|
||||
// the manifest entries.
|
||||
// 4. Long-term: break out concept of signing from manifests. This is
|
||||
// really a part of the distribution sprint.
|
||||
// 5. Long-term: Manifest should be an interface. This code shouldn't
|
||||
// really be concerned with the storage format.
|
||||
}
|
||||
|
||||
// SignatureService provides operations on signatures.
|
||||
type SignatureService interface {
|
||||
// Get retrieves all of the signature blobs for the specified digest.
|
||||
Get(dgst digest.Digest) ([][]byte, error)
|
||||
|
||||
// Put stores the signature for the provided digest.
|
||||
Put(dgst digest.Digest, signatures ...[]byte) error
|
||||
}
|
58
vendor/src/github.com/docker/distribution/registry/client/auth/api_version.go
vendored
Normal file
58
vendor/src/github.com/docker/distribution/registry/client/auth/api_version.go
vendored
Normal file
|
@ -0,0 +1,58 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// APIVersion represents a version of an API including its
|
||||
// type and version number.
|
||||
type APIVersion struct {
|
||||
// Type refers to the name of a specific API specification
|
||||
// such as "registry"
|
||||
Type string
|
||||
|
||||
// Version is the version of the API specification implemented,
|
||||
// This may omit the revision number and only include
|
||||
// the major and minor version, such as "2.0"
|
||||
Version string
|
||||
}
|
||||
|
||||
// String returns the string formatted API Version
|
||||
func (v APIVersion) String() string {
|
||||
return v.Type + "/" + v.Version
|
||||
}
|
||||
|
||||
// APIVersions gets the API versions out of an HTTP response using the provided
|
||||
// version header as the key for the HTTP header.
|
||||
func APIVersions(resp *http.Response, versionHeader string) []APIVersion {
|
||||
versions := []APIVersion{}
|
||||
if versionHeader != "" {
|
||||
for _, supportedVersions := range resp.Header[http.CanonicalHeaderKey(versionHeader)] {
|
||||
for _, version := range strings.Fields(supportedVersions) {
|
||||
versions = append(versions, ParseAPIVersion(version))
|
||||
}
|
||||
}
|
||||
}
|
||||
return versions
|
||||
}
|
||||
|
||||
// ParseAPIVersion parses an API version string into an APIVersion
|
||||
// Format (Expected, not enforced):
|
||||
// API version string = <API type> '/' <API version>
|
||||
// API type = [a-z][a-z0-9]*
|
||||
// API version = [0-9]+(\.[0-9]+)?
|
||||
// TODO(dmcgowan): Enforce format, add error condition, remove unknown type
|
||||
func ParseAPIVersion(versionStr string) APIVersion {
|
||||
idx := strings.IndexRune(versionStr, '/')
|
||||
if idx == -1 {
|
||||
return APIVersion{
|
||||
Type: "unknown",
|
||||
Version: versionStr,
|
||||
}
|
||||
}
|
||||
return APIVersion{
|
||||
Type: strings.ToLower(versionStr[:idx]),
|
||||
Version: versionStr[idx+1:],
|
||||
}
|
||||
}
|
219
vendor/src/github.com/docker/distribution/registry/client/auth/authchallenge.go
vendored
Normal file
219
vendor/src/github.com/docker/distribution/registry/client/auth/authchallenge.go
vendored
Normal file
|
@ -0,0 +1,219 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Challenge carries information from a WWW-Authenticate response header.
|
||||
// See RFC 2617.
|
||||
type Challenge struct {
|
||||
// Scheme is the auth-scheme according to RFC 2617
|
||||
Scheme string
|
||||
|
||||
// Parameters are the auth-params according to RFC 2617
|
||||
Parameters map[string]string
|
||||
}
|
||||
|
||||
// ChallengeManager manages the challenges for endpoints.
|
||||
// The challenges are pulled out of HTTP responses. Only
|
||||
// responses which expect challenges should be added to
|
||||
// the manager, since a non-unauthorized request will be
|
||||
// viewed as not requiring challenges.
|
||||
type ChallengeManager interface {
|
||||
// GetChallenges returns the challenges for the given
|
||||
// endpoint URL.
|
||||
GetChallenges(endpoint string) ([]Challenge, error)
|
||||
|
||||
// AddResponse adds the response to the challenge
|
||||
// manager. The challenges will be parsed out of
|
||||
// the WWW-Authenicate headers and added to the
|
||||
// URL which was produced the response. If the
|
||||
// response was authorized, any challenges for the
|
||||
// endpoint will be cleared.
|
||||
AddResponse(resp *http.Response) error
|
||||
}
|
||||
|
||||
// NewSimpleChallengeManager returns an instance of
|
||||
// ChallengeManger which only maps endpoints to challenges
|
||||
// based on the responses which have been added the
|
||||
// manager. The simple manager will make no attempt to
|
||||
// perform requests on the endpoints or cache the responses
|
||||
// to a backend.
|
||||
func NewSimpleChallengeManager() ChallengeManager {
|
||||
return simpleChallengeManager{}
|
||||
}
|
||||
|
||||
type simpleChallengeManager map[string][]Challenge
|
||||
|
||||
func (m simpleChallengeManager) GetChallenges(endpoint string) ([]Challenge, error) {
|
||||
challenges := m[endpoint]
|
||||
return challenges, nil
|
||||
}
|
||||
|
||||
func (m simpleChallengeManager) AddResponse(resp *http.Response) error {
|
||||
challenges := ResponseChallenges(resp)
|
||||
if resp.Request == nil {
|
||||
return fmt.Errorf("missing request reference")
|
||||
}
|
||||
urlCopy := url.URL{
|
||||
Path: resp.Request.URL.Path,
|
||||
Host: resp.Request.URL.Host,
|
||||
Scheme: resp.Request.URL.Scheme,
|
||||
}
|
||||
m[urlCopy.String()] = challenges
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Octet types from RFC 2616.
|
||||
type octetType byte
|
||||
|
||||
var octetTypes [256]octetType
|
||||
|
||||
const (
|
||||
isToken octetType = 1 << iota
|
||||
isSpace
|
||||
)
|
||||
|
||||
func init() {
|
||||
// OCTET = <any 8-bit sequence of data>
|
||||
// CHAR = <any US-ASCII character (octets 0 - 127)>
|
||||
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
|
||||
// CR = <US-ASCII CR, carriage return (13)>
|
||||
// LF = <US-ASCII LF, linefeed (10)>
|
||||
// SP = <US-ASCII SP, space (32)>
|
||||
// HT = <US-ASCII HT, horizontal-tab (9)>
|
||||
// <"> = <US-ASCII double-quote mark (34)>
|
||||
// CRLF = CR LF
|
||||
// LWS = [CRLF] 1*( SP | HT )
|
||||
// TEXT = <any OCTET except CTLs, but including LWS>
|
||||
// separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
|
||||
// | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
|
||||
// token = 1*<any CHAR except CTLs or separators>
|
||||
// qdtext = <any TEXT except <">>
|
||||
|
||||
for c := 0; c < 256; c++ {
|
||||
var t octetType
|
||||
isCtl := c <= 31 || c == 127
|
||||
isChar := 0 <= c && c <= 127
|
||||
isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0
|
||||
if strings.IndexRune(" \t\r\n", rune(c)) >= 0 {
|
||||
t |= isSpace
|
||||
}
|
||||
if isChar && !isCtl && !isSeparator {
|
||||
t |= isToken
|
||||
}
|
||||
octetTypes[c] = t
|
||||
}
|
||||
}
|
||||
|
||||
// ResponseChallenges returns a list of authorization challenges
|
||||
// for the given http Response. Challenges are only checked if
|
||||
// the response status code was a 401.
|
||||
func ResponseChallenges(resp *http.Response) []Challenge {
|
||||
if resp.StatusCode == http.StatusUnauthorized {
|
||||
// Parse the WWW-Authenticate Header and store the challenges
|
||||
// on this endpoint object.
|
||||
return parseAuthHeader(resp.Header)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseAuthHeader(header http.Header) []Challenge {
|
||||
challenges := []Challenge{}
|
||||
for _, h := range header[http.CanonicalHeaderKey("WWW-Authenticate")] {
|
||||
v, p := parseValueAndParams(h)
|
||||
if v != "" {
|
||||
challenges = append(challenges, Challenge{Scheme: v, Parameters: p})
|
||||
}
|
||||
}
|
||||
return challenges
|
||||
}
|
||||
|
||||
func parseValueAndParams(header string) (value string, params map[string]string) {
|
||||
params = make(map[string]string)
|
||||
value, s := expectToken(header)
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
value = strings.ToLower(value)
|
||||
s = "," + skipSpace(s)
|
||||
for strings.HasPrefix(s, ",") {
|
||||
var pkey string
|
||||
pkey, s = expectToken(skipSpace(s[1:]))
|
||||
if pkey == "" {
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(s, "=") {
|
||||
return
|
||||
}
|
||||
var pvalue string
|
||||
pvalue, s = expectTokenOrQuoted(s[1:])
|
||||
if pvalue == "" {
|
||||
return
|
||||
}
|
||||
pkey = strings.ToLower(pkey)
|
||||
params[pkey] = pvalue
|
||||
s = skipSpace(s)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func skipSpace(s string) (rest string) {
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
if octetTypes[s[i]]&isSpace == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return s[i:]
|
||||
}
|
||||
|
||||
func expectToken(s string) (token, rest string) {
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
if octetTypes[s[i]]&isToken == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return s[:i], s[i:]
|
||||
}
|
||||
|
||||
func expectTokenOrQuoted(s string) (value string, rest string) {
|
||||
if !strings.HasPrefix(s, "\"") {
|
||||
return expectToken(s)
|
||||
}
|
||||
s = s[1:]
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '"':
|
||||
return s[:i], s[i+1:]
|
||||
case '\\':
|
||||
p := make([]byte, len(s)-1)
|
||||
j := copy(p, s[:i])
|
||||
escape := true
|
||||
for i = i + 1; i < len(s); i++ {
|
||||
b := s[i]
|
||||
switch {
|
||||
case escape:
|
||||
escape = false
|
||||
p[j] = b
|
||||
j++
|
||||
case b == '\\':
|
||||
escape = true
|
||||
case b == '"':
|
||||
return string(p[:j]), s[i+1:]
|
||||
default:
|
||||
p[j] = b
|
||||
j++
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
255
vendor/src/github.com/docker/distribution/registry/client/auth/session.go
vendored
Normal file
255
vendor/src/github.com/docker/distribution/registry/client/auth/session.go
vendored
Normal file
|
@ -0,0 +1,255 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
)
|
||||
|
||||
// AuthenticationHandler is an interface for authorizing a request from
|
||||
// params from a "WWW-Authenicate" header for a single scheme.
|
||||
type AuthenticationHandler interface {
|
||||
// Scheme returns the scheme as expected from the "WWW-Authenicate" header.
|
||||
Scheme() string
|
||||
|
||||
// AuthorizeRequest adds the authorization header to a request (if needed)
|
||||
// using the parameters from "WWW-Authenticate" method. The parameters
|
||||
// values depend on the scheme.
|
||||
AuthorizeRequest(req *http.Request, params map[string]string) error
|
||||
}
|
||||
|
||||
// CredentialStore is an interface for getting credentials for
|
||||
// a given URL
|
||||
type CredentialStore interface {
|
||||
// Basic returns basic auth for the given URL
|
||||
Basic(*url.URL) (string, string)
|
||||
}
|
||||
|
||||
// NewAuthorizer creates an authorizer which can handle multiple authentication
|
||||
// schemes. The handlers are tried in order, the higher priority authentication
|
||||
// methods should be first. The challengeMap holds a list of challenges for
|
||||
// a given root API endpoint (for example "https://registry-1.docker.io/v2/").
|
||||
func NewAuthorizer(manager ChallengeManager, handlers ...AuthenticationHandler) transport.RequestModifier {
|
||||
return &endpointAuthorizer{
|
||||
challenges: manager,
|
||||
handlers: handlers,
|
||||
}
|
||||
}
|
||||
|
||||
type endpointAuthorizer struct {
|
||||
challenges ChallengeManager
|
||||
handlers []AuthenticationHandler
|
||||
transport http.RoundTripper
|
||||
}
|
||||
|
||||
func (ea *endpointAuthorizer) ModifyRequest(req *http.Request) error {
|
||||
v2Root := strings.Index(req.URL.Path, "/v2/")
|
||||
if v2Root == -1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ping := url.URL{
|
||||
Host: req.URL.Host,
|
||||
Scheme: req.URL.Scheme,
|
||||
Path: req.URL.Path[:v2Root+4],
|
||||
}
|
||||
|
||||
pingEndpoint := ping.String()
|
||||
|
||||
challenges, err := ea.challenges.GetChallenges(pingEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(challenges) > 0 {
|
||||
for _, handler := range ea.handlers {
|
||||
for _, challenge := range challenges {
|
||||
if challenge.Scheme != handler.Scheme() {
|
||||
continue
|
||||
}
|
||||
if err := handler.AuthorizeRequest(req, challenge.Parameters); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type tokenHandler struct {
|
||||
header http.Header
|
||||
creds CredentialStore
|
||||
scope tokenScope
|
||||
transport http.RoundTripper
|
||||
|
||||
tokenLock sync.Mutex
|
||||
tokenCache string
|
||||
tokenExpiration time.Time
|
||||
}
|
||||
|
||||
// tokenScope represents the scope at which a token will be requested.
|
||||
// This represents a specific action on a registry resource.
|
||||
type tokenScope struct {
|
||||
Resource string
|
||||
Scope string
|
||||
Actions []string
|
||||
}
|
||||
|
||||
func (ts tokenScope) String() string {
|
||||
return fmt.Sprintf("%s:%s:%s", ts.Resource, ts.Scope, strings.Join(ts.Actions, ","))
|
||||
}
|
||||
|
||||
// NewTokenHandler creates a new AuthenicationHandler which supports
|
||||
// fetching tokens from a remote token server.
|
||||
func NewTokenHandler(transport http.RoundTripper, creds CredentialStore, scope string, actions ...string) AuthenticationHandler {
|
||||
return &tokenHandler{
|
||||
transport: transport,
|
||||
creds: creds,
|
||||
scope: tokenScope{
|
||||
Resource: "repository",
|
||||
Scope: scope,
|
||||
Actions: actions,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (th *tokenHandler) client() *http.Client {
|
||||
return &http.Client{
|
||||
Transport: th.transport,
|
||||
Timeout: 15 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func (th *tokenHandler) Scheme() string {
|
||||
return "bearer"
|
||||
}
|
||||
|
||||
func (th *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
|
||||
if err := th.refreshToken(params); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", th.tokenCache))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (th *tokenHandler) refreshToken(params map[string]string) error {
|
||||
th.tokenLock.Lock()
|
||||
defer th.tokenLock.Unlock()
|
||||
now := time.Now()
|
||||
if now.After(th.tokenExpiration) {
|
||||
token, err := th.fetchToken(params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
th.tokenCache = token
|
||||
th.tokenExpiration = now.Add(time.Minute)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type tokenResponse struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func (th *tokenHandler) fetchToken(params map[string]string) (token string, err error) {
|
||||
//log.Debugf("Getting bearer token with %s for %s", challenge.Parameters, ta.auth.Username)
|
||||
realm, ok := params["realm"]
|
||||
if !ok {
|
||||
return "", errors.New("no realm specified for token auth challenge")
|
||||
}
|
||||
|
||||
// TODO(dmcgowan): Handle empty scheme
|
||||
|
||||
realmURL, err := url.Parse(realm)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid token auth challenge realm: %s", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", realmURL.String(), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
reqParams := req.URL.Query()
|
||||
service := params["service"]
|
||||
scope := th.scope.String()
|
||||
|
||||
if service != "" {
|
||||
reqParams.Add("service", service)
|
||||
}
|
||||
|
||||
for _, scopeField := range strings.Fields(scope) {
|
||||
reqParams.Add("scope", scopeField)
|
||||
}
|
||||
|
||||
if th.creds != nil {
|
||||
username, password := th.creds.Basic(realmURL)
|
||||
if username != "" && password != "" {
|
||||
reqParams.Add("account", username)
|
||||
req.SetBasicAuth(username, password)
|
||||
}
|
||||
}
|
||||
|
||||
req.URL.RawQuery = reqParams.Encode()
|
||||
|
||||
resp, err := th.client().Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("token auth attempt for registry: %s request failed with status: %d %s", req.URL, resp.StatusCode, http.StatusText(resp.StatusCode))
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
|
||||
tr := new(tokenResponse)
|
||||
if err = decoder.Decode(tr); err != nil {
|
||||
return "", fmt.Errorf("unable to decode token response: %s", err)
|
||||
}
|
||||
|
||||
if tr.Token == "" {
|
||||
return "", errors.New("authorization server did not include a token in the response")
|
||||
}
|
||||
|
||||
return tr.Token, nil
|
||||
}
|
||||
|
||||
type basicHandler struct {
|
||||
creds CredentialStore
|
||||
}
|
||||
|
||||
// NewBasicHandler creaters a new authentiation handler which adds
|
||||
// basic authentication credentials to a request.
|
||||
func NewBasicHandler(creds CredentialStore) AuthenticationHandler {
|
||||
return &basicHandler{
|
||||
creds: creds,
|
||||
}
|
||||
}
|
||||
|
||||
func (*basicHandler) Scheme() string {
|
||||
return "basic"
|
||||
}
|
||||
|
||||
func (bh *basicHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
|
||||
if bh.creds != nil {
|
||||
username, password := bh.creds.Basic(req.URL)
|
||||
if username != "" && password != "" {
|
||||
req.SetBasicAuth(username, password)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("no basic auth credentials")
|
||||
}
|
174
vendor/src/github.com/docker/distribution/registry/client/blob_writer.go
vendored
Normal file
174
vendor/src/github.com/docker/distribution/registry/client/blob_writer.go
vendored
Normal file
|
@ -0,0 +1,174 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/context"
|
||||
)
|
||||
|
||||
type httpBlobUpload struct {
|
||||
statter distribution.BlobStatter
|
||||
client *http.Client
|
||||
|
||||
uuid string
|
||||
startedAt time.Time
|
||||
|
||||
location string // always the last value of the location header.
|
||||
offset int64
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (hbu *httpBlobUpload) handleErrorResponse(resp *http.Response) error {
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return distribution.ErrBlobUploadUnknown
|
||||
}
|
||||
return handleErrorResponse(resp)
|
||||
}
|
||||
|
||||
func (hbu *httpBlobUpload) ReadFrom(r io.Reader) (n int64, err error) {
|
||||
req, err := http.NewRequest("PATCH", hbu.location, ioutil.NopCloser(r))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer req.Body.Close()
|
||||
|
||||
resp, err := hbu.client.Do(req)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusAccepted {
|
||||
return 0, hbu.handleErrorResponse(resp)
|
||||
}
|
||||
|
||||
hbu.uuid = resp.Header.Get("Docker-Upload-UUID")
|
||||
hbu.location, err = sanitizeLocation(resp.Header.Get("Location"), hbu.location)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
rng := resp.Header.Get("Range")
|
||||
var start, end int64
|
||||
if n, err := fmt.Sscanf(rng, "%d-%d", &start, &end); err != nil {
|
||||
return 0, err
|
||||
} else if n != 2 || end < start {
|
||||
return 0, fmt.Errorf("bad range format: %s", rng)
|
||||
}
|
||||
|
||||
return (end - start + 1), nil
|
||||
|
||||
}
|
||||
|
||||
func (hbu *httpBlobUpload) Write(p []byte) (n int, err error) {
|
||||
req, err := http.NewRequest("PATCH", hbu.location, bytes.NewReader(p))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
req.Header.Set("Content-Range", fmt.Sprintf("%d-%d", hbu.offset, hbu.offset+int64(len(p)-1)))
|
||||
req.Header.Set("Content-Length", fmt.Sprintf("%d", len(p)))
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
|
||||
resp, err := hbu.client.Do(req)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusAccepted {
|
||||
return 0, hbu.handleErrorResponse(resp)
|
||||
}
|
||||
|
||||
hbu.uuid = resp.Header.Get("Docker-Upload-UUID")
|
||||
hbu.location, err = sanitizeLocation(resp.Header.Get("Location"), hbu.location)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
rng := resp.Header.Get("Range")
|
||||
var start, end int
|
||||
if n, err := fmt.Sscanf(rng, "%d-%d", &start, &end); err != nil {
|
||||
return 0, err
|
||||
} else if n != 2 || end < start {
|
||||
return 0, fmt.Errorf("bad range format: %s", rng)
|
||||
}
|
||||
|
||||
return (end - start + 1), nil
|
||||
|
||||
}
|
||||
|
||||
func (hbu *httpBlobUpload) Seek(offset int64, whence int) (int64, error) {
|
||||
newOffset := hbu.offset
|
||||
|
||||
switch whence {
|
||||
case os.SEEK_CUR:
|
||||
newOffset += int64(offset)
|
||||
case os.SEEK_END:
|
||||
newOffset += int64(offset)
|
||||
case os.SEEK_SET:
|
||||
newOffset = int64(offset)
|
||||
}
|
||||
|
||||
hbu.offset = newOffset
|
||||
|
||||
return hbu.offset, nil
|
||||
}
|
||||
|
||||
func (hbu *httpBlobUpload) ID() string {
|
||||
return hbu.uuid
|
||||
}
|
||||
|
||||
func (hbu *httpBlobUpload) StartedAt() time.Time {
|
||||
return hbu.startedAt
|
||||
}
|
||||
|
||||
func (hbu *httpBlobUpload) Commit(ctx context.Context, desc distribution.Descriptor) (distribution.Descriptor, error) {
|
||||
// TODO(dmcgowan): Check if already finished, if so just fetch
|
||||
req, err := http.NewRequest("PUT", hbu.location, nil)
|
||||
if err != nil {
|
||||
return distribution.Descriptor{}, err
|
||||
}
|
||||
|
||||
values := req.URL.Query()
|
||||
values.Set("digest", desc.Digest.String())
|
||||
req.URL.RawQuery = values.Encode()
|
||||
|
||||
resp, err := hbu.client.Do(req)
|
||||
if err != nil {
|
||||
return distribution.Descriptor{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
return distribution.Descriptor{}, hbu.handleErrorResponse(resp)
|
||||
}
|
||||
|
||||
return hbu.statter.Stat(ctx, desc.Digest)
|
||||
}
|
||||
|
||||
func (hbu *httpBlobUpload) Cancel(ctx context.Context) error {
|
||||
req, err := http.NewRequest("DELETE", hbu.location, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := hbu.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusNoContent, http.StatusNotFound:
|
||||
return nil
|
||||
default:
|
||||
return hbu.handleErrorResponse(resp)
|
||||
}
|
||||
}
|
||||
|
||||
func (hbu *httpBlobUpload) Close() error {
|
||||
hbu.closed = true
|
||||
return nil
|
||||
}
|
66
vendor/src/github.com/docker/distribution/registry/client/errors.go
vendored
Normal file
66
vendor/src/github.com/docker/distribution/registry/client/errors.go
vendored
Normal file
|
@ -0,0 +1,66 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/distribution/registry/api/errcode"
|
||||
"github.com/docker/distribution/registry/api/v2"
|
||||
)
|
||||
|
||||
// UnexpectedHTTPStatusError is returned when an unexpected HTTP status is
|
||||
// returned when making a registry api call.
|
||||
type UnexpectedHTTPStatusError struct {
|
||||
Status string
|
||||
}
|
||||
|
||||
func (e *UnexpectedHTTPStatusError) Error() string {
|
||||
return fmt.Sprintf("Received unexpected HTTP status: %s", e.Status)
|
||||
}
|
||||
|
||||
// UnexpectedHTTPResponseError is returned when an expected HTTP status code
|
||||
// is returned, but the content was unexpected and failed to be parsed.
|
||||
type UnexpectedHTTPResponseError struct {
|
||||
ParseErr error
|
||||
Response []byte
|
||||
}
|
||||
|
||||
func (e *UnexpectedHTTPResponseError) Error() string {
|
||||
return fmt.Sprintf("Error parsing HTTP response: %s: %q", e.ParseErr.Error(), string(e.Response))
|
||||
}
|
||||
|
||||
func parseHTTPErrorResponse(r io.Reader) error {
|
||||
var errors errcode.Errors
|
||||
body, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &errors); err != nil {
|
||||
return &UnexpectedHTTPResponseError{
|
||||
ParseErr: err,
|
||||
Response: body,
|
||||
}
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
func handleErrorResponse(resp *http.Response) error {
|
||||
if resp.StatusCode == 401 {
|
||||
err := parseHTTPErrorResponse(resp.Body)
|
||||
if uErr, ok := err.(*UnexpectedHTTPResponseError); ok {
|
||||
return &errcode.Error{
|
||||
Code: v2.ErrorCodeUnauthorized,
|
||||
Detail: uErr.Response,
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
|
||||
return parseHTTPErrorResponse(resp.Body)
|
||||
}
|
||||
return &UnexpectedHTTPStatusError{Status: resp.Status}
|
||||
}
|
412
vendor/src/github.com/docker/distribution/registry/client/repository.go
vendored
Normal file
412
vendor/src/github.com/docker/distribution/registry/client/repository.go
vendored
Normal file
|
@ -0,0 +1,412 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/context"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/manifest"
|
||||
"github.com/docker/distribution/registry/api/v2"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
"github.com/docker/distribution/registry/storage/cache"
|
||||
"github.com/docker/distribution/registry/storage/cache/memory"
|
||||
)
|
||||
|
||||
// NewRepository creates a new Repository for the given repository name and base URL
|
||||
func NewRepository(ctx context.Context, name, baseURL string, transport http.RoundTripper) (distribution.Repository, error) {
|
||||
if err := v2.ValidateRepositoryName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ub, err := v2.NewURLBuilderFromString(baseURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
// TODO(dmcgowan): create cookie jar
|
||||
}
|
||||
|
||||
return &repository{
|
||||
client: client,
|
||||
ub: ub,
|
||||
name: name,
|
||||
context: ctx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
client *http.Client
|
||||
ub *v2.URLBuilder
|
||||
context context.Context
|
||||
name string
|
||||
}
|
||||
|
||||
func (r *repository) Name() string {
|
||||
return r.name
|
||||
}
|
||||
|
||||
func (r *repository) Blobs(ctx context.Context) distribution.BlobStore {
|
||||
statter := &blobStatter{
|
||||
name: r.Name(),
|
||||
ub: r.ub,
|
||||
client: r.client,
|
||||
}
|
||||
return &blobs{
|
||||
name: r.Name(),
|
||||
ub: r.ub,
|
||||
client: r.client,
|
||||
statter: cache.NewCachedBlobStatter(memory.NewInMemoryBlobDescriptorCacheProvider(), statter),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *repository) Manifests() distribution.ManifestService {
|
||||
return &manifests{
|
||||
name: r.Name(),
|
||||
ub: r.ub,
|
||||
client: r.client,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *repository) Signatures() distribution.SignatureService {
|
||||
return &signatures{
|
||||
manifests: r.Manifests(),
|
||||
}
|
||||
}
|
||||
|
||||
type signatures struct {
|
||||
manifests distribution.ManifestService
|
||||
}
|
||||
|
||||
func (s *signatures) Get(dgst digest.Digest) ([][]byte, error) {
|
||||
m, err := s.manifests.Get(dgst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m.Signatures()
|
||||
}
|
||||
|
||||
func (s *signatures) Put(dgst digest.Digest, signatures ...[]byte) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
type manifests struct {
|
||||
name string
|
||||
ub *v2.URLBuilder
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func (ms *manifests) Tags() ([]string, error) {
|
||||
u, err := ms.ub.BuildTagsURL(ms.name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := ms.client.Get(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tagsResponse := struct {
|
||||
Tags []string `json:"tags"`
|
||||
}{}
|
||||
if err := json.Unmarshal(b, &tagsResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tagsResponse.Tags, nil
|
||||
case http.StatusNotFound:
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, handleErrorResponse(resp)
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *manifests) Exists(dgst digest.Digest) (bool, error) {
|
||||
// Call by Tag endpoint since the API uses the same
|
||||
// URL endpoint for tags and digests.
|
||||
return ms.ExistsByTag(dgst.String())
|
||||
}
|
||||
|
||||
func (ms *manifests) ExistsByTag(tag string) (bool, error) {
|
||||
u, err := ms.ub.BuildManifestURL(ms.name, tag)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
resp, err := ms.client.Head(u)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return true, nil
|
||||
case http.StatusNotFound:
|
||||
return false, nil
|
||||
default:
|
||||
return false, handleErrorResponse(resp)
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *manifests) Get(dgst digest.Digest) (*manifest.SignedManifest, error) {
|
||||
// Call by Tag endpoint since the API uses the same
|
||||
// URL endpoint for tags and digests.
|
||||
return ms.GetByTag(dgst.String())
|
||||
}
|
||||
|
||||
func (ms *manifests) GetByTag(tag string) (*manifest.SignedManifest, error) {
|
||||
u, err := ms.ub.BuildManifestURL(ms.name, tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := ms.client.Get(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
var sm manifest.SignedManifest
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
|
||||
if err := decoder.Decode(&sm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &sm, nil
|
||||
default:
|
||||
return nil, handleErrorResponse(resp)
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *manifests) Put(m *manifest.SignedManifest) error {
|
||||
manifestURL, err := ms.ub.BuildManifestURL(ms.name, m.Tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
putRequest, err := http.NewRequest("PUT", manifestURL, bytes.NewReader(m.Raw))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := ms.client.Do(putRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusAccepted:
|
||||
// TODO(dmcgowan): make use of digest header
|
||||
return nil
|
||||
default:
|
||||
return handleErrorResponse(resp)
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *manifests) Delete(dgst digest.Digest) error {
|
||||
u, err := ms.ub.BuildManifestURL(ms.name, dgst.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := ms.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return nil
|
||||
default:
|
||||
return handleErrorResponse(resp)
|
||||
}
|
||||
}
|
||||
|
||||
type blobs struct {
|
||||
name string
|
||||
ub *v2.URLBuilder
|
||||
client *http.Client
|
||||
|
||||
statter distribution.BlobStatter
|
||||
}
|
||||
|
||||
func sanitizeLocation(location, source string) (string, error) {
|
||||
locationURL, err := url.Parse(location)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if locationURL.Scheme == "" {
|
||||
sourceURL, err := url.Parse(source)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
locationURL = &url.URL{
|
||||
Scheme: sourceURL.Scheme,
|
||||
Host: sourceURL.Host,
|
||||
Path: location,
|
||||
}
|
||||
location = locationURL.String()
|
||||
}
|
||||
return location, nil
|
||||
}
|
||||
|
||||
func (bs *blobs) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
|
||||
return bs.statter.Stat(ctx, dgst)
|
||||
|
||||
}
|
||||
|
||||
func (bs *blobs) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) {
|
||||
desc, err := bs.Stat(ctx, dgst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reader, err := bs.Open(ctx, desc.Digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
return ioutil.ReadAll(reader)
|
||||
}
|
||||
|
||||
func (bs *blobs) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) {
|
||||
stat, err := bs.statter.Stat(ctx, dgst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blobURL, err := bs.ub.BuildBlobURL(bs.name, stat.Digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return transport.NewHTTPReadSeeker(bs.client, blobURL, stat.Length), nil
|
||||
}
|
||||
|
||||
func (bs *blobs) ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (bs *blobs) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) {
|
||||
writer, err := bs.Create(ctx)
|
||||
if err != nil {
|
||||
return distribution.Descriptor{}, err
|
||||
}
|
||||
dgstr := digest.Canonical.New()
|
||||
n, err := io.Copy(writer, io.TeeReader(bytes.NewReader(p), dgstr.Hash()))
|
||||
if err != nil {
|
||||
return distribution.Descriptor{}, err
|
||||
}
|
||||
if n < int64(len(p)) {
|
||||
return distribution.Descriptor{}, fmt.Errorf("short copy: wrote %d of %d", n, len(p))
|
||||
}
|
||||
|
||||
desc := distribution.Descriptor{
|
||||
MediaType: mediaType,
|
||||
Length: int64(len(p)),
|
||||
Digest: dgstr.Digest(),
|
||||
}
|
||||
|
||||
return writer.Commit(ctx, desc)
|
||||
}
|
||||
|
||||
func (bs *blobs) Create(ctx context.Context) (distribution.BlobWriter, error) {
|
||||
u, err := bs.ub.BuildBlobUploadURL(bs.name)
|
||||
|
||||
resp, err := bs.client.Post(u, "", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusAccepted:
|
||||
// TODO(dmcgowan): Check for invalid UUID
|
||||
uuid := resp.Header.Get("Docker-Upload-UUID")
|
||||
location, err := sanitizeLocation(resp.Header.Get("Location"), u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &httpBlobUpload{
|
||||
statter: bs.statter,
|
||||
client: bs.client,
|
||||
uuid: uuid,
|
||||
startedAt: time.Now(),
|
||||
location: location,
|
||||
}, nil
|
||||
default:
|
||||
return nil, handleErrorResponse(resp)
|
||||
}
|
||||
}
|
||||
|
||||
func (bs *blobs) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
type blobStatter struct {
|
||||
name string
|
||||
ub *v2.URLBuilder
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func (bs *blobStatter) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
|
||||
u, err := bs.ub.BuildBlobURL(bs.name, dgst)
|
||||
if err != nil {
|
||||
return distribution.Descriptor{}, err
|
||||
}
|
||||
|
||||
resp, err := bs.client.Head(u)
|
||||
if err != nil {
|
||||
return distribution.Descriptor{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
lengthHeader := resp.Header.Get("Content-Length")
|
||||
length, err := strconv.ParseInt(lengthHeader, 10, 64)
|
||||
if err != nil {
|
||||
return distribution.Descriptor{}, fmt.Errorf("error parsing content-length: %v", err)
|
||||
}
|
||||
|
||||
return distribution.Descriptor{
|
||||
MediaType: resp.Header.Get("Content-Type"),
|
||||
Length: length,
|
||||
Digest: dgst,
|
||||
}, nil
|
||||
case http.StatusNotFound:
|
||||
return distribution.Descriptor{}, distribution.ErrBlobUnknown
|
||||
default:
|
||||
return distribution.Descriptor{}, handleErrorResponse(resp)
|
||||
}
|
||||
}
|
172
vendor/src/github.com/docker/distribution/registry/client/transport/http_reader.go
vendored
Normal file
172
vendor/src/github.com/docker/distribution/registry/client/transport/http_reader.go
vendored
Normal file
|
@ -0,0 +1,172 @@
|
|||
package transport
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
// ReadSeekCloser combines io.ReadSeeker with io.Closer.
|
||||
type ReadSeekCloser interface {
|
||||
io.ReadSeeker
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// NewHTTPReadSeeker handles reading from an HTTP endpoint using a GET
|
||||
// request. When seeking and starting a read from a non-zero offset
|
||||
// the a "Range" header will be added which sets the offset.
|
||||
// TODO(dmcgowan): Move this into a separate utility package
|
||||
func NewHTTPReadSeeker(client *http.Client, url string, size int64) ReadSeekCloser {
|
||||
return &httpReadSeeker{
|
||||
client: client,
|
||||
url: url,
|
||||
size: size,
|
||||
}
|
||||
}
|
||||
|
||||
type httpReadSeeker struct {
|
||||
client *http.Client
|
||||
url string
|
||||
|
||||
size int64
|
||||
|
||||
rc io.ReadCloser // remote read closer
|
||||
brd *bufio.Reader // internal buffered io
|
||||
offset int64
|
||||
err error
|
||||
}
|
||||
|
||||
func (hrs *httpReadSeeker) Read(p []byte) (n int, err error) {
|
||||
if hrs.err != nil {
|
||||
return 0, hrs.err
|
||||
}
|
||||
|
||||
rd, err := hrs.reader()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
n, err = rd.Read(p)
|
||||
hrs.offset += int64(n)
|
||||
|
||||
// Simulate io.EOF error if we reach filesize.
|
||||
if err == nil && hrs.offset >= hrs.size {
|
||||
err = io.EOF
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (hrs *httpReadSeeker) Seek(offset int64, whence int) (int64, error) {
|
||||
if hrs.err != nil {
|
||||
return 0, hrs.err
|
||||
}
|
||||
|
||||
var err error
|
||||
newOffset := hrs.offset
|
||||
|
||||
switch whence {
|
||||
case os.SEEK_CUR:
|
||||
newOffset += int64(offset)
|
||||
case os.SEEK_END:
|
||||
newOffset = hrs.size + int64(offset)
|
||||
case os.SEEK_SET:
|
||||
newOffset = int64(offset)
|
||||
}
|
||||
|
||||
if newOffset < 0 {
|
||||
err = errors.New("cannot seek to negative position")
|
||||
} else {
|
||||
if hrs.offset != newOffset {
|
||||
hrs.reset()
|
||||
}
|
||||
|
||||
// No problems, set the offset.
|
||||
hrs.offset = newOffset
|
||||
}
|
||||
|
||||
return hrs.offset, err
|
||||
}
|
||||
|
||||
func (hrs *httpReadSeeker) Close() error {
|
||||
if hrs.err != nil {
|
||||
return hrs.err
|
||||
}
|
||||
|
||||
// close and release reader chain
|
||||
if hrs.rc != nil {
|
||||
hrs.rc.Close()
|
||||
}
|
||||
|
||||
hrs.rc = nil
|
||||
hrs.brd = nil
|
||||
|
||||
hrs.err = errors.New("httpLayer: closed")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hrs *httpReadSeeker) reset() {
|
||||
if hrs.err != nil {
|
||||
return
|
||||
}
|
||||
if hrs.rc != nil {
|
||||
hrs.rc.Close()
|
||||
hrs.rc = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (hrs *httpReadSeeker) reader() (io.Reader, error) {
|
||||
if hrs.err != nil {
|
||||
return nil, hrs.err
|
||||
}
|
||||
|
||||
if hrs.rc != nil {
|
||||
return hrs.brd, nil
|
||||
}
|
||||
|
||||
// If the offset is great than or equal to size, return a empty, noop reader.
|
||||
if hrs.offset >= hrs.size {
|
||||
return ioutil.NopCloser(bytes.NewReader([]byte{})), nil
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", hrs.url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if hrs.offset > 0 {
|
||||
// TODO(stevvooe): Get this working correctly.
|
||||
|
||||
// If we are at different offset, issue a range request from there.
|
||||
req.Header.Add("Range", "1-")
|
||||
// TODO: get context in here
|
||||
// context.GetLogger(hrs.context).Infof("Range: %s", req.Header.Get("Range"))
|
||||
}
|
||||
|
||||
resp, err := hrs.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case resp.StatusCode == 200:
|
||||
hrs.rc = resp.Body
|
||||
default:
|
||||
defer resp.Body.Close()
|
||||
return nil, fmt.Errorf("unexpected status resolving reader: %v", resp.Status)
|
||||
}
|
||||
|
||||
if hrs.brd == nil {
|
||||
hrs.brd = bufio.NewReader(hrs.rc)
|
||||
} else {
|
||||
hrs.brd.Reset(hrs.rc)
|
||||
}
|
||||
|
||||
return hrs.brd, nil
|
||||
}
|
|
@ -6,17 +6,16 @@ import (
|
|||
"sync"
|
||||
)
|
||||
|
||||
// RequestModifier represents an object which will do an inplace
|
||||
// modification of an HTTP request.
|
||||
type RequestModifier interface {
|
||||
ModifyRequest(*http.Request) error
|
||||
}
|
||||
|
||||
type headerModifier http.Header
|
||||
|
||||
// NewHeaderRequestModifier returns a RequestModifier that merges the HTTP headers
|
||||
// passed as an argument, with the HTTP headers of a request.
|
||||
//
|
||||
// If the same key is present in both, the modifying header values for that key,
|
||||
// are appended to the values for that same key in the request header.
|
||||
// NewHeaderRequestModifier returns a new RequestModifier which will
|
||||
// add the given headers to a request.
|
||||
func NewHeaderRequestModifier(header http.Header) RequestModifier {
|
||||
return headerModifier(header)
|
||||
}
|
||||
|
@ -29,9 +28,8 @@ func (h headerModifier) ModifyRequest(req *http.Request) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// NewTransport returns an http.RoundTripper that modifies requests according to
|
||||
// the RequestModifiers passed in the arguments, before sending the requests to
|
||||
// the base http.RoundTripper (which, if nil, defaults to http.DefaultTransport).
|
||||
// NewTransport creates a new transport which will apply modifiers to
|
||||
// the request on a RoundTrip call.
|
||||
func NewTransport(base http.RoundTripper, modifiers ...RequestModifier) http.RoundTripper {
|
||||
return &transport{
|
||||
Modifiers: modifiers,
|
||||
|
@ -49,8 +47,11 @@ type transport struct {
|
|||
modReq map[*http.Request]*http.Request // original -> modified
|
||||
}
|
||||
|
||||
// RoundTrip authorizes and authenticates the request with an
|
||||
// access token. If no token exists or token is expired,
|
||||
// tries to refresh/fetch a new token.
|
||||
func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
req2 := CloneRequest(req)
|
||||
req2 := cloneRequest(req)
|
||||
for _, modifier := range t.Modifiers {
|
||||
if err := modifier.ModifyRequest(req2); err != nil {
|
||||
return nil, err
|
||||
|
@ -63,9 +64,9 @@ func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|||
t.setModReq(req, nil)
|
||||
return nil, err
|
||||
}
|
||||
res.Body = &OnEOFReader{
|
||||
Rc: res.Body,
|
||||
Fn: func() { t.setModReq(req, nil) },
|
||||
res.Body = &onEOFReader{
|
||||
rc: res.Body,
|
||||
fn: func() { t.setModReq(req, nil) },
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
@ -104,9 +105,9 @@ func (t *transport) setModReq(orig, mod *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
// CloneRequest returns a clone of the provided *http.Request.
|
||||
// cloneRequest returns a clone of the provided *http.Request.
|
||||
// The clone is a shallow copy of the struct and its Header map.
|
||||
func CloneRequest(r *http.Request) *http.Request {
|
||||
func cloneRequest(r *http.Request) *http.Request {
|
||||
// shallow copy of the struct
|
||||
r2 := new(http.Request)
|
||||
*r2 = *r
|
||||
|
@ -119,30 +120,28 @@ func CloneRequest(r *http.Request) *http.Request {
|
|||
return r2
|
||||
}
|
||||
|
||||
// OnEOFReader ensures a callback function is called
|
||||
// on Close() and when the underlying Reader returns an io.EOF error
|
||||
type OnEOFReader struct {
|
||||
Rc io.ReadCloser
|
||||
Fn func()
|
||||
type onEOFReader struct {
|
||||
rc io.ReadCloser
|
||||
fn func()
|
||||
}
|
||||
|
||||
func (r *OnEOFReader) Read(p []byte) (n int, err error) {
|
||||
n, err = r.Rc.Read(p)
|
||||
func (r *onEOFReader) Read(p []byte) (n int, err error) {
|
||||
n, err = r.rc.Read(p)
|
||||
if err == io.EOF {
|
||||
r.runFunc()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *OnEOFReader) Close() error {
|
||||
err := r.Rc.Close()
|
||||
func (r *onEOFReader) Close() error {
|
||||
err := r.rc.Close()
|
||||
r.runFunc()
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *OnEOFReader) runFunc() {
|
||||
if fn := r.Fn; fn != nil {
|
||||
func (r *onEOFReader) runFunc() {
|
||||
if fn := r.fn; fn != nil {
|
||||
fn()
|
||||
r.Fn = nil
|
||||
r.fn = nil
|
||||
}
|
||||
}
|
35
vendor/src/github.com/docker/distribution/registry/storage/cache/cache.go
vendored
Normal file
35
vendor/src/github.com/docker/distribution/registry/storage/cache/cache.go
vendored
Normal file
|
@ -0,0 +1,35 @@
|
|||
// Package cache provides facilities to speed up access to the storage
|
||||
// backend.
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/distribution"
|
||||
)
|
||||
|
||||
// BlobDescriptorCacheProvider provides repository scoped
|
||||
// BlobDescriptorService cache instances and a global descriptor cache.
|
||||
type BlobDescriptorCacheProvider interface {
|
||||
distribution.BlobDescriptorService
|
||||
|
||||
RepositoryScoped(repo string) (distribution.BlobDescriptorService, error)
|
||||
}
|
||||
|
||||
// ValidateDescriptor provides a helper function to ensure that caches have
|
||||
// common criteria for admitting descriptors.
|
||||
func ValidateDescriptor(desc distribution.Descriptor) error {
|
||||
if err := desc.Digest.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if desc.Length < 0 {
|
||||
return fmt.Errorf("cache: invalid length in descriptor: %v < 0", desc.Length)
|
||||
}
|
||||
|
||||
if desc.MediaType == "" {
|
||||
return fmt.Errorf("cache: empty mediatype on descriptor: %v", desc)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
80
vendor/src/github.com/docker/distribution/registry/storage/cache/cachedblobdescriptorstore.go
vendored
Normal file
80
vendor/src/github.com/docker/distribution/registry/storage/cache/cachedblobdescriptorstore.go
vendored
Normal file
|
@ -0,0 +1,80 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"github.com/docker/distribution/context"
|
||||
"github.com/docker/distribution/digest"
|
||||
|
||||
"github.com/docker/distribution"
|
||||
)
|
||||
|
||||
// Metrics is used to hold metric counters
|
||||
// related to the number of times a cache was
|
||||
// hit or missed.
|
||||
type Metrics struct {
|
||||
Requests uint64
|
||||
Hits uint64
|
||||
Misses uint64
|
||||
}
|
||||
|
||||
// MetricsTracker represents a metric tracker
|
||||
// which simply counts the number of hits and misses.
|
||||
type MetricsTracker interface {
|
||||
Hit()
|
||||
Miss()
|
||||
Metrics() Metrics
|
||||
}
|
||||
|
||||
type cachedBlobStatter struct {
|
||||
cache distribution.BlobDescriptorService
|
||||
backend distribution.BlobStatter
|
||||
tracker MetricsTracker
|
||||
}
|
||||
|
||||
// NewCachedBlobStatter creates a new statter which prefers a cache and
|
||||
// falls back to a backend.
|
||||
func NewCachedBlobStatter(cache distribution.BlobDescriptorService, backend distribution.BlobStatter) distribution.BlobStatter {
|
||||
return &cachedBlobStatter{
|
||||
cache: cache,
|
||||
backend: backend,
|
||||
}
|
||||
}
|
||||
|
||||
// NewCachedBlobStatterWithMetrics creates a new statter which prefers a cache and
|
||||
// falls back to a backend. Hits and misses will send to the tracker.
|
||||
func NewCachedBlobStatterWithMetrics(cache distribution.BlobDescriptorService, backend distribution.BlobStatter, tracker MetricsTracker) distribution.BlobStatter {
|
||||
return &cachedBlobStatter{
|
||||
cache: cache,
|
||||
backend: backend,
|
||||
tracker: tracker,
|
||||
}
|
||||
}
|
||||
|
||||
func (cbds *cachedBlobStatter) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
|
||||
desc, err := cbds.cache.Stat(ctx, dgst)
|
||||
if err != nil {
|
||||
if err != distribution.ErrBlobUnknown {
|
||||
context.GetLogger(ctx).Errorf("error retrieving descriptor from cache: %v", err)
|
||||
}
|
||||
|
||||
goto fallback
|
||||
}
|
||||
|
||||
if cbds.tracker != nil {
|
||||
cbds.tracker.Hit()
|
||||
}
|
||||
return desc, nil
|
||||
fallback:
|
||||
if cbds.tracker != nil {
|
||||
cbds.tracker.Miss()
|
||||
}
|
||||
desc, err = cbds.backend.Stat(ctx, dgst)
|
||||
if err != nil {
|
||||
return desc, err
|
||||
}
|
||||
|
||||
if err := cbds.cache.SetDescriptor(ctx, dgst, desc); err != nil {
|
||||
context.GetLogger(ctx).Errorf("error adding descriptor %v to cache: %v", desc.Digest, err)
|
||||
}
|
||||
|
||||
return desc, err
|
||||
}
|
150
vendor/src/github.com/docker/distribution/registry/storage/cache/memory/memory.go
vendored
Normal file
150
vendor/src/github.com/docker/distribution/registry/storage/cache/memory/memory.go
vendored
Normal file
|
@ -0,0 +1,150 @@
|
|||
package memory
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/context"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/registry/api/v2"
|
||||
"github.com/docker/distribution/registry/storage/cache"
|
||||
)
|
||||
|
||||
type inMemoryBlobDescriptorCacheProvider struct {
|
||||
global *mapBlobDescriptorCache
|
||||
repositories map[string]*mapBlobDescriptorCache
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewInMemoryBlobDescriptorCacheProvider returns a new mapped-based cache for
|
||||
// storing blob descriptor data.
|
||||
func NewInMemoryBlobDescriptorCacheProvider() cache.BlobDescriptorCacheProvider {
|
||||
return &inMemoryBlobDescriptorCacheProvider{
|
||||
global: newMapBlobDescriptorCache(),
|
||||
repositories: make(map[string]*mapBlobDescriptorCache),
|
||||
}
|
||||
}
|
||||
|
||||
func (imbdcp *inMemoryBlobDescriptorCacheProvider) RepositoryScoped(repo string) (distribution.BlobDescriptorService, error) {
|
||||
if err := v2.ValidateRepositoryName(repo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
imbdcp.mu.RLock()
|
||||
defer imbdcp.mu.RUnlock()
|
||||
|
||||
return &repositoryScopedInMemoryBlobDescriptorCache{
|
||||
repo: repo,
|
||||
parent: imbdcp,
|
||||
repository: imbdcp.repositories[repo],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (imbdcp *inMemoryBlobDescriptorCacheProvider) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
|
||||
return imbdcp.global.Stat(ctx, dgst)
|
||||
}
|
||||
|
||||
func (imbdcp *inMemoryBlobDescriptorCacheProvider) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error {
|
||||
_, err := imbdcp.Stat(ctx, dgst)
|
||||
if err == distribution.ErrBlobUnknown {
|
||||
|
||||
if dgst.Algorithm() != desc.Digest.Algorithm() && dgst != desc.Digest {
|
||||
// if the digests differ, set the other canonical mapping
|
||||
if err := imbdcp.global.SetDescriptor(ctx, desc.Digest, desc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// unknown, just set it
|
||||
return imbdcp.global.SetDescriptor(ctx, dgst, desc)
|
||||
}
|
||||
|
||||
// we already know it, do nothing
|
||||
return err
|
||||
}
|
||||
|
||||
// repositoryScopedInMemoryBlobDescriptorCache provides the request scoped
|
||||
// repository cache. Instances are not thread-safe but the delegated
|
||||
// operations are.
|
||||
type repositoryScopedInMemoryBlobDescriptorCache struct {
|
||||
repo string
|
||||
parent *inMemoryBlobDescriptorCacheProvider // allows lazy allocation of repo's map
|
||||
repository *mapBlobDescriptorCache
|
||||
}
|
||||
|
||||
func (rsimbdcp *repositoryScopedInMemoryBlobDescriptorCache) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
|
||||
if rsimbdcp.repository == nil {
|
||||
return distribution.Descriptor{}, distribution.ErrBlobUnknown
|
||||
}
|
||||
|
||||
return rsimbdcp.repository.Stat(ctx, dgst)
|
||||
}
|
||||
|
||||
func (rsimbdcp *repositoryScopedInMemoryBlobDescriptorCache) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error {
|
||||
if rsimbdcp.repository == nil {
|
||||
// allocate map since we are setting it now.
|
||||
rsimbdcp.parent.mu.Lock()
|
||||
var ok bool
|
||||
// have to read back value since we may have allocated elsewhere.
|
||||
rsimbdcp.repository, ok = rsimbdcp.parent.repositories[rsimbdcp.repo]
|
||||
if !ok {
|
||||
rsimbdcp.repository = newMapBlobDescriptorCache()
|
||||
rsimbdcp.parent.repositories[rsimbdcp.repo] = rsimbdcp.repository
|
||||
}
|
||||
|
||||
rsimbdcp.parent.mu.Unlock()
|
||||
}
|
||||
|
||||
if err := rsimbdcp.repository.SetDescriptor(ctx, dgst, desc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return rsimbdcp.parent.SetDescriptor(ctx, dgst, desc)
|
||||
}
|
||||
|
||||
// mapBlobDescriptorCache provides a simple map-based implementation of the
|
||||
// descriptor cache.
|
||||
type mapBlobDescriptorCache struct {
|
||||
descriptors map[digest.Digest]distribution.Descriptor
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var _ distribution.BlobDescriptorService = &mapBlobDescriptorCache{}
|
||||
|
||||
func newMapBlobDescriptorCache() *mapBlobDescriptorCache {
|
||||
return &mapBlobDescriptorCache{
|
||||
descriptors: make(map[digest.Digest]distribution.Descriptor),
|
||||
}
|
||||
}
|
||||
|
||||
func (mbdc *mapBlobDescriptorCache) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
|
||||
if err := dgst.Validate(); err != nil {
|
||||
return distribution.Descriptor{}, err
|
||||
}
|
||||
|
||||
mbdc.mu.RLock()
|
||||
defer mbdc.mu.RUnlock()
|
||||
|
||||
desc, ok := mbdc.descriptors[dgst]
|
||||
if !ok {
|
||||
return distribution.Descriptor{}, distribution.ErrBlobUnknown
|
||||
}
|
||||
|
||||
return desc, nil
|
||||
}
|
||||
|
||||
func (mbdc *mapBlobDescriptorCache) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error {
|
||||
if err := dgst.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := cache.ValidateDescriptor(desc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mbdc.mu.Lock()
|
||||
defer mbdc.mu.Unlock()
|
||||
|
||||
mbdc.descriptors[dgst] = desc
|
||||
return nil
|
||||
}
|
141
vendor/src/github.com/docker/distribution/registry/storage/cache/suite.go
vendored
Normal file
141
vendor/src/github.com/docker/distribution/registry/storage/cache/suite.go
vendored
Normal file
|
@ -0,0 +1,141 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/context"
|
||||
"github.com/docker/distribution/digest"
|
||||
)
|
||||
|
||||
// CheckBlobDescriptorCache takes a cache implementation through a common set
|
||||
// of operations. If adding new tests, please add them here so new
|
||||
// implementations get the benefit. This should be used for unit tests.
|
||||
func CheckBlobDescriptorCache(t *testing.T, provider BlobDescriptorCacheProvider) {
|
||||
ctx := context.Background()
|
||||
|
||||
checkBlobDescriptorCacheEmptyRepository(t, ctx, provider)
|
||||
checkBlobDescriptorCacheSetAndRead(t, ctx, provider)
|
||||
}
|
||||
|
||||
func checkBlobDescriptorCacheEmptyRepository(t *testing.T, ctx context.Context, provider BlobDescriptorCacheProvider) {
|
||||
if _, err := provider.Stat(ctx, "sha384:abc"); err != distribution.ErrBlobUnknown {
|
||||
t.Fatalf("expected unknown blob error with empty store: %v", err)
|
||||
}
|
||||
|
||||
cache, err := provider.RepositoryScoped("")
|
||||
if err == nil {
|
||||
t.Fatalf("expected an error when asking for invalid repo")
|
||||
}
|
||||
|
||||
cache, err = provider.RepositoryScoped("foo/bar")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error getting repository: %v", err)
|
||||
}
|
||||
|
||||
if err := cache.SetDescriptor(ctx, "", distribution.Descriptor{
|
||||
Digest: "sha384:abc",
|
||||
Length: 10,
|
||||
MediaType: "application/octet-stream"}); err != digest.ErrDigestInvalidFormat {
|
||||
t.Fatalf("expected error with invalid digest: %v", err)
|
||||
}
|
||||
|
||||
if err := cache.SetDescriptor(ctx, "sha384:abc", distribution.Descriptor{
|
||||
Digest: "",
|
||||
Length: 10,
|
||||
MediaType: "application/octet-stream"}); err == nil {
|
||||
t.Fatalf("expected error setting value on invalid descriptor")
|
||||
}
|
||||
|
||||
if _, err := cache.Stat(ctx, ""); err != digest.ErrDigestInvalidFormat {
|
||||
t.Fatalf("expected error checking for cache item with empty digest: %v", err)
|
||||
}
|
||||
|
||||
if _, err := cache.Stat(ctx, "sha384:abc"); err != distribution.ErrBlobUnknown {
|
||||
t.Fatalf("expected unknown blob error with empty repo: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func checkBlobDescriptorCacheSetAndRead(t *testing.T, ctx context.Context, provider BlobDescriptorCacheProvider) {
|
||||
localDigest := digest.Digest("sha384:abc")
|
||||
expected := distribution.Descriptor{
|
||||
Digest: "sha256:abc",
|
||||
Length: 10,
|
||||
MediaType: "application/octet-stream"}
|
||||
|
||||
cache, err := provider.RepositoryScoped("foo/bar")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error getting scoped cache: %v", err)
|
||||
}
|
||||
|
||||
if err := cache.SetDescriptor(ctx, localDigest, expected); err != nil {
|
||||
t.Fatalf("error setting descriptor: %v", err)
|
||||
}
|
||||
|
||||
desc, err := cache.Stat(ctx, localDigest)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error statting fake2:abc: %v", err)
|
||||
}
|
||||
|
||||
if expected != desc {
|
||||
t.Fatalf("unexpected descriptor: %#v != %#v", expected, desc)
|
||||
}
|
||||
|
||||
// also check that we set the canonical key ("fake:abc")
|
||||
desc, err = cache.Stat(ctx, localDigest)
|
||||
if err != nil {
|
||||
t.Fatalf("descriptor not returned for canonical key: %v", err)
|
||||
}
|
||||
|
||||
if expected != desc {
|
||||
t.Fatalf("unexpected descriptor: %#v != %#v", expected, desc)
|
||||
}
|
||||
|
||||
// ensure that global gets extra descriptor mapping
|
||||
desc, err = provider.Stat(ctx, localDigest)
|
||||
if err != nil {
|
||||
t.Fatalf("expected blob unknown in global cache: %v, %v", err, desc)
|
||||
}
|
||||
|
||||
if desc != expected {
|
||||
t.Fatalf("unexpected descriptor: %#v != %#v", expected, desc)
|
||||
}
|
||||
|
||||
// get at it through canonical descriptor
|
||||
desc, err = provider.Stat(ctx, expected.Digest)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error checking glboal descriptor: %v", err)
|
||||
}
|
||||
|
||||
if desc != expected {
|
||||
t.Fatalf("unexpected descriptor: %#v != %#v", expected, desc)
|
||||
}
|
||||
|
||||
// now, we set the repo local mediatype to something else and ensure it
|
||||
// doesn't get changed in the provider cache.
|
||||
expected.MediaType = "application/json"
|
||||
|
||||
if err := cache.SetDescriptor(ctx, localDigest, expected); err != nil {
|
||||
t.Fatalf("unexpected error setting descriptor: %v", err)
|
||||
}
|
||||
|
||||
desc, err = cache.Stat(ctx, localDigest)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error getting descriptor: %v", err)
|
||||
}
|
||||
|
||||
if desc != expected {
|
||||
t.Fatalf("unexpected descriptor: %#v != %#v", desc, expected)
|
||||
}
|
||||
|
||||
desc, err = provider.Stat(ctx, localDigest)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error getting global descriptor: %v", err)
|
||||
}
|
||||
|
||||
expected.MediaType = "application/octet-stream" // expect original mediatype in global
|
||||
|
||||
if desc != expected {
|
||||
t.Fatalf("unexpected descriptor: %#v != %#v", desc, expected)
|
||||
}
|
||||
}
|
125
vendor/src/github.com/docker/distribution/uuid/uuid.go
vendored
Normal file
125
vendor/src/github.com/docker/distribution/uuid/uuid.go
vendored
Normal file
|
@ -0,0 +1,125 @@
|
|||
// Package uuid provides simple UUID generation. Only version 4 style UUIDs
|
||||
// can be generated.
|
||||
//
|
||||
// Please see http://tools.ietf.org/html/rfc4122 for details on UUIDs.
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// Bits is the number of bits in a UUID
|
||||
Bits = 128
|
||||
|
||||
// Size is the number of bytes in a UUID
|
||||
Size = Bits / 8
|
||||
|
||||
format = "%08x-%04x-%04x-%04x-%012x"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrUUIDInvalid indicates a parsed string is not a valid uuid.
|
||||
ErrUUIDInvalid = fmt.Errorf("invalid uuid")
|
||||
|
||||
// Loggerf can be used to override the default logging destination. Such
|
||||
// log messages in this library should be logged at warning or higher.
|
||||
Loggerf = log.Printf
|
||||
)
|
||||
|
||||
// UUID represents a UUID value. UUIDs can be compared and set to other values
|
||||
// and accessed by byte.
|
||||
type UUID [Size]byte
|
||||
|
||||
// Generate creates a new, version 4 uuid.
|
||||
func Generate() (u UUID) {
|
||||
const (
|
||||
// ensures we backoff for less than 450ms total. Use the following to
|
||||
// select new value, in units of 10ms:
|
||||
// n*(n+1)/2 = d -> n^2 + n - 2d -> n = (sqrt(8d + 1) - 1)/2
|
||||
maxretries = 9
|
||||
backoff = time.Millisecond * 10
|
||||
)
|
||||
|
||||
var (
|
||||
totalBackoff time.Duration
|
||||
retries int
|
||||
)
|
||||
|
||||
for {
|
||||
// This should never block but the read may fail. Because of this,
|
||||
// we just try to read the random number generator until we get
|
||||
// something. This is a very rare condition but may happen.
|
||||
b := time.Duration(retries) * backoff
|
||||
time.Sleep(b)
|
||||
totalBackoff += b
|
||||
|
||||
_, err := io.ReadFull(rand.Reader, u[:])
|
||||
if err != nil {
|
||||
if retryOnError(err) && retries < maxretries {
|
||||
retries++
|
||||
Loggerf("error generating version 4 uuid, retrying: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Any other errors represent a system problem. What did someone
|
||||
// do to /dev/urandom?
|
||||
panic(fmt.Errorf("error reading random number generator, retried for %v: %v", totalBackoff.String(), err))
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
u[6] = (u[6] & 0x0f) | 0x40 // set version byte
|
||||
u[8] = (u[8] & 0x3f) | 0x80 // set high order byte 0b10{8,9,a,b}
|
||||
|
||||
return u
|
||||
}
|
||||
|
||||
// Parse attempts to extract a uuid from the string or returns an error.
|
||||
func Parse(s string) (u UUID, err error) {
|
||||
if len(s) != 36 {
|
||||
return UUID{}, ErrUUIDInvalid
|
||||
}
|
||||
|
||||
// create stack addresses for each section of the uuid.
|
||||
p := make([][]byte, 5)
|
||||
|
||||
if _, err := fmt.Sscanf(s, format, &p[0], &p[1], &p[2], &p[3], &p[4]); err != nil {
|
||||
return u, err
|
||||
}
|
||||
|
||||
copy(u[0:4], p[0])
|
||||
copy(u[4:6], p[1])
|
||||
copy(u[6:8], p[2])
|
||||
copy(u[8:10], p[3])
|
||||
copy(u[10:16], p[4])
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (u UUID) String() string {
|
||||
return fmt.Sprintf(format, u[:4], u[4:6], u[6:8], u[8:10], u[10:])
|
||||
}
|
||||
|
||||
// retryOnError tries to detect whether or not retrying would be fruitful.
|
||||
func retryOnError(err error) bool {
|
||||
switch err := err.(type) {
|
||||
case *os.PathError:
|
||||
return retryOnError(err.Err) // unpack the target error
|
||||
case syscall.Errno:
|
||||
if err == syscall.EPERM {
|
||||
// EPERM represents an entropy pool exhaustion, a condition under
|
||||
// which we backoff and retry.
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
|
@ -142,7 +142,7 @@ func (k *ecPublicKey) PEMBlock() (*pem.Block, error) {
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to serialize EC PublicKey to DER-encoded PKIX format: %s", err)
|
||||
}
|
||||
k.extended["keyID"] = k.KeyID() // For display purposes.
|
||||
k.extended["kid"] = k.KeyID() // For display purposes.
|
||||
return createPemBlock("PUBLIC KEY", derBytes, k.extended)
|
||||
}
|
||||
|
||||
|
|
153
vendor/src/github.com/docker/libtrust/jsonsign.go
vendored
153
vendor/src/github.com/docker/libtrust/jsonsign.go
vendored
|
@ -8,6 +8,7 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
@ -31,9 +32,25 @@ type jsHeader struct {
|
|||
}
|
||||
|
||||
type jsSignature struct {
|
||||
Header *jsHeader `json:"header"`
|
||||
Signature string `json:"signature"`
|
||||
Protected string `json:"protected,omitempty"`
|
||||
Header jsHeader `json:"header"`
|
||||
Signature string `json:"signature"`
|
||||
Protected string `json:"protected,omitempty"`
|
||||
}
|
||||
|
||||
type jsSignaturesSorted []jsSignature
|
||||
|
||||
func (jsbkid jsSignaturesSorted) Swap(i, j int) { jsbkid[i], jsbkid[j] = jsbkid[j], jsbkid[i] }
|
||||
func (jsbkid jsSignaturesSorted) Len() int { return len(jsbkid) }
|
||||
|
||||
func (jsbkid jsSignaturesSorted) Less(i, j int) bool {
|
||||
ki, kj := jsbkid[i].Header.JWK.KeyID(), jsbkid[j].Header.JWK.KeyID()
|
||||
si, sj := jsbkid[i].Signature, jsbkid[j].Signature
|
||||
|
||||
if ki == kj {
|
||||
return si < sj
|
||||
}
|
||||
|
||||
return ki < kj
|
||||
}
|
||||
|
||||
type signKey struct {
|
||||
|
@ -44,7 +61,7 @@ type signKey struct {
|
|||
// JSONSignature represents a signature of a json object.
|
||||
type JSONSignature struct {
|
||||
payload string
|
||||
signatures []*jsSignature
|
||||
signatures []jsSignature
|
||||
indent string
|
||||
formatLength int
|
||||
formatTail []byte
|
||||
|
@ -52,7 +69,7 @@ type JSONSignature struct {
|
|||
|
||||
func newJSONSignature() *JSONSignature {
|
||||
return &JSONSignature{
|
||||
signatures: make([]*jsSignature, 0, 1),
|
||||
signatures: make([]jsSignature, 0, 1),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,17 +116,14 @@ func (js *JSONSignature) Sign(key PrivateKey) error {
|
|||
return err
|
||||
}
|
||||
|
||||
header := &jsHeader{
|
||||
JWK: key.PublicKey(),
|
||||
Algorithm: algorithm,
|
||||
}
|
||||
sig := &jsSignature{
|
||||
Header: header,
|
||||
js.signatures = append(js.signatures, jsSignature{
|
||||
Header: jsHeader{
|
||||
JWK: key.PublicKey(),
|
||||
Algorithm: algorithm,
|
||||
},
|
||||
Signature: joseBase64UrlEncode(sigBytes),
|
||||
Protected: protected,
|
||||
}
|
||||
|
||||
js.signatures = append(js.signatures, sig)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -136,7 +150,7 @@ func (js *JSONSignature) SignWithChain(key PrivateKey, chain []*x509.Certificate
|
|||
return err
|
||||
}
|
||||
|
||||
header := &jsHeader{
|
||||
header := jsHeader{
|
||||
Chain: make([]string, len(chain)),
|
||||
Algorithm: algorithm,
|
||||
}
|
||||
|
@ -145,13 +159,11 @@ func (js *JSONSignature) SignWithChain(key PrivateKey, chain []*x509.Certificate
|
|||
header.Chain[i] = base64.StdEncoding.EncodeToString(cert.Raw)
|
||||
}
|
||||
|
||||
sig := &jsSignature{
|
||||
js.signatures = append(js.signatures, jsSignature{
|
||||
Header: header,
|
||||
Signature: joseBase64UrlEncode(sigBytes),
|
||||
Protected: protected,
|
||||
}
|
||||
|
||||
js.signatures = append(js.signatures, sig)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -272,6 +284,9 @@ func (js *JSONSignature) JWS() ([]byte, error) {
|
|||
if len(js.signatures) == 0 {
|
||||
return nil, errors.New("missing signature")
|
||||
}
|
||||
|
||||
sort.Sort(jsSignaturesSorted(js.signatures))
|
||||
|
||||
jsonMap := map[string]interface{}{
|
||||
"payload": js.payload,
|
||||
"signatures": js.signatures,
|
||||
|
@ -301,16 +316,16 @@ type jsParsedHeader struct {
|
|||
}
|
||||
|
||||
type jsParsedSignature struct {
|
||||
Header *jsParsedHeader `json:"header"`
|
||||
Signature string `json:"signature"`
|
||||
Protected string `json:"protected"`
|
||||
Header jsParsedHeader `json:"header"`
|
||||
Signature string `json:"signature"`
|
||||
Protected string `json:"protected"`
|
||||
}
|
||||
|
||||
// ParseJWS parses a JWS serialized JSON object into a Json Signature.
|
||||
func ParseJWS(content []byte) (*JSONSignature, error) {
|
||||
type jsParsed struct {
|
||||
Payload string `json:"payload"`
|
||||
Signatures []*jsParsedSignature `json:"signatures"`
|
||||
Payload string `json:"payload"`
|
||||
Signatures []jsParsedSignature `json:"signatures"`
|
||||
}
|
||||
parsed := &jsParsed{}
|
||||
err := json.Unmarshal(content, parsed)
|
||||
|
@ -329,9 +344,9 @@ func ParseJWS(content []byte) (*JSONSignature, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
js.signatures = make([]*jsSignature, len(parsed.Signatures))
|
||||
js.signatures = make([]jsSignature, len(parsed.Signatures))
|
||||
for i, signature := range parsed.Signatures {
|
||||
header := &jsHeader{
|
||||
header := jsHeader{
|
||||
Algorithm: signature.Header.Algorithm,
|
||||
}
|
||||
if signature.Header.Chain != nil {
|
||||
|
@ -344,7 +359,7 @@ func ParseJWS(content []byte) (*JSONSignature, error) {
|
|||
}
|
||||
header.JWK = publicKey
|
||||
}
|
||||
js.signatures[i] = &jsSignature{
|
||||
js.signatures[i] = jsSignature{
|
||||
Header: header,
|
||||
Signature: signature.Signature,
|
||||
Protected: signature.Protected,
|
||||
|
@ -356,7 +371,11 @@ func ParseJWS(content []byte) (*JSONSignature, error) {
|
|||
|
||||
// NewJSONSignature returns a new unsigned JWS from a json byte array.
|
||||
// JSONSignature will need to be signed before serializing or storing.
|
||||
func NewJSONSignature(content []byte) (*JSONSignature, error) {
|
||||
// Optionally, one or more signatures can be provided as byte buffers,
|
||||
// containing serialized JWS signatures, to assemble a fully signed JWS
|
||||
// package. It is the callers responsibility to ensure uniqueness of the
|
||||
// provided signatures.
|
||||
func NewJSONSignature(content []byte, signatures ...[]byte) (*JSONSignature, error) {
|
||||
var dataMap map[string]interface{}
|
||||
err := json.Unmarshal(content, &dataMap)
|
||||
if err != nil {
|
||||
|
@ -380,6 +399,40 @@ func NewJSONSignature(content []byte) (*JSONSignature, error) {
|
|||
js.formatLength = lastRuneIndex + 1
|
||||
js.formatTail = content[js.formatLength:]
|
||||
|
||||
if len(signatures) > 0 {
|
||||
for _, signature := range signatures {
|
||||
var parsedJSig jsParsedSignature
|
||||
|
||||
if err := json.Unmarshal(signature, &parsedJSig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO(stevvooe): A lot of the code below is repeated in
|
||||
// ParseJWS. It will require more refactoring to fix that.
|
||||
jsig := jsSignature{
|
||||
Header: jsHeader{
|
||||
Algorithm: parsedJSig.Header.Algorithm,
|
||||
},
|
||||
Signature: parsedJSig.Signature,
|
||||
Protected: parsedJSig.Protected,
|
||||
}
|
||||
|
||||
if parsedJSig.Header.Chain != nil {
|
||||
jsig.Header.Chain = parsedJSig.Header.Chain
|
||||
}
|
||||
|
||||
if parsedJSig.Header.JWK != nil {
|
||||
publicKey, err := UnmarshalPublicKeyJWK([]byte(parsedJSig.Header.JWK))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jsig.Header.JWK = publicKey
|
||||
}
|
||||
|
||||
js.signatures = append(js.signatures, jsig)
|
||||
}
|
||||
}
|
||||
|
||||
return js, nil
|
||||
}
|
||||
|
||||
|
@ -455,7 +508,7 @@ func ParsePrettySignature(content []byte, signatureKey string) (*JSONSignature,
|
|||
}
|
||||
|
||||
js := newJSONSignature()
|
||||
js.signatures = make([]*jsSignature, len(signatureBlocks))
|
||||
js.signatures = make([]jsSignature, len(signatureBlocks))
|
||||
|
||||
for i, signatureBlock := range signatureBlocks {
|
||||
protectedBytes, err := joseBase64UrlDecode(signatureBlock.Protected)
|
||||
|
@ -491,7 +544,7 @@ func ParsePrettySignature(content []byte, signatureKey string) (*JSONSignature,
|
|||
return nil, errors.New("conflicting format tail")
|
||||
}
|
||||
|
||||
header := &jsHeader{
|
||||
header := jsHeader{
|
||||
Algorithm: signatureBlock.Header.Algorithm,
|
||||
Chain: signatureBlock.Header.Chain,
|
||||
}
|
||||
|
@ -502,7 +555,7 @@ func ParsePrettySignature(content []byte, signatureKey string) (*JSONSignature,
|
|||
}
|
||||
header.JWK = publicKey
|
||||
}
|
||||
js.signatures[i] = &jsSignature{
|
||||
js.signatures[i] = jsSignature{
|
||||
Header: header,
|
||||
Signature: signatureBlock.Signature,
|
||||
Protected: signatureBlock.Protected,
|
||||
|
@ -532,6 +585,8 @@ func (js *JSONSignature) PrettySignature(signatureKey string) ([]byte, error) {
|
|||
}
|
||||
payload = payload[:js.formatLength]
|
||||
|
||||
sort.Sort(jsSignaturesSorted(js.signatures))
|
||||
|
||||
var marshalled []byte
|
||||
var marshallErr error
|
||||
if js.indent != "" {
|
||||
|
@ -564,3 +619,39 @@ func (js *JSONSignature) PrettySignature(signatureKey string) ([]byte, error) {
|
|||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// Signatures provides the signatures on this JWS as opaque blobs, sorted by
|
||||
// keyID. These blobs can be stored and reassembled with payloads. Internally,
|
||||
// they are simply marshaled json web signatures but implementations should
|
||||
// not rely on this.
|
||||
func (js *JSONSignature) Signatures() ([][]byte, error) {
|
||||
sort.Sort(jsSignaturesSorted(js.signatures))
|
||||
|
||||
var sb [][]byte
|
||||
for _, jsig := range js.signatures {
|
||||
p, err := json.Marshal(jsig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sb = append(sb, p)
|
||||
}
|
||||
|
||||
return sb, nil
|
||||
}
|
||||
|
||||
// Merge combines the signatures from one or more other signatures into the
|
||||
// method receiver. If the payloads differ for any argument, an error will be
|
||||
// returned and the receiver will not be modified.
|
||||
func (js *JSONSignature) Merge(others ...*JSONSignature) error {
|
||||
merged := js.signatures
|
||||
for _, other := range others {
|
||||
if js.payload != other.payload {
|
||||
return fmt.Errorf("payloads differ from merge target")
|
||||
}
|
||||
merged = append(merged, other.signatures...)
|
||||
}
|
||||
|
||||
js.signatures = merged
|
||||
return nil
|
||||
}
|
||||
|
|
175
vendor/src/github.com/docker/libtrust/key_manager.go
vendored
Normal file
175
vendor/src/github.com/docker/libtrust/key_manager.go
vendored
Normal file
|
@ -0,0 +1,175 @@
|
|||
package libtrust
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ClientKeyManager manages client keys on the filesystem
|
||||
type ClientKeyManager struct {
|
||||
key PrivateKey
|
||||
clientFile string
|
||||
clientDir string
|
||||
|
||||
clientLock sync.RWMutex
|
||||
clients []PublicKey
|
||||
|
||||
configLock sync.Mutex
|
||||
configs []*tls.Config
|
||||
}
|
||||
|
||||
// NewClientKeyManager loads a new manager from a set of key files
|
||||
// and managed by the given private key.
|
||||
func NewClientKeyManager(trustKey PrivateKey, clientFile, clientDir string) (*ClientKeyManager, error) {
|
||||
m := &ClientKeyManager{
|
||||
key: trustKey,
|
||||
clientFile: clientFile,
|
||||
clientDir: clientDir,
|
||||
}
|
||||
if err := m.loadKeys(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// TODO Start watching file and directory
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (c *ClientKeyManager) loadKeys() (err error) {
|
||||
// Load authorized keys file
|
||||
var clients []PublicKey
|
||||
if c.clientFile != "" {
|
||||
clients, err = LoadKeySetFile(c.clientFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load authorized keys: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add clients from authorized keys directory
|
||||
files, err := ioutil.ReadDir(c.clientDir)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("unable to open authorized keys directory: %s", err)
|
||||
}
|
||||
for _, f := range files {
|
||||
if !f.IsDir() {
|
||||
publicKey, err := LoadPublicKeyFile(path.Join(c.clientDir, f.Name()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load authorized key file: %s", err)
|
||||
}
|
||||
clients = append(clients, publicKey)
|
||||
}
|
||||
}
|
||||
|
||||
c.clientLock.Lock()
|
||||
c.clients = clients
|
||||
c.clientLock.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterTLSConfig registers a tls configuration to manager
|
||||
// such that any changes to the keys may be reflected in
|
||||
// the tls client CA pool
|
||||
func (c *ClientKeyManager) RegisterTLSConfig(tlsConfig *tls.Config) error {
|
||||
c.clientLock.RLock()
|
||||
certPool, err := GenerateCACertPool(c.key, c.clients)
|
||||
if err != nil {
|
||||
return fmt.Errorf("CA pool generation error: %s", err)
|
||||
}
|
||||
c.clientLock.RUnlock()
|
||||
|
||||
tlsConfig.ClientCAs = certPool
|
||||
|
||||
c.configLock.Lock()
|
||||
c.configs = append(c.configs, tlsConfig)
|
||||
c.configLock.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewIdentityAuthTLSConfig creates a tls.Config for the server to use for
|
||||
// libtrust identity authentication for the domain specified
|
||||
func NewIdentityAuthTLSConfig(trustKey PrivateKey, clients *ClientKeyManager, addr string, domain string) (*tls.Config, error) {
|
||||
tlsConfig := newTLSConfig()
|
||||
|
||||
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
if err := clients.RegisterTLSConfig(tlsConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Generate cert
|
||||
ips, domains, err := parseAddr(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// add domain that it expects clients to use
|
||||
domains = append(domains, domain)
|
||||
x509Cert, err := GenerateSelfSignedServerCert(trustKey, domains, ips)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("certificate generation error: %s", err)
|
||||
}
|
||||
tlsConfig.Certificates = []tls.Certificate{{
|
||||
Certificate: [][]byte{x509Cert.Raw},
|
||||
PrivateKey: trustKey.CryptoPrivateKey(),
|
||||
Leaf: x509Cert,
|
||||
}}
|
||||
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
// NewCertAuthTLSConfig creates a tls.Config for the server to use for
|
||||
// certificate authentication
|
||||
func NewCertAuthTLSConfig(caPath, certPath, keyPath string) (*tls.Config, error) {
|
||||
tlsConfig := newTLSConfig()
|
||||
|
||||
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Couldn't load X509 key pair (%s, %s): %s. Key encrypted?", certPath, keyPath, err)
|
||||
}
|
||||
tlsConfig.Certificates = []tls.Certificate{cert}
|
||||
|
||||
// Verify client certificates against a CA?
|
||||
if caPath != "" {
|
||||
certPool := x509.NewCertPool()
|
||||
file, err := ioutil.ReadFile(caPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Couldn't read CA certificate: %s", err)
|
||||
}
|
||||
certPool.AppendCertsFromPEM(file)
|
||||
|
||||
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
tlsConfig.ClientCAs = certPool
|
||||
}
|
||||
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
func newTLSConfig() *tls.Config {
|
||||
return &tls.Config{
|
||||
NextProtos: []string{"http/1.1"},
|
||||
// Avoid fallback on insecure SSL protocols
|
||||
MinVersion: tls.VersionTLS10,
|
||||
}
|
||||
}
|
||||
|
||||
// parseAddr parses an address into an array of IPs and domains
|
||||
func parseAddr(addr string) ([]net.IP, []string, error) {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var domains []string
|
||||
var ips []net.IP
|
||||
ip := net.ParseIP(host)
|
||||
if ip != nil {
|
||||
ips = []net.IP{ip}
|
||||
} else {
|
||||
domains = []string{host}
|
||||
}
|
||||
return ips, domains, nil
|
||||
}
|
|
@ -99,7 +99,7 @@ func (k *rsaPublicKey) PEMBlock() (*pem.Block, error) {
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to serialize RSA PublicKey to DER-encoded PKIX format: %s", err)
|
||||
}
|
||||
k.extended["keyID"] = k.KeyID() // For display purposes.
|
||||
k.extended["kid"] = k.KeyID() // For display purposes.
|
||||
return createPemBlock("PUBLIC KEY", derBytes, k.extended)
|
||||
}
|
||||
|
||||
|
|
138
vendor/src/github.com/docker/libtrust/util.go
vendored
138
vendor/src/github.com/docker/libtrust/util.go
vendored
|
@ -4,6 +4,7 @@ import (
|
|||
"bytes"
|
||||
"crypto"
|
||||
"crypto/elliptic"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base32"
|
||||
"encoding/base64"
|
||||
|
@ -12,9 +13,144 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LoadOrCreateTrustKey will load a PrivateKey from the specified path
|
||||
func LoadOrCreateTrustKey(trustKeyPath string) (PrivateKey, error) {
|
||||
if err := os.MkdirAll(filepath.Dir(trustKeyPath), 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
trustKey, err := LoadKeyFile(trustKeyPath)
|
||||
if err == ErrKeyFileDoesNotExist {
|
||||
trustKey, err = GenerateECP256PrivateKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error generating key: %s", err)
|
||||
}
|
||||
|
||||
if err := SaveKey(trustKeyPath, trustKey); err != nil {
|
||||
return nil, fmt.Errorf("error saving key file: %s", err)
|
||||
}
|
||||
|
||||
dir, file := filepath.Split(trustKeyPath)
|
||||
if err := SavePublicKey(filepath.Join(dir, "public-"+file), trustKey.PublicKey()); err != nil {
|
||||
return nil, fmt.Errorf("error saving public key file: %s", err)
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("error loading key file: %s", err)
|
||||
}
|
||||
return trustKey, nil
|
||||
}
|
||||
|
||||
// NewIdentityAuthTLSClientConfig returns a tls.Config configured to use identity
|
||||
// based authentication from the specified dockerUrl, the rootConfigPath and
|
||||
// the server name to which it is connecting.
|
||||
// If trustUnknownHosts is true it will automatically add the host to the
|
||||
// known-hosts.json in rootConfigPath.
|
||||
func NewIdentityAuthTLSClientConfig(dockerUrl string, trustUnknownHosts bool, rootConfigPath string, serverName string) (*tls.Config, error) {
|
||||
tlsConfig := newTLSConfig()
|
||||
|
||||
trustKeyPath := filepath.Join(rootConfigPath, "key.json")
|
||||
knownHostsPath := filepath.Join(rootConfigPath, "known-hosts.json")
|
||||
|
||||
u, err := url.Parse(dockerUrl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse machine url")
|
||||
}
|
||||
|
||||
if u.Scheme == "unix" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
addr := u.Host
|
||||
proto := "tcp"
|
||||
|
||||
trustKey, err := LoadOrCreateTrustKey(trustKeyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to load trust key: %s", err)
|
||||
}
|
||||
|
||||
knownHosts, err := LoadKeySetFile(knownHostsPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not load trusted hosts file: %s", err)
|
||||
}
|
||||
|
||||
allowedHosts, err := FilterByHosts(knownHosts, addr, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error filtering hosts: %s", err)
|
||||
}
|
||||
|
||||
certPool, err := GenerateCACertPool(trustKey, allowedHosts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not create CA pool: %s", err)
|
||||
}
|
||||
|
||||
tlsConfig.ServerName = serverName
|
||||
tlsConfig.RootCAs = certPool
|
||||
|
||||
x509Cert, err := GenerateSelfSignedClientCert(trustKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("certificate generation error: %s", err)
|
||||
}
|
||||
|
||||
tlsConfig.Certificates = []tls.Certificate{{
|
||||
Certificate: [][]byte{x509Cert.Raw},
|
||||
PrivateKey: trustKey.CryptoPrivateKey(),
|
||||
Leaf: x509Cert,
|
||||
}}
|
||||
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
|
||||
testConn, err := tls.Dial(proto, addr, tlsConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tls Handshake error: %s", err)
|
||||
}
|
||||
|
||||
opts := x509.VerifyOptions{
|
||||
Roots: tlsConfig.RootCAs,
|
||||
CurrentTime: time.Now(),
|
||||
DNSName: tlsConfig.ServerName,
|
||||
Intermediates: x509.NewCertPool(),
|
||||
}
|
||||
|
||||
certs := testConn.ConnectionState().PeerCertificates
|
||||
for i, cert := range certs {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
opts.Intermediates.AddCert(cert)
|
||||
}
|
||||
|
||||
if _, err := certs[0].Verify(opts); err != nil {
|
||||
if _, ok := err.(x509.UnknownAuthorityError); ok {
|
||||
if trustUnknownHosts {
|
||||
pubKey, err := FromCryptoPublicKey(certs[0].PublicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error extracting public key from cert: %s", err)
|
||||
}
|
||||
|
||||
pubKey.AddExtendedField("hosts", []string{addr})
|
||||
|
||||
if err := AddKeySetFile(knownHostsPath, pubKey); err != nil {
|
||||
return nil, fmt.Errorf("error adding machine to known hosts: %s", err)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("unable to connect. unknown host: %s", addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
testConn.Close()
|
||||
tlsConfig.InsecureSkipVerify = false
|
||||
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
// joseBase64UrlEncode encodes the given data using the standard base64 url
|
||||
// encoding format but with all trailing '=' characters ommitted in accordance
|
||||
// with the jose specification.
|
||||
|
@ -28,6 +164,8 @@ func joseBase64UrlEncode(b []byte) string {
|
|||
// accordance with the jose specification.
|
||||
// http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31#section-2
|
||||
func joseBase64UrlDecode(s string) ([]byte, error) {
|
||||
s = strings.Replace(s, "\n", "", -1)
|
||||
s = strings.Replace(s, " ", "", -1)
|
||||
switch len(s) % 4 {
|
||||
case 0:
|
||||
case 2:
|
||||
|
|
447
vendor/src/golang.org/x/net/context/context.go
vendored
Normal file
447
vendor/src/golang.org/x/net/context/context.go
vendored
Normal file
|
@ -0,0 +1,447 @@
|
|||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package context defines the Context type, which carries deadlines,
|
||||
// cancelation signals, and other request-scoped values across API boundaries
|
||||
// and between processes.
|
||||
//
|
||||
// Incoming requests to a server should create a Context, and outgoing calls to
|
||||
// servers should accept a Context. The chain of function calls between must
|
||||
// propagate the Context, optionally replacing it with a modified copy created
|
||||
// using WithDeadline, WithTimeout, WithCancel, or WithValue.
|
||||
//
|
||||
// Programs that use Contexts should follow these rules to keep interfaces
|
||||
// consistent across packages and enable static analysis tools to check context
|
||||
// propagation:
|
||||
//
|
||||
// Do not store Contexts inside a struct type; instead, pass a Context
|
||||
// explicitly to each function that needs it. The Context should be the first
|
||||
// parameter, typically named ctx:
|
||||
//
|
||||
// func DoSomething(ctx context.Context, arg Arg) error {
|
||||
// // ... use ctx ...
|
||||
// }
|
||||
//
|
||||
// Do not pass a nil Context, even if a function permits it. Pass context.TODO
|
||||
// if you are unsure about which Context to use.
|
||||
//
|
||||
// Use context Values only for request-scoped data that transits processes and
|
||||
// APIs, not for passing optional parameters to functions.
|
||||
//
|
||||
// The same Context may be passed to functions running in different goroutines;
|
||||
// Contexts are safe for simultaneous use by multiple goroutines.
|
||||
//
|
||||
// See http://blog.golang.org/context for example code for a server that uses
|
||||
// Contexts.
|
||||
package context // import "golang.org/x/net/context"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A Context carries a deadline, a cancelation signal, and other values across
|
||||
// API boundaries.
|
||||
//
|
||||
// Context's methods may be called by multiple goroutines simultaneously.
|
||||
type Context interface {
|
||||
// Deadline returns the time when work done on behalf of this context
|
||||
// should be canceled. Deadline returns ok==false when no deadline is
|
||||
// set. Successive calls to Deadline return the same results.
|
||||
Deadline() (deadline time.Time, ok bool)
|
||||
|
||||
// Done returns a channel that's closed when work done on behalf of this
|
||||
// context should be canceled. Done may return nil if this context can
|
||||
// never be canceled. Successive calls to Done return the same value.
|
||||
//
|
||||
// WithCancel arranges for Done to be closed when cancel is called;
|
||||
// WithDeadline arranges for Done to be closed when the deadline
|
||||
// expires; WithTimeout arranges for Done to be closed when the timeout
|
||||
// elapses.
|
||||
//
|
||||
// Done is provided for use in select statements:
|
||||
//
|
||||
// // Stream generates values with DoSomething and sends them to out
|
||||
// // until DoSomething returns an error or ctx.Done is closed.
|
||||
// func Stream(ctx context.Context, out <-chan Value) error {
|
||||
// for {
|
||||
// v, err := DoSomething(ctx)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// select {
|
||||
// case <-ctx.Done():
|
||||
// return ctx.Err()
|
||||
// case out <- v:
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// See http://blog.golang.org/pipelines for more examples of how to use
|
||||
// a Done channel for cancelation.
|
||||
Done() <-chan struct{}
|
||||
|
||||
// Err returns a non-nil error value after Done is closed. Err returns
|
||||
// Canceled if the context was canceled or DeadlineExceeded if the
|
||||
// context's deadline passed. No other values for Err are defined.
|
||||
// After Done is closed, successive calls to Err return the same value.
|
||||
Err() error
|
||||
|
||||
// Value returns the value associated with this context for key, or nil
|
||||
// if no value is associated with key. Successive calls to Value with
|
||||
// the same key returns the same result.
|
||||
//
|
||||
// Use context values only for request-scoped data that transits
|
||||
// processes and API boundaries, not for passing optional parameters to
|
||||
// functions.
|
||||
//
|
||||
// A key identifies a specific value in a Context. Functions that wish
|
||||
// to store values in Context typically allocate a key in a global
|
||||
// variable then use that key as the argument to context.WithValue and
|
||||
// Context.Value. A key can be any type that supports equality;
|
||||
// packages should define keys as an unexported type to avoid
|
||||
// collisions.
|
||||
//
|
||||
// Packages that define a Context key should provide type-safe accessors
|
||||
// for the values stores using that key:
|
||||
//
|
||||
// // Package user defines a User type that's stored in Contexts.
|
||||
// package user
|
||||
//
|
||||
// import "golang.org/x/net/context"
|
||||
//
|
||||
// // User is the type of value stored in the Contexts.
|
||||
// type User struct {...}
|
||||
//
|
||||
// // key is an unexported type for keys defined in this package.
|
||||
// // This prevents collisions with keys defined in other packages.
|
||||
// type key int
|
||||
//
|
||||
// // userKey is the key for user.User values in Contexts. It is
|
||||
// // unexported; clients use user.NewContext and user.FromContext
|
||||
// // instead of using this key directly.
|
||||
// var userKey key = 0
|
||||
//
|
||||
// // NewContext returns a new Context that carries value u.
|
||||
// func NewContext(ctx context.Context, u *User) context.Context {
|
||||
// return context.WithValue(ctx, userKey, u)
|
||||
// }
|
||||
//
|
||||
// // FromContext returns the User value stored in ctx, if any.
|
||||
// func FromContext(ctx context.Context) (*User, bool) {
|
||||
// u, ok := ctx.Value(userKey).(*User)
|
||||
// return u, ok
|
||||
// }
|
||||
Value(key interface{}) interface{}
|
||||
}
|
||||
|
||||
// Canceled is the error returned by Context.Err when the context is canceled.
|
||||
var Canceled = errors.New("context canceled")
|
||||
|
||||
// DeadlineExceeded is the error returned by Context.Err when the context's
|
||||
// deadline passes.
|
||||
var DeadlineExceeded = errors.New("context deadline exceeded")
|
||||
|
||||
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
|
||||
// struct{}, since vars of this type must have distinct addresses.
|
||||
type emptyCtx int
|
||||
|
||||
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
|
||||
return
|
||||
}
|
||||
|
||||
func (*emptyCtx) Done() <-chan struct{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*emptyCtx) Err() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*emptyCtx) Value(key interface{}) interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *emptyCtx) String() string {
|
||||
switch e {
|
||||
case background:
|
||||
return "context.Background"
|
||||
case todo:
|
||||
return "context.TODO"
|
||||
}
|
||||
return "unknown empty Context"
|
||||
}
|
||||
|
||||
var (
|
||||
background = new(emptyCtx)
|
||||
todo = new(emptyCtx)
|
||||
)
|
||||
|
||||
// Background returns a non-nil, empty Context. It is never canceled, has no
|
||||
// values, and has no deadline. It is typically used by the main function,
|
||||
// initialization, and tests, and as the top-level Context for incoming
|
||||
// requests.
|
||||
func Background() Context {
|
||||
return background
|
||||
}
|
||||
|
||||
// TODO returns a non-nil, empty Context. Code should use context.TODO when
|
||||
// it's unclear which Context to use or it's is not yet available (because the
|
||||
// surrounding function has not yet been extended to accept a Context
|
||||
// parameter). TODO is recognized by static analysis tools that determine
|
||||
// whether Contexts are propagated correctly in a program.
|
||||
func TODO() Context {
|
||||
return todo
|
||||
}
|
||||
|
||||
// A CancelFunc tells an operation to abandon its work.
|
||||
// A CancelFunc does not wait for the work to stop.
|
||||
// After the first call, subsequent calls to a CancelFunc do nothing.
|
||||
type CancelFunc func()
|
||||
|
||||
// WithCancel returns a copy of parent with a new Done channel. The returned
|
||||
// context's Done channel is closed when the returned cancel function is called
|
||||
// or when the parent context's Done channel is closed, whichever happens first.
|
||||
//
|
||||
// Canceling this context releases resources associated with it, so code should
|
||||
// call cancel as soon as the operations running in this Context complete.
|
||||
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
|
||||
c := newCancelCtx(parent)
|
||||
propagateCancel(parent, &c)
|
||||
return &c, func() { c.cancel(true, Canceled) }
|
||||
}
|
||||
|
||||
// newCancelCtx returns an initialized cancelCtx.
|
||||
func newCancelCtx(parent Context) cancelCtx {
|
||||
return cancelCtx{
|
||||
Context: parent,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// propagateCancel arranges for child to be canceled when parent is.
|
||||
func propagateCancel(parent Context, child canceler) {
|
||||
if parent.Done() == nil {
|
||||
return // parent is never canceled
|
||||
}
|
||||
if p, ok := parentCancelCtx(parent); ok {
|
||||
p.mu.Lock()
|
||||
if p.err != nil {
|
||||
// parent has already been canceled
|
||||
child.cancel(false, p.err)
|
||||
} else {
|
||||
if p.children == nil {
|
||||
p.children = make(map[canceler]bool)
|
||||
}
|
||||
p.children[child] = true
|
||||
}
|
||||
p.mu.Unlock()
|
||||
} else {
|
||||
go func() {
|
||||
select {
|
||||
case <-parent.Done():
|
||||
child.cancel(false, parent.Err())
|
||||
case <-child.Done():
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// parentCancelCtx follows a chain of parent references until it finds a
|
||||
// *cancelCtx. This function understands how each of the concrete types in this
|
||||
// package represents its parent.
|
||||
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
|
||||
for {
|
||||
switch c := parent.(type) {
|
||||
case *cancelCtx:
|
||||
return c, true
|
||||
case *timerCtx:
|
||||
return &c.cancelCtx, true
|
||||
case *valueCtx:
|
||||
parent = c.Context
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// removeChild removes a context from its parent.
|
||||
func removeChild(parent Context, child canceler) {
|
||||
p, ok := parentCancelCtx(parent)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
p.mu.Lock()
|
||||
if p.children != nil {
|
||||
delete(p.children, child)
|
||||
}
|
||||
p.mu.Unlock()
|
||||
}
|
||||
|
||||
// A canceler is a context type that can be canceled directly. The
|
||||
// implementations are *cancelCtx and *timerCtx.
|
||||
type canceler interface {
|
||||
cancel(removeFromParent bool, err error)
|
||||
Done() <-chan struct{}
|
||||
}
|
||||
|
||||
// A cancelCtx can be canceled. When canceled, it also cancels any children
|
||||
// that implement canceler.
|
||||
type cancelCtx struct {
|
||||
Context
|
||||
|
||||
done chan struct{} // closed by the first cancel call.
|
||||
|
||||
mu sync.Mutex
|
||||
children map[canceler]bool // set to nil by the first cancel call
|
||||
err error // set to non-nil by the first cancel call
|
||||
}
|
||||
|
||||
func (c *cancelCtx) Done() <-chan struct{} {
|
||||
return c.done
|
||||
}
|
||||
|
||||
func (c *cancelCtx) Err() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.err
|
||||
}
|
||||
|
||||
func (c *cancelCtx) String() string {
|
||||
return fmt.Sprintf("%v.WithCancel", c.Context)
|
||||
}
|
||||
|
||||
// cancel closes c.done, cancels each of c's children, and, if
|
||||
// removeFromParent is true, removes c from its parent's children.
|
||||
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
|
||||
if err == nil {
|
||||
panic("context: internal error: missing cancel error")
|
||||
}
|
||||
c.mu.Lock()
|
||||
if c.err != nil {
|
||||
c.mu.Unlock()
|
||||
return // already canceled
|
||||
}
|
||||
c.err = err
|
||||
close(c.done)
|
||||
for child := range c.children {
|
||||
// NOTE: acquiring the child's lock while holding parent's lock.
|
||||
child.cancel(false, err)
|
||||
}
|
||||
c.children = nil
|
||||
c.mu.Unlock()
|
||||
|
||||
if removeFromParent {
|
||||
removeChild(c.Context, c)
|
||||
}
|
||||
}
|
||||
|
||||
// WithDeadline returns a copy of the parent context with the deadline adjusted
|
||||
// to be no later than d. If the parent's deadline is already earlier than d,
|
||||
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
|
||||
// context's Done channel is closed when the deadline expires, when the returned
|
||||
// cancel function is called, or when the parent context's Done channel is
|
||||
// closed, whichever happens first.
|
||||
//
|
||||
// Canceling this context releases resources associated with it, so code should
|
||||
// call cancel as soon as the operations running in this Context complete.
|
||||
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
|
||||
if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
|
||||
// The current deadline is already sooner than the new one.
|
||||
return WithCancel(parent)
|
||||
}
|
||||
c := &timerCtx{
|
||||
cancelCtx: newCancelCtx(parent),
|
||||
deadline: deadline,
|
||||
}
|
||||
propagateCancel(parent, c)
|
||||
d := deadline.Sub(time.Now())
|
||||
if d <= 0 {
|
||||
c.cancel(true, DeadlineExceeded) // deadline has already passed
|
||||
return c, func() { c.cancel(true, Canceled) }
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.err == nil {
|
||||
c.timer = time.AfterFunc(d, func() {
|
||||
c.cancel(true, DeadlineExceeded)
|
||||
})
|
||||
}
|
||||
return c, func() { c.cancel(true, Canceled) }
|
||||
}
|
||||
|
||||
// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
|
||||
// implement Done and Err. It implements cancel by stopping its timer then
|
||||
// delegating to cancelCtx.cancel.
|
||||
type timerCtx struct {
|
||||
cancelCtx
|
||||
timer *time.Timer // Under cancelCtx.mu.
|
||||
|
||||
deadline time.Time
|
||||
}
|
||||
|
||||
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
|
||||
return c.deadline, true
|
||||
}
|
||||
|
||||
func (c *timerCtx) String() string {
|
||||
return fmt.Sprintf("%v.WithDeadline(%s [%s])", c.cancelCtx.Context, c.deadline, c.deadline.Sub(time.Now()))
|
||||
}
|
||||
|
||||
func (c *timerCtx) cancel(removeFromParent bool, err error) {
|
||||
c.cancelCtx.cancel(false, err)
|
||||
if removeFromParent {
|
||||
// Remove this timerCtx from its parent cancelCtx's children.
|
||||
removeChild(c.cancelCtx.Context, c)
|
||||
}
|
||||
c.mu.Lock()
|
||||
if c.timer != nil {
|
||||
c.timer.Stop()
|
||||
c.timer = nil
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
|
||||
//
|
||||
// Canceling this context releases resources associated with it, so code should
|
||||
// call cancel as soon as the operations running in this Context complete:
|
||||
//
|
||||
// func slowOperationWithTimeout(ctx context.Context) (Result, error) {
|
||||
// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
|
||||
// defer cancel() // releases resources if slowOperation completes before timeout elapses
|
||||
// return slowOperation(ctx)
|
||||
// }
|
||||
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
|
||||
return WithDeadline(parent, time.Now().Add(timeout))
|
||||
}
|
||||
|
||||
// WithValue returns a copy of parent in which the value associated with key is
|
||||
// val.
|
||||
//
|
||||
// Use context Values only for request-scoped data that transits processes and
|
||||
// APIs, not for passing optional parameters to functions.
|
||||
func WithValue(parent Context, key interface{}, val interface{}) Context {
|
||||
return &valueCtx{parent, key, val}
|
||||
}
|
||||
|
||||
// A valueCtx carries a key-value pair. It implements Value for that key and
|
||||
// delegates all other calls to the embedded Context.
|
||||
type valueCtx struct {
|
||||
Context
|
||||
key, val interface{}
|
||||
}
|
||||
|
||||
func (c *valueCtx) String() string {
|
||||
return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
|
||||
}
|
||||
|
||||
func (c *valueCtx) Value(key interface{}) interface{} {
|
||||
if c.key == key {
|
||||
return c.val
|
||||
}
|
||||
return c.Context.Value(key)
|
||||
}
|
Loading…
Reference in a new issue