diff --git a/docker/daemon/daemon_dest.go b/docker/daemon/daemon_dest.go new file mode 100644 index 000000000..82fe19e61 --- /dev/null +++ b/docker/daemon/daemon_dest.go @@ -0,0 +1,233 @@ +package daemon + +import ( + "archive/tar" + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "time" + + "github.com/Sirupsen/logrus" + "github.com/containers/image/manifest" + "github.com/containers/image/types" + "github.com/docker/engine-api/client" + "golang.org/x/net/context" +) + +type daemonImageDestination struct { + ref daemonReference + // For talking to imageLoadGoroutine + goroutineCancel context.CancelFunc + statusChannel <-chan error + writer *io.PipeWriter + tar *tar.Writer + // Other state + committed bool // writer has been closed +} + +// newImageDestination returns a types.ImageDestination for the specified image reference. +func newImageDestination(systemCtx *types.SystemContext, ref daemonReference) (types.ImageDestination, error) { + // FIXME: Do something with ref + c, err := client.NewClient(client.DefaultDockerHost, "1.22", nil, nil) // FIXME: overridable host + if err != nil { + return nil, fmt.Errorf("Error initializing docker engine client: %v", err) + } + + reader, writer := io.Pipe() + // Commit() may never be called, so we may never read from this channel; so, make this buffered to allow imageLoadGoroutine to write status and terminate even if we never read it. + statusChannel := make(chan error, 1) + + ctx, goroutineCancel := context.WithCancel(context.Background()) + go imageLoadGoroutine(ctx, c, reader, statusChannel) + + return &daemonImageDestination{ + ref: ref, + goroutineCancel: goroutineCancel, + statusChannel: statusChannel, + writer: writer, + tar: tar.NewWriter(writer), + committed: false, + }, nil +} + +// imageLoadGoroutine accepts tar stream on reader, sends it to c, and reports error or success by writing to statusChannel +func imageLoadGoroutine(ctx context.Context, c *client.Client, reader *io.PipeReader, statusChannel chan<- error) { + err := errors.New("Internal error: unexpected panic in imageLoadGoroutine") + defer func() { + logrus.Debugf("docker-daemon: sending done, status %v", err) + statusChannel <- err + }() + defer func() { + if err == nil { + reader.Close() + } else { + reader.CloseWithError(err) + } + }() + + resp, err := c.ImageLoad(ctx, reader, true) + if err != nil { + err = fmt.Errorf("Error saving image to docker engine: %v", err) + return + } + defer resp.Body.Close() +} + +// Close removes resources associated with an initialized ImageDestination, if any. +func (d *daemonImageDestination) Close() { + if !d.committed { + logrus.Debugf("docker-daemon: Closing tar stream to abort loading") + // In principle, goroutineCancel() should abort the HTTP request and stop the process from continuing. + // In practice, though, https://github.com/docker/engine-api/blob/master/client/transport/cancellable/cancellable.go + // currently just runs the HTTP request to completion in a goroutine, and returns early if the context is canceled + // without terminating the HTTP request at all. So we need this CloseWithError to terminate sending the HTTP request Body + // immediately, and hopefully, through terminating the sending which uses "Transfer-Encoding: chunked"" without sending + // the terminating zero-length chunk, prevent the docker daemon from processing the tar stream at all. + // Whether that works or not, closing the PipeWriter seems desirable in any case. + d.writer.CloseWithError(errors.New("Aborting upload, daemonImageDestination closed without a previous .Commit()")) + } + d.goroutineCancel() +} + +// Reference returns the reference used to set up this destination. Note that this should directly correspond to user's intent, +// e.g. it should use the public hostname instead of the result of resolving CNAMEs or following redirects. +func (d *daemonImageDestination) Reference() types.ImageReference { + return d.ref +} + +// SupportedManifestMIMETypes tells which manifest mime types the destination supports +// If an empty slice or nil it's returned, then any mime type can be tried to upload +func (d *daemonImageDestination) SupportedManifestMIMETypes() []string { + return []string{ + manifest.DockerV2Schema2MediaType, // FIXME: Handle others. + } +} + +// SupportsSignatures returns an error (to be displayed to the user) if the destination certainly can't store signatures. +// Note: It is still possible for PutSignatures to fail if SupportsSignatures returns nil. +func (d *daemonImageDestination) SupportsSignatures() error { + return fmt.Errorf("Storing signatures for docker-daemon: destinations is not supported") +} + +// PutBlob writes contents of stream and returns its computed digest and size. +// A digest can be optionally provided if known, the specific image destination can decide to play with it or not. +// The length of stream is expected to be expectedSize; if expectedSize == -1, it is not known. +// WARNING: The contents of stream are being verified on the fly. Until stream.Read() returns io.EOF, the contents of the data SHOULD NOT be available +// to any other readers for download using the supplied digest. +// If stream.Read() at any time, ESPECIALLY at end of input, returns an error, PutBlob MUST 1) fail, and 2) delete any data stored so far. +func (d *daemonImageDestination) PutBlob(stream io.Reader, digest string, expectedSize int64) (string, int64, error) { + if expectedSize == -1 { + return "", -1, fmt.Errorf("Can not stream blob %s with unknown size to docker-daemon:", digest) + } + hash := sha256.New() + tee := io.TeeReader(stream, hash) + if err := d.sendFile(digest, expectedSize, tee); err != nil { + return "", -1, err + } + return "sha256:" + hex.EncodeToString(hash.Sum(nil)), expectedSize, nil +} + +func (d *daemonImageDestination) PutManifest(m []byte) error { + var man schema2Manifest + if err := json.Unmarshal(m, &man); err != nil { + return fmt.Errorf("Error parsing manifest: %v", err) + } + if man.SchemaVersion != 2 || man.MediaType != manifest.DockerV2Schema2MediaType { + // FIXME FIXME: Teach copy.go about this. + return fmt.Errorf("Unsupported manifest type, need a Docker schema 2 manifest") + } + + layerPaths := []string{} + for _, l := range man.Layers { + layerPaths = append(layerPaths, l.Digest) + } + items := []manifestItem{{ + Config: man.Config.Digest, + RepoTags: []string{string(d.ref)}, // FIXME: Only if ref is a NamedTagged + Layers: layerPaths, + Parent: "", + LayerSources: nil, + }} + itemsBytes, err := json.Marshal(&items) + if err != nil { + return err + } + + // FIXME? Do we also need to support the legacy format? + return d.sendFile(manifestFileName, int64(len(itemsBytes)), bytes.NewReader(itemsBytes)) +} + +type tarFI struct { + path string + size int64 +} + +func (t *tarFI) Name() string { + return t.path +} +func (t *tarFI) Size() int64 { + return t.size +} +func (t *tarFI) Mode() os.FileMode { + return 0444 +} +func (t *tarFI) ModTime() time.Time { + return time.Unix(0, 0) +} +func (t *tarFI) IsDir() bool { + return false +} +func (t *tarFI) Sys() interface{} { + return nil +} + +// sendFile sends a file into the tar stream. +func (d *daemonImageDestination) sendFile(path string, expectedSize int64, stream io.Reader) error { + hdr, err := tar.FileInfoHeader(&tarFI{path: path, size: expectedSize}, "") + if err != nil { + return nil + } + logrus.Debugf("Sending as tar file %s", path) + if err := d.tar.WriteHeader(hdr); err != nil { + return err + } + size, err := io.Copy(d.tar, stream) + if err != nil { + return err + } + if size != expectedSize { + return fmt.Errorf("Size mismatch when copying %s, expected %d, got %d", path, expectedSize, size) + } + return nil +} + +func (d *daemonImageDestination) PutSignatures(signatures [][]byte) error { + if len(signatures) != 0 { + return fmt.Errorf("Storing signatures for docker-daemon: destinations is not supported") + } + return nil +} + +// 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 +// - Uploaded data MAY be removed or MAY remain around if Close() is called without Commit() (i.e. rollback is allowed but not guaranteed) +func (d *daemonImageDestination) Commit() error { + logrus.Debugf("docker-daemon: Closing tar stream") + if err := d.tar.Close(); err != nil { + return err + } + if err := d.writer.Close(); err != nil { + return err + } + d.committed = true // We may still fail, but we are done sending to imageLoadGoroutine. + + logrus.Debugf("docker-daemon: Waiting for status") + err := <-d.statusChannel + return err +} diff --git a/docker/daemon/daemon_src.go b/docker/daemon/daemon_src.go new file mode 100644 index 000000000..38db6f501 --- /dev/null +++ b/docker/daemon/daemon_src.go @@ -0,0 +1,355 @@ +package daemon + +import ( + "archive/tar" + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path" + + "github.com/Sirupsen/logrus" + "github.com/containers/image/manifest" + "github.com/containers/image/types" + "github.com/docker/engine-api/client" + "golang.org/x/net/context" +) + +const temporaryDirectoryForBigFiles = "/var/tmp" // Do not use the system default of os.TempDir(), usually /tmp, because with systemd it could be a tmpfs. + +type daemonImageSource struct { + ref daemonReference + tarCopyPath string + // The following data is only available after ensureCachedDataIsPresent() succeeds + tarManifest *manifestItem // nil if not available yet. + configBytes []byte + configDigest string + orderedDiffIDList []diffID + knownLayers map[diffID]*layerInfo + // Other state + generatedManifest []byte // Private cache for GetManifest(), nil if not set yet. +} + +type layerInfo struct { + path string + size int64 +} + +// newImageSource returns a types.ImageSource for the specified image reference. +// The caller must call .Close() on the returned ImageSource. +// +// It would be great if we were able to stream the input tar as it is being +// sent; but Docker sends the top-level manifest, which determines which paths +// to look for, at the end, so in we will need to seek back and re-read, several times. +// (We could, perhaps, expect an exact sequence, assume that the first plaintext file +// is the config, and that the following len(RootFS) files are the layers, but that feels +// way too brittle.) +func newImageSource(ctx *types.SystemContext, ref daemonReference) (types.ImageSource, error) { + c, err := client.NewClient(client.DefaultDockerHost, "1.22", nil, nil) // FIXME: overridable host + if err != nil { + return nil, fmt.Errorf("Error initializing docker engine client: %v", err) + } + inputStream, err := c.ImageSave(context.TODO(), []string{string(ref)}) // FIXME: ref should be per docker/reference.ParseIDOrReference, and we don't want NameOnly + if err != nil { + return nil, fmt.Errorf("Error loading image from docker engine: %v", err) + } + defer inputStream.Close() + + // FIXME: use SystemContext here. + tarCopyFile, err := ioutil.TempFile(temporaryDirectoryForBigFiles, "docker-daemon-tar") + if err != nil { + return nil, err + } + defer tarCopyFile.Close() + + succeeded := false + defer func() { + if !succeeded { + os.Remove(tarCopyFile.Name()) + } + }() + + if _, err := io.Copy(tarCopyFile, inputStream); err != nil { + return nil, err + } + + succeeded = true + return &daemonImageSource{ + ref: ref, + tarCopyPath: tarCopyFile.Name(), + }, nil +} + +// Reference returns the reference used to set up this source, _as specified by the user_ +// (not as the image itself, or its underlying storage, claims). This can be used e.g. to determine which public keys are trusted for this image. +func (s *daemonImageSource) Reference() types.ImageReference { + return s.ref +} + +// Close removes resources associated with an initialized ImageSource, if any. +func (s *daemonImageSource) Close() { + _ = os.Remove(s.tarCopyPath) +} + +// tarReadCloser is a way to close the backing file of a tar.Reader when the user no longer needs the tar component. +type tarReadCloser struct { + *tar.Reader + backingFile *os.File +} + +func (t *tarReadCloser) Close() error { + return t.backingFile.Close() +} + +// openTarComponent returns a ReadCloser for the specific file within the archive. +// This is linear scan; we assume that the tar file will have a fairly small amount of files (~layers), +// and that filesystem caching will make the repeated seeking over the (uncompressed) tarCopyPath cheap enough. +// The caller should call .Close() on the returned stream. +func (s *daemonImageSource) openTarComponent(componentPath string) (io.ReadCloser, error) { + f, err := os.Open(s.tarCopyPath) + if err != nil { + return nil, err + } + succeeded := false + defer func() { + if !succeeded { + f.Close() + } + }() + + tarReader, header, err := findTarComponent(f, componentPath) + if err != nil { + return nil, err + } + if header == nil { + return nil, os.ErrNotExist + } + if header.FileInfo().Mode()&os.ModeType == os.ModeSymlink { // FIXME: untested + // We follow only one symlink; so no loops are possible. + if _, err := f.Seek(0, os.SEEK_SET); err != nil { + return nil, err + } + // The new path could easily point "outside" the archive, but we only compare it to existing tar headers without extracting the archive, + // so we don't care. + tarReader, header, err = findTarComponent(f, path.Join(path.Dir(componentPath), header.Linkname)) + if err != nil { + return nil, err + } + if header == nil { + return nil, os.ErrNotExist + } + } + + if !header.FileInfo().Mode().IsRegular() { + return nil, fmt.Errorf("Error reading tar archive component %s: not a regular file", header.Name) + } + succeeded = true + return &tarReadCloser{Reader: tarReader, backingFile: f}, nil +} + +// findTarComponent returns a header and a reader matching path within inputFile, +// or (nil, nil, nil) if not found. +func findTarComponent(inputFile io.Reader, path string) (*tar.Reader, *tar.Header, error) { + t := tar.NewReader(inputFile) + for { + h, err := t.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, nil, err + } + if h.Name == path { + return t, h, nil + } + } + return nil, nil, nil +} + +// readTarComponent returns full contents of componentPath. +func (s *daemonImageSource) readTarComponent(path string) ([]byte, error) { + file, err := s.openTarComponent(path) + if err != nil { + return nil, fmt.Errorf("Error loading tar component %s: %v", path, err) + } + defer file.Close() + bytes, err := ioutil.ReadAll(file) + if err != nil { + return nil, err + } + return bytes, nil +} + +// ensureCachedDataIsPresent loads data necessary for any of the public accessors. +func (s *daemonImageSource) ensureCachedDataIsPresent() error { + if s.tarManifest != nil { + return nil + } + + // Read and parse manifest.json + tarManifest, err := s.loadTarManifest() + if err != nil { + return err + } + + // Read and parse config. + configBytes, err := s.readTarComponent(tarManifest.Config) + if err != nil { + return err + } + var parsedConfig image // Most fields ommitted, we only care about layer DiffIDs. + if err := json.Unmarshal(configBytes, &parsedConfig); err != nil { + return fmt.Errorf("Error decoding tar config %s: %v", tarManifest.Config, err) + } + + knownLayers, err := s.prepareLayerData(tarManifest, &parsedConfig) + if err != nil { + return err + } + + // Success; commit. + configHash := sha256.Sum256(configBytes) + s.tarManifest = tarManifest + s.configBytes = configBytes + s.configDigest = "sha256:" + hex.EncodeToString(configHash[:]) + s.orderedDiffIDList = parsedConfig.RootFS.DiffIDs + s.knownLayers = knownLayers + return nil +} + +// loadTarManifest loads and decodes the manifest.json. +func (s *daemonImageSource) loadTarManifest() (*manifestItem, error) { + // FIXME? Do we need to deal with the legacy format? + bytes, err := s.readTarComponent(manifestFileName) + if err != nil { + return nil, err + } + var items []manifestItem + if err := json.Unmarshal(bytes, &items); err != nil { + return nil, fmt.Errorf("Error decoding tar manifest.json: %v", err) + } + if len(items) != 1 { + return nil, fmt.Errorf("Unexpected tar manifest.json: expected 1 item, got %d", len(items)) + } + return &items[0], nil +} + +func (s *daemonImageSource) prepareLayerData(tarManifest *manifestItem, parsedConfig *image) (map[diffID]*layerInfo, error) { + // Collect layer data available in manifest and config. + if len(tarManifest.Layers) != len(parsedConfig.RootFS.DiffIDs) { + return nil, fmt.Errorf("Inconsistent layer count: %d in manifest, %d in config", len(tarManifest.Layers), len(parsedConfig.RootFS.DiffIDs)) + } + knownLayers := map[diffID]*layerInfo{} + unknownLayerSizes := map[string]*layerInfo{} // Points into knownLayers, a "to do list" of items with unknown sizes. + for i, diffID := range parsedConfig.RootFS.DiffIDs { + if _, ok := knownLayers[diffID]; ok { + // Apparently it really can happen that a single image contains the same layer diff more than once. + // In that case, the diffID validation ensures that both layers truly are the same, and it should not matter + // which of the tarManifest.Layers paths is used; (docker save) actually makes the duplicates symlinks to the original. + continue + } + layerPath := tarManifest.Layers[i] + if _, ok := unknownLayerSizes[layerPath]; ok { + return nil, fmt.Errorf("Layer tarfile %s used for two different DiffID values", layerPath) + } + li := &layerInfo{ // A new element in each iteration + path: layerPath, + size: -1, + } + knownLayers[diffID] = li + unknownLayerSizes[layerPath] = li + } + + // Scan the tar file to collect layer sizes. + file, err := os.Open(s.tarCopyPath) + if err != nil { + return nil, err + } + defer file.Close() + t := tar.NewReader(file) + for { + h, err := t.Next() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + if li, ok := unknownLayerSizes[h.Name]; ok { + li.size = h.Size + delete(unknownLayerSizes, h.Name) + } + } + if len(unknownLayerSizes) != 0 { + return nil, fmt.Errorf("Some layer tarfiles are missing in the tarball") // This could do with a better error reporting, if this ever happened in practice. + } + + return knownLayers, nil +} + +// GetManifest returns the image's manifest along with its MIME type. The empty string is returned if the MIME type is unknown. +// It may use a remote (= slow) service. +func (s *daemonImageSource) GetManifest() ([]byte, string, error) { + if s.generatedManifest == nil { + if err := s.ensureCachedDataIsPresent(); err != nil { + return nil, "", err + } + m := schema2Manifest{ + SchemaVersion: 2, + MediaType: manifest.DockerV2Schema2MediaType, + Config: distributionDescriptor{ + MediaType: manifest.DockerV2Schema2ConfigMediaType, + Size: int64(len(s.configBytes)), + Digest: s.configDigest, + }, + Layers: []distributionDescriptor{}, + } + for _, diffID := range s.orderedDiffIDList { + li, ok := s.knownLayers[diffID] + if !ok { + return nil, "", fmt.Errorf("Internal inconsistency: Information about layer %s missing", diffID) + } + m.Layers = append(m.Layers, distributionDescriptor{ + Digest: string(diffID), // diffID is a digest of the uncompressed tarball + MediaType: manifest.DockerV2Schema2LayerMediaType, + Size: li.size, + }) + } + manifestBytes, err := json.Marshal(&m) + if err != nil { + return nil, "", err + } + s.generatedManifest = manifestBytes + } + return s.generatedManifest, manifest.DockerV2Schema2MediaType, nil +} + +// GetBlob returns a stream for the specified blob, and the blob’s size (or -1 if unknown). +func (s *daemonImageSource) GetBlob(digest string) (io.ReadCloser, int64, error) { + if err := s.ensureCachedDataIsPresent(); err != nil { + return nil, 0, err + } + + if digest == s.configDigest { // FIXME? Implement a more general algorithm matching instead of assuming sha256. + return ioutil.NopCloser(bytes.NewReader(s.configBytes)), int64(len(s.configBytes)), nil + } + + if li, ok := s.knownLayers[diffID(digest)]; ok { // diffID is a digest of the uncompressed tarball, + stream, err := s.openTarComponent(li.path) + if err != nil { + return nil, 0, err + } + return stream, li.size, nil + } + + return nil, 0, fmt.Errorf("Unknown blob %s", digest) +} + +// GetSignatures returns the image's signatures. It may use a remote (= slow) service. +func (s *daemonImageSource) GetSignatures() ([][]byte, error) { + return [][]byte{}, nil +} diff --git a/docker/daemon/daemon_transport.go b/docker/daemon/daemon_transport.go new file mode 100644 index 000000000..64eb4ae2a --- /dev/null +++ b/docker/daemon/daemon_transport.go @@ -0,0 +1,108 @@ +package daemon + +import ( + "fmt" + + "github.com/containers/image/types" + "github.com/docker/docker/reference" +) + +// Transport is an ImageTransport for images managed by a local Docker daemon. +var Transport = daemonTransport{} + +type daemonTransport struct{} + +// Name returns the name of the transport, which must be unique among other transports. +func (t daemonTransport) Name() string { + return "docker-daemon" +} + +// ParseReference converts a string, which should not start with the ImageTransport.Name prefix, into an ImageReference. +func (t daemonTransport) ParseReference(reference string) (types.ImageReference, error) { + return ParseReference(reference) +} + +// ValidatePolicyConfigurationScope checks that scope is a valid name for a signature.PolicyTransportScopes keys +// (i.e. a valid PolicyConfigurationIdentity() or PolicyConfigurationNamespaces() return value). +// It is acceptable to allow an invalid value which will never be matched, it can "only" cause user confusion. +// scope passed to this function will not be "", that value is always allowed. +func (t daemonTransport) ValidatePolicyConfigurationScope(scope string) error { + // FIXME FIXME + return nil +} + +// daemonReference is an ImageReference for images managed by a local Docker daemon. +type daemonReference string // FIXME FIXME + +// ParseReference converts a string, which should not start with the ImageTransport.Name prefix, into an ImageReference. +func ParseReference(reference string) (types.ImageReference, error) { + return daemonReference(reference), nil // FIXME FIXME +} + +// FIXME FIXME: NewReference? + +func (ref daemonReference) Transport() types.ImageTransport { + return Transport +} + +// StringWithinTransport returns a string representation of the reference, which MUST be such that +// reference.Transport().ParseReference(reference.StringWithinTransport()) returns an equivalent reference. +// NOTE: The returned string is not promised to be equal to the original input to ParseReference; +// e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa. +// WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix; +// instead, see transports.ImageName(). +func (ref daemonReference) StringWithinTransport() string { + return string(ref) // FIXME FIXME +} + +// DockerReference returns a Docker reference associated with this reference +// (fully explicit, i.e. !reference.IsNameOnly, but reflecting user intent, +// not e.g. after redirect or alias processing), or nil if unknown/not applicable. +func (ref daemonReference) DockerReference() reference.Named { + return nil // FIXME FIXME +} + +// PolicyConfigurationIdentity returns a string representation of the reference, suitable for policy lookup. +// This MUST reflect user intent, not e.g. after processing of third-party redirects or aliases; +// The value SHOULD be fully explicit about its semantics, with no hidden defaults, AND canonical +// (i.e. various references with exactly the same semantics should return the same configuration identity) +// It is fine for the return value to be equal to StringWithinTransport(), and it is desirable but +// not required/guaranteed that it will be a valid input to Transport().ParseReference(). +// Returns "" if configuration identities for these references are not supported. +func (ref daemonReference) PolicyConfigurationIdentity() string { + return string(ref) // FIXME FIXME +} + +// PolicyConfigurationNamespaces returns a list of other policy configuration namespaces to search +// for if explicit configuration for PolicyConfigurationIdentity() is not set. The list will be processed +// in order, terminating on first match, and an implicit "" is always checked at the end. +// It is STRONGLY recommended for the first element, if any, to be a prefix of PolicyConfigurationIdentity(), +// and each following element to be a prefix of the element preceding it. +func (ref daemonReference) PolicyConfigurationNamespaces() []string { + return []string{} // FIXME FIXME? +} + +// NewImage returns a types.Image for this reference. +// The caller must call .Close() on the returned Image. +func (ref daemonReference) NewImage(ctx *types.SystemContext) (types.Image, error) { + panic("FIXME FIXME") +} + +// NewImageSource returns a types.ImageSource for this 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 (ref daemonReference) NewImageSource(ctx *types.SystemContext, requestedManifestMIMETypes []string) (types.ImageSource, error) { + return newImageSource(ctx, ref) +} + +// NewImageDestination returns a types.ImageDestination for this reference. +// The caller must call .Close() on the returned ImageDestination. +func (ref daemonReference) NewImageDestination(ctx *types.SystemContext) (types.ImageDestination, error) { + return newImageDestination(ctx, ref) +} + +// DeleteImage deletes the named image from the registry, if supported. +func (ref daemonReference) DeleteImage(ctx *types.SystemContext) error { + return fmt.Errorf("Deleting images not implemented for docker-daemon: images") // FIXME FIXME? +} diff --git a/docker/daemon/daemon_types.go b/docker/daemon/daemon_types.go new file mode 100644 index 000000000..aedf92fe9 --- /dev/null +++ b/docker/daemon/daemon_types.go @@ -0,0 +1,51 @@ +package daemon + +// Various data structures. + +// Based on github.com/docker/docker/image/tarexport/tarexport.go +const ( + manifestFileName = "manifest.json" + // legacyLayerFileName = "layer.tar" + // legacyConfigFileName = "json" + // legacyVersionFileName = "VERSION" + // legacyRepositoriesFileName = "repositories" +) + +type manifestItem struct { + Config string + RepoTags []string + Layers []string + Parent imageID `json:",omitempty"` + LayerSources map[diffID]distributionDescriptor `json:",omitempty"` +} + +type imageID string +type diffID string + +// Based on github.com/docker/distribution/blobs.go +type distributionDescriptor struct { + MediaType string `json:"mediaType,omitempty"` + Size int64 `json:"size,omitempty"` + Digest string `json:"digest,omitempty"` + URLs []string `json:"urls,omitempty"` +} + +// Based on github.com/docker/distribution/manifest/schema2/manifest.go +// FIXME: We are repeating this all over the place; make a public copy? +type schema2Manifest struct { + SchemaVersion int `json:"schemaVersion"` + MediaType string `json:"mediaType,omitempty"` + Config distributionDescriptor `json:"config"` + Layers []distributionDescriptor `json:"layers"` +} + +// Based on github.com/docker/docker/image/image.go +// MOST CONTENT OMITTED AS UNNECESSARY +type image struct { + RootFS *rootFS `json:"rootfs,omitempty"` +} + +type rootFS struct { + Type string `json:"type"` + DiffIDs []diffID `json:"diff_ids,omitempty"` +} diff --git a/manifest/manifest.go b/manifest/manifest.go index 6344c9649..be4ee9ca0 100644 --- a/manifest/manifest.go +++ b/manifest/manifest.go @@ -19,6 +19,10 @@ const ( DockerV2Schema1SignedMediaType = "application/vnd.docker.distribution.manifest.v1+prettyjws" // DockerV2Schema2MediaType MIME type represents Docker manifest schema 2 DockerV2Schema2MediaType = "application/vnd.docker.distribution.manifest.v2+json" + // DockerV2Schema2ConfigMediaType is the MIME type used for schema 2 config blobs. + DockerV2Schema2ConfigMediaType = "application/vnd.docker.container.image.v1+json" + // DockerV2Schema2LayerMediaType is the MIME type used for schema 2 layers. + DockerV2Schema2LayerMediaType = "application/vnd.docker.image.rootfs.diff.tar.gzip" // DockerV2ListMediaType MIME type represents Docker manifest schema 2 list DockerV2ListMediaType = "application/vnd.docker.distribution.manifest.list.v2+json" ) diff --git a/transports/transports.go b/transports/transports.go index 2b7e4f13c..2caceaada 100644 --- a/transports/transports.go +++ b/transports/transports.go @@ -6,6 +6,7 @@ import ( "github.com/containers/image/directory" "github.com/containers/image/docker" + "github.com/containers/image/docker/daemon" ociLayout "github.com/containers/image/oci/layout" "github.com/containers/image/openshift" "github.com/containers/image/types" @@ -19,6 +20,7 @@ func init() { for _, t := range []types.ImageTransport{ directory.Transport, docker.Transport, + daemon.Transport, ociLayout.Transport, openshift.Transport, } { diff --git a/transports/transports_test.go b/transports/transports_test.go index 592eac020..60e694bd4 100644 --- a/transports/transports_test.go +++ b/transports/transports_test.go @@ -32,6 +32,7 @@ func TestImageNameHandling(t *testing.T) { {"dir", "/etc", "/etc"}, {"docker", "//busybox", "//busybox:latest"}, {"docker", "//busybox:notlatest", "//busybox:notlatest"}, // This also tests handling of multiple ":" characters + {"docker-daemon", "FIXME FIXME", "FIXME FIXME"}, {"oci", "/etc:sometag", "/etc:sometag"}, // "atomic" not tested here because it depends on per-user configuration for the default cluster. } {