Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 17 additions & 10 deletions docker/docker_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,17 @@ type dockerClient struct {
wwwAuthenticate string // Cache of a value set by ping() if scheme is not empty
scheme string // Cache of a value returned by a successful ping() if not empty
client *http.Client
signatureBase signatureStorageBase
}

// newDockerClient returns a new dockerClient instance for refHostname (a host a specified in the Docker image reference, not canonicalized to dockerRegistry)
func newDockerClient(ctx *types.SystemContext, refHostname string) (*dockerClient, error) {
var registry string
if refHostname == dockerHostname {
// “write” specifies whether the client will be used for "write" access (in particular passed to lookaside.go:toplevelFromSection)
func newDockerClient(ctx *types.SystemContext, ref dockerReference, write bool) (*dockerClient, error) {
registry := ref.ref.Hostname()
if registry == dockerHostname {
registry = dockerRegistry
} else {
registry = refHostname
}
username, password, err := getAuth(refHostname)
username, password, err := getAuth(ref.ref.Hostname())
if err != nil {
return nil, err
}
Expand All @@ -78,11 +78,18 @@ func newDockerClient(ctx *types.SystemContext, refHostname string) (*dockerClien
if tr != nil {
client.Transport = tr
}

sigBase, err := configuredSignatureStorageBase(ctx, ref, write)
if err != nil {
return nil, err
}

return &dockerClient{
registry: registry,
username: username,
password: password,
client: client,
registry: registry,
username: username,
password: password,
client: client,
signatureBase: sigBase,
}, nil
}

Expand Down
91 changes: 88 additions & 3 deletions docker/docker_image_dest.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import (
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"

"github.com/Sirupsen/logrus"
Expand All @@ -18,11 +21,13 @@ import (
type dockerImageDestination struct {
ref dockerReference
c *dockerClient
// State
manifestDigest string // or "" if not yet known.
}

// newImageDestination creates a new ImageDestination for the specified image reference.
func newImageDestination(ctx *types.SystemContext, ref dockerReference) (types.ImageDestination, error) {
c, err := newDockerClient(ctx, ref.ref.Hostname())
c, err := newDockerClient(ctx, ref, true)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -144,6 +149,7 @@ func (d *dockerImageDestination) PutManifest(m []byte) error {
if err != nil {
return err
}
d.manifestDigest = digest
url := fmt.Sprintf(manifestURL, d.ref.ref.RemoteName(), digest)

headers := map[string][]string{}
Expand All @@ -168,12 +174,91 @@ func (d *dockerImageDestination) PutManifest(m []byte) error {
}

func (d *dockerImageDestination) PutSignatures(signatures [][]byte) error {
if len(signatures) != 0 {
return fmt.Errorf("Pushing signatures to a Docker Registry is not supported")
// FIXME? This overwrites files one at a time, definitely not atomic.
// A failure when updating signatures with a reordered copy could lose some of them.

// Skip dealing with the manifest digest if not necessary.
if len(signatures) == 0 {
return nil
}
if d.c.signatureBase == nil {
return fmt.Errorf("Pushing signatures to a Docker Registry is not supported, and there is no applicable signature storage configured")
}

// FIXME: This assumption that signatures are stored after the manifest rather breaks the model.
if d.manifestDigest == "" {
return fmt.Errorf("Unknown manifest digest, can't add signatures")
}

for i, signature := range signatures {
url := signatureStorageURL(d.c.signatureBase, d.manifestDigest, i)
if url == nil {
return fmt.Errorf("Internal error: signatureStorageURL with non-nil base returned nil")
}
err := d.putOneSignature(url, signature)
if err != nil {
return err
}
}
// Remove any other signatures, if present.
// We stop at the first missing signature; if a previous deleting loop aborted
// prematurely, this may not clean up all of them, but one missing signature
// is enough for dockerImageSource to stop looking for other signatures, so that
// is sufficient.
for i := len(signatures); ; i++ {
url := signatureStorageURL(d.c.signatureBase, d.manifestDigest, i)
if url == nil {
return fmt.Errorf("Internal error: signatureStorageURL with non-nil base returned nil")
}
missing, err := d.c.deleteOneSignature(url)
if err != nil {
return err
}
if missing {
break
}
}

return nil
}

// putOneSignature stores one signature to url.
func (d *dockerImageDestination) putOneSignature(url *url.URL, signature []byte) error {
switch url.Scheme {
case "file":
logrus.Debugf("Writing to %s", url.Path)
err := os.MkdirAll(filepath.Dir(url.Path), 0755)
if err != nil {
return err
}
err = ioutil.WriteFile(url.Path, signature, 0644)
if err != nil {
return err
}
return nil

default:
return fmt.Errorf("Unsupported scheme when writing signature to %s", url.String())
}
}

// deleteOneSignature deletes a signature from url, if it exists.
// If it successfully determines that the signature does not exist, returns (true, nil)
func (c *dockerClient) deleteOneSignature(url *url.URL) (missing bool, err error) {
switch url.Scheme {
case "file":
logrus.Debugf("Deleting %s", url.Path)
err := os.Remove(url.Path)
if err != nil && os.IsNotExist(err) {
return true, nil
}
return false, err

default:
return false, fmt.Errorf("Unsupported scheme when deleting signature from %s", url.String())
}
}

// Commit marks the process of storing the image as successful and asks for the image to be persisted.
// WARNING: This does not have any transactional semantics:
// - Uploaded data MAY be visible to others before Commit() is called
Expand Down
134 changes: 123 additions & 11 deletions docker/docker_image_src.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"io/ioutil"
"mime"
"net/http"
"net/url"
"os"
"strconv"

"github.com/Sirupsen/logrus"
Expand All @@ -26,14 +28,17 @@ type dockerImageSource struct {
ref dockerReference
requestedManifestMIMETypes []string
c *dockerClient
// State
cachedManifest []byte // nil if not loaded yet
cachedManifestMIMEType string // Only valid if cachedManifest != nil
}

// newImageSource creates a new ImageSource for the specified image reference,
// asking the backend to use a manifest from requestedManifestMIMETypes if possible.
// nil requestedManifestMIMETypes means manifest.DefaultRequestedManifestMIMETypes.
// The caller must call .Close() on the returned ImageSource.
func newImageSource(ctx *types.SystemContext, ref dockerReference, requestedManifestMIMETypes []string) (*dockerImageSource, error) {
c, err := newDockerClient(ctx, ref.ref.Hostname())
c, err := newDockerClient(ctx, ref, false)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -71,29 +76,50 @@ func simplifyContentType(contentType string) string {
}

func (s *dockerImageSource) GetManifest() ([]byte, string, error) {
reference, err := s.ref.tagOrDigest()
err := s.ensureManifestIsLoaded()
if err != nil {
return nil, "", err
}
return s.cachedManifest, s.cachedManifestMIMEType, nil
}

// ensureManifestIsLoaded sets s.cachedManifest and s.cachedManifestMIMEType
//
// ImageSource implementations are not required or expected to do any caching,
// but because our signatures are “attached” to the manifest digest,
// we need to ensure that the digest of the manifest returned by GetManifest
// and used by GetSignatures are consistent, otherwise we would get spurious
// signature verification failures when pulling while a tag is being updated.
func (s *dockerImageSource) ensureManifestIsLoaded() error {
if s.cachedManifest != nil {
return nil
}

reference, err := s.ref.tagOrDigest()
if err != nil {
return err
}
url := fmt.Sprintf(manifestURL, s.ref.ref.RemoteName(), reference)
// TODO(runcom) set manifest version header! schema1 for now - then schema2 etc etc and v1
// TODO(runcom) NO, switch on the resulter manifest like Docker is doing
headers := make(map[string][]string)
headers["Accept"] = s.requestedManifestMIMETypes
res, err := s.c.makeRequest("GET", url, headers, nil)
if err != nil {
return nil, "", err
return err
}
defer res.Body.Close()
manblob, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, "", err
return err
}
if res.StatusCode != http.StatusOK {
return nil, "", errFetchManifest{res.StatusCode, manblob}
return errFetchManifest{res.StatusCode, manblob}
}
// We might validate manblob against the Docker-Content-Digest header here to protect against transport errors.
return manblob, simplifyContentType(res.Header.Get("Content-Type")), nil
s.cachedManifest = manblob
s.cachedManifestMIMEType = simplifyContentType(res.Header.Get("Content-Type"))
return nil
}

// GetBlob returns a stream for the specified blob, and the blob’s size (or -1 if unknown).
Expand All @@ -116,12 +142,77 @@ func (s *dockerImageSource) GetBlob(digest string) (io.ReadCloser, int64, error)
}

func (s *dockerImageSource) GetSignatures() ([][]byte, error) {
return [][]byte{}, nil
if s.c.signatureBase == nil { // Skip dealing with the manifest digest if not necessary.
return [][]byte{}, nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can do return nil, nil right? the only thing you can do from a slice is to range over it (and len(nil) == 0)) or access it w/o checking its length in which case everything breaks - so, I think it's safe to just return nil.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of like returning a real slice in here, as opposed to “no value”. You are right that this is safe.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of like returning a real slice in here, as opposed to “no value”. You are right that this is safe.

yeah, only overhead is the slice creation but I believe it shouldn't impact anything here (if the go runtime allocates with {} as opposed to allocating when something is actually appended there)

}

if err := s.ensureManifestIsLoaded(); err != nil {
return nil, err
}
manifestDigest, err := manifest.Digest(s.cachedManifest)
if err != nil {
return nil, err
}

signatures := [][]byte{}
for i := 0; ; i++ {
url := signatureStorageURL(s.c.signatureBase, manifestDigest, i)
if url == nil {
return nil, fmt.Errorf("Internal error: signatureStorageURL with non-nil base returned nil")
}
signature, missing, err := s.getOneSignature(url)
if err != nil {
return nil, err
}
if missing {
break
}
signatures = append(signatures, signature)
}
return signatures, nil
}

// getOneSignature downloads one signature from url.
// If it successfully determines that the signature does not exist, returns with missing set to true and error set to nil.
func (s *dockerImageSource) getOneSignature(url *url.URL) (signature []byte, missing bool, err error) {
switch url.Scheme {
case "file":
logrus.Debugf("Reading %s", url.Path)
sig, err := ioutil.ReadFile(url.Path)
if err != nil {
if os.IsNotExist(err) {
return nil, true, nil
}
return nil, false, err
}
return sig, false, nil

case "http", "https":
logrus.Debugf("GET %s", url)
res, err := s.c.client.Get(url.String())
if err != nil {
return nil, false, err
}
defer res.Body.Close()
if res.StatusCode == http.StatusNotFound {
return nil, true, nil
} else if res.StatusCode != http.StatusOK {
return nil, false, fmt.Errorf("Error reading signature from %s: status %d", url.String(), res.StatusCode)
}
sig, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, false, err
}
return sig, false, nil

default:
return nil, false, fmt.Errorf("Unsupported scheme when reading signature from %s", url.String())
}
}

// deleteImage deletes the named image from the registry, if supported.
func deleteImage(ctx *types.SystemContext, ref dockerReference) error {
c, err := newDockerClient(ctx, ref.ref.Hostname())
c, err := newDockerClient(ctx, ref, true)
if err != nil {
return err
}
Expand All @@ -141,7 +232,7 @@ func deleteImage(ctx *types.SystemContext, ref dockerReference) error {
return err
}
defer get.Body.Close()
body, err := ioutil.ReadAll(get.Body)
manifestBody, err := ioutil.ReadAll(get.Body)
if err != nil {
return err
}
Expand All @@ -150,7 +241,7 @@ func deleteImage(ctx *types.SystemContext, ref dockerReference) error {
case http.StatusNotFound:
return fmt.Errorf("Unable to delete %v. Image may not exist or is not stored with a v2 Schema in a v2 registry.", ref.ref)
default:
return fmt.Errorf("Failed to delete %v: %s (%v)", ref.ref, string(body), get.Status)
return fmt.Errorf("Failed to delete %v: %s (%v)", ref.ref, manifestBody, get.Status)
}

digest := get.Header.Get("Docker-Content-Digest")
Expand All @@ -164,13 +255,34 @@ func deleteImage(ctx *types.SystemContext, ref dockerReference) error {
}
defer delete.Body.Close()

body, err = ioutil.ReadAll(delete.Body)
body, err := ioutil.ReadAll(delete.Body)
if err != nil {
return err
}
if delete.StatusCode != http.StatusAccepted {
return fmt.Errorf("Failed to delete %v: %s (%v)", deleteURL, string(body), delete.Status)
}

if c.signatureBase != nil {
manifestDigest, err := manifest.Digest(manifestBody)
if err != nil {
return err
}

for i := 0; ; i++ {
url := signatureStorageURL(c.signatureBase, manifestDigest, i)
if url == nil {
return fmt.Errorf("Internal error: signatureStorageURL with non-nil base returned nil")
}
missing, err := c.deleteOneSignature(url)
if err != nil {
return err
}
if missing {
break
}
}
}

return nil
}
Loading