sources: basic pull code
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>docker-18.09
parent
58c113167f
commit
35e661faf3
2
Makefile
2
Makefile
|
@ -1,5 +1,5 @@
|
||||||
test:
|
test:
|
||||||
docker build -f ./hack/dockerfiles/test.Dockerfile .
|
./hack/test
|
||||||
|
|
||||||
vendor:
|
vendor:
|
||||||
./hack/update-vendor
|
./hack/update-vendor
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// +build linux
|
||||||
|
|
||||||
package control
|
package control
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -19,15 +21,45 @@ import (
|
||||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/tonistiigi/buildkit_poc/cachemanager"
|
||||||
|
"github.com/tonistiigi/buildkit_poc/sources"
|
||||||
|
"github.com/tonistiigi/buildkit_poc/sources/containerimage"
|
||||||
|
"github.com/tonistiigi/buildkit_poc/sources/identifier"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestControl(t *testing.T) {
|
func TestControl(t *testing.T) {
|
||||||
tmpdir, err := ioutil.TempDir("", "cachemanager")
|
tmpdir, err := ioutil.TempDir("", "controltest")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
defer os.RemoveAll(tmpdir)
|
// defer os.RemoveAll(tmpdir)
|
||||||
|
|
||||||
_, err = localContainerd(tmpdir)
|
cd, err := localContainerd(tmpdir)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
cm, err := cachemanager.NewCacheManager(cachemanager.CacheManagerOpt{
|
||||||
|
Snapshotter: cd.Snapshotter,
|
||||||
|
Root: filepath.Join(tmpdir, "cachemanager"),
|
||||||
|
})
|
||||||
|
|
||||||
|
sm, err := sources.NewSourceManager()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
is, err := containerimage.NewContainerImageSource(containerimage.ContainerImageSourceOpt{
|
||||||
|
Snapshotter: cd.Snapshotter,
|
||||||
|
ContentStore: cd.ContentStore,
|
||||||
|
Applier: cd.Applier,
|
||||||
|
CacheAccessor: cm,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
sm.Register(is)
|
||||||
|
|
||||||
|
img, err := identifier.NewImageIdentifier("docker.io/library/redis:latest")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
snap, err := sm.Pull(context.TODO(), img)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
_ = snap
|
||||||
}
|
}
|
||||||
|
|
||||||
type containerd struct {
|
type containerd struct {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
FROM golang:1.8-alpine AS vndr
|
FROM golang:1.8-alpine AS vndr
|
||||||
RUN apk add --no-cache g++ linux-headers
|
RUN apk add --no-cache g++ linux-headers
|
||||||
WORKDIR /go/src/github.com/tonistiigi/buildkit_poc
|
WORKDIR /go/src/github.com/tonistiigi/buildkit_poc
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN go test ./...
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -eu -o pipefail -x
|
||||||
|
|
||||||
|
# update this to iidfile after 17.06
|
||||||
|
docker build -t buildkit_poc:test -f ./hack/dockerfiles/test.Dockerfile --force-rm .
|
||||||
|
docker run --cap-add=SYS_ADMIN buildkit_poc:test go test ${TESTFLAGS:--v} ${TESTPKGS:-./...}
|
|
@ -0,0 +1,123 @@
|
||||||
|
package containerimage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd/content"
|
||||||
|
"github.com/containerd/containerd/images"
|
||||||
|
"github.com/containerd/containerd/remotes"
|
||||||
|
"github.com/containerd/containerd/remotes/docker"
|
||||||
|
"github.com/containerd/containerd/rootfs"
|
||||||
|
"github.com/containerd/containerd/snapshot"
|
||||||
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/tonistiigi/buildkit_poc/cachemanager"
|
||||||
|
"github.com/tonistiigi/buildkit_poc/sources"
|
||||||
|
"github.com/tonistiigi/buildkit_poc/sources/identifier"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: break apart containerd specifics like contentstore so the resolver
|
||||||
|
// code can be used with any implementation
|
||||||
|
|
||||||
|
type ContainerImageSourceOpt struct {
|
||||||
|
Snapshotter snapshot.Snapshotter
|
||||||
|
ContentStore content.Store
|
||||||
|
Applier rootfs.Applier
|
||||||
|
CacheAccessor cachemanager.CacheAccessor
|
||||||
|
}
|
||||||
|
|
||||||
|
type imageSource struct {
|
||||||
|
ContainerImageSourceOpt
|
||||||
|
resolver remotes.Resolver
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewContainerImageSource(opt ContainerImageSourceOpt) (sources.Source, error) {
|
||||||
|
is := &imageSource{
|
||||||
|
ContainerImageSourceOpt: opt,
|
||||||
|
resolver: docker.NewResolver(docker.ResolverOptions{
|
||||||
|
Client: http.DefaultClient,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
return is, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (is *imageSource) ID() string {
|
||||||
|
return identifier.DockerImageScheme
|
||||||
|
}
|
||||||
|
|
||||||
|
func (is *imageSource) Pull(ctx context.Context, id identifier.Identifier) (cachemanager.SnapshotRef, error) {
|
||||||
|
// TODO: update this to always centralize layer downloads/unpacks
|
||||||
|
// TODO: progress status
|
||||||
|
|
||||||
|
imageIdentifier, ok := id.(*identifier.ImageIdentifier)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("invalid identifier")
|
||||||
|
}
|
||||||
|
|
||||||
|
ref, desc, err := is.resolver.Resolve(ctx, imageIdentifier.Reference.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fetcher, err := is.resolver.Fetcher(ctx, ref)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
handlers := []images.Handler{
|
||||||
|
remotes.FetchHandler(is.ContentStore, fetcher),
|
||||||
|
images.ChildrenHandler(is.ContentStore),
|
||||||
|
}
|
||||||
|
if err := images.Dispatch(ctx, images.Handlers(handlers...), desc); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := is.unpack(ctx, desc); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (is *imageSource) unpack(ctx context.Context, desc ocispec.Descriptor) error {
|
||||||
|
layers, err := getLayers(ctx, is.ContentStore, desc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
chainid, err := rootfs.ApplyLayers(ctx, layers, is.Snapshotter, is.Applier)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = chainid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLayers(ctx context.Context, provider content.Provider, desc ocispec.Descriptor) ([]rootfs.Layer, error) {
|
||||||
|
p, err := content.ReadBlob(ctx, provider, desc.Digest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to read manifest blob")
|
||||||
|
}
|
||||||
|
var manifest ocispec.Manifest
|
||||||
|
if err := json.Unmarshal(p, &manifest); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to unmarshal manifest")
|
||||||
|
}
|
||||||
|
image := images.Image{Target: desc}
|
||||||
|
diffIDs, err := image.RootFS(ctx, provider)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to resolve rootfs")
|
||||||
|
}
|
||||||
|
if len(diffIDs) != len(manifest.Layers) {
|
||||||
|
return nil, errors.Errorf("mismatched image rootfs and manifest layers")
|
||||||
|
}
|
||||||
|
layers := make([]rootfs.Layer, len(diffIDs))
|
||||||
|
for i := range diffIDs {
|
||||||
|
layers[i].Diff = ocispec.Descriptor{
|
||||||
|
// TODO: derive media type from compressed type
|
||||||
|
MediaType: ocispec.MediaTypeImageLayer,
|
||||||
|
Digest: diffIDs[i],
|
||||||
|
}
|
||||||
|
layers[i].Blob = manifest.Layers[i]
|
||||||
|
}
|
||||||
|
return layers, nil
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package identifier
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd/reference"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errInvalid = errors.New("invalid")
|
||||||
|
errNotFound = errors.New("not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DockerImageScheme = "docker-image"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Identifier interface {
|
||||||
|
ID() string // until sources are in process this string comparison could be avoided
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromString(s string) (Identifier, error) {
|
||||||
|
// TODO: improve this
|
||||||
|
parts := strings.SplitN(s, "://", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return nil, errors.Wrapf(errInvalid, "failed to parse %s", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch parts[0] {
|
||||||
|
case DockerImageScheme:
|
||||||
|
return NewImageIdentifier(parts[1])
|
||||||
|
default:
|
||||||
|
return nil, errors.Wrapf(errNotFound, "unknown schema %s", parts[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageIdentifier struct {
|
||||||
|
Reference reference.Spec
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewImageIdentifier(str string) (*ImageIdentifier, error) {
|
||||||
|
ref, err := reference.Parse(str)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ref.Object == "" {
|
||||||
|
return nil, errors.WithStack(reference.ErrObjectRequired)
|
||||||
|
}
|
||||||
|
return &ImageIdentifier{Reference: ref}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ImageIdentifier) ID() string {
|
||||||
|
return DockerImageScheme
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
package sources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd/content"
|
||||||
|
"github.com/containerd/containerd/rootfs"
|
||||||
|
"github.com/containerd/containerd/snapshot"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/tonistiigi/buildkit_poc/cachemanager"
|
||||||
|
"github.com/tonistiigi/buildkit_poc/sources/identifier"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ContainerImageSourceOpt struct {
|
||||||
|
Snapshotter snapshot.Snapshotter
|
||||||
|
ContentStore content.Store
|
||||||
|
Applier rootfs.Applier
|
||||||
|
CacheAccessor cachemanager.CacheAccessor
|
||||||
|
}
|
||||||
|
|
||||||
|
type Source interface {
|
||||||
|
ID() string
|
||||||
|
Pull(ctx context.Context, id identifier.Identifier) (cachemanager.SnapshotRef, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SourceManager struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
sources map[string]Source
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSourceManager() (*SourceManager, error) {
|
||||||
|
return &SourceManager{
|
||||||
|
sources: make(map[string]Source),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *SourceManager) Register(src Source) {
|
||||||
|
sm.mu.Lock()
|
||||||
|
sm.sources[src.ID()] = src
|
||||||
|
sm.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *SourceManager) Pull(ctx context.Context, id identifier.Identifier) (cachemanager.SnapshotRef, error) {
|
||||||
|
sm.mu.Lock()
|
||||||
|
src, ok := sm.sources[id.ID()]
|
||||||
|
sm.mu.Unlock()
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.Errorf("no handler fro %s", id.ID())
|
||||||
|
}
|
||||||
|
|
||||||
|
return src.Pull(ctx, id)
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
package images
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd/content"
|
||||||
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
var SkipDesc = fmt.Errorf("skip descriptor")
|
||||||
|
|
||||||
|
type Handler interface {
|
||||||
|
Handle(ctx context.Context, desc ocispec.Descriptor) (subdescs []ocispec.Descriptor, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type HandlerFunc func(ctx context.Context, desc ocispec.Descriptor) (subdescs []ocispec.Descriptor, err error)
|
||||||
|
|
||||||
|
func (fn HandlerFunc) Handle(ctx context.Context, desc ocispec.Descriptor) (subdescs []ocispec.Descriptor, err error) {
|
||||||
|
return fn(ctx, desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handlers returns a handler that will run the handlers in sequence.
|
||||||
|
func Handlers(handlers ...Handler) HandlerFunc {
|
||||||
|
return func(ctx context.Context, desc ocispec.Descriptor) (subdescs []ocispec.Descriptor, err error) {
|
||||||
|
var children []ocispec.Descriptor
|
||||||
|
for _, handler := range handlers {
|
||||||
|
ch, err := handler.Handle(ctx, desc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
children = append(children, ch...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return children, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk the resources of an image and call the handler for each. If the handler
|
||||||
|
// decodes the sub-resources for each image,
|
||||||
|
//
|
||||||
|
// This differs from dispatch in that each sibling resource is considered
|
||||||
|
// synchronously.
|
||||||
|
func Walk(ctx context.Context, handler Handler, descs ...ocispec.Descriptor) error {
|
||||||
|
for _, desc := range descs {
|
||||||
|
|
||||||
|
children, err := handler.Handle(ctx, desc)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Cause(err) == SkipDesc {
|
||||||
|
return nil // don't traverse the children.
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(children) > 0 {
|
||||||
|
if err := Walk(ctx, handler, children...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch runs the provided handler for content specified by the descriptors.
|
||||||
|
// If the handler decode subresources, they will be visited, as well.
|
||||||
|
//
|
||||||
|
// Handlers for siblings are run in parallel on the provided descriptors. A
|
||||||
|
// handler may return `SkipDesc` to signal to the dispatcher to not traverse
|
||||||
|
// any children.
|
||||||
|
//
|
||||||
|
// Typically, this function will be used with `FetchHandler`, often composed
|
||||||
|
// with other handlers.
|
||||||
|
//
|
||||||
|
// If any handler returns an error, the dispatch session will be canceled.
|
||||||
|
func Dispatch(ctx context.Context, handler Handler, descs ...ocispec.Descriptor) error {
|
||||||
|
eg, ctx := errgroup.WithContext(ctx)
|
||||||
|
for _, desc := range descs {
|
||||||
|
desc := desc
|
||||||
|
|
||||||
|
eg.Go(func() error {
|
||||||
|
desc := desc
|
||||||
|
|
||||||
|
children, err := handler.Handle(ctx, desc)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Cause(err) == SkipDesc {
|
||||||
|
return nil // don't traverse the children.
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(children) > 0 {
|
||||||
|
return Dispatch(ctx, handler, children...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return eg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChildrenHandler decodes well-known manifests types and returns their children.
|
||||||
|
//
|
||||||
|
// This is useful for supporting recursive fetch and other use cases where you
|
||||||
|
// want to do a full walk of resources.
|
||||||
|
//
|
||||||
|
// One can also replace this with another implementation to allow descending of
|
||||||
|
// arbitrary types.
|
||||||
|
func ChildrenHandler(provider content.Provider) HandlerFunc {
|
||||||
|
return func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
||||||
|
switch desc.MediaType {
|
||||||
|
case MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
|
||||||
|
case MediaTypeDockerSchema2Layer, MediaTypeDockerSchema2LayerGzip,
|
||||||
|
MediaTypeDockerSchema2Config:
|
||||||
|
return nil, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("%v not yet supported", desc.MediaType)
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := content.ReadBlob(ctx, provider, desc.Digest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(stevvooe): We just assume oci manifest, for now. There may be
|
||||||
|
// subtle differences from the docker version.
|
||||||
|
var manifest ocispec.Manifest
|
||||||
|
if err := json.Unmarshal(p, &manifest); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var descs []ocispec.Descriptor
|
||||||
|
|
||||||
|
descs = append(descs, manifest.Config)
|
||||||
|
descs = append(descs, manifest.Layers...)
|
||||||
|
|
||||||
|
return descs, nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
package images
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd/content"
|
||||||
|
digest "github.com/opencontainers/go-digest"
|
||||||
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Image provides the model for how containerd views container images.
|
||||||
|
type Image struct {
|
||||||
|
Name string
|
||||||
|
Target ocispec.Descriptor
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(stevvooe): Many of these functions make strong platform assumptions,
|
||||||
|
// which are untrue in a lot of cases. More refactoring must be done here to
|
||||||
|
// make this work in all cases.
|
||||||
|
|
||||||
|
// Config resolves the image configuration descriptor.
|
||||||
|
//
|
||||||
|
// The caller can then use the descriptor to resolve and process the
|
||||||
|
// configuration of the image.
|
||||||
|
func (image *Image) Config(ctx context.Context, provider content.Provider) (ocispec.Descriptor, error) {
|
||||||
|
var configDesc ocispec.Descriptor
|
||||||
|
return configDesc, Walk(ctx, HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
||||||
|
switch image.Target.MediaType {
|
||||||
|
case MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
|
||||||
|
rc, err := provider.Reader(ctx, image.Target.Digest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rc.Close()
|
||||||
|
|
||||||
|
p, err := ioutil.ReadAll(rc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var manifest ocispec.Manifest
|
||||||
|
if err := json.Unmarshal(p, &manifest); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
configDesc = manifest.Config
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
default:
|
||||||
|
return nil, errors.New("could not resolve config")
|
||||||
|
}
|
||||||
|
|
||||||
|
}), image.Target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RootFS returns the unpacked diffids that make up and images rootfs.
|
||||||
|
//
|
||||||
|
// These are used to verify that a set of layers unpacked to the expected
|
||||||
|
// values.
|
||||||
|
func (image *Image) RootFS(ctx context.Context, provider content.Provider) ([]digest.Digest, error) {
|
||||||
|
desc, err := image.Config(ctx, provider)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return RootFS(ctx, provider, desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the total size of an image's packed resources.
|
||||||
|
func (image *Image) Size(ctx context.Context, provider content.Provider) (int64, error) {
|
||||||
|
var size int64
|
||||||
|
return size, Walk(ctx, HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
||||||
|
switch image.Target.MediaType {
|
||||||
|
case MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
|
||||||
|
size += desc.Size
|
||||||
|
rc, err := provider.Reader(ctx, image.Target.Digest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rc.Close()
|
||||||
|
|
||||||
|
p, err := ioutil.ReadAll(rc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var manifest ocispec.Manifest
|
||||||
|
if err := json.Unmarshal(p, &manifest); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
size += manifest.Config.Size
|
||||||
|
|
||||||
|
for _, layer := range manifest.Layers {
|
||||||
|
size += layer.Size
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unsupported type")
|
||||||
|
}
|
||||||
|
|
||||||
|
}), image.Target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RootFS returns the unpacked diffids that make up and images rootfs.
|
||||||
|
//
|
||||||
|
// These are used to verify that a set of layers unpacked to the expected
|
||||||
|
// values.
|
||||||
|
func RootFS(ctx context.Context, provider content.Provider, desc ocispec.Descriptor) ([]digest.Digest, error) {
|
||||||
|
p, err := content.ReadBlob(ctx, provider, desc.Digest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var config ocispec.Image
|
||||||
|
if err := json.Unmarshal(p, &config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(stevvooe): Remove this bit when OCI structure uses correct type for
|
||||||
|
// rootfs.DiffIDs.
|
||||||
|
var diffIDs []digest.Digest
|
||||||
|
for _, diffID := range config.RootFS.DiffIDs {
|
||||||
|
diffIDs = append(diffIDs, digest.Digest(diffID))
|
||||||
|
}
|
||||||
|
|
||||||
|
return diffIDs, nil
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package images
|
||||||
|
|
||||||
|
// mediatype definitions for image components handled in containerd.
|
||||||
|
//
|
||||||
|
// oci components are generally referenced directly, although we may centralize
|
||||||
|
// here for clarity.
|
||||||
|
const (
|
||||||
|
MediaTypeDockerSchema2Layer = "application/vnd.docker.image.rootfs.diff.tar"
|
||||||
|
MediaTypeDockerSchema2LayerGzip = "application/vnd.docker.image.rootfs.diff.tar.gzip"
|
||||||
|
MediaTypeDockerSchema2Config = "application/vnd.docker.container.image.v1+json"
|
||||||
|
MediaTypeDockerSchema2Manifest = "application/vnd.docker.distribution.manifest.v2+json"
|
||||||
|
MediaTypeDockerSchema2ManifestList = "application/vnd.docker.distribution.manifest.list.v2+json"
|
||||||
|
// Checkpoint/Restore Media Types
|
||||||
|
MediaTypeContainerd1Checkpoint = "application/vnd.containerd.container.criu.checkpoint.criu.tar"
|
||||||
|
MediaTypeContainerd1CheckpointPreDump = "application/vnd.containerd.container.criu.checkpoint.predump.tar"
|
||||||
|
MediaTypeContainerd1Resource = "application/vnd.containerd.container.resource.tar"
|
||||||
|
MediaTypeContainerd1RW = "application/vnd.containerd.container.rw.tar"
|
||||||
|
MediaTypeContainerd1CheckpointConfig = "application/vnd.containerd.container.checkpoint.config.v1+json"
|
||||||
|
)
|
|
@ -0,0 +1,219 @@
|
||||||
|
package images
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
|
"github.com/containerd/containerd/log"
|
||||||
|
digest "github.com/opencontainers/go-digest"
|
||||||
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrExists = errors.New("images: exists")
|
||||||
|
ErrNotFound = errors.New("images: not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Store interface {
|
||||||
|
Put(ctx context.Context, name string, desc ocispec.Descriptor) error
|
||||||
|
Get(ctx context.Context, name string) (Image, error)
|
||||||
|
List(ctx context.Context) ([]Image, error)
|
||||||
|
Delete(ctx context.Context, name string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNotFound returns true if the error is due to a missing image.
|
||||||
|
func IsNotFound(err error) bool {
|
||||||
|
return errors.Cause(err) == ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsExists(err error) bool {
|
||||||
|
return errors.Cause(err) == ErrExists
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
bucketKeyStorageVersion = []byte("v1")
|
||||||
|
bucketKeyImages = []byte("images")
|
||||||
|
bucketKeyDigest = []byte("digest")
|
||||||
|
bucketKeyMediaType = []byte("mediatype")
|
||||||
|
bucketKeySize = []byte("size")
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO(stevvooe): This file comprises the data required to implement the
|
||||||
|
// "metadata" store. For now, it is bound tightly to the local machine and bolt
|
||||||
|
// but we can take this and use it to define a service interface.
|
||||||
|
|
||||||
|
// InitDB will initialize the database for use. The database must be opened for
|
||||||
|
// write and the caller must not be holding an open transaction.
|
||||||
|
func InitDB(db *bolt.DB) error {
|
||||||
|
log.L.Debug("init db")
|
||||||
|
return db.Update(func(tx *bolt.Tx) error {
|
||||||
|
_, err := createBucketIfNotExists(tx, bucketKeyStorageVersion, bucketKeyImages)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStore(tx *bolt.Tx) Store {
|
||||||
|
return &storage{tx: tx}
|
||||||
|
}
|
||||||
|
|
||||||
|
type storage struct {
|
||||||
|
tx *bolt.Tx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *storage) Get(ctx context.Context, name string) (Image, error) {
|
||||||
|
var image Image
|
||||||
|
if err := withImageBucket(s.tx, name, func(bkt *bolt.Bucket) error {
|
||||||
|
image.Name = name
|
||||||
|
return readImage(&image, bkt)
|
||||||
|
}); err != nil {
|
||||||
|
return Image{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return image, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *storage) Put(ctx context.Context, name string, desc ocispec.Descriptor) error {
|
||||||
|
return withImagesBucket(s.tx, func(bkt *bolt.Bucket) error {
|
||||||
|
ibkt, err := bkt.CreateBucketIfNotExists([]byte(name))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
buf [binary.MaxVarintLen64]byte
|
||||||
|
sizeEncoded []byte = buf[:]
|
||||||
|
)
|
||||||
|
sizeEncoded = sizeEncoded[:binary.PutVarint(sizeEncoded, desc.Size)]
|
||||||
|
|
||||||
|
if len(sizeEncoded) == 0 {
|
||||||
|
return fmt.Errorf("failed encoding size = %v", desc.Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range [][2][]byte{
|
||||||
|
{bucketKeyDigest, []byte(desc.Digest)},
|
||||||
|
{bucketKeyMediaType, []byte(desc.MediaType)},
|
||||||
|
{bucketKeySize, sizeEncoded},
|
||||||
|
} {
|
||||||
|
if err := ibkt.Put(v[0], v[1]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *storage) List(ctx context.Context) ([]Image, error) {
|
||||||
|
var images []Image
|
||||||
|
|
||||||
|
if err := withImagesBucket(s.tx, func(bkt *bolt.Bucket) error {
|
||||||
|
return bkt.ForEach(func(k, v []byte) error {
|
||||||
|
var (
|
||||||
|
image = Image{
|
||||||
|
Name: string(k),
|
||||||
|
}
|
||||||
|
kbkt = bkt.Bucket(k)
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := readImage(&image, kbkt); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
images = append(images, image)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return images, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *storage) Delete(ctx context.Context, name string) error {
|
||||||
|
return withImagesBucket(s.tx, func(bkt *bolt.Bucket) error {
|
||||||
|
err := bkt.DeleteBucket([]byte(name))
|
||||||
|
if err == bolt.ErrBucketNotFound {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func readImage(image *Image, bkt *bolt.Bucket) error {
|
||||||
|
return bkt.ForEach(func(k, v []byte) error {
|
||||||
|
if v == nil {
|
||||||
|
return nil // skip it? a bkt maybe?
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(stevvooe): This is why we need to use byte values for
|
||||||
|
// keys, rather than full arrays.
|
||||||
|
switch string(k) {
|
||||||
|
case string(bucketKeyDigest):
|
||||||
|
image.Target.Digest = digest.Digest(v)
|
||||||
|
case string(bucketKeyMediaType):
|
||||||
|
image.Target.MediaType = string(v)
|
||||||
|
case string(bucketKeySize):
|
||||||
|
image.Target.Size, _ = binary.Varint(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func createBucketIfNotExists(tx *bolt.Tx, keys ...[]byte) (*bolt.Bucket, error) {
|
||||||
|
bkt, err := tx.CreateBucketIfNotExists(keys[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range keys[1:] {
|
||||||
|
bkt, err = bkt.CreateBucketIfNotExists(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bkt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func withImagesBucket(tx *bolt.Tx, fn func(bkt *bolt.Bucket) error) error {
|
||||||
|
bkt := getImagesBucket(tx)
|
||||||
|
if bkt == nil {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return fn(bkt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func withImageBucket(tx *bolt.Tx, name string, fn func(bkt *bolt.Bucket) error) error {
|
||||||
|
bkt := getImageBucket(tx, name)
|
||||||
|
if bkt == nil {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return fn(bkt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getImagesBucket(tx *bolt.Tx) *bolt.Bucket {
|
||||||
|
return getBucket(tx, bucketKeyStorageVersion, bucketKeyImages)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getImageBucket(tx *bolt.Tx, name string) *bolt.Bucket {
|
||||||
|
return getBucket(tx, bucketKeyStorageVersion, bucketKeyImages, []byte(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBucket(tx *bolt.Tx, keys ...[]byte) *bolt.Bucket {
|
||||||
|
bkt := tx.Bucket(keys[0])
|
||||||
|
|
||||||
|
for _, key := range keys[1:] {
|
||||||
|
if bkt == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
bkt = bkt.Bucket(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bkt
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
package reference
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
digest "github.com/opencontainers/go-digest"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalid = errors.New("invalid reference")
|
||||||
|
ErrObjectRequired = errors.New("object required")
|
||||||
|
ErrHostnameRequired = errors.New("hostname required")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Spec defines the main components of a reference specification.
|
||||||
|
//
|
||||||
|
// A reference specification is a schema-less URI parsed into common
|
||||||
|
// components. The two main components, locator and object, are required to be
|
||||||
|
// supported by remotes. It represents a superset of the naming define in
|
||||||
|
// docker's reference schema. It aims to be compatible but not prescriptive.
|
||||||
|
//
|
||||||
|
// While the interpretation of the components, locator and object, are up to
|
||||||
|
// the remote, we define a few common parts, accessible via helper methods.
|
||||||
|
//
|
||||||
|
// The first is the hostname, which is part of the locator. This doesn't need
|
||||||
|
// to map to a physical resource, but it must parse as a hostname. We refer to
|
||||||
|
// this as the namespace.
|
||||||
|
//
|
||||||
|
// The other component made accessible by helper method is the digest. This is
|
||||||
|
// part of the object identifier, always prefixed with an '@'. If present, the
|
||||||
|
// remote may use the digest portion directly or resolve it against a prefix.
|
||||||
|
// If the object does not include the `@` symbol, the return value for `Digest`
|
||||||
|
// will be empty.
|
||||||
|
type Spec struct {
|
||||||
|
// Locator is the host and path portion of the specification. The host
|
||||||
|
// portion may refer to an actual host or just a namespace of related
|
||||||
|
// images.
|
||||||
|
//
|
||||||
|
// Typically, the locator may used to resolve the remote to fetch specific
|
||||||
|
// resources.
|
||||||
|
Locator string
|
||||||
|
|
||||||
|
// Object contains the identifier for the remote resource. Classically,
|
||||||
|
// this is a tag but can refer to anything in a remote. By convention, any
|
||||||
|
// portion that may be a partial or whole digest will be preceeded by an
|
||||||
|
// `@`. Anything preceeding the `@` will be referred to as the "tag".
|
||||||
|
//
|
||||||
|
// In practice, we will see this broken down into the following formats:
|
||||||
|
//
|
||||||
|
// 1. <tag>
|
||||||
|
// 2. <tag>@<digest spec>
|
||||||
|
// 3. @<digest spec>
|
||||||
|
//
|
||||||
|
// We define the tag to be anything except '@' and ':'. <digest spec> may
|
||||||
|
// be a full valid digest or shortened version, possibly with elided
|
||||||
|
// algorithm.
|
||||||
|
Object string
|
||||||
|
}
|
||||||
|
|
||||||
|
var splitRe = regexp.MustCompile(`[:@]`)
|
||||||
|
|
||||||
|
// Parse parses the string into a structured ref.
|
||||||
|
func Parse(s string) (Spec, error) {
|
||||||
|
u, err := url.Parse("dummy://" + s)
|
||||||
|
if err != nil {
|
||||||
|
return Spec{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Scheme != "dummy" {
|
||||||
|
return Spec{}, ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Host == "" {
|
||||||
|
return Spec{}, ErrHostnameRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
var object string
|
||||||
|
|
||||||
|
if idx := splitRe.FindStringIndex(u.Path); idx != nil {
|
||||||
|
// This allows us to retain the @ to signify digests or shortend digests in
|
||||||
|
// the object.
|
||||||
|
object = u.Path[idx[0]:]
|
||||||
|
if object[:1] == ":" {
|
||||||
|
object = object[1:]
|
||||||
|
}
|
||||||
|
u.Path = u.Path[:idx[0]]
|
||||||
|
}
|
||||||
|
|
||||||
|
return Spec{
|
||||||
|
Locator: path.Join(u.Host, u.Path),
|
||||||
|
Object: object,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hostname returns the hostname portion of the locator.
|
||||||
|
//
|
||||||
|
// Remotes are not required to directly access the resources at this host. This
|
||||||
|
// method is provided for convenience.
|
||||||
|
func (r Spec) Hostname() string {
|
||||||
|
i := strings.Index(r.Locator, "/")
|
||||||
|
|
||||||
|
if i < 0 {
|
||||||
|
i = len(r.Locator) + 1
|
||||||
|
}
|
||||||
|
return r.Locator[:i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Digest returns the digest portion of the reference spec. This may be a
|
||||||
|
// partial or invalid digest, which may be used to lookup a complete digest.
|
||||||
|
func (r Spec) Digest() digest.Digest {
|
||||||
|
_, dgst := SplitObject(r.Object)
|
||||||
|
return dgst
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the normalized string for the ref.
|
||||||
|
func (r Spec) String() string {
|
||||||
|
if r.Object == "" {
|
||||||
|
return r.Locator
|
||||||
|
}
|
||||||
|
if r.Object[:1] == "@" {
|
||||||
|
return fmt.Sprintf("%v%v", r.Locator, r.Object)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%v:%v", r.Locator, r.Object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SplitObject provides two parts of the object spec, delimiited by an `@`
|
||||||
|
// symbol.
|
||||||
|
//
|
||||||
|
// Either may be empty and it is the callers job to validate them
|
||||||
|
// appropriately.
|
||||||
|
func SplitObject(obj string) (tag string, dgst digest.Digest) {
|
||||||
|
parts := strings.SplitAfterN(obj, "@", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return parts[0], ""
|
||||||
|
} else {
|
||||||
|
return parts[0], digest.Digest(parts[1])
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,182 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type authenticationScheme byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
basicAuth authenticationScheme = 1 << iota // Defined in RFC 7617
|
||||||
|
digestAuth // Defined in RFC 7616
|
||||||
|
bearerAuth // Defined in RFC 6750
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 authenticationScheme
|
||||||
|
|
||||||
|
// parameters are the auth-params according to RFC 2617
|
||||||
|
parameters map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type byScheme []challenge
|
||||||
|
|
||||||
|
func (bs byScheme) Len() int { return len(bs) }
|
||||||
|
func (bs byScheme) Swap(i, j int) { bs[i], bs[j] = bs[j], bs[i] }
|
||||||
|
|
||||||
|
// Sort in priority order: token > digest > basic
|
||||||
|
func (bs byScheme) Less(i, j int) bool { return bs[i].scheme > bs[j].scheme }
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAuthHeader(header http.Header) []challenge {
|
||||||
|
challenges := []challenge{}
|
||||||
|
for _, h := range header[http.CanonicalHeaderKey("WWW-Authenticate")] {
|
||||||
|
v, p := parseValueAndParams(h)
|
||||||
|
var s authenticationScheme
|
||||||
|
switch v {
|
||||||
|
case "basic":
|
||||||
|
s = basicAuth
|
||||||
|
case "digest":
|
||||||
|
s = digestAuth
|
||||||
|
case "bearer":
|
||||||
|
s = bearerAuth
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
challenges = append(challenges, challenge{scheme: s, parameters: p})
|
||||||
|
}
|
||||||
|
sort.Stable(byScheme(challenges))
|
||||||
|
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)
|
||||||
|
for {
|
||||||
|
var pkey string
|
||||||
|
pkey, s = expectToken(skipSpace(s))
|
||||||
|
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)
|
||||||
|
if !strings.HasPrefix(s, ",") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s = s[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 "", ""
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/containerd/containerd/images"
|
||||||
|
"github.com/containerd/containerd/log"
|
||||||
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dockerFetcher struct {
|
||||||
|
*dockerBase
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r dockerFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) {
|
||||||
|
ctx = log.WithLogger(ctx, log.G(ctx).WithFields(
|
||||||
|
logrus.Fields{
|
||||||
|
"base": r.base.String(),
|
||||||
|
"digest": desc.Digest,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
|
||||||
|
paths, err := getV2URLPaths(desc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range paths {
|
||||||
|
u := r.url(path)
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, u, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Accept", strings.Join([]string{desc.MediaType, `*`}, ", "))
|
||||||
|
resp, err := r.doRequestWithRetries(ctx, req, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode > 299 {
|
||||||
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
|
continue // try one of the other urls.
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
return nil, errors.Errorf("unexpected status code %v: %v", u, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.Body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getV2URLPaths generates the candidate urls paths for the object based on the
|
||||||
|
// set of hints and the provided object id. URLs are returned in the order of
|
||||||
|
// most to least likely succeed.
|
||||||
|
func getV2URLPaths(desc ocispec.Descriptor) ([]string, error) {
|
||||||
|
var urls []string
|
||||||
|
|
||||||
|
switch desc.MediaType {
|
||||||
|
case images.MediaTypeDockerSchema2Manifest, images.MediaTypeDockerSchema2ManifestList,
|
||||||
|
ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex:
|
||||||
|
urls = append(urls, path.Join("manifests", desc.Digest.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// always fallback to attempting to get the object out of the blobs store.
|
||||||
|
urls = append(urls, path.Join("blobs", desc.Digest.String()))
|
||||||
|
|
||||||
|
return urls, nil
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd/images"
|
||||||
|
"github.com/containerd/containerd/log"
|
||||||
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dockerPusher struct {
|
||||||
|
*dockerBase
|
||||||
|
tag string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p dockerPusher) Push(ctx context.Context, desc ocispec.Descriptor, r io.Reader) error {
|
||||||
|
var (
|
||||||
|
isManifest bool
|
||||||
|
existCheck string
|
||||||
|
)
|
||||||
|
|
||||||
|
switch desc.MediaType {
|
||||||
|
case images.MediaTypeDockerSchema2Manifest, images.MediaTypeDockerSchema2ManifestList,
|
||||||
|
ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex:
|
||||||
|
isManifest = true
|
||||||
|
existCheck = path.Join("manifests", desc.Digest.String())
|
||||||
|
default:
|
||||||
|
existCheck = path.Join("blobs", desc.Digest.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodHead, p.url(existCheck), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Accept", strings.Join([]string{desc.MediaType, `*`}, ", "))
|
||||||
|
resp, err := p.doRequestWithRetries(ctx, req, nil)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Cause(err) != ErrInvalidAuthorization {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.G(ctx).WithError(err).Debugf("Unable to check existence, continuing with push")
|
||||||
|
} else {
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusNotFound {
|
||||||
|
// TODO: log error
|
||||||
|
return errors.Errorf("unexpected response: %s", resp.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Lookup related objects for cross repository push
|
||||||
|
|
||||||
|
if isManifest {
|
||||||
|
// Read all to use bytes.Reader for using GetBody
|
||||||
|
b, err := ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to read manifest")
|
||||||
|
}
|
||||||
|
var putPath string
|
||||||
|
if p.tag != "" {
|
||||||
|
putPath = path.Join("manifests", p.tag)
|
||||||
|
} else {
|
||||||
|
putPath = path.Join("manifests", desc.Digest.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPut, p.url(putPath), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.ContentLength = int64(len(b))
|
||||||
|
req.Body = ioutil.NopCloser(bytes.NewReader(b))
|
||||||
|
req.GetBody = func() (io.ReadCloser, error) {
|
||||||
|
return ioutil.NopCloser(bytes.NewReader(b)), nil
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", desc.MediaType)
|
||||||
|
|
||||||
|
resp, err := p.doRequestWithRetries(ctx, req, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusCreated {
|
||||||
|
// TODO: log error
|
||||||
|
return errors.Errorf("unexpected response: %s", resp.Status)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO: Do monolithic upload if size is small
|
||||||
|
|
||||||
|
// TODO: Turn multi-request blob uploader into ingester
|
||||||
|
|
||||||
|
// Start upload request
|
||||||
|
req, err := http.NewRequest(http.MethodPost, p.url("blobs", "uploads")+"/", nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := p.doRequestWithRetries(ctx, req, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusAccepted {
|
||||||
|
// TODO: log error
|
||||||
|
return errors.Errorf("unexpected response: %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
location := resp.Header.Get("Location")
|
||||||
|
// Support paths without host in location
|
||||||
|
if strings.HasPrefix(location, "/") {
|
||||||
|
u := p.base
|
||||||
|
u.Path = location
|
||||||
|
location = u.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Support chunked upload
|
||||||
|
req, err = http.NewRequest(http.MethodPut, location, r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
q := req.URL.Query()
|
||||||
|
q.Add("digest", desc.Digest.String())
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
req.ContentLength = desc.Size
|
||||||
|
|
||||||
|
resp, err = p.doRequest(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusCreated {
|
||||||
|
// TODO: log error
|
||||||
|
return errors.Errorf("unexpected response: %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
564
vendor/github.com/containerd/containerd/remotes/docker/resolver.go
generated
vendored
Normal file
564
vendor/github.com/containerd/containerd/remotes/docker/resolver.go
generated
vendored
Normal file
|
@ -0,0 +1,564 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/textproto"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/containerd/containerd/images"
|
||||||
|
"github.com/containerd/containerd/log"
|
||||||
|
"github.com/containerd/containerd/reference"
|
||||||
|
"github.com/containerd/containerd/remotes"
|
||||||
|
digest "github.com/opencontainers/go-digest"
|
||||||
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"golang.org/x/net/context/ctxhttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrNoToken is returned if a request is successful but the body does not
|
||||||
|
// contain an authorization token.
|
||||||
|
ErrNoToken = errors.New("authorization server did not include a token in the response")
|
||||||
|
|
||||||
|
// ErrInvalidAuthorization is used when credentials are passed to a server but
|
||||||
|
// those credentials are rejected.
|
||||||
|
ErrInvalidAuthorization = errors.New("authorization failed")
|
||||||
|
)
|
||||||
|
|
||||||
|
type dockerResolver struct {
|
||||||
|
credentials func(string) (string, string, error)
|
||||||
|
plainHTTP bool
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolverOptions are used to configured a new Docker register resolver
|
||||||
|
type ResolverOptions struct {
|
||||||
|
// Credentials provides username and secret given a host.
|
||||||
|
// If username is empty but a secret is given, that secret
|
||||||
|
// is interpretted as a long lived token.
|
||||||
|
Credentials func(string) (string, string, error)
|
||||||
|
|
||||||
|
// PlainHTTP specifies to use plain http and not https
|
||||||
|
PlainHTTP bool
|
||||||
|
|
||||||
|
// Client is the http client to used when making registry requests
|
||||||
|
Client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewResolver returns a new resolver to a Docker registry
|
||||||
|
func NewResolver(options ResolverOptions) remotes.Resolver {
|
||||||
|
return &dockerResolver{
|
||||||
|
credentials: options.Credentials,
|
||||||
|
plainHTTP: options.PlainHTTP,
|
||||||
|
client: options.Client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ remotes.Resolver = &dockerResolver{}
|
||||||
|
|
||||||
|
func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocispec.Descriptor, error) {
|
||||||
|
refspec, err := reference.Parse(ref)
|
||||||
|
if err != nil {
|
||||||
|
return "", ocispec.Descriptor{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if refspec.Object == "" {
|
||||||
|
return "", ocispec.Descriptor{}, reference.ErrObjectRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
base, err := r.base(refspec)
|
||||||
|
if err != nil {
|
||||||
|
return "", ocispec.Descriptor{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fetcher := dockerFetcher{
|
||||||
|
dockerBase: base,
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
urls []string
|
||||||
|
dgst = refspec.Digest()
|
||||||
|
)
|
||||||
|
|
||||||
|
if dgst != "" {
|
||||||
|
if err := dgst.Validate(); err != nil {
|
||||||
|
// need to fail here, since we can't actually resolve the invalid
|
||||||
|
// digest.
|
||||||
|
return "", ocispec.Descriptor{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// turns out, we have a valid digest, make a url.
|
||||||
|
urls = append(urls, fetcher.url("manifests", dgst.String()))
|
||||||
|
} else {
|
||||||
|
urls = append(urls, fetcher.url("manifests", refspec.Object))
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback to blobs on not found.
|
||||||
|
urls = append(urls, fetcher.url("blobs", dgst.String()))
|
||||||
|
|
||||||
|
for _, u := range urls {
|
||||||
|
req, err := http.NewRequest(http.MethodHead, u, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", ocispec.Descriptor{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// set headers for all the types we support for resolution.
|
||||||
|
req.Header.Set("Accept", strings.Join([]string{
|
||||||
|
images.MediaTypeDockerSchema2Manifest,
|
||||||
|
images.MediaTypeDockerSchema2ManifestList,
|
||||||
|
ocispec.MediaTypeImageManifest,
|
||||||
|
ocispec.MediaTypeImageIndex, "*"}, ", "))
|
||||||
|
|
||||||
|
log.G(ctx).Debug("resolving")
|
||||||
|
resp, err := fetcher.doRequestWithRetries(ctx, req, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", ocispec.Descriptor{}, err
|
||||||
|
}
|
||||||
|
resp.Body.Close() // don't care about body contents.
|
||||||
|
|
||||||
|
if resp.StatusCode > 299 {
|
||||||
|
if resp.StatusCode == http.StatusNotFound {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return "", ocispec.Descriptor{}, errors.Errorf("unexpected status code %v: %v", u, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is the only point at which we trust the registry. we use the
|
||||||
|
// content headers to assemble a descriptor for the name. when this becomes
|
||||||
|
// more robust, we mostly get this information from a secure trust store.
|
||||||
|
dgstHeader := digest.Digest(resp.Header.Get("Docker-Content-Digest"))
|
||||||
|
|
||||||
|
if dgstHeader != "" {
|
||||||
|
if err := dgstHeader.Validate(); err != nil {
|
||||||
|
return "", ocispec.Descriptor{}, errors.Wrapf(err, "%q in header not a valid digest", dgstHeader)
|
||||||
|
}
|
||||||
|
dgst = dgstHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
if dgst == "" {
|
||||||
|
return "", ocispec.Descriptor{}, errors.Errorf("could not resolve digest for %v", ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
size int64
|
||||||
|
sizeHeader = resp.Header.Get("Content-Length")
|
||||||
|
)
|
||||||
|
|
||||||
|
size, err = strconv.ParseInt(sizeHeader, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
return "", ocispec.Descriptor{}, errors.Wrapf(err, "invalid size header: %q", sizeHeader)
|
||||||
|
}
|
||||||
|
if size < 0 {
|
||||||
|
return "", ocispec.Descriptor{}, errors.Errorf("%q in header not a valid size", sizeHeader)
|
||||||
|
}
|
||||||
|
|
||||||
|
desc := ocispec.Descriptor{
|
||||||
|
Digest: dgst,
|
||||||
|
MediaType: resp.Header.Get("Content-Type"), // need to strip disposition?
|
||||||
|
Size: size,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.G(ctx).WithField("desc.digest", desc.Digest).Debug("resolved")
|
||||||
|
return ref, desc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", ocispec.Descriptor{}, errors.Errorf("%v not found", ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *dockerResolver) Fetcher(ctx context.Context, ref string) (remotes.Fetcher, error) {
|
||||||
|
refspec, err := reference.Parse(ref)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
base, err := r.base(refspec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dockerFetcher{
|
||||||
|
dockerBase: base,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *dockerResolver) Pusher(ctx context.Context, ref string) (remotes.Pusher, error) {
|
||||||
|
refspec, err := reference.Parse(ref)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manifests can be pushed by digest like any other object, but the passed in
|
||||||
|
// reference cannot take a digest without the associated content. A tag is allowed
|
||||||
|
// and will be used to tag pushed manifests.
|
||||||
|
if refspec.Object != "" && strings.Contains(refspec.Object, "@") {
|
||||||
|
return nil, errors.New("cannot use digest reference for push locator")
|
||||||
|
}
|
||||||
|
|
||||||
|
base, err := r.base(refspec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dockerPusher{
|
||||||
|
dockerBase: base,
|
||||||
|
tag: refspec.Object,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type dockerBase struct {
|
||||||
|
base url.URL
|
||||||
|
token string
|
||||||
|
|
||||||
|
client *http.Client
|
||||||
|
useBasic bool
|
||||||
|
username string
|
||||||
|
secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *dockerResolver) base(refspec reference.Spec) (*dockerBase, error) {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
base url.URL
|
||||||
|
username, secret string
|
||||||
|
)
|
||||||
|
|
||||||
|
host := refspec.Hostname()
|
||||||
|
base.Scheme = "https"
|
||||||
|
|
||||||
|
if host == "docker.io" {
|
||||||
|
base.Host = "registry-1.docker.io"
|
||||||
|
} else {
|
||||||
|
base.Host = host
|
||||||
|
|
||||||
|
if r.plainHTTP || strings.HasPrefix(host, "localhost:") {
|
||||||
|
base.Scheme = "http"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.credentials != nil {
|
||||||
|
username, secret, err = r.credentials(base.Host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := strings.TrimPrefix(refspec.Locator, host+"/")
|
||||||
|
base.Path = path.Join("/v2", prefix)
|
||||||
|
|
||||||
|
return &dockerBase{
|
||||||
|
base: base,
|
||||||
|
client: r.client,
|
||||||
|
username: username,
|
||||||
|
secret: secret,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *dockerBase) url(ps ...string) string {
|
||||||
|
url := r.base
|
||||||
|
url.Path = path.Join(url.Path, path.Join(ps...))
|
||||||
|
return url.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *dockerBase) authorize(req *http.Request) {
|
||||||
|
if r.useBasic {
|
||||||
|
req.SetBasicAuth(r.username, r.secret)
|
||||||
|
} else if r.token != "" {
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", r.token))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *dockerBase) doRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
|
||||||
|
ctx = log.WithLogger(ctx, log.G(ctx).WithField("url", req.URL.String()))
|
||||||
|
log.G(ctx).WithField("request.headers", req.Header).WithField("request.method", req.Method).Debug("Do request")
|
||||||
|
r.authorize(req)
|
||||||
|
resp, err := ctxhttp.Do(ctx, r.client, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to do request")
|
||||||
|
}
|
||||||
|
log.G(ctx).WithFields(logrus.Fields{
|
||||||
|
"status": resp.Status,
|
||||||
|
"response.headers": resp.Header,
|
||||||
|
}).Debug("fetch response received")
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *dockerBase) doRequestWithRetries(ctx context.Context, req *http.Request, responses []*http.Response) (*http.Response, error) {
|
||||||
|
resp, err := r.doRequest(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
responses = append(responses, resp)
|
||||||
|
req, err = r.retryRequest(ctx, req, responses)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if req != nil {
|
||||||
|
return r.doRequestWithRetries(ctx, req, responses)
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *dockerBase) retryRequest(ctx context.Context, req *http.Request, responses []*http.Response) (*http.Request, error) {
|
||||||
|
if len(responses) > 5 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
last := responses[len(responses)-1]
|
||||||
|
if last.StatusCode == http.StatusUnauthorized {
|
||||||
|
log.G(ctx).WithField("header", last.Header.Get("WWW-Authenticate")).Debug("Unauthorized")
|
||||||
|
for _, c := range parseAuthHeader(last.Header) {
|
||||||
|
if c.scheme == bearerAuth {
|
||||||
|
if err := invalidAuthorization(c, responses); err != nil {
|
||||||
|
r.token = ""
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := r.setTokenAuth(ctx, c.parameters); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return copyRequest(req)
|
||||||
|
} else if c.scheme == basicAuth {
|
||||||
|
if r.username != "" && r.secret != "" {
|
||||||
|
r.useBasic = true
|
||||||
|
}
|
||||||
|
return copyRequest(req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
} else if last.StatusCode == http.StatusMethodNotAllowed && req.Method == http.MethodHead {
|
||||||
|
// Support registries which have not properly implemented the HEAD method for
|
||||||
|
// manifests endpoint
|
||||||
|
if strings.Contains(req.URL.Path, "/manifests/") {
|
||||||
|
// TODO: copy request?
|
||||||
|
req.Method = http.MethodGet
|
||||||
|
return copyRequest(req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Handle 50x errors accounting for attempt history
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func invalidAuthorization(c challenge, responses []*http.Response) error {
|
||||||
|
errStr := c.parameters["error"]
|
||||||
|
if errStr == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
n := len(responses)
|
||||||
|
if n == 1 || (n > 1 && !sameRequest(responses[n-2].Request, responses[n-1].Request)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Wrapf(ErrInvalidAuthorization, "server message: %s", errStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sameRequest(r1, r2 *http.Request) bool {
|
||||||
|
if r1.Method != r2.Method {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if *r1.URL != *r2.URL {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyRequest(req *http.Request) (*http.Request, error) {
|
||||||
|
ireq := *req
|
||||||
|
if ireq.GetBody != nil {
|
||||||
|
var err error
|
||||||
|
ireq.Body, err = ireq.GetBody()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &ireq, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isManifestAccept(h http.Header) bool {
|
||||||
|
for _, ah := range h[textproto.CanonicalMIMEHeaderKey("Accept")] {
|
||||||
|
switch ah {
|
||||||
|
case images.MediaTypeDockerSchema2Manifest:
|
||||||
|
fallthrough
|
||||||
|
case images.MediaTypeDockerSchema2ManifestList:
|
||||||
|
fallthrough
|
||||||
|
case ocispec.MediaTypeImageManifest:
|
||||||
|
fallthrough
|
||||||
|
case ocispec.MediaTypeImageIndex:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *dockerBase) setTokenAuth(ctx context.Context, params map[string]string) error {
|
||||||
|
realm, ok := params["realm"]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("no realm specified for token auth challenge")
|
||||||
|
}
|
||||||
|
|
||||||
|
realmURL, err := url.Parse(realm)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid token auth challenge realm: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
to := tokenOptions{
|
||||||
|
realm: realmURL.String(),
|
||||||
|
service: params["service"],
|
||||||
|
}
|
||||||
|
|
||||||
|
scope, ok := params["scope"]
|
||||||
|
if !ok {
|
||||||
|
return errors.Errorf("no scope specified for token auth challenge")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Get added scopes from context
|
||||||
|
to.scopes = []string{scope}
|
||||||
|
|
||||||
|
if r.secret != "" {
|
||||||
|
// Credential information is provided, use oauth POST endpoint
|
||||||
|
r.token, err = r.fetchTokenWithOAuth(ctx, to)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to fetch oauth token")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Do request anonymously
|
||||||
|
r.token, err = r.getToken(ctx, to)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to fetch anonymous token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type tokenOptions struct {
|
||||||
|
realm string
|
||||||
|
service string
|
||||||
|
scopes []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type postTokenResponse struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
IssuedAt time.Time `json:"issued_at"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *dockerBase) fetchTokenWithOAuth(ctx context.Context, to tokenOptions) (string, error) {
|
||||||
|
form := url.Values{}
|
||||||
|
form.Set("scope", strings.Join(to.scopes, " "))
|
||||||
|
form.Set("service", to.service)
|
||||||
|
// TODO: Allow setting client_id
|
||||||
|
form.Set("client_id", "containerd-dist-tool")
|
||||||
|
|
||||||
|
if r.username == "" {
|
||||||
|
form.Set("grant_type", "refresh_token")
|
||||||
|
form.Set("refresh_token", r.secret)
|
||||||
|
} else {
|
||||||
|
form.Set("grant_type", "password")
|
||||||
|
form.Set("username", r.username)
|
||||||
|
form.Set("password", r.secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := ctxhttp.PostForm(ctx, r.client, to.realm, form)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == 405 && r.username != "" {
|
||||||
|
// It would be nice if registries would implement the specifications
|
||||||
|
return r.getToken(ctx, to)
|
||||||
|
} else if resp.StatusCode < 200 || resp.StatusCode >= 400 {
|
||||||
|
b, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
log.G(ctx).WithFields(logrus.Fields{
|
||||||
|
"status": resp.Status,
|
||||||
|
"body": string(b),
|
||||||
|
}).Debugf("token request failed")
|
||||||
|
// TODO: handle error body and write debug output
|
||||||
|
return "", errors.Errorf("unexpected status: %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(resp.Body)
|
||||||
|
|
||||||
|
var tr postTokenResponse
|
||||||
|
if err = decoder.Decode(&tr); err != nil {
|
||||||
|
return "", fmt.Errorf("unable to decode token response: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tr.AccessToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type getTokenResponse struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
IssuedAt time.Time `json:"issued_at"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// getToken fetches a token using a GET request
|
||||||
|
func (r *dockerBase) getToken(ctx context.Context, to tokenOptions) (string, error) {
|
||||||
|
req, err := http.NewRequest("GET", to.realm, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqParams := req.URL.Query()
|
||||||
|
|
||||||
|
if to.service != "" {
|
||||||
|
reqParams.Add("service", to.service)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, scope := range to.scopes {
|
||||||
|
reqParams.Add("scope", scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.secret != "" {
|
||||||
|
req.SetBasicAuth(r.username, r.secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.URL.RawQuery = reqParams.Encode()
|
||||||
|
|
||||||
|
resp, err := ctxhttp.Do(ctx, r.client, req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
|
||||||
|
// TODO: handle error body and write debug output
|
||||||
|
return "", errors.Errorf("unexpected status: %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(resp.Body)
|
||||||
|
|
||||||
|
var tr getTokenResponse
|
||||||
|
if err = decoder.Decode(&tr); err != nil {
|
||||||
|
return "", fmt.Errorf("unable to decode token response: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// `access_token` is equivalent to `token` and if both are specified
|
||||||
|
// the choice is undefined. Canonicalize `access_token` by sticking
|
||||||
|
// things in `token`.
|
||||||
|
if tr.AccessToken != "" {
|
||||||
|
tr.Token = tr.AccessToken
|
||||||
|
}
|
||||||
|
|
||||||
|
if tr.Token == "" {
|
||||||
|
return "", ErrNoToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return tr.Token, nil
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
package remotes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/containerd/containerd/content"
|
||||||
|
"github.com/containerd/containerd/images"
|
||||||
|
"github.com/containerd/containerd/log"
|
||||||
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MakeRef returns a unique reference for the descriptor. This reference can be
|
||||||
|
// used to lookup ongoing processes related to the descriptor. This function
|
||||||
|
// may look to the context to namespace the reference appropriately.
|
||||||
|
func MakeRefKey(ctx context.Context, desc ocispec.Descriptor) string {
|
||||||
|
// TODO(stevvooe): Need better remote key selection here. Should be a
|
||||||
|
// product of the context, which may include information about the ongoing
|
||||||
|
// fetch process.
|
||||||
|
switch desc.MediaType {
|
||||||
|
case images.MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest,
|
||||||
|
images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex:
|
||||||
|
return "manifest-" + desc.Digest.String()
|
||||||
|
case images.MediaTypeDockerSchema2Layer, images.MediaTypeDockerSchema2LayerGzip:
|
||||||
|
return "layer-" + desc.Digest.String()
|
||||||
|
case "application/vnd.docker.container.image.v1+json":
|
||||||
|
return "config-" + desc.Digest.String()
|
||||||
|
default:
|
||||||
|
log.G(ctx).Warnf("reference for unknown type: %s", desc.MediaType)
|
||||||
|
return "unknown-" + desc.Digest.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchHandler returns a handler that will fetch all content into the ingester
|
||||||
|
// discovered in a call to Dispatch. Use with ChildrenHandler to do a full
|
||||||
|
// recursive fetch.
|
||||||
|
func FetchHandler(ingester content.Ingester, fetcher Fetcher) images.HandlerFunc {
|
||||||
|
return func(ctx context.Context, desc ocispec.Descriptor) (subdescs []ocispec.Descriptor, err error) {
|
||||||
|
ctx = log.WithLogger(ctx, log.G(ctx).WithFields(logrus.Fields{
|
||||||
|
"digest": desc.Digest,
|
||||||
|
"mediatype": desc.MediaType,
|
||||||
|
"size": desc.Size,
|
||||||
|
}))
|
||||||
|
|
||||||
|
switch desc.MediaType {
|
||||||
|
case images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex:
|
||||||
|
return nil, fmt.Errorf("%v not yet supported", desc.MediaType)
|
||||||
|
default:
|
||||||
|
err := fetch(ctx, ingester, fetcher, desc)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetch(ctx context.Context, ingester content.Ingester, fetcher Fetcher, desc ocispec.Descriptor) error {
|
||||||
|
log.G(ctx).Debug("fetch")
|
||||||
|
ref := MakeRefKey(ctx, desc)
|
||||||
|
rc, err := fetcher.Fetch(ctx, desc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rc.Close()
|
||||||
|
|
||||||
|
return content.WriteBlob(ctx, ingester, ref, rc, desc.Size, desc.Digest)
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package remotes
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// HintExists returns true if a hint of the provided kind and values exists in
|
||||||
|
// the set of provided hints.
|
||||||
|
func HintExists(kind, value string, hints ...string) bool {
|
||||||
|
for _, hint := range hints {
|
||||||
|
if strings.HasPrefix(hint, kind) && strings.HasSuffix(hint, value) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// HintValues returns a slice of the values of the hints that match kind.
|
||||||
|
func HintValues(kind string, hints ...string) []string {
|
||||||
|
var values []string
|
||||||
|
for _, hint := range hints {
|
||||||
|
if strings.HasPrefix(hint, kind) {
|
||||||
|
parts := strings.SplitN(hint, ":", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
values = append(values, parts[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return values
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package remotes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Resolver provides remotes based on a locator.
|
||||||
|
type Resolver interface {
|
||||||
|
// Resolve attempts to resolve the reference into a name and descriptor.
|
||||||
|
//
|
||||||
|
// The argument `ref` should be a scheme-less URI representing the remote.
|
||||||
|
// Structurally, it has a host and path. The "host" can be used to directly
|
||||||
|
// reference a specific host or be matched against a specific handler.
|
||||||
|
//
|
||||||
|
// The returned name should be used to identify the referenced entity.
|
||||||
|
// Dependending on the remote namespace, this may be immutable or mutable.
|
||||||
|
// While the name may differ from ref, it should itself be a valid ref.
|
||||||
|
//
|
||||||
|
// If the resolution fails, an error will be returned.
|
||||||
|
Resolve(ctx context.Context, ref string) (name string, desc ocispec.Descriptor, err error)
|
||||||
|
|
||||||
|
// Fetcher returns a new fetcher for the provided reference.
|
||||||
|
// All content fetched from the returned fetcher will be
|
||||||
|
// from the namespace referred to by ref.
|
||||||
|
Fetcher(ctx context.Context, ref string) (Fetcher, error)
|
||||||
|
|
||||||
|
// Pusher returns a new pusher for the provided reference
|
||||||
|
Pusher(ctx context.Context, ref string) (Pusher, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Fetcher interface {
|
||||||
|
// Fetch the resource identified by the descriptor.
|
||||||
|
Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Pusher interface {
|
||||||
|
// Push pushes the resource identified by the descriptor using the
|
||||||
|
// passed in reader.
|
||||||
|
Push(ctx context.Context, d ocispec.Descriptor, r io.Reader) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetcherFunc allows package users to implement a Fetcher with just a
|
||||||
|
// function.
|
||||||
|
type FetcherFunc func(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error)
|
||||||
|
|
||||||
|
func (fn FetcherFunc) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) {
|
||||||
|
return fn(ctx, desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PusherFunc allows package users to implement a Pusher with just a
|
||||||
|
// function.
|
||||||
|
type PusherFunc func(ctx context.Context, desc ocispec.Descriptor, r io.Reader) error
|
||||||
|
|
||||||
|
func (fn PusherFunc) Pusher(ctx context.Context, desc ocispec.Descriptor, r io.Reader) error {
|
||||||
|
return fn(ctx, desc, r)
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
// Copyright 2016 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.
|
||||||
|
|
||||||
|
// +build go1.7
|
||||||
|
|
||||||
|
// Package ctxhttp provides helper functions for performing context-aware HTTP requests.
|
||||||
|
package ctxhttp // import "golang.org/x/net/context/ctxhttp"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Do sends an HTTP request with the provided http.Client and returns
|
||||||
|
// an HTTP response.
|
||||||
|
//
|
||||||
|
// If the client is nil, http.DefaultClient is used.
|
||||||
|
//
|
||||||
|
// The provided ctx must be non-nil. If it is canceled or times out,
|
||||||
|
// ctx.Err() will be returned.
|
||||||
|
func Do(ctx context.Context, client *http.Client, req *http.Request) (*http.Response, error) {
|
||||||
|
if client == nil {
|
||||||
|
client = http.DefaultClient
|
||||||
|
}
|
||||||
|
resp, err := client.Do(req.WithContext(ctx))
|
||||||
|
// If we got an error, and the context has been canceled,
|
||||||
|
// the context's error is probably more useful.
|
||||||
|
if err != nil {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
err = ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get issues a GET request via the Do function.
|
||||||
|
func Get(ctx context.Context, client *http.Client, url string) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return Do(ctx, client, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Head issues a HEAD request via the Do function.
|
||||||
|
func Head(ctx context.Context, client *http.Client, url string) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequest("HEAD", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return Do(ctx, client, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post issues a POST request via the Do function.
|
||||||
|
func Post(ctx context.Context, client *http.Client, url string, bodyType string, body io.Reader) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequest("POST", url, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", bodyType)
|
||||||
|
return Do(ctx, client, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostForm issues a POST request via the Do function.
|
||||||
|
func PostForm(ctx context.Context, client *http.Client, url string, data url.Values) (*http.Response, error) {
|
||||||
|
return Post(ctx, client, url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
|
||||||
|
}
|
|
@ -0,0 +1,147 @@
|
||||||
|
// Copyright 2015 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.
|
||||||
|
|
||||||
|
// +build !go1.7
|
||||||
|
|
||||||
|
package ctxhttp // import "golang.org/x/net/context/ctxhttp"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
func nop() {}
|
||||||
|
|
||||||
|
var (
|
||||||
|
testHookContextDoneBeforeHeaders = nop
|
||||||
|
testHookDoReturned = nop
|
||||||
|
testHookDidBodyClose = nop
|
||||||
|
)
|
||||||
|
|
||||||
|
// Do sends an HTTP request with the provided http.Client and returns an HTTP response.
|
||||||
|
// If the client is nil, http.DefaultClient is used.
|
||||||
|
// If the context is canceled or times out, ctx.Err() will be returned.
|
||||||
|
func Do(ctx context.Context, client *http.Client, req *http.Request) (*http.Response, error) {
|
||||||
|
if client == nil {
|
||||||
|
client = http.DefaultClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(djd): Respect any existing value of req.Cancel.
|
||||||
|
cancel := make(chan struct{})
|
||||||
|
req.Cancel = cancel
|
||||||
|
|
||||||
|
type responseAndError struct {
|
||||||
|
resp *http.Response
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
result := make(chan responseAndError, 1)
|
||||||
|
|
||||||
|
// Make local copies of test hooks closed over by goroutines below.
|
||||||
|
// Prevents data races in tests.
|
||||||
|
testHookDoReturned := testHookDoReturned
|
||||||
|
testHookDidBodyClose := testHookDidBodyClose
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
testHookDoReturned()
|
||||||
|
result <- responseAndError{resp, err}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var resp *http.Response
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
testHookContextDoneBeforeHeaders()
|
||||||
|
close(cancel)
|
||||||
|
// Clean up after the goroutine calling client.Do:
|
||||||
|
go func() {
|
||||||
|
if r := <-result; r.resp != nil {
|
||||||
|
testHookDidBodyClose()
|
||||||
|
r.resp.Body.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil, ctx.Err()
|
||||||
|
case r := <-result:
|
||||||
|
var err error
|
||||||
|
resp, err = r.resp, r.err
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
close(cancel)
|
||||||
|
case <-c:
|
||||||
|
// The response's Body is closed.
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
resp.Body = ¬ifyingReader{resp.Body, c}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get issues a GET request via the Do function.
|
||||||
|
func Get(ctx context.Context, client *http.Client, url string) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return Do(ctx, client, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Head issues a HEAD request via the Do function.
|
||||||
|
func Head(ctx context.Context, client *http.Client, url string) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequest("HEAD", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return Do(ctx, client, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Post issues a POST request via the Do function.
|
||||||
|
func Post(ctx context.Context, client *http.Client, url string, bodyType string, body io.Reader) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequest("POST", url, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", bodyType)
|
||||||
|
return Do(ctx, client, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostForm issues a POST request via the Do function.
|
||||||
|
func PostForm(ctx context.Context, client *http.Client, url string, data url.Values) (*http.Response, error) {
|
||||||
|
return Post(ctx, client, url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// notifyingReader is an io.ReadCloser that closes the notify channel after
|
||||||
|
// Close is called or a Read fails on the underlying ReadCloser.
|
||||||
|
type notifyingReader struct {
|
||||||
|
io.ReadCloser
|
||||||
|
notify chan<- struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *notifyingReader) Read(p []byte) (int, error) {
|
||||||
|
n, err := r.ReadCloser.Read(p)
|
||||||
|
if err != nil && r.notify != nil {
|
||||||
|
close(r.notify)
|
||||||
|
r.notify = nil
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *notifyingReader) Close() error {
|
||||||
|
err := r.ReadCloser.Close()
|
||||||
|
if r.notify != nil {
|
||||||
|
close(r.notify)
|
||||||
|
r.notify = nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
Loading…
Reference in New Issue