Bump containers/image and containers/storage

Update to proposed changes in containers/image, and bump
containers/storage to 04ad0b827097209ca65e59b5fd768511f3b1ae91, which is
currently the tip of the master branch.

Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>
This commit is contained in:
Nalin Dahyabhai 2017-10-24 23:07:20 -04:00
parent c5e73ba65f
commit 1346755565
32 changed files with 1705 additions and 852 deletions

View file

@ -4,10 +4,10 @@ k8s.io/apimachinery release-1.7 https://github.com/kubernetes/apimachinery
k8s.io/apiserver release-1.7 https://github.com/kubernetes/apiserver k8s.io/apiserver release-1.7 https://github.com/kubernetes/apiserver
# #
github.com/sirupsen/logrus v1.0.0 github.com/sirupsen/logrus v1.0.0
github.com/containers/image 57b257d128d6075ea3287991ee408d24c7bd2758 github.com/containers/image storage-update https://github.com/nalind/image
github.com/docker/docker-credential-helpers d68f9aeca33f5fd3f08eeae5e9d175edf4e731d1 github.com/docker/docker-credential-helpers d68f9aeca33f5fd3f08eeae5e9d175edf4e731d1
github.com/ostreedev/ostree-go master github.com/ostreedev/ostree-go master
github.com/containers/storage d7921c6facc516358070a1306689eda18adaa20a github.com/containers/storage 9e0c323a4b425557f8310ee8d125634acd39d8f5
github.com/containernetworking/cni v0.4.0 github.com/containernetworking/cni v0.4.0
google.golang.org/grpc v1.0.4 https://github.com/grpc/grpc-go google.golang.org/grpc v1.0.4 https://github.com/grpc/grpc-go
github.com/opencontainers/selinux b29023b86e4a69d1b46b7e7b4e2b6fda03f0b9cd github.com/opencontainers/selinux b29023b86e4a69d1b46b7e7b4e2b6fda03f0b9cd

View file

@ -320,6 +320,15 @@ func (ic *imageCopier) copyLayers() error {
srcInfos := ic.src.LayerInfos() srcInfos := ic.src.LayerInfos()
destInfos := []types.BlobInfo{} destInfos := []types.BlobInfo{}
diffIDs := []digest.Digest{} diffIDs := []digest.Digest{}
updatedSrcInfos := ic.src.UpdatedLayerInfos()
srcInfosUpdated := false
if updatedSrcInfos != nil && !reflect.DeepEqual(srcInfos, updatedSrcInfos) {
if !ic.canModifyManifest {
return errors.Errorf("Internal error: copyLayers() needs to use an updated manifest but that was known to be forbidden")
}
srcInfos = updatedSrcInfos
srcInfosUpdated = true
}
for _, srcLayer := range srcInfos { for _, srcLayer := range srcInfos {
var ( var (
destInfo types.BlobInfo destInfo types.BlobInfo
@ -348,7 +357,7 @@ func (ic *imageCopier) copyLayers() error {
if ic.diffIDsAreNeeded { if ic.diffIDsAreNeeded {
ic.manifestUpdates.InformationOnly.LayerDiffIDs = diffIDs ic.manifestUpdates.InformationOnly.LayerDiffIDs = diffIDs
} }
if layerDigestsDiffer(srcInfos, destInfos) { if srcInfosUpdated || layerDigestsDiffer(srcInfos, destInfos) {
ic.manifestUpdates.LayerInfos = destInfos ic.manifestUpdates.LayerInfos = destInfos
} }
return nil return nil

View file

@ -74,3 +74,8 @@ func (s *dirImageSource) GetSignatures(ctx context.Context) ([][]byte, error) {
} }
return signatures, nil return signatures, nil
} }
// UpdatedLayerInfos() returns updated layer info that should be used when reading, in preference to values in the manifest, if specified.
func (s *dirImageSource) UpdatedLayerInfos() []types.BlobInfo {
return nil
}

View file

@ -34,3 +34,8 @@ func (s *archiveImageSource) Reference() types.ImageReference {
func (s *archiveImageSource) Close() error { func (s *archiveImageSource) Close() error {
return nil return nil
} }
// UpdatedLayerInfos() returns updated layer info that should be used when reading, in preference to values in the manifest, if specified.
func (s *archiveImageSource) UpdatedLayerInfos() []types.BlobInfo {
return nil
}

View file

@ -83,3 +83,8 @@ func (s *daemonImageSource) Reference() types.ImageReference {
func (s *daemonImageSource) Close() error { func (s *daemonImageSource) Close() error {
return os.Remove(s.tarCopyPath) return os.Remove(s.tarCopyPath)
} }
// UpdatedLayerInfos() returns updated layer info that should be used when reading, in preference to values in the manifest, if specified.
func (s *daemonImageSource) UpdatedLayerInfos() []types.BlobInfo {
return nil
}

View file

@ -8,7 +8,6 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
@ -125,69 +124,6 @@ func dockerCertDir(ctx *types.SystemContext, hostPort string) string {
return filepath.Join(hostCertDir, hostPort) return filepath.Join(hostCertDir, hostPort)
} }
func setupCertificates(dir string, tlsc *tls.Config) error {
logrus.Debugf("Looking for TLS certificates and private keys in %s", dir)
fs, err := ioutil.ReadDir(dir)
if err != nil {
if os.IsNotExist(err) {
return nil
}
if os.IsPermission(err) {
logrus.Debugf("Skipping scan of %s due to permission error: %v", dir, err)
return nil
}
return err
}
for _, f := range fs {
fullPath := filepath.Join(dir, f.Name())
if strings.HasSuffix(f.Name(), ".crt") {
systemPool, err := tlsconfig.SystemCertPool()
if err != nil {
return errors.Wrap(err, "unable to get system cert pool")
}
tlsc.RootCAs = systemPool
logrus.Debugf(" crt: %s", fullPath)
data, err := ioutil.ReadFile(fullPath)
if err != nil {
return err
}
tlsc.RootCAs.AppendCertsFromPEM(data)
}
if strings.HasSuffix(f.Name(), ".cert") {
certName := f.Name()
keyName := certName[:len(certName)-5] + ".key"
logrus.Debugf(" cert: %s", fullPath)
if !hasFile(fs, keyName) {
return errors.Errorf("missing key %s for client certificate %s. Note that CA certificates should use the extension .crt", keyName, certName)
}
cert, err := tls.LoadX509KeyPair(filepath.Join(dir, certName), filepath.Join(dir, keyName))
if err != nil {
return err
}
tlsc.Certificates = append(tlsc.Certificates, cert)
}
if strings.HasSuffix(f.Name(), ".key") {
keyName := f.Name()
certName := keyName[:len(keyName)-4] + ".cert"
logrus.Debugf(" key: %s", fullPath)
if !hasFile(fs, certName) {
return errors.Errorf("missing client certificate %s for key %s", certName, keyName)
}
}
}
return nil
}
func hasFile(files []os.FileInfo, name string) bool {
for _, f := range files {
if f.Name() == name {
return true
}
}
return false
}
// newDockerClientFromRef returns a new dockerClient instance for refHostname (a host a specified in the Docker image reference, not canonicalized to dockerRegistry) // newDockerClientFromRef returns a new dockerClient instance for refHostname (a host a specified in the Docker image reference, not canonicalized to dockerRegistry)
// “write” specifies whether the client will be used for "write" access (in particular passed to lookaside.go:toplevelFromSection) // “write” specifies whether the client will be used for "write" access (in particular passed to lookaside.go:toplevelFromSection)
func newDockerClientFromRef(ctx *types.SystemContext, ref dockerReference, write bool, actions string) (*dockerClient, error) { func newDockerClientFromRef(ctx *types.SystemContext, ref dockerReference, write bool, actions string) (*dockerClient, error) {

View file

@ -52,6 +52,11 @@ func (s *dockerImageSource) Close() error {
return nil return nil
} }
// UpdatedLayerInfos() returns updated layer info that should be used when reading, in preference to values in the manifest, if specified.
func (s *dockerImageSource) UpdatedLayerInfos() []types.BlobInfo {
return nil
}
// simplifyContentType drops parameters from a HTTP media type (see https://tools.ietf.org/html/rfc7231#section-3.1.1.1) // simplifyContentType drops parameters from a HTTP media type (see https://tools.ietf.org/html/rfc7231#section-3.1.1.1)
// Alternatively, an empty string is returned unchanged, and invalid values are "simplified" to an empty string. // Alternatively, an empty string is returned unchanged, and invalid values are "simplified" to an empty string.
func simplifyContentType(contentType string) string { func simplifyContentType(contentType string) string {

View file

@ -168,7 +168,7 @@ func (d *Destination) ReapplyBlob(info types.BlobInfo) (types.BlobInfo, error) {
func (d *Destination) PutManifest(m []byte) error { func (d *Destination) PutManifest(m []byte) error {
// We do not bother with types.ManifestTypeRejectedError; our .SupportedManifestMIMETypes() above is already providing only one alternative, // We do not bother with types.ManifestTypeRejectedError; our .SupportedManifestMIMETypes() above is already providing only one alternative,
// so the caller trying a different manifest kind would be pointless. // so the caller trying a different manifest kind would be pointless.
var man schema2Manifest var man manifest.Schema2
if err := json.Unmarshal(m, &man); err != nil { if err := json.Unmarshal(m, &man); err != nil {
return errors.Wrap(err, "Error parsing manifest") return errors.Wrap(err, "Error parsing manifest")
} }
@ -177,12 +177,12 @@ func (d *Destination) PutManifest(m []byte) error {
} }
layerPaths := []string{} layerPaths := []string{}
for _, l := range man.Layers { for _, l := range man.LayersDescriptors {
layerPaths = append(layerPaths, l.Digest.String()) layerPaths = append(layerPaths, l.Digest.String())
} }
items := []ManifestItem{{ items := []ManifestItem{{
Config: man.Config.Digest.String(), Config: man.ConfigDescriptor.Digest.String(),
RepoTags: []string{d.repoTag}, RepoTags: []string{d.repoTag},
Layers: layerPaths, Layers: layerPaths,
Parent: "", Parent: "",

View file

@ -254,22 +254,22 @@ func (s *Source) GetManifest() ([]byte, string, error) {
if err := s.ensureCachedDataIsPresent(); err != nil { if err := s.ensureCachedDataIsPresent(); err != nil {
return nil, "", err return nil, "", err
} }
m := schema2Manifest{ m := manifest.Schema2{
SchemaVersion: 2, SchemaVersion: 2,
MediaType: manifest.DockerV2Schema2MediaType, MediaType: manifest.DockerV2Schema2MediaType,
Config: distributionDescriptor{ ConfigDescriptor: manifest.Schema2Descriptor{
MediaType: manifest.DockerV2Schema2ConfigMediaType, MediaType: manifest.DockerV2Schema2ConfigMediaType,
Size: int64(len(s.configBytes)), Size: int64(len(s.configBytes)),
Digest: s.configDigest, Digest: s.configDigest,
}, },
Layers: []distributionDescriptor{}, LayersDescriptors: []manifest.Schema2Descriptor{},
} }
for _, diffID := range s.orderedDiffIDList { for _, diffID := range s.orderedDiffIDList {
li, ok := s.knownLayers[diffID] li, ok := s.knownLayers[diffID]
if !ok { if !ok {
return nil, "", errors.Errorf("Internal inconsistency: Information about layer %s missing", diffID) return nil, "", errors.Errorf("Internal inconsistency: Information about layer %s missing", diffID)
} }
m.Layers = append(m.Layers, distributionDescriptor{ m.LayersDescriptors = append(m.LayersDescriptors, manifest.Schema2Descriptor{
Digest: digest.Digest(diffID), // diffID is a digest of the uncompressed tarball Digest: digest.Digest(diffID), // diffID is a digest of the uncompressed tarball
MediaType: manifest.DockerV2Schema2LayerMediaType, MediaType: manifest.DockerV2Schema2LayerMediaType,
Size: li.size, Size: li.size,

View file

@ -1,6 +1,9 @@
package tarfile package tarfile
import "github.com/opencontainers/go-digest" import (
"github.com/containers/image/manifest"
"github.com/opencontainers/go-digest"
)
// Various data structures. // Various data structures.
@ -18,30 +21,13 @@ type ManifestItem struct {
Config string Config string
RepoTags []string RepoTags []string
Layers []string Layers []string
Parent imageID `json:",omitempty"` Parent imageID `json:",omitempty"`
LayerSources map[diffID]distributionDescriptor `json:",omitempty"` LayerSources map[diffID]manifest.Schema2Descriptor `json:",omitempty"`
} }
type imageID string type imageID string
type diffID digest.Digest type diffID digest.Digest
// Based on github.com/docker/distribution/blobs.go
type distributionDescriptor struct {
MediaType string `json:"mediaType,omitempty"`
Size int64 `json:"size,omitempty"`
Digest digest.Digest `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 // Based on github.com/docker/docker/image/image.go
// MOST CONTENT OMITTED AS UNNECESSARY // MOST CONTENT OMITTED AS UNNECESSARY
type image struct { type image struct {

View file

@ -21,7 +21,7 @@ type platformSpec struct {
// A manifestDescriptor references a platform-specific manifest. // A manifestDescriptor references a platform-specific manifest.
type manifestDescriptor struct { type manifestDescriptor struct {
descriptor manifest.Schema2Descriptor
Platform platformSpec `json:"platform"` Platform platformSpec `json:"platform"`
} }

View file

@ -2,9 +2,7 @@ package image
import ( import (
"encoding/json" "encoding/json"
"regexp"
"strings" "strings"
"time"
"github.com/containers/image/docker/reference" "github.com/containers/image/docker/reference"
"github.com/containers/image/manifest" "github.com/containers/image/manifest"
@ -14,87 +12,25 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
var (
validHex = regexp.MustCompile(`^([a-f0-9]{64})$`)
)
type fsLayersSchema1 struct {
BlobSum digest.Digest `json:"blobSum"`
}
type historySchema1 struct {
V1Compatibility string `json:"v1Compatibility"`
}
// historySchema1 is a string containing this. It is similar to v1Image but not the same, in particular note the ThrowAway field.
type v1Compatibility struct {
ID string `json:"id"`
Parent string `json:"parent,omitempty"`
Comment string `json:"comment,omitempty"`
Created time.Time `json:"created"`
ContainerConfig struct {
Cmd []string
} `json:"container_config,omitempty"`
Author string `json:"author,omitempty"`
ThrowAway bool `json:"throwaway,omitempty"`
}
type manifestSchema1 struct { type manifestSchema1 struct {
Name string `json:"name"` m *manifest.Schema1
Tag string `json:"tag"`
Architecture string `json:"architecture"`
FSLayers []fsLayersSchema1 `json:"fsLayers"`
History []historySchema1 `json:"history"`
SchemaVersion int `json:"schemaVersion"`
} }
func manifestSchema1FromManifest(manifest []byte) (genericManifest, error) { func manifestSchema1FromManifest(manifestBlob []byte) (genericManifest, error) {
mschema1 := &manifestSchema1{} m, err := manifest.Schema1FromManifest(manifestBlob)
if err := json.Unmarshal(manifest, mschema1); err != nil {
return nil, err
}
if mschema1.SchemaVersion != 1 {
return nil, errors.Errorf("unsupported schema version %d", mschema1.SchemaVersion)
}
if len(mschema1.FSLayers) != len(mschema1.History) {
return nil, errors.New("length of history not equal to number of layers")
}
if len(mschema1.FSLayers) == 0 {
return nil, errors.New("no FSLayers in manifest")
}
if err := fixManifestLayers(mschema1); err != nil {
return nil, err
}
return mschema1, nil
}
// manifestSchema1FromComponents builds a new manifestSchema1 from the supplied data.
func manifestSchema1FromComponents(ref reference.Named, fsLayers []fsLayersSchema1, history []historySchema1, architecture string) genericManifest {
var name, tag string
if ref != nil { // Well, what to do if it _is_ nil? Most consumers actually don't use these fields nowadays, so we might as well try not supplying them.
name = reference.Path(ref)
if tagged, ok := ref.(reference.NamedTagged); ok {
tag = tagged.Tag()
}
}
return &manifestSchema1{
Name: name,
Tag: tag,
Architecture: architecture,
FSLayers: fsLayers,
History: history,
SchemaVersion: 1,
}
}
func (m *manifestSchema1) serialize() ([]byte, error) {
// docker/distribution requires a signature even if the incoming data uses the nominally unsigned DockerV2Schema1MediaType.
unsigned, err := json.Marshal(*m)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return manifest.AddDummyV2S1Signature(unsigned) return &manifestSchema1{m: m}, nil
}
// manifestSchema1FromComponents builds a new manifestSchema1 from the supplied data.
func manifestSchema1FromComponents(ref reference.Named, fsLayers []manifest.Schema1FSLayers, history []manifest.Schema1History, architecture string) genericManifest {
return &manifestSchema1{m: manifest.Schema1FromComponents(ref, fsLayers, history, architecture)}
}
func (m *manifestSchema1) serialize() ([]byte, error) {
return m.m.Serialize()
} }
func (m *manifestSchema1) manifestMIMEType() string { func (m *manifestSchema1) manifestMIMEType() string {
@ -104,7 +40,7 @@ func (m *manifestSchema1) manifestMIMEType() string {
// ConfigInfo returns a complete BlobInfo for the separate config object, or a BlobInfo{Digest:""} if there isn't a separate object. // ConfigInfo returns a complete BlobInfo for the separate config object, or a BlobInfo{Digest:""} if there isn't a separate object.
// Note that the config object may not exist in the underlying storage in the return value of UpdatedImage! Use ConfigBlob() below. // Note that the config object may not exist in the underlying storage in the return value of UpdatedImage! Use ConfigBlob() below.
func (m *manifestSchema1) ConfigInfo() types.BlobInfo { func (m *manifestSchema1) ConfigInfo() types.BlobInfo {
return types.BlobInfo{} return m.m.ConfigInfo()
} }
// ConfigBlob returns the blob described by ConfigInfo, iff ConfigInfo().Digest != ""; nil otherwise. // ConfigBlob returns the blob described by ConfigInfo, iff ConfigInfo().Digest != ""; nil otherwise.
@ -128,11 +64,7 @@ func (m *manifestSchema1) OCIConfig() (*imgspecv1.Image, error) {
// The Digest field is guaranteed to be provided; Size may be -1. // The Digest field is guaranteed to be provided; Size may be -1.
// WARNING: The list may contain duplicates, and they are semantically relevant. // WARNING: The list may contain duplicates, and they are semantically relevant.
func (m *manifestSchema1) LayerInfos() []types.BlobInfo { func (m *manifestSchema1) LayerInfos() []types.BlobInfo {
layers := make([]types.BlobInfo, len(m.FSLayers)) return m.m.LayerInfos()
for i, layer := range m.FSLayers { // NOTE: This includes empty layers (where m.History.V1Compatibility->ThrowAway)
layers[(len(m.FSLayers)-1)-i] = types.BlobInfo{Digest: layer.BlobSum, Size: -1}
}
return layers
} }
// EmbeddedDockerReferenceConflicts whether a Docker reference embedded in the manifest, if any, conflicts with destination ref. // EmbeddedDockerReferenceConflicts whether a Docker reference embedded in the manifest, if any, conflicts with destination ref.
@ -153,22 +85,25 @@ func (m *manifestSchema1) EmbeddedDockerReferenceConflicts(ref reference.Named)
} else { } else {
tag = "" tag = ""
} }
return m.Name != name || m.Tag != tag return m.m.Name != name || m.m.Tag != tag
} }
func (m *manifestSchema1) imageInspectInfo() (*types.ImageInspectInfo, error) { func (m *manifestSchema1) imageInspectInfo() (*types.ImageInspectInfo, error) {
v1 := &v1Image{} v1 := &v1Image{}
if err := json.Unmarshal([]byte(m.History[0].V1Compatibility), v1); err != nil { if err := json.Unmarshal([]byte(m.m.History[0].V1Compatibility), v1); err != nil {
return nil, err return nil, err
} }
return &types.ImageInspectInfo{ i := &types.ImageInspectInfo{
Tag: m.Tag, Tag: m.m.Tag,
DockerVersion: v1.DockerVersion, DockerVersion: v1.DockerVersion,
Created: v1.Created, Created: v1.Created,
Labels: v1.Config.Labels,
Architecture: v1.Architecture, Architecture: v1.Architecture,
Os: v1.OS, Os: v1.OS,
}, nil }
if v1.Config != nil {
i.Labels = v1.Config.Labels
}
return i, nil
} }
// UpdatedImageNeedsLayerDiffIDs returns true iff UpdatedImage(options) needs InformationOnly.LayerDiffIDs. // UpdatedImageNeedsLayerDiffIDs returns true iff UpdatedImage(options) needs InformationOnly.LayerDiffIDs.
@ -181,25 +116,18 @@ func (m *manifestSchema1) UpdatedImageNeedsLayerDiffIDs(options types.ManifestUp
// UpdatedImage returns a types.Image modified according to options. // UpdatedImage returns a types.Image modified according to options.
// This does not change the state of the original Image object. // This does not change the state of the original Image object.
func (m *manifestSchema1) UpdatedImage(options types.ManifestUpdateOptions) (types.Image, error) { func (m *manifestSchema1) UpdatedImage(options types.ManifestUpdateOptions) (types.Image, error) {
copy := *m copy := manifestSchema1{m: manifest.Schema1Clone(m.m)}
if options.LayerInfos != nil { if options.LayerInfos != nil {
// Our LayerInfos includes empty layers (where m.History.V1Compatibility->ThrowAway), so expect them to be included here as well. if err := copy.m.UpdateLayerInfos(options.LayerInfos); err != nil {
if len(copy.FSLayers) != len(options.LayerInfos) { return nil, err
return nil, errors.Errorf("Error preparing updated manifest: layer count changed from %d to %d", len(copy.FSLayers), len(options.LayerInfos))
}
for i, info := range options.LayerInfos {
// (docker push) sets up m.History.V1Compatibility->{Id,Parent} based on values of info.Digest,
// but (docker pull) ignores them in favor of computing DiffIDs from uncompressed data, except verifying the child->parent links and uniqueness.
// So, we don't bother recomputing the IDs in m.History.V1Compatibility.
copy.FSLayers[(len(options.LayerInfos)-1)-i].BlobSum = info.Digest
} }
} }
if options.EmbeddedDockerReference != nil { if options.EmbeddedDockerReference != nil {
copy.Name = reference.Path(options.EmbeddedDockerReference) copy.m.Name = reference.Path(options.EmbeddedDockerReference)
if tagged, isTagged := options.EmbeddedDockerReference.(reference.NamedTagged); isTagged { if tagged, isTagged := options.EmbeddedDockerReference.(reference.NamedTagged); isTagged {
copy.Tag = tagged.Tag() copy.m.Tag = tagged.Tag()
} else { } else {
copy.Tag = "" copy.m.Tag = ""
} }
} }
@ -217,78 +145,20 @@ func (m *manifestSchema1) UpdatedImage(options types.ManifestUpdateOptions) (typ
return memoryImageFromManifest(&copy), nil return memoryImageFromManifest(&copy), nil
} }
// fixManifestLayers, after validating the supplied manifest
// (to use correctly-formatted IDs, and to not have non-consecutive ID collisions in manifest.History),
// modifies manifest to only have one entry for each layer ID in manifest.History (deleting the older duplicates,
// both from manifest.History and manifest.FSLayers).
// Note that even after this succeeds, manifest.FSLayers may contain duplicate entries
// (for Dockerfile operations which change the configuration but not the filesystem).
func fixManifestLayers(manifest *manifestSchema1) error {
type imageV1 struct {
ID string
Parent string
}
// Per the specification, we can assume that len(manifest.FSLayers) == len(manifest.History)
imgs := make([]*imageV1, len(manifest.FSLayers))
for i := range manifest.FSLayers {
img := &imageV1{}
if err := json.Unmarshal([]byte(manifest.History[i].V1Compatibility), img); err != nil {
return err
}
imgs[i] = img
if err := validateV1ID(img.ID); err != nil {
return err
}
}
if imgs[len(imgs)-1].Parent != "" {
return errors.New("Invalid parent ID in the base layer of the image")
}
// check general duplicates to error instead of a deadlock
idmap := make(map[string]struct{})
var lastID string
for _, img := range imgs {
// skip IDs that appear after each other, we handle those later
if _, exists := idmap[img.ID]; img.ID != lastID && exists {
return errors.Errorf("ID %+v appears multiple times in manifest", img.ID)
}
lastID = img.ID
idmap[lastID] = struct{}{}
}
// backwards loop so that we keep the remaining indexes after removing items
for i := len(imgs) - 2; i >= 0; i-- {
if imgs[i].ID == imgs[i+1].ID { // repeated ID. remove and continue
manifest.FSLayers = append(manifest.FSLayers[:i], manifest.FSLayers[i+1:]...)
manifest.History = append(manifest.History[:i], manifest.History[i+1:]...)
} else if imgs[i].Parent != imgs[i+1].ID {
return errors.Errorf("Invalid parent ID. Expected %v, got %v", imgs[i+1].ID, imgs[i].Parent)
}
}
return nil
}
func validateV1ID(id string) error {
if ok := validHex.MatchString(id); !ok {
return errors.Errorf("image ID %q is invalid", id)
}
return nil
}
// Based on github.com/docker/docker/distribution/pull_v2.go // Based on github.com/docker/docker/distribution/pull_v2.go
func (m *manifestSchema1) convertToManifestSchema2(uploadedLayerInfos []types.BlobInfo, layerDiffIDs []digest.Digest) (types.Image, error) { func (m *manifestSchema1) convertToManifestSchema2(uploadedLayerInfos []types.BlobInfo, layerDiffIDs []digest.Digest) (types.Image, error) {
if len(m.History) == 0 { if len(m.m.History) == 0 {
// What would this even mean?! Anyhow, the rest of the code depends on fsLayers[0] and history[0] existing. // What would this even mean?! Anyhow, the rest of the code depends on fsLayers[0] and history[0] existing.
return nil, errors.Errorf("Cannot convert an image with 0 history entries to %s", manifest.DockerV2Schema2MediaType) return nil, errors.Errorf("Cannot convert an image with 0 history entries to %s", manifest.DockerV2Schema2MediaType)
} }
if len(m.History) != len(m.FSLayers) { if len(m.m.History) != len(m.m.FSLayers) {
return nil, errors.Errorf("Inconsistent schema 1 manifest: %d history entries, %d fsLayers entries", len(m.History), len(m.FSLayers)) return nil, errors.Errorf("Inconsistent schema 1 manifest: %d history entries, %d fsLayers entries", len(m.m.History), len(m.m.FSLayers))
} }
if uploadedLayerInfos != nil && len(uploadedLayerInfos) != len(m.FSLayers) { if uploadedLayerInfos != nil && len(uploadedLayerInfos) != len(m.m.FSLayers) {
return nil, errors.Errorf("Internal error: uploaded %d blobs, but schema1 manifest has %d fsLayers", len(uploadedLayerInfos), len(m.FSLayers)) return nil, errors.Errorf("Internal error: uploaded %d blobs, but schema1 manifest has %d fsLayers", len(uploadedLayerInfos), len(m.m.FSLayers))
} }
if layerDiffIDs != nil && len(layerDiffIDs) != len(m.FSLayers) { if layerDiffIDs != nil && len(layerDiffIDs) != len(m.m.FSLayers) {
return nil, errors.Errorf("Internal error: collected %d DiffID values, but schema1 manifest has %d fsLayers", len(layerDiffIDs), len(m.FSLayers)) return nil, errors.Errorf("Internal error: collected %d DiffID values, but schema1 manifest has %d fsLayers", len(layerDiffIDs), len(m.m.FSLayers))
} }
rootFS := rootFS{ rootFS := rootFS{
@ -296,13 +166,13 @@ func (m *manifestSchema1) convertToManifestSchema2(uploadedLayerInfos []types.Bl
DiffIDs: []digest.Digest{}, DiffIDs: []digest.Digest{},
BaseLayer: "", BaseLayer: "",
} }
var layers []descriptor var layers []manifest.Schema2Descriptor
history := make([]imageHistory, len(m.History)) history := make([]imageHistory, len(m.m.History))
for v1Index := len(m.History) - 1; v1Index >= 0; v1Index-- { for v1Index := len(m.m.History) - 1; v1Index >= 0; v1Index-- {
v2Index := (len(m.History) - 1) - v1Index v2Index := (len(m.m.History) - 1) - v1Index
var v1compat v1Compatibility var v1compat manifest.Schema1V1Compatibility
if err := json.Unmarshal([]byte(m.History[v1Index].V1Compatibility), &v1compat); err != nil { if err := json.Unmarshal([]byte(m.m.History[v1Index].V1Compatibility), &v1compat); err != nil {
return nil, errors.Wrapf(err, "Error decoding history entry %d", v1Index) return nil, errors.Wrapf(err, "Error decoding history entry %d", v1Index)
} }
history[v2Index] = imageHistory{ history[v2Index] = imageHistory{
@ -322,19 +192,19 @@ func (m *manifestSchema1) convertToManifestSchema2(uploadedLayerInfos []types.Bl
if layerDiffIDs != nil { if layerDiffIDs != nil {
d = layerDiffIDs[v2Index] d = layerDiffIDs[v2Index]
} }
layers = append(layers, descriptor{ layers = append(layers, manifest.Schema2Descriptor{
MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip",
Size: size, Size: size,
Digest: m.FSLayers[v1Index].BlobSum, Digest: m.m.FSLayers[v1Index].BlobSum,
}) })
rootFS.DiffIDs = append(rootFS.DiffIDs, d) rootFS.DiffIDs = append(rootFS.DiffIDs, d)
} }
} }
configJSON, err := configJSONFromV1Config([]byte(m.History[0].V1Compatibility), rootFS, history) configJSON, err := configJSONFromV1Config([]byte(m.m.History[0].V1Compatibility), rootFS, history)
if err != nil { if err != nil {
return nil, err return nil, err
} }
configDescriptor := descriptor{ configDescriptor := manifest.Schema2Descriptor{
MediaType: "application/vnd.docker.container.image.v1+json", MediaType: "application/vnd.docker.container.image.v1+json",
Size: int64(len(configJSON)), Size: int64(len(configJSON)),
Digest: digest.FromBytes(configJSON), Digest: digest.FromBytes(configJSON),

View file

@ -29,54 +29,44 @@ var gzippedEmptyLayer = []byte{
// gzippedEmptyLayerDigest is a digest of gzippedEmptyLayer // gzippedEmptyLayerDigest is a digest of gzippedEmptyLayer
const gzippedEmptyLayerDigest = digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4") const gzippedEmptyLayerDigest = digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")
type descriptor struct {
MediaType string `json:"mediaType"`
Size int64 `json:"size"`
Digest digest.Digest `json:"digest"`
URLs []string `json:"urls,omitempty"`
}
type manifestSchema2 struct { type manifestSchema2 struct {
src types.ImageSource // May be nil if configBlob is not nil src types.ImageSource // May be nil if configBlob is not nil
configBlob []byte // If set, corresponds to contents of ConfigDescriptor. configBlob []byte // If set, corresponds to contents of ConfigDescriptor.
SchemaVersion int `json:"schemaVersion"` m *manifest.Schema2
MediaType string `json:"mediaType"`
ConfigDescriptor descriptor `json:"config"`
LayersDescriptors []descriptor `json:"layers"`
} }
func manifestSchema2FromManifest(src types.ImageSource, manifest []byte) (genericManifest, error) { func manifestSchema2FromManifest(src types.ImageSource, manifestBlob []byte) (genericManifest, error) {
v2s2 := manifestSchema2{src: src} m, err := manifest.Schema2FromManifest(manifestBlob)
if err := json.Unmarshal(manifest, &v2s2); err != nil { if err != nil {
return nil, err return nil, err
} }
return &v2s2, nil return &manifestSchema2{
src: src,
m: m,
}, nil
} }
// manifestSchema2FromComponents builds a new manifestSchema2 from the supplied data: // manifestSchema2FromComponents builds a new manifestSchema2 from the supplied data:
func manifestSchema2FromComponents(config descriptor, src types.ImageSource, configBlob []byte, layers []descriptor) genericManifest { func manifestSchema2FromComponents(config manifest.Schema2Descriptor, src types.ImageSource, configBlob []byte, layers []manifest.Schema2Descriptor) genericManifest {
return &manifestSchema2{ return &manifestSchema2{
src: src, src: src,
configBlob: configBlob, configBlob: configBlob,
SchemaVersion: 2, m: manifest.Schema2FromComponents(config, layers),
MediaType: manifest.DockerV2Schema2MediaType,
ConfigDescriptor: config,
LayersDescriptors: layers,
} }
} }
func (m *manifestSchema2) serialize() ([]byte, error) { func (m *manifestSchema2) serialize() ([]byte, error) {
return json.Marshal(*m) return m.m.Serialize()
} }
func (m *manifestSchema2) manifestMIMEType() string { func (m *manifestSchema2) manifestMIMEType() string {
return m.MediaType return m.m.MediaType
} }
// ConfigInfo returns a complete BlobInfo for the separate config object, or a BlobInfo{Digest:""} if there isn't a separate object. // ConfigInfo returns a complete BlobInfo for the separate config object, or a BlobInfo{Digest:""} if there isn't a separate object.
// Note that the config object may not exist in the underlying storage in the return value of UpdatedImage! Use ConfigBlob() below. // Note that the config object may not exist in the underlying storage in the return value of UpdatedImage! Use ConfigBlob() below.
func (m *manifestSchema2) ConfigInfo() types.BlobInfo { func (m *manifestSchema2) ConfigInfo() types.BlobInfo {
return types.BlobInfo{Digest: m.ConfigDescriptor.Digest, Size: m.ConfigDescriptor.Size} return m.m.ConfigInfo()
} }
// OCIConfig returns the image configuration as per OCI v1 image-spec. Information about // OCIConfig returns the image configuration as per OCI v1 image-spec. Information about
@ -105,9 +95,9 @@ func (m *manifestSchema2) ConfigBlob() ([]byte, error) {
return nil, errors.Errorf("Internal error: neither src nor configBlob set in manifestSchema2") return nil, errors.Errorf("Internal error: neither src nor configBlob set in manifestSchema2")
} }
stream, _, err := m.src.GetBlob(types.BlobInfo{ stream, _, err := m.src.GetBlob(types.BlobInfo{
Digest: m.ConfigDescriptor.Digest, Digest: m.m.ConfigDescriptor.Digest,
Size: m.ConfigDescriptor.Size, Size: m.m.ConfigDescriptor.Size,
URLs: m.ConfigDescriptor.URLs, URLs: m.m.ConfigDescriptor.URLs,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@ -118,8 +108,8 @@ func (m *manifestSchema2) ConfigBlob() ([]byte, error) {
return nil, err return nil, err
} }
computedDigest := digest.FromBytes(blob) computedDigest := digest.FromBytes(blob)
if computedDigest != m.ConfigDescriptor.Digest { if computedDigest != m.m.ConfigDescriptor.Digest {
return nil, errors.Errorf("Download config.json digest %s does not match expected %s", computedDigest, m.ConfigDescriptor.Digest) return nil, errors.Errorf("Download config.json digest %s does not match expected %s", computedDigest, m.m.ConfigDescriptor.Digest)
} }
m.configBlob = blob m.configBlob = blob
} }
@ -130,15 +120,7 @@ func (m *manifestSchema2) ConfigBlob() ([]byte, error) {
// The Digest field is guaranteed to be provided; Size may be -1. // The Digest field is guaranteed to be provided; Size may be -1.
// WARNING: The list may contain duplicates, and they are semantically relevant. // WARNING: The list may contain duplicates, and they are semantically relevant.
func (m *manifestSchema2) LayerInfos() []types.BlobInfo { func (m *manifestSchema2) LayerInfos() []types.BlobInfo {
blobs := []types.BlobInfo{} return m.m.LayerInfos()
for _, layer := range m.LayersDescriptors {
blobs = append(blobs, types.BlobInfo{
Digest: layer.Digest,
Size: layer.Size,
URLs: layer.URLs,
})
}
return blobs
} }
// EmbeddedDockerReferenceConflicts whether a Docker reference embedded in the manifest, if any, conflicts with destination ref. // EmbeddedDockerReferenceConflicts whether a Docker reference embedded in the manifest, if any, conflicts with destination ref.
@ -157,13 +139,16 @@ func (m *manifestSchema2) imageInspectInfo() (*types.ImageInspectInfo, error) {
if err := json.Unmarshal(config, v1); err != nil { if err := json.Unmarshal(config, v1); err != nil {
return nil, err return nil, err
} }
return &types.ImageInspectInfo{ i := &types.ImageInspectInfo{
DockerVersion: v1.DockerVersion, DockerVersion: v1.DockerVersion,
Created: v1.Created, Created: v1.Created,
Labels: v1.Config.Labels,
Architecture: v1.Architecture, Architecture: v1.Architecture,
Os: v1.OS, Os: v1.OS,
}, nil }
if v1.Config != nil {
i.Labels = v1.Config.Labels
}
return i, nil
} }
// UpdatedImageNeedsLayerDiffIDs returns true iff UpdatedImage(options) needs InformationOnly.LayerDiffIDs. // UpdatedImageNeedsLayerDiffIDs returns true iff UpdatedImage(options) needs InformationOnly.LayerDiffIDs.
@ -176,17 +161,14 @@ func (m *manifestSchema2) UpdatedImageNeedsLayerDiffIDs(options types.ManifestUp
// UpdatedImage returns a types.Image modified according to options. // UpdatedImage returns a types.Image modified according to options.
// This does not change the state of the original Image object. // This does not change the state of the original Image object.
func (m *manifestSchema2) UpdatedImage(options types.ManifestUpdateOptions) (types.Image, error) { func (m *manifestSchema2) UpdatedImage(options types.ManifestUpdateOptions) (types.Image, error) {
copy := *m // NOTE: This is not a deep copy, it still shares slices etc. copy := manifestSchema2{ // NOTE: This is not a deep copy, it still shares slices etc.
src: m.src,
configBlob: m.configBlob,
m: manifest.Schema2Clone(m.m),
}
if options.LayerInfos != nil { if options.LayerInfos != nil {
if len(copy.LayersDescriptors) != len(options.LayerInfos) { if err := copy.m.UpdateLayerInfos(options.LayerInfos); err != nil {
return nil, errors.Errorf("Error preparing updated manifest: layer count changed from %d to %d", len(copy.LayersDescriptors), len(options.LayerInfos)) return nil, err
}
copy.LayersDescriptors = make([]descriptor, len(options.LayerInfos))
for i, info := range options.LayerInfos {
copy.LayersDescriptors[i].MediaType = m.LayersDescriptors[i].MediaType
copy.LayersDescriptors[i].Digest = info.Digest
copy.LayersDescriptors[i].Size = info.Size
copy.LayersDescriptors[i].URLs = info.URLs
} }
} }
// Ignore options.EmbeddedDockerReference: it may be set when converting from schema1 to schema2, but we really don't care. // Ignore options.EmbeddedDockerReference: it may be set when converting from schema1 to schema2, but we really don't care.
@ -204,6 +186,15 @@ func (m *manifestSchema2) UpdatedImage(options types.ManifestUpdateOptions) (typ
return memoryImageFromManifest(&copy), nil return memoryImageFromManifest(&copy), nil
} }
func oci1DescriptorFromSchema2Descriptor(d manifest.Schema2Descriptor) imgspecv1.Descriptor {
return imgspecv1.Descriptor{
MediaType: d.MediaType,
Size: d.Size,
Digest: d.Digest,
URLs: d.URLs,
}
}
func (m *manifestSchema2) convertToManifestOCI1() (types.Image, error) { func (m *manifestSchema2) convertToManifestOCI1() (types.Image, error) {
configOCI, err := m.OCIConfig() configOCI, err := m.OCIConfig()
if err != nil { if err != nil {
@ -214,18 +205,16 @@ func (m *manifestSchema2) convertToManifestOCI1() (types.Image, error) {
return nil, err return nil, err
} }
config := descriptorOCI1{ config := imgspecv1.Descriptor{
descriptor: descriptor{ MediaType: imgspecv1.MediaTypeImageConfig,
MediaType: imgspecv1.MediaTypeImageConfig, Size: int64(len(configOCIBytes)),
Size: int64(len(configOCIBytes)), Digest: digest.FromBytes(configOCIBytes),
Digest: digest.FromBytes(configOCIBytes),
},
} }
layers := make([]descriptorOCI1, len(m.LayersDescriptors)) layers := make([]imgspecv1.Descriptor, len(m.m.LayersDescriptors))
for idx := range layers { for idx := range layers {
layers[idx] = descriptorOCI1{descriptor: m.LayersDescriptors[idx]} layers[idx] = oci1DescriptorFromSchema2Descriptor(m.m.LayersDescriptors[idx])
if m.LayersDescriptors[idx].MediaType == manifest.DockerV2Schema2ForeignLayerMediaType { if m.m.LayersDescriptors[idx].MediaType == manifest.DockerV2Schema2ForeignLayerMediaType {
layers[idx].MediaType = imgspecv1.MediaTypeImageLayerNonDistributable layers[idx].MediaType = imgspecv1.MediaTypeImageLayerNonDistributable
} else { } else {
// we assume layers are gzip'ed because docker v2s2 only deals with // we assume layers are gzip'ed because docker v2s2 only deals with
@ -250,8 +239,8 @@ func (m *manifestSchema2) convertToManifestSchema1(dest types.ImageDestination)
} }
// Build fsLayers and History, discarding all configs. We will patch the top-level config in later. // Build fsLayers and History, discarding all configs. We will patch the top-level config in later.
fsLayers := make([]fsLayersSchema1, len(imageConfig.History)) fsLayers := make([]manifest.Schema1FSLayers, len(imageConfig.History))
history := make([]historySchema1, len(imageConfig.History)) history := make([]manifest.Schema1History, len(imageConfig.History))
nonemptyLayerIndex := 0 nonemptyLayerIndex := 0
var parentV1ID string // Set in the loop var parentV1ID string // Set in the loop
v1ID := "" v1ID := ""
@ -279,10 +268,10 @@ func (m *manifestSchema2) convertToManifestSchema1(dest types.ImageDestination)
} }
blobDigest = gzippedEmptyLayerDigest blobDigest = gzippedEmptyLayerDigest
} else { } else {
if nonemptyLayerIndex >= len(m.LayersDescriptors) { if nonemptyLayerIndex >= len(m.m.LayersDescriptors) {
return nil, errors.Errorf("Invalid image configuration, needs more than the %d distributed layers", len(m.LayersDescriptors)) return nil, errors.Errorf("Invalid image configuration, needs more than the %d distributed layers", len(m.m.LayersDescriptors))
} }
blobDigest = m.LayersDescriptors[nonemptyLayerIndex].Digest blobDigest = m.m.LayersDescriptors[nonemptyLayerIndex].Digest
nonemptyLayerIndex++ nonemptyLayerIndex++
} }
@ -293,7 +282,7 @@ func (m *manifestSchema2) convertToManifestSchema1(dest types.ImageDestination)
} }
v1ID = v v1ID = v
fakeImage := v1Compatibility{ fakeImage := manifest.Schema1V1Compatibility{
ID: v1ID, ID: v1ID,
Parent: parentV1ID, Parent: parentV1ID,
Comment: historyEntry.Comment, Comment: historyEntry.Comment,
@ -307,8 +296,8 @@ func (m *manifestSchema2) convertToManifestSchema1(dest types.ImageDestination)
return nil, errors.Errorf("Internal error: Error creating v1compatibility for %#v", fakeImage) return nil, errors.Errorf("Internal error: Error creating v1compatibility for %#v", fakeImage)
} }
fsLayers[v1Index] = fsLayersSchema1{BlobSum: blobDigest} fsLayers[v1Index] = manifest.Schema1FSLayers{BlobSum: blobDigest}
history[v1Index] = historySchema1{V1Compatibility: string(v1CompatibilityBytes)} history[v1Index] = manifest.Schema1History{V1Compatibility: string(v1CompatibilityBytes)}
// Note that parentV1ID of the top layer is preserved when exiting this loop // Note that parentV1ID of the top layer is preserved when exiting this loop
} }

View file

@ -1,6 +1,7 @@
package image package image
import ( import (
"fmt"
"time" "time"
"github.com/containers/image/docker/reference" "github.com/containers/image/docker/reference"
@ -88,11 +89,8 @@ type genericManifest interface {
} }
func manifestInstanceFromBlob(src types.ImageSource, manblob []byte, mt string) (genericManifest, error) { func manifestInstanceFromBlob(src types.ImageSource, manblob []byte, mt string) (genericManifest, error) {
switch mt { switch manifest.NormalizedMIMEType(mt) {
// "application/json" is a valid v2s1 value per https://github.com/docker/distribution/blob/master/docs/spec/manifest-v2-1.md . case manifest.DockerV2Schema1MediaType, manifest.DockerV2Schema1SignedMediaType:
// This works for now, when nothing else seems to return "application/json"; if that were not true, the mapping/detection might
// need to happen within the ImageSource.
case manifest.DockerV2Schema1MediaType, manifest.DockerV2Schema1SignedMediaType, "application/json":
return manifestSchema1FromManifest(manblob) return manifestSchema1FromManifest(manblob)
case imgspecv1.MediaTypeImageManifest: case imgspecv1.MediaTypeImageManifest:
return manifestOCI1FromManifest(src, manblob) return manifestOCI1FromManifest(src, manblob)
@ -100,17 +98,8 @@ func manifestInstanceFromBlob(src types.ImageSource, manblob []byte, mt string)
return manifestSchema2FromManifest(src, manblob) return manifestSchema2FromManifest(src, manblob)
case manifest.DockerV2ListMediaType: case manifest.DockerV2ListMediaType:
return manifestSchema2FromManifestList(src, manblob) return manifestSchema2FromManifestList(src, manblob)
default: default: // Note that this may not be reachable, manifest.NormalizedMIMEType has a default for unknown values.
// If it's not a recognized manifest media type, or we have failed determining the type, we'll try one last time return nil, fmt.Errorf("Unimplemented manifest MIME type %s", mt)
// to deserialize using v2s1 as per https://github.com/docker/distribution/blob/master/manifests.go#L108
// and https://github.com/docker/distribution/blob/master/manifest/schema1/manifest.go#L50
//
// Crane registries can also return "text/plain", or pretty much anything else depending on a file extension “recognized” in the tag.
// This makes no real sense, but it happens
// because requests for manifests are
// redirected to a content distribution
// network which is configured that way. See https://bugzilla.redhat.com/show_bug.cgi?id=1389442
return manifestSchema1FromManifest(manblob)
} }
} }

View file

@ -71,3 +71,10 @@ func (i *memoryImage) Inspect() (*types.ImageInspectInfo, error) {
func (i *memoryImage) IsMultiImage() bool { func (i *memoryImage) IsMultiImage() bool {
return false return false
} }
// UpdatedLayerInfos returns an updated set of layer blob information which may not match the manifest.
// The Digest field is guaranteed to be provided; Size may be -1.
// WARNING: The list may contain duplicates, and they are semantically relevant.
func (i *memoryImage) UpdatedLayerInfos() []types.BlobInfo {
return i.LayerInfos()
}

View file

@ -12,41 +12,34 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
type descriptorOCI1 struct {
descriptor
Annotations map[string]string `json:"annotations,omitempty"`
}
type manifestOCI1 struct { type manifestOCI1 struct {
src types.ImageSource // May be nil if configBlob is not nil src types.ImageSource // May be nil if configBlob is not nil
configBlob []byte // If set, corresponds to contents of ConfigDescriptor. configBlob []byte // If set, corresponds to contents of m.Config.
SchemaVersion int `json:"schemaVersion"` m *manifest.OCI1
ConfigDescriptor descriptorOCI1 `json:"config"`
LayersDescriptors []descriptorOCI1 `json:"layers"`
Annotations map[string]string `json:"annotations,omitempty"`
} }
func manifestOCI1FromManifest(src types.ImageSource, manifest []byte) (genericManifest, error) { func manifestOCI1FromManifest(src types.ImageSource, manifestBlob []byte) (genericManifest, error) {
oci := manifestOCI1{src: src} m, err := manifest.OCI1FromManifest(manifestBlob)
if err := json.Unmarshal(manifest, &oci); err != nil { if err != nil {
return nil, err return nil, err
} }
return &oci, nil return &manifestOCI1{
src: src,
m: m,
}, nil
} }
// manifestOCI1FromComponents builds a new manifestOCI1 from the supplied data: // manifestOCI1FromComponents builds a new manifestOCI1 from the supplied data:
func manifestOCI1FromComponents(config descriptorOCI1, src types.ImageSource, configBlob []byte, layers []descriptorOCI1) genericManifest { func manifestOCI1FromComponents(config imgspecv1.Descriptor, src types.ImageSource, configBlob []byte, layers []imgspecv1.Descriptor) genericManifest {
return &manifestOCI1{ return &manifestOCI1{
src: src, src: src,
configBlob: configBlob, configBlob: configBlob,
SchemaVersion: 2, m: manifest.OCI1FromComponents(config, layers),
ConfigDescriptor: config,
LayersDescriptors: layers,
} }
} }
func (m *manifestOCI1) serialize() ([]byte, error) { func (m *manifestOCI1) serialize() ([]byte, error) {
return json.Marshal(*m) return m.m.Serialize()
} }
func (m *manifestOCI1) manifestMIMEType() string { func (m *manifestOCI1) manifestMIMEType() string {
@ -56,7 +49,7 @@ func (m *manifestOCI1) manifestMIMEType() string {
// ConfigInfo returns a complete BlobInfo for the separate config object, or a BlobInfo{Digest:""} if there isn't a separate object. // ConfigInfo returns a complete BlobInfo for the separate config object, or a BlobInfo{Digest:""} if there isn't a separate object.
// Note that the config object may not exist in the underlying storage in the return value of UpdatedImage! Use ConfigBlob() below. // Note that the config object may not exist in the underlying storage in the return value of UpdatedImage! Use ConfigBlob() below.
func (m *manifestOCI1) ConfigInfo() types.BlobInfo { func (m *manifestOCI1) ConfigInfo() types.BlobInfo {
return types.BlobInfo{Digest: m.ConfigDescriptor.Digest, Size: m.ConfigDescriptor.Size, Annotations: m.ConfigDescriptor.Annotations} return m.m.ConfigInfo()
} }
// ConfigBlob returns the blob described by ConfigInfo, iff ConfigInfo().Digest != ""; nil otherwise. // ConfigBlob returns the blob described by ConfigInfo, iff ConfigInfo().Digest != ""; nil otherwise.
@ -67,9 +60,9 @@ func (m *manifestOCI1) ConfigBlob() ([]byte, error) {
return nil, errors.Errorf("Internal error: neither src nor configBlob set in manifestOCI1") return nil, errors.Errorf("Internal error: neither src nor configBlob set in manifestOCI1")
} }
stream, _, err := m.src.GetBlob(types.BlobInfo{ stream, _, err := m.src.GetBlob(types.BlobInfo{
Digest: m.ConfigDescriptor.Digest, Digest: m.m.Config.Digest,
Size: m.ConfigDescriptor.Size, Size: m.m.Config.Size,
URLs: m.ConfigDescriptor.URLs, URLs: m.m.Config.URLs,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@ -80,8 +73,8 @@ func (m *manifestOCI1) ConfigBlob() ([]byte, error) {
return nil, err return nil, err
} }
computedDigest := digest.FromBytes(blob) computedDigest := digest.FromBytes(blob)
if computedDigest != m.ConfigDescriptor.Digest { if computedDigest != m.m.Config.Digest {
return nil, errors.Errorf("Download config.json digest %s does not match expected %s", computedDigest, m.ConfigDescriptor.Digest) return nil, errors.Errorf("Download config.json digest %s does not match expected %s", computedDigest, m.m.Config.Digest)
} }
m.configBlob = blob m.configBlob = blob
} }
@ -107,11 +100,7 @@ func (m *manifestOCI1) OCIConfig() (*imgspecv1.Image, error) {
// The Digest field is guaranteed to be provided; Size may be -1. // The Digest field is guaranteed to be provided; Size may be -1.
// WARNING: The list may contain duplicates, and they are semantically relevant. // WARNING: The list may contain duplicates, and they are semantically relevant.
func (m *manifestOCI1) LayerInfos() []types.BlobInfo { func (m *manifestOCI1) LayerInfos() []types.BlobInfo {
blobs := []types.BlobInfo{} return m.m.LayerInfos()
for _, layer := range m.LayersDescriptors {
blobs = append(blobs, types.BlobInfo{Digest: layer.Digest, Size: layer.Size, Annotations: layer.Annotations, URLs: layer.URLs})
}
return blobs
} }
// EmbeddedDockerReferenceConflicts whether a Docker reference embedded in the manifest, if any, conflicts with destination ref. // EmbeddedDockerReferenceConflicts whether a Docker reference embedded in the manifest, if any, conflicts with destination ref.
@ -130,13 +119,16 @@ func (m *manifestOCI1) imageInspectInfo() (*types.ImageInspectInfo, error) {
if err := json.Unmarshal(config, v1); err != nil { if err := json.Unmarshal(config, v1); err != nil {
return nil, err return nil, err
} }
return &types.ImageInspectInfo{ i := &types.ImageInspectInfo{
DockerVersion: v1.DockerVersion, DockerVersion: v1.DockerVersion,
Created: v1.Created, Created: v1.Created,
Labels: v1.Config.Labels,
Architecture: v1.Architecture, Architecture: v1.Architecture,
Os: v1.OS, Os: v1.OS,
}, nil }
if v1.Config != nil {
i.Labels = v1.Config.Labels
}
return i, nil
} }
// UpdatedImageNeedsLayerDiffIDs returns true iff UpdatedImage(options) needs InformationOnly.LayerDiffIDs. // UpdatedImageNeedsLayerDiffIDs returns true iff UpdatedImage(options) needs InformationOnly.LayerDiffIDs.
@ -149,18 +141,14 @@ func (m *manifestOCI1) UpdatedImageNeedsLayerDiffIDs(options types.ManifestUpdat
// UpdatedImage returns a types.Image modified according to options. // UpdatedImage returns a types.Image modified according to options.
// This does not change the state of the original Image object. // This does not change the state of the original Image object.
func (m *manifestOCI1) UpdatedImage(options types.ManifestUpdateOptions) (types.Image, error) { func (m *manifestOCI1) UpdatedImage(options types.ManifestUpdateOptions) (types.Image, error) {
copy := *m // NOTE: This is not a deep copy, it still shares slices etc. copy := manifestOCI1{ // NOTE: This is not a deep copy, it still shares slices etc.
src: m.src,
configBlob: m.configBlob,
m: manifest.OCI1Clone(m.m),
}
if options.LayerInfos != nil { if options.LayerInfos != nil {
if len(copy.LayersDescriptors) != len(options.LayerInfos) { if err := copy.m.UpdateLayerInfos(options.LayerInfos); err != nil {
return nil, errors.Errorf("Error preparing updated manifest: layer count changed from %d to %d", len(copy.LayersDescriptors), len(options.LayerInfos)) return nil, err
}
copy.LayersDescriptors = make([]descriptorOCI1, len(options.LayerInfos))
for i, info := range options.LayerInfos {
copy.LayersDescriptors[i].MediaType = m.LayersDescriptors[i].MediaType
copy.LayersDescriptors[i].Digest = info.Digest
copy.LayersDescriptors[i].Size = info.Size
copy.LayersDescriptors[i].Annotations = info.Annotations
copy.LayersDescriptors[i].URLs = info.URLs
} }
} }
// Ignore options.EmbeddedDockerReference: it may be set when converting from schema1, but we really don't care. // Ignore options.EmbeddedDockerReference: it may be set when converting from schema1, but we really don't care.
@ -176,17 +164,26 @@ func (m *manifestOCI1) UpdatedImage(options types.ManifestUpdateOptions) (types.
return memoryImageFromManifest(&copy), nil return memoryImageFromManifest(&copy), nil
} }
func schema2DescriptorFromOCI1Descriptor(d imgspecv1.Descriptor) manifest.Schema2Descriptor {
return manifest.Schema2Descriptor{
MediaType: d.MediaType,
Size: d.Size,
Digest: d.Digest,
URLs: d.URLs,
}
}
func (m *manifestOCI1) convertToManifestSchema2() (types.Image, error) { func (m *manifestOCI1) convertToManifestSchema2() (types.Image, error) {
// Create a copy of the descriptor. // Create a copy of the descriptor.
config := m.ConfigDescriptor.descriptor config := schema2DescriptorFromOCI1Descriptor(m.m.Config)
// The only difference between OCI and DockerSchema2 is the mediatypes. The // The only difference between OCI and DockerSchema2 is the mediatypes. The
// media type of the manifest is handled by manifestSchema2FromComponents. // media type of the manifest is handled by manifestSchema2FromComponents.
config.MediaType = manifest.DockerV2Schema2ConfigMediaType config.MediaType = manifest.DockerV2Schema2ConfigMediaType
layers := make([]descriptor, len(m.LayersDescriptors)) layers := make([]manifest.Schema2Descriptor, len(m.m.Layers))
for idx := range layers { for idx := range layers {
layers[idx] = m.LayersDescriptors[idx].descriptor layers[idx] = schema2DescriptorFromOCI1Descriptor(m.m.Layers[idx])
layers[idx].MediaType = manifest.DockerV2Schema2LayerMediaType layers[idx].MediaType = manifest.DockerV2Schema2LayerMediaType
} }

View file

@ -88,3 +88,7 @@ func (i *sourcedImage) Inspect() (*types.ImageInspectInfo, error) {
func (i *sourcedImage) IsMultiImage() bool { func (i *sourcedImage) IsMultiImage() bool {
return i.manifestMIMEType == manifest.DockerV2ListMediaType return i.manifestMIMEType == manifest.DockerV2ListMediaType
} }
func (i *sourcedImage) UpdatedLayerInfos() []types.BlobInfo {
return i.UnparsedImage.UpdatedLayerInfos()
}

View file

@ -83,3 +83,10 @@ func (i *UnparsedImage) Signatures(ctx context.Context) ([][]byte, error) {
} }
return i.cachedSignatures, nil return i.cachedSignatures, nil
} }
// UpdatedLayerInfos returns an updated set of layer blob information which may not match the manifest.
// The Digest field is guaranteed to be provided; Size may be -1.
// WARNING: The list may contain duplicates, and they are semantically relevant.
func (i *UnparsedImage) UpdatedLayerInfos() []types.BlobInfo {
return i.src.UpdatedLayerInfos()
}

View file

@ -0,0 +1,212 @@
package manifest
import (
"encoding/json"
"regexp"
"time"
"github.com/containers/image/docker/reference"
"github.com/containers/image/types"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
)
// Schema1FSLayers is an entry of the "fsLayers" array in docker/distribution schema 1.
type Schema1FSLayers struct {
BlobSum digest.Digest `json:"blobSum"`
}
// Schema1History is an entry of the "history" array in docker/distribution schema 1.
type Schema1History struct {
V1Compatibility string `json:"v1Compatibility"`
}
// Schema1 is a manifest in docker/distribution schema 1.
type Schema1 struct {
Name string `json:"name"`
Tag string `json:"tag"`
Architecture string `json:"architecture"`
FSLayers []Schema1FSLayers `json:"fsLayers"`
History []Schema1History `json:"history"`
SchemaVersion int `json:"schemaVersion"`
}
// Schema1V1Compatibility is a v1Compatibility in docker/distribution schema 1.
type Schema1V1Compatibility struct {
ID string `json:"id"`
Parent string `json:"parent,omitempty"`
Comment string `json:"comment,omitempty"`
Created time.Time `json:"created"`
ContainerConfig struct {
Cmd []string
} `json:"container_config,omitempty"`
Author string `json:"author,omitempty"`
ThrowAway bool `json:"throwaway,omitempty"`
}
// Schema1FromManifest creates a Schema1 manifest instance from a manifest blob.
// (NOTE: The instance is not necessary a literal representation of the original blob,
// layers with duplicate IDs are eliminated.)
func Schema1FromManifest(manifest []byte) (*Schema1, error) {
s1 := Schema1{}
if err := json.Unmarshal(manifest, &s1); err != nil {
return nil, err
}
if s1.SchemaVersion != 1 {
return nil, errors.Errorf("unsupported schema version %d", s1.SchemaVersion)
}
if len(s1.FSLayers) != len(s1.History) {
return nil, errors.New("length of history not equal to number of layers")
}
if len(s1.FSLayers) == 0 {
return nil, errors.New("no FSLayers in manifest")
}
if err := s1.fixManifestLayers(); err != nil {
return nil, err
}
return &s1, nil
}
// Schema1FromComponents creates an Schema1 manifest instance from the supplied data.
func Schema1FromComponents(ref reference.Named, fsLayers []Schema1FSLayers, history []Schema1History, architecture string) *Schema1 {
var name, tag string
if ref != nil { // Well, what to do if it _is_ nil? Most consumers actually don't use these fields nowadays, so we might as well try not supplying them.
name = reference.Path(ref)
if tagged, ok := ref.(reference.NamedTagged); ok {
tag = tagged.Tag()
}
}
return &Schema1{
Name: name,
Tag: tag,
Architecture: architecture,
FSLayers: fsLayers,
History: history,
SchemaVersion: 1,
}
}
// Schema1Clone creates a copy of the supplied Schema1 manifest.
func Schema1Clone(src *Schema1) *Schema1 {
copy := *src
return &copy
}
// ConfigInfo returns a complete BlobInfo for the separate config object, or a BlobInfo{Digest:""} if there isn't a separate object.
func (m *Schema1) ConfigInfo() types.BlobInfo {
return types.BlobInfo{}
}
// LayerInfos returns a list of BlobInfos of layers referenced by this image, in order (the root layer first, and then successive layered layers).
// The Digest field is guaranteed to be provided; Size may be -1.
// WARNING: The list may contain duplicates, and they are semantically relevant.
func (m *Schema1) LayerInfos() []types.BlobInfo {
layers := make([]types.BlobInfo, len(m.FSLayers))
for i, layer := range m.FSLayers { // NOTE: This includes empty layers (where m.History.V1Compatibility->ThrowAway)
layers[(len(m.FSLayers)-1)-i] = types.BlobInfo{Digest: layer.BlobSum, Size: -1}
}
return layers
}
// UpdateLayerInfos replaces the original layers with the specified BlobInfos (size+digest+urls), in order (the root layer first, and then successive layered layers)
func (m *Schema1) UpdateLayerInfos(layerInfos []types.BlobInfo) error {
// Our LayerInfos includes empty layers (where m.History.V1Compatibility->ThrowAway), so expect them to be included here as well.
if len(m.FSLayers) != len(layerInfos) {
return errors.Errorf("Error preparing updated manifest: layer count changed from %d to %d", len(m.FSLayers), len(layerInfos))
}
for i, info := range layerInfos {
// (docker push) sets up m.History.V1Compatibility->{Id,Parent} based on values of info.Digest,
// but (docker pull) ignores them in favor of computing DiffIDs from uncompressed data, except verifying the child->parent links and uniqueness.
// So, we don't bother recomputing the IDs in m.History.V1Compatibility.
m.FSLayers[(len(layerInfos)-1)-i].BlobSum = info.Digest
}
return nil
}
// Serialize returns the manifest in a blob format.
// NOTE: Serialize() does not in general reproduce the original blob if this object was loaded from one, even if no modifications were made!
func (m *Schema1) Serialize() ([]byte, error) {
// docker/distribution requires a signature even if the incoming data uses the nominally unsigned DockerV2Schema1MediaType.
unsigned, err := json.Marshal(*m)
if err != nil {
return nil, err
}
return AddDummyV2S1Signature(unsigned)
}
// fixManifestLayers, after validating the supplied manifest
// (to use correctly-formatted IDs, and to not have non-consecutive ID collisions in m.History),
// modifies manifest to only have one entry for each layer ID in m.History (deleting the older duplicates,
// both from m.History and m.FSLayers).
// Note that even after this succeeds, m.FSLayers may contain duplicate entries
// (for Dockerfile operations which change the configuration but not the filesystem).
func (m *Schema1) fixManifestLayers() error {
type imageV1 struct {
ID string
Parent string
}
// Per the specification, we can assume that len(m.FSLayers) == len(m.History)
imgs := make([]*imageV1, len(m.FSLayers))
for i := range m.FSLayers {
img := &imageV1{}
if err := json.Unmarshal([]byte(m.History[i].V1Compatibility), img); err != nil {
return err
}
imgs[i] = img
if err := validateV1ID(img.ID); err != nil {
return err
}
}
if imgs[len(imgs)-1].Parent != "" {
return errors.New("Invalid parent ID in the base layer of the image")
}
// check general duplicates to error instead of a deadlock
idmap := make(map[string]struct{})
var lastID string
for _, img := range imgs {
// skip IDs that appear after each other, we handle those later
if _, exists := idmap[img.ID]; img.ID != lastID && exists {
return errors.Errorf("ID %+v appears multiple times in manifest", img.ID)
}
lastID = img.ID
idmap[lastID] = struct{}{}
}
// backwards loop so that we keep the remaining indexes after removing items
for i := len(imgs) - 2; i >= 0; i-- {
if imgs[i].ID == imgs[i+1].ID { // repeated ID. remove and continue
m.FSLayers = append(m.FSLayers[:i], m.FSLayers[i+1:]...)
m.History = append(m.History[:i], m.History[i+1:]...)
} else if imgs[i].Parent != imgs[i+1].ID {
return errors.Errorf("Invalid parent ID. Expected %v, got %v", imgs[i+1].ID, imgs[i].Parent)
}
}
return nil
}
var validHex = regexp.MustCompile(`^([a-f0-9]{64})$`)
func validateV1ID(id string) error {
if ok := validHex.MatchString(id); !ok {
return errors.Errorf("image ID %q is invalid", id)
}
return nil
}
// Inspect returns various information for (skopeo inspect) parsed from the manifest and configuration.
func (m *Schema1) Inspect(_ func(types.BlobInfo) ([]byte, error)) (*types.ImageInspectInfo, error) {
s1 := &Schema1V1Compatibility{}
if err := json.Unmarshal([]byte(m.History[0].V1Compatibility), s1); err != nil {
return nil, err
}
return &types.ImageInspectInfo{
Tag: m.Tag,
Created: s1.Created,
DockerVersion: "",
Labels: make(map[string]string),
Architecture: "",
Os: "",
Layers: []string{},
}, nil
}

View file

@ -0,0 +1,241 @@
package manifest
import (
"encoding/json"
"time"
"github.com/containers/image/pkg/strslice"
"github.com/containers/image/types"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
)
// Schema2Descriptor is a “descriptor” in docker/distribution schema 2.
type Schema2Descriptor struct {
MediaType string `json:"mediaType"`
Size int64 `json:"size"`
Digest digest.Digest `json:"digest"`
URLs []string `json:"urls,omitempty"`
}
// Schema2 is a manifest in docker/distribution schema 2.
type Schema2 struct {
SchemaVersion int `json:"schemaVersion"`
MediaType string `json:"mediaType"`
ConfigDescriptor Schema2Descriptor `json:"config"`
LayersDescriptors []Schema2Descriptor `json:"layers"`
}
// Schema2Port is a Port, a string containing port number and protocol in the
// format "80/tcp", from docker/go-connections/nat.
type Schema2Port string
// Schema2PortSet is a PortSet, a collection of structs indexed by Port, from
// docker/go-connections/nat.
type Schema2PortSet map[Schema2Port]struct{}
// Schema2HealthConfig is a HealthConfig, which holds configuration settings
// for the HEALTHCHECK feature, from docker/docker/api/types/container.
type Schema2HealthConfig struct {
// Test is the test to perform to check that the container is healthy.
// An empty slice means to inherit the default.
// The options are:
// {} : inherit healthcheck
// {"NONE"} : disable healthcheck
// {"CMD", args...} : exec arguments directly
// {"CMD-SHELL", command} : run command with system's default shell
Test []string `json:",omitempty"`
// Zero means to inherit. Durations are expressed as integer nanoseconds.
Interval time.Duration `json:",omitempty"` // Interval is the time to wait between checks.
Timeout time.Duration `json:",omitempty"` // Timeout is the time to wait before considering the check to have hung.
// Retries is the number of consecutive failures needed to consider a container as unhealthy.
// Zero means inherit.
Retries int `json:",omitempty"`
}
// Schema2Config is a Config in docker/docker/api/types/container.
type Schema2Config struct {
Hostname string // Hostname
Domainname string // Domainname
User string // User that will run the command(s) inside the container, also support user:group
AttachStdin bool // Attach the standard input, makes possible user interaction
AttachStdout bool // Attach the standard output
AttachStderr bool // Attach the standard error
ExposedPorts Schema2PortSet `json:",omitempty"` // List of exposed ports
Tty bool // Attach standard streams to a tty, including stdin if it is not closed.
OpenStdin bool // Open stdin
StdinOnce bool // If true, close stdin after the 1 attached client disconnects.
Env []string // List of environment variable to set in the container
Cmd strslice.StrSlice // Command to run when starting the container
Healthcheck *Schema2HealthConfig `json:",omitempty"` // Healthcheck describes how to check the container is healthy
ArgsEscaped bool `json:",omitempty"` // True if command is already escaped (Windows specific)
Image string // Name of the image as it was passed by the operator (e.g. could be symbolic)
Volumes map[string]struct{} // List of volumes (mounts) used for the container
WorkingDir string // Current directory (PWD) in the command will be launched
Entrypoint strslice.StrSlice // Entrypoint to run when starting the container
NetworkDisabled bool `json:",omitempty"` // Is network disabled
MacAddress string `json:",omitempty"` // Mac Address of the container
OnBuild []string // ONBUILD metadata that were defined on the image Dockerfile
Labels map[string]string // List of labels set to this container
StopSignal string `json:",omitempty"` // Signal to stop a container
StopTimeout *int `json:",omitempty"` // Timeout (in seconds) to stop a container
Shell strslice.StrSlice `json:",omitempty"` // Shell for shell-form of RUN, CMD, ENTRYPOINT
}
// Schema2V1Image is a V1Image in docker/docker/image.
type Schema2V1Image struct {
// ID is a unique 64 character identifier of the image
ID string `json:"id,omitempty"`
// Parent is the ID of the parent image
Parent string `json:"parent,omitempty"`
// Comment is the commit message that was set when committing the image
Comment string `json:"comment,omitempty"`
// Created is the timestamp at which the image was created
Created time.Time `json:"created"`
// Container is the id of the container used to commit
Container string `json:"container,omitempty"`
// ContainerConfig is the configuration of the container that is committed into the image
ContainerConfig Schema2Config `json:"container_config,omitempty"`
// DockerVersion specifies the version of Docker that was used to build the image
DockerVersion string `json:"docker_version,omitempty"`
// Author is the name of the author that was specified when committing the image
Author string `json:"author,omitempty"`
// Config is the configuration of the container received from the client
Config *Schema2Config `json:"config,omitempty"`
// Architecture is the hardware that the image is build and runs on
Architecture string `json:"architecture,omitempty"`
// OS is the operating system used to build and run the image
OS string `json:"os,omitempty"`
// Size is the total size of the image including all layers it is composed of
Size int64 `json:",omitempty"`
}
// Schema2RootFS is a description of how to build up an image's root filesystem, from docker/docker/image.
type Schema2RootFS struct {
Type string `json:"type"`
DiffIDs []digest.Digest `json:"diff_ids,omitempty"`
}
// Schema2History stores build commands that were used to create an image, from docker/docker/image.
type Schema2History struct {
// Created is the timestamp at which the image was created
Created time.Time `json:"created"`
// Author is the name of the author that was specified when committing the image
Author string `json:"author,omitempty"`
// CreatedBy keeps the Dockerfile command used while building the image
CreatedBy string `json:"created_by,omitempty"`
// Comment is the commit message that was set when committing the image
Comment string `json:"comment,omitempty"`
// EmptyLayer is set to true if this history item did not generate a
// layer. Otherwise, the history item is associated with the next
// layer in the RootFS section.
EmptyLayer bool `json:"empty_layer,omitempty"`
}
// Schema2Image is an Image in docker/docker/image.
type Schema2Image struct {
Schema2V1Image
Parent digest.Digest `json:"parent,omitempty"`
RootFS *Schema2RootFS `json:"rootfs,omitempty"`
History []Schema2History `json:"history,omitempty"`
OSVersion string `json:"os.version,omitempty"`
OSFeatures []string `json:"os.features,omitempty"`
// rawJSON caches the immutable JSON associated with this image.
rawJSON []byte
// computedID is the ID computed from the hash of the image config.
// Not to be confused with the legacy V1 ID in V1Image.
computedID digest.Digest
}
// Schema2FromManifest creates a Schema2 manifest instance from a manifest blob.
func Schema2FromManifest(manifest []byte) (*Schema2, error) {
s2 := Schema2{}
if err := json.Unmarshal(manifest, &s2); err != nil {
return nil, err
}
return &s2, nil
}
// Schema2FromComponents creates an Schema2 manifest instance from the supplied data.
func Schema2FromComponents(config Schema2Descriptor, layers []Schema2Descriptor) *Schema2 {
return &Schema2{
SchemaVersion: 2,
MediaType: DockerV2Schema2MediaType,
ConfigDescriptor: config,
LayersDescriptors: layers,
}
}
// Schema2Clone creates a copy of the supplied Schema2 manifest.
func Schema2Clone(src *Schema2) *Schema2 {
copy := *src
return &copy
}
// ConfigInfo returns a complete BlobInfo for the separate config object, or a BlobInfo{Digest:""} if there isn't a separate object.
func (m *Schema2) ConfigInfo() types.BlobInfo {
return types.BlobInfo{Digest: m.ConfigDescriptor.Digest, Size: m.ConfigDescriptor.Size, MediaType: DockerV2Schema2ConfigMediaType}
}
// LayerInfos returns a list of BlobInfos of layers referenced by this image, in order (the root layer first, and then successive layered layers).
// The Digest field is guaranteed to be provided; Size may be -1.
// WARNING: The list may contain duplicates, and they are semantically relevant.
func (m *Schema2) LayerInfos() []types.BlobInfo {
blobs := []types.BlobInfo{}
for _, layer := range m.LayersDescriptors {
blobs = append(blobs, types.BlobInfo{
Digest: layer.Digest,
Size: layer.Size,
URLs: layer.URLs,
MediaType: layer.MediaType,
})
}
return blobs
}
// UpdateLayerInfos replaces the original layers with the specified BlobInfos (size+digest+urls), in order (the root layer first, and then successive layered layers)
func (m *Schema2) UpdateLayerInfos(layerInfos []types.BlobInfo) error {
if len(m.LayersDescriptors) != len(layerInfos) {
return errors.Errorf("Error preparing updated manifest: layer count changed from %d to %d", len(m.LayersDescriptors), len(layerInfos))
}
original := m.LayersDescriptors
m.LayersDescriptors = make([]Schema2Descriptor, len(layerInfos))
for i, info := range layerInfos {
m.LayersDescriptors[i].MediaType = original[i].MediaType
m.LayersDescriptors[i].Digest = info.Digest
m.LayersDescriptors[i].Size = info.Size
m.LayersDescriptors[i].URLs = info.URLs
}
return nil
}
// Serialize returns the manifest in a blob format.
// NOTE: Serialize() does not in general reproduce the original blob if this object was loaded from one, even if no modifications were made!
func (m *Schema2) Serialize() ([]byte, error) {
return json.Marshal(*m)
}
// Inspect returns various information for (skopeo inspect) parsed from the manifest and configuration.
func (m *Schema2) Inspect(configGetter func(types.BlobInfo) ([]byte, error)) (*types.ImageInspectInfo, error) {
config, err := configGetter(m.ConfigInfo())
if err != nil {
return nil, err
}
s2 := &Schema2Image{}
if err := json.Unmarshal(config, s2); err != nil {
return nil, err
}
return &types.ImageInspectInfo{
Tag: "",
Created: s2.Created,
DockerVersion: s2.DockerVersion,
Labels: s2.Config.Labels,
Architecture: s2.Architecture,
Os: s2.OS,
Layers: []string{},
}, nil
}

View file

@ -2,7 +2,9 @@ package manifest
import ( import (
"encoding/json" "encoding/json"
"fmt"
"github.com/containers/image/types"
"github.com/docker/libtrust" "github.com/docker/libtrust"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
@ -38,6 +40,33 @@ var DefaultRequestedManifestMIMETypes = []string{
// DockerV2ListMediaType, // FIXME: Restore this ASAP // DockerV2ListMediaType, // FIXME: Restore this ASAP
} }
// Manifest is an interface for parsing, modifying image manifests in isolation.
// Callers can either use this abstract interface without understanding the details of the formats,
// or instantiate a specific implementation (e.g. manifest.OCI1) and access the public members
// directly.
//
// See types.Image for functionality not limited to manifests, including format conversions and config parsing.
// This interface is similar to, but not strictly equivalent to, the equivalent methods in types.Image.
type Manifest interface {
// ConfigInfo returns a complete BlobInfo for the separate config object, or a BlobInfo{Digest:""} if there isn't a separate object.
ConfigInfo() types.BlobInfo
// LayerInfos returns a list of BlobInfos of layers referenced by this image, in order (the root layer first, and then successive layered layers).
// The Digest field is guaranteed to be provided; Size may be -1.
// WARNING: The list may contain duplicates, and they are semantically relevant.
LayerInfos() []types.BlobInfo
// UpdateLayerInfos replaces the original layers with the specified BlobInfos (size+digest+urls), in order (the root layer first, and then successive layered layers)
UpdateLayerInfos(layerInfos []types.BlobInfo) error
// Inspect returns various information for (skopeo inspect) parsed from the manifest,
// incorporating information from a configuration blob returned by configGetter, if
// the underlying image format is expected to include a configuration blob.
Inspect(configGetter func(types.BlobInfo) ([]byte, error)) (*types.ImageInspectInfo, error)
// Serialize returns the manifest in a blob format.
// NOTE: Serialize() does not in general reproduce the original blob if this object was loaded from one, even if no modifications were made!
Serialize() ([]byte, error)
}
// GuessMIMEType guesses MIME type of a manifest and returns it _if it is recognized_, or "" if unknown or unrecognized. // GuessMIMEType guesses MIME type of a manifest and returns it _if it is recognized_, or "" if unknown or unrecognized.
// FIXME? We should, in general, prefer out-of-band MIME type instead of blindly parsing the manifest, // FIXME? We should, in general, prefer out-of-band MIME type instead of blindly parsing the manifest,
// but we may not have such metadata available (e.g. when the manifest is a local file). // but we may not have such metadata available (e.g. when the manifest is a local file).
@ -142,3 +171,47 @@ func AddDummyV2S1Signature(manifest []byte) ([]byte, error) {
} }
return js.PrettySignature("signatures") return js.PrettySignature("signatures")
} }
// NormalizedMIMEType returns the effective MIME type of a manifest MIME type returned by a server,
// centralizing various workarounds.
func NormalizedMIMEType(input string) string {
switch input {
// "application/json" is a valid v2s1 value per https://github.com/docker/distribution/blob/master/docs/spec/manifest-v2-1.md .
// This works for now, when nothing else seems to return "application/json"; if that were not true, the mapping/detection might
// need to happen within the ImageSource.
case "application/json":
return DockerV2Schema1SignedMediaType
case DockerV2Schema1MediaType, DockerV2Schema1SignedMediaType,
imgspecv1.MediaTypeImageManifest,
DockerV2Schema2MediaType,
DockerV2ListMediaType:
return input
default:
// If it's not a recognized manifest media type, or we have failed determining the type, we'll try one last time
// to deserialize using v2s1 as per https://github.com/docker/distribution/blob/master/manifests.go#L108
// and https://github.com/docker/distribution/blob/master/manifest/schema1/manifest.go#L50
//
// Crane registries can also return "text/plain", or pretty much anything else depending on a file extension “recognized” in the tag.
// This makes no real sense, but it happens
// because requests for manifests are
// redirected to a content distribution
// network which is configured that way. See https://bugzilla.redhat.com/show_bug.cgi?id=1389442
return DockerV2Schema1SignedMediaType
}
}
// FromBlob returns a Manifest instance for the specified manifest blob and the corresponding MIME type
func FromBlob(manblob []byte, mt string) (Manifest, error) {
switch NormalizedMIMEType(mt) {
case DockerV2Schema1MediaType, DockerV2Schema1SignedMediaType:
return Schema1FromManifest(manblob)
case imgspecv1.MediaTypeImageManifest:
return OCI1FromManifest(manblob)
case DockerV2Schema2MediaType:
return Schema2FromManifest(manblob)
case DockerV2ListMediaType:
return nil, fmt.Errorf("Treating manifest lists as individual manifests is not implemented")
default: // Note that this may not be reachable, NormalizedMIMEType has a default for unknown values.
return nil, fmt.Errorf("Unimplemented manifest MIME type %s", mt)
}
}

108
vendor/github.com/containers/image/manifest/oci.go generated vendored Normal file
View file

@ -0,0 +1,108 @@
package manifest
import (
"encoding/json"
"time"
"github.com/containers/image/types"
"github.com/opencontainers/image-spec/specs-go"
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
// OCI1 is a manifest.Manifest implementation for OCI images.
// The underlying data from imgspecv1.Manifest is also available.
type OCI1 struct {
imgspecv1.Manifest
}
// OCI1FromManifest creates an OCI1 manifest instance from a manifest blob.
func OCI1FromManifest(manifest []byte) (*OCI1, error) {
oci1 := OCI1{}
if err := json.Unmarshal(manifest, &oci1); err != nil {
return nil, err
}
return &oci1, nil
}
// OCI1FromComponents creates an OCI1 manifest instance from the supplied data.
func OCI1FromComponents(config imgspecv1.Descriptor, layers []imgspecv1.Descriptor) *OCI1 {
return &OCI1{
imgspecv1.Manifest{
Versioned: specs.Versioned{SchemaVersion: 2},
Config: config,
Layers: layers,
},
}
}
// OCI1Clone creates a copy of the supplied OCI1 manifest.
func OCI1Clone(src *OCI1) *OCI1 {
return &OCI1{
Manifest: src.Manifest,
}
}
// ConfigInfo returns a complete BlobInfo for the separate config object, or a BlobInfo{Digest:""} if there isn't a separate object.
func (m *OCI1) ConfigInfo() types.BlobInfo {
return types.BlobInfo{Digest: m.Config.Digest, Size: m.Config.Size, Annotations: m.Config.Annotations, MediaType: imgspecv1.MediaTypeImageConfig}
}
// LayerInfos returns a list of BlobInfos of layers referenced by this image, in order (the root layer first, and then successive layered layers).
// The Digest field is guaranteed to be provided; Size may be -1.
// WARNING: The list may contain duplicates, and they are semantically relevant.
func (m *OCI1) LayerInfos() []types.BlobInfo {
blobs := []types.BlobInfo{}
for _, layer := range m.Layers {
blobs = append(blobs, types.BlobInfo{Digest: layer.Digest, Size: layer.Size, Annotations: layer.Annotations, URLs: layer.URLs, MediaType: layer.MediaType})
}
return blobs
}
// UpdateLayerInfos replaces the original layers with the specified BlobInfos (size+digest+urls), in order (the root layer first, and then successive layered layers)
func (m *OCI1) UpdateLayerInfos(layerInfos []types.BlobInfo) error {
if len(m.Layers) != len(layerInfos) {
return errors.Errorf("Error preparing updated manifest: layer count changed from %d to %d", len(m.Layers), len(layerInfos))
}
original := m.Layers
m.Layers = make([]imgspecv1.Descriptor, len(layerInfos))
for i, info := range layerInfos {
m.Layers[i].MediaType = original[i].MediaType
m.Layers[i].Digest = info.Digest
m.Layers[i].Size = info.Size
m.Layers[i].Annotations = info.Annotations
m.Layers[i].URLs = info.URLs
}
return nil
}
// Serialize returns the manifest in a blob format.
// NOTE: Serialize() does not in general reproduce the original blob if this object was loaded from one, even if no modifications were made!
func (m *OCI1) Serialize() ([]byte, error) {
return json.Marshal(*m)
}
// Inspect returns various information for (skopeo inspect) parsed from the manifest and configuration.
func (m *OCI1) Inspect(configGetter func(types.BlobInfo) ([]byte, error)) (*types.ImageInspectInfo, error) {
config, err := configGetter(m.ConfigInfo())
if err != nil {
return nil, err
}
v1 := &imgspecv1.Image{}
if err := json.Unmarshal(config, v1); err != nil {
return nil, err
}
created := time.Time{}
if v1.Created != nil {
created = *v1.Created
}
return &types.ImageInspectInfo{
Tag: "",
Created: created,
DockerVersion: "",
Labels: v1.Config.Labels,
Architecture: v1.Architecture,
Os: v1.OS,
Layers: []string{},
}, nil
}

View file

@ -86,3 +86,7 @@ func (s *ociArchiveImageSource) GetBlob(info types.BlobInfo) (io.ReadCloser, int
func (s *ociArchiveImageSource) GetSignatures(c context.Context) ([][]byte, error) { func (s *ociArchiveImageSource) GetSignatures(c context.Context) ([][]byte, error) {
return s.unpackedSrc.GetSignatures(c) return s.unpackedSrc.GetSignatures(c)
} }
func (s *ociArchiveImageSource) UpdatedLayerInfos() []types.BlobInfo {
return nil
}

View file

@ -133,6 +133,11 @@ func (s *ociImageSource) getExternalBlob(urls []string) (io.ReadCloser, int64, e
return nil, 0, errWrap return nil, 0, errWrap
} }
// UpdatedLayerInfos() returns updated layer info that should be used when reading, in preference to values in the manifest, if specified.
func (s *ociImageSource) UpdatedLayerInfos() []types.BlobInfo {
return nil
}
func getBlobSize(resp *http.Response) int64 { func getBlobSize(resp *http.Response) int64 {
size, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64) size, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
if err != nil { if err != nil {

View file

@ -242,6 +242,10 @@ func (s *openshiftImageSource) GetSignatures(ctx context.Context) ([][]byte, err
return sigs, nil return sigs, nil
} }
func (s *openshiftImageSource) UpdatedLayerInfos() []types.BlobInfo {
return nil
}
// ensureImageIsResolved sets up s.docker and s.imageStreamImageName // ensureImageIsResolved sets up s.docker and s.imageStreamImageName
func (s *openshiftImageSource) ensureImageIsResolved(ctx context.Context) error { func (s *openshiftImageSource) ensureImageIsResolved(ctx context.Context) error {
if s.docker != nil { if s.docker != nil {

File diff suppressed because it is too large Load diff

View file

@ -6,6 +6,7 @@ import (
"github.com/containers/image/docker/reference" "github.com/containers/image/docker/reference"
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/containers/storage" "github.com/containers/storage"
digest "github.com/opencontainers/go-digest"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -18,9 +19,11 @@ type storageReference struct {
reference string reference string
id string id string
name reference.Named name reference.Named
tag string
digest digest.Digest
} }
func newReference(transport storageTransport, reference, id string, name reference.Named) *storageReference { func newReference(transport storageTransport, reference, id string, name reference.Named, tag string, digest digest.Digest) *storageReference {
// We take a copy of the transport, which contains a pointer to the // We take a copy of the transport, which contains a pointer to the
// store that it used for resolving this reference, so that the // store that it used for resolving this reference, so that the
// transport that we'll return from Transport() won't be affected by // transport that we'll return from Transport() won't be affected by
@ -30,6 +33,8 @@ func newReference(transport storageTransport, reference, id string, name referen
reference: reference, reference: reference,
id: id, id: id,
name: name, name: name,
tag: tag,
digest: digest,
} }
} }
@ -76,8 +81,21 @@ func (s storageReference) Transport() types.ImageTransport {
} }
} }
// Return a name with a tag, if we have a name to base them on. // Return a name with a tag or digest, if we have either, else return it bare.
func (s storageReference) DockerReference() reference.Named { func (s storageReference) DockerReference() reference.Named {
if s.name == nil {
return nil
}
if s.tag != "" {
if namedTagged, err := reference.WithTag(s.name, s.tag); err == nil {
return namedTagged
}
}
if s.digest != "" {
if canonical, err := reference.WithDigest(s.name, s.digest); err == nil {
return canonical
}
}
return s.name return s.name
} }
@ -91,7 +109,7 @@ func (s storageReference) StringWithinTransport() string {
optionsList = ":" + strings.Join(options, ",") optionsList = ":" + strings.Join(options, ",")
} }
storeSpec := "[" + s.transport.store.GraphDriverName() + "@" + s.transport.store.GraphRoot() + "+" + s.transport.store.RunRoot() + optionsList + "]" storeSpec := "[" + s.transport.store.GraphDriverName() + "@" + s.transport.store.GraphRoot() + "+" + s.transport.store.RunRoot() + optionsList + "]"
if s.name == nil { if s.reference == "" {
return storeSpec + "@" + s.id return storeSpec + "@" + s.id
} }
if s.id == "" { if s.id == "" {
@ -120,11 +138,8 @@ func (s storageReference) PolicyConfigurationNamespaces() []string {
driverlessStoreSpec := "[" + s.transport.store.GraphRoot() + "]" driverlessStoreSpec := "[" + s.transport.store.GraphRoot() + "]"
namespaces := []string{} namespaces := []string{}
if s.name != nil { if s.name != nil {
if s.id != "" { name := reference.TrimNamed(s.name)
// The reference without the ID is also a valid namespace. components := strings.Split(name.String(), "/")
namespaces = append(namespaces, storeSpec+s.reference)
}
components := strings.Split(s.name.Name(), "/")
for len(components) > 0 { for len(components) > 0 {
namespaces = append(namespaces, storeSpec+strings.Join(components, "/")) namespaces = append(namespaces, storeSpec+strings.Join(components, "/"))
components = components[:len(components)-1] components = components[:len(components)-1]

View file

@ -11,11 +11,14 @@ import (
"github.com/containers/image/types" "github.com/containers/image/types"
"github.com/containers/storage" "github.com/containers/storage"
"github.com/containers/storage/pkg/idtools" "github.com/containers/storage/pkg/idtools"
"github.com/opencontainers/go-digest" digest "github.com/opencontainers/go-digest"
ddigest "github.com/opencontainers/go-digest"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
const (
minimumTruncatedIDLength = 3
)
func init() { func init() {
transports.Register(Transport) transports.Register(Transport)
} }
@ -101,60 +104,124 @@ func (s *storageTransport) DefaultGIDMap() []idtools.IDMap {
// relative to the given store, and returns it in a reference object. // relative to the given store, and returns it in a reference object.
func (s storageTransport) ParseStoreReference(store storage.Store, ref string) (*storageReference, error) { func (s storageTransport) ParseStoreReference(store storage.Store, ref string) (*storageReference, error) {
var name reference.Named var name reference.Named
var sum digest.Digest
var err error
if ref == "" { if ref == "" {
return nil, ErrInvalidReference return nil, errors.Wrapf(ErrInvalidReference, "%q is an empty reference")
} }
if ref[0] == '[' { if ref[0] == '[' {
// Ignore the store specifier. // Ignore the store specifier.
closeIndex := strings.IndexRune(ref, ']') closeIndex := strings.IndexRune(ref, ']')
if closeIndex < 1 { if closeIndex < 1 {
return nil, ErrInvalidReference return nil, errors.Wrapf(ErrInvalidReference, "store specifier in %q did not end", ref)
} }
ref = ref[closeIndex+1:] ref = ref[closeIndex+1:]
} }
refInfo := strings.SplitN(ref, "@", 2)
if len(refInfo) == 1 { // The last segment, if there's more than one, is either a digest from a reference, or an image ID.
// A name. split := strings.LastIndex(ref, "@")
name, err = reference.ParseNormalizedNamed(refInfo[0]) idOrDigest := ""
if err != nil { if split != -1 {
return nil, err // Peel off that last bit so that we can work on the rest.
idOrDigest = ref[split+1:]
if idOrDigest == "" {
return nil, errors.Wrapf(ErrInvalidReference, "%q does not look like a digest or image ID", idOrDigest)
} }
} else if len(refInfo) == 2 { ref = ref[:split]
// An ID, possibly preceded by a name.
if refInfo[0] != "" {
name, err = reference.ParseNormalizedNamed(refInfo[0])
if err != nil {
return nil, err
}
}
sum, err = digest.Parse(refInfo[1])
if err != nil || sum.Validate() != nil {
sum, err = digest.Parse("sha256:" + refInfo[1])
if err != nil || sum.Validate() != nil {
return nil, err
}
}
} else { // Coverage: len(refInfo) is always 1 or 2
// Anything else: store specified in a form we don't
// recognize.
return nil, ErrInvalidReference
} }
// The middle segment (now the last segment), if there is one, is a digest.
split = strings.LastIndex(ref, "@")
sum := digest.Digest("")
if split != -1 {
sum = digest.Digest(ref[split+1:])
if sum == "" {
return nil, errors.Wrapf(ErrInvalidReference, "%q does not look like an image digest", sum)
}
ref = ref[:split]
}
// If we have something that unambiguously should be a digest, validate it, and then the third part,
// if we have one, as an ID.
id := ""
if sum != "" {
if idSum, err := digest.Parse("sha256:" + idOrDigest); err != nil || idSum.Validate() != nil {
return nil, errors.Wrapf(ErrInvalidReference, "%q does not look like an image ID", idOrDigest)
}
if err := sum.Validate(); err != nil {
return nil, errors.Wrapf(ErrInvalidReference, "%q does not look like an image digest", sum)
}
id = idOrDigest
if img, err := store.Image(idOrDigest); err == nil && img != nil && len(id) >= minimumTruncatedIDLength {
// The ID is a truncated version of the ID of an image that's present in local storage,
// so we might as well use the expanded value.
id = img.ID
}
} else if idOrDigest != "" {
// There was no middle portion, so the final portion could be either a digest or an ID.
if idSum, err := digest.Parse("sha256:" + idOrDigest); err == nil && idSum.Validate() == nil {
// It's an ID.
id = idOrDigest
} else if idSum, err := digest.Parse(idOrDigest); err == nil && idSum.Validate() == nil {
// It's a digest.
sum = idSum
} else if img, err := store.Image(idOrDigest); err == nil && img != nil && len(idOrDigest) >= minimumTruncatedIDLength {
// It's a truncated version of the ID of an image that's present in local storage,
// and we may need the expanded value.
id = img.ID
} else {
return nil, errors.Wrapf(ErrInvalidReference, "%q does not look like a digest or image ID", idOrDigest)
}
}
// If we only had one portion, then _maybe_ it's a truncated image ID. Only check on that if it's
// at least of what we guess is a reasonable minimum length, because we don't want a really short value
// like "a" matching an image by ID prefix when the input was actually meant to specify an image name.
if len(ref) >= minimumTruncatedIDLength && sum == "" && id == "" {
if img, err := store.Image(idOrDigest); err == nil && img != nil {
// It's a truncated version of the ID of an image that's present in local storage;
// we need to expand it.
id = img.ID
ref = ""
}
}
// The initial portion is probably a name, possibly with a tag.
if ref != "" {
var err error
if name, err = reference.ParseNormalizedNamed(ref); err != nil {
return nil, errors.Wrapf(err, "error parsing named reference %q", ref)
}
}
if name == nil && sum == "" && id == "" {
return nil, errors.Errorf("error parsing reference")
}
// Construct a copy of the store spec.
optionsList := "" optionsList := ""
options := store.GraphOptions() options := store.GraphOptions()
if len(options) > 0 { if len(options) > 0 {
optionsList = ":" + strings.Join(options, ",") optionsList = ":" + strings.Join(options, ",")
} }
storeSpec := "[" + store.GraphDriverName() + "@" + store.GraphRoot() + "+" + store.RunRoot() + optionsList + "]" storeSpec := "[" + store.GraphDriverName() + "@" + store.GraphRoot() + "+" + store.RunRoot() + optionsList + "]"
id := ""
if sum.Validate() == nil { // Convert the name back into a reference string, if we got a name.
id = sum.Hex()
}
refname := "" refname := ""
tag := ""
if name != nil { if name != nil {
name = reference.TagNameOnly(name) if sum.Validate() == nil {
refname = verboseName(name) canonical, err := reference.WithDigest(name, sum)
if err != nil {
return nil, errors.Wrapf(err, "error mixing name %q with digest %q", name, sum)
}
refname = verboseName(canonical)
} else {
name = reference.TagNameOnly(name)
tagged, ok := name.(reference.Tagged)
if !ok {
return nil, errors.Errorf("error parsing possibly-tagless name %q", ref)
}
refname = verboseName(name)
tag = tagged.Tag()
}
} }
if refname == "" { if refname == "" {
logrus.Debugf("parsed reference into %q", storeSpec+"@"+id) logrus.Debugf("parsed reference into %q", storeSpec+"@"+id)
@ -163,7 +230,7 @@ func (s storageTransport) ParseStoreReference(store storage.Store, ref string) (
} else { } else {
logrus.Debugf("parsed reference into %q", storeSpec+refname+"@"+id) logrus.Debugf("parsed reference into %q", storeSpec+refname+"@"+id)
} }
return newReference(storageTransport{store: store, defaultUIDMap: s.defaultUIDMap, defaultGIDMap: s.defaultGIDMap}, refname, id, name), nil return newReference(storageTransport{store: store, defaultUIDMap: s.defaultUIDMap, defaultGIDMap: s.defaultGIDMap}, refname, id, name, tag, sum), nil
} }
func (s *storageTransport) GetStore() (storage.Store, error) { func (s *storageTransport) GetStore() (storage.Store, error) {
@ -182,11 +249,14 @@ func (s *storageTransport) GetStore() (storage.Store, error) {
return s.store, nil return s.store, nil
} }
// ParseReference takes a name and/or an ID ("_name_"/"@_id_"/"_name_@_id_"), // ParseReference takes a name and a tag or digest and/or ID
// ("_name_"/"@_id_"/"_name_:_tag_"/"_name_:_tag_@_id_"/"_name_@_digest_"/"_name_@_digest_@_id_"),
// possibly prefixed with a store specifier in the form "[_graphroot_]" or // possibly prefixed with a store specifier in the form "[_graphroot_]" or
// "[_driver_@_graphroot_]" or "[_driver_@_graphroot_+_runroot_]" or // "[_driver_@_graphroot_]" or "[_driver_@_graphroot_+_runroot_]" or
// "[_driver_@_graphroot_:_options_]" or "[_driver_@_graphroot_+_runroot_:_options_]", // "[_driver_@_graphroot_:_options_]" or "[_driver_@_graphroot_+_runroot_:_options_]",
// tries to figure out which it is, and returns it in a reference object. // tries to figure out which it is, and returns it in a reference object.
// If _id_ is the ID of an image that's present in local storage, it can be truncated, and
// even be specified as if it were a _name_, value.
func (s *storageTransport) ParseReference(reference string) (types.ImageReference, error) { func (s *storageTransport) ParseReference(reference string) (types.ImageReference, error) {
var store storage.Store var store storage.Store
// Check if there's a store location prefix. If there is, then it // Check if there's a store location prefix. If there is, then it
@ -335,7 +405,7 @@ func (s storageTransport) ValidatePolicyConfigurationScope(scope string) error {
if err != nil { if err != nil {
return err return err
} }
_, err = ddigest.Parse("sha256:" + scopeInfo[1]) _, err = digest.Parse("sha256:" + scopeInfo[1])
if err != nil { if err != nil {
return err return err
} }
@ -345,11 +415,28 @@ func (s storageTransport) ValidatePolicyConfigurationScope(scope string) error {
return nil return nil
} }
func verboseName(name reference.Named) string { func verboseName(r reference.Reference) string {
name = reference.TagNameOnly(name) if r == nil {
tag := "" return ""
if tagged, ok := name.(reference.NamedTagged); ok {
tag = ":" + tagged.Tag()
} }
return name.Name() + tag named, isNamed := r.(reference.Named)
digested, isDigested := r.(reference.Digested)
tagged, isTagged := r.(reference.Tagged)
name := ""
tag := ""
sum := ""
if isNamed {
name = (reference.TrimNamed(named)).String()
}
if isTagged {
if tagged.Tag() != "" {
tag = ":" + tagged.Tag()
}
}
if isDigested {
if digested.Digest().Validate() == nil {
sum = "@" + digested.Digest().String()
}
}
return name + tag + sum
} }

View file

@ -96,6 +96,7 @@ type BlobInfo struct {
Size int64 // -1 if unknown Size int64 // -1 if unknown
URLs []string URLs []string
Annotations map[string]string Annotations map[string]string
MediaType string
} }
// ImageSource is a service, possibly remote (= slow), to download components of a single image. // ImageSource is a service, possibly remote (= slow), to download components of a single image.
@ -118,10 +119,14 @@ type ImageSource interface {
// out of a manifest list. // out of a manifest list.
GetTargetManifest(digest digest.Digest) ([]byte, string, error) GetTargetManifest(digest digest.Digest) ([]byte, string, error)
// GetBlob returns a stream for the specified blob, and the blobs size (or -1 if unknown). // GetBlob returns a stream for the specified blob, and the blobs size (or -1 if unknown).
// The Digest field in BlobInfo is guaranteed to be provided; Size may be -1. // The Digest field in BlobInfo is guaranteed to be provided, Size may be -1 and MediaType may be optionally provided.
GetBlob(BlobInfo) (io.ReadCloser, int64, error) GetBlob(BlobInfo) (io.ReadCloser, int64, error)
// GetSignatures returns the image's signatures. It may use a remote (= slow) service. // GetSignatures returns the image's signatures. It may use a remote (= slow) service.
GetSignatures(context.Context) ([][]byte, error) GetSignatures(context.Context) ([][]byte, error)
// UpdatedLayerInfos returns either nil (meaning there are no updates), or updated values for the layer blobsums that are listed in the image's manifest.
// The Digest field is guaranteed to be provided; Size may be -1.
// WARNING: The list may contain duplicates, and they are semantically relevant.
UpdatedLayerInfos() []BlobInfo
} }
// ImageDestination is a service, possibly remote (= slow), to store components of a single image. // ImageDestination is a service, possibly remote (= slow), to store components of a single image.
@ -153,9 +158,10 @@ type ImageDestination interface {
AcceptsForeignLayerURLs() bool AcceptsForeignLayerURLs() bool
// MustMatchRuntimeOS returns true iff the destination can store only images targeted for the current runtime OS. False otherwise. // MustMatchRuntimeOS returns true iff the destination can store only images targeted for the current runtime OS. False otherwise.
MustMatchRuntimeOS() bool MustMatchRuntimeOS() bool
// PutBlob writes contents of stream and returns data representing the result (with all data filled in). // PutBlob writes contents of stream and returns data representing the result.
// inputInfo.Digest can be optionally provided if known; it is not mandatory for the implementation to verify it. // inputInfo.Digest can be optionally provided if known; it is not mandatory for the implementation to verify it.
// inputInfo.Size is the expected length of stream, if known. // inputInfo.Size is the expected length of stream, if known.
// inputInfo.MediaType describes the blob format, if 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 // 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. // 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. // 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.
@ -205,6 +211,10 @@ type UnparsedImage interface {
Manifest() ([]byte, string, error) Manifest() ([]byte, string, error)
// Signatures is like ImageSource.GetSignatures, but the result is cached; it is OK to call this however often you need. // Signatures is like ImageSource.GetSignatures, but the result is cached; it is OK to call this however often you need.
Signatures(ctx context.Context) ([][]byte, error) Signatures(ctx context.Context) ([][]byte, error)
// UpdatedLayerInfos returns either nil (meaning there are no updates), or updated values for the layer blobsums that are listed in the image's manifest.
// The Digest field is guaranteed to be provided, Size may be -1 and MediaType may be optionally provided.
// WARNING: The list may contain duplicates, and they are semantically relevant.
UpdatedLayerInfos() []BlobInfo
} }
// Image is the primary API for inspecting properties of images. // Image is the primary API for inspecting properties of images.
@ -215,7 +225,7 @@ type Image interface {
// ConfigInfo returns a complete BlobInfo for the separate config object, or a BlobInfo{Digest:""} if there isn't a separate object. // ConfigInfo returns a complete BlobInfo for the separate config object, or a BlobInfo{Digest:""} if there isn't a separate object.
// Note that the config object may not exist in the underlying storage in the return value of UpdatedImage! Use ConfigBlob() below. // Note that the config object may not exist in the underlying storage in the return value of UpdatedImage! Use ConfigBlob() below.
ConfigInfo() BlobInfo ConfigInfo() BlobInfo
// ConfigBlob returns the blob described by ConfigInfo, iff ConfigInfo().Digest != ""; nil otherwise. // ConfigBlob returns the blob described by ConfigInfo, if ConfigInfo().Digest != ""; nil otherwise.
// The result is cached; it is OK to call this however often you need. // The result is cached; it is OK to call this however often you need.
ConfigBlob() ([]byte, error) ConfigBlob() ([]byte, error)
// OCIConfig returns the image configuration as per OCI v1 image-spec. Information about // OCIConfig returns the image configuration as per OCI v1 image-spec. Information about
@ -223,7 +233,7 @@ type Image interface {
// old image manifests work (docker v2s1 especially). // old image manifests work (docker v2s1 especially).
OCIConfig() (*v1.Image, error) OCIConfig() (*v1.Image, error)
// LayerInfos returns a list of BlobInfos of layers referenced by this image, in order (the root layer first, and then successive layered layers). // LayerInfos returns a list of BlobInfos of layers referenced by this image, in order (the root layer first, and then successive layered layers).
// The Digest field is guaranteed to be provided; Size may be -1. // The Digest field is guaranteed to be provided, Size may be -1 and MediaType may be optionally provided.
// WARNING: The list may contain duplicates, and they are semantically relevant. // WARNING: The list may contain duplicates, and they are semantically relevant.
LayerInfos() []BlobInfo LayerInfos() []BlobInfo
// EmbeddedDockerReferenceConflicts whether a Docker reference embedded in the manifest, if any, conflicts with destination ref. // EmbeddedDockerReferenceConflicts whether a Docker reference embedded in the manifest, if any, conflicts with destination ref.
@ -249,7 +259,7 @@ type Image interface {
// ManifestUpdateOptions is a way to pass named optional arguments to Image.UpdatedManifest // ManifestUpdateOptions is a way to pass named optional arguments to Image.UpdatedManifest
type ManifestUpdateOptions struct { type ManifestUpdateOptions struct {
LayerInfos []BlobInfo // Complete BlobInfos (size+digest+urls) which should replace the originals, in order (the root layer first, and then successive layered layers) LayerInfos []BlobInfo // Complete BlobInfos (size+digest+urls+annotations) which should replace the originals, in order (the root layer first, and then successive layered layers). BlobInfos' MediaType fields are ignored.
EmbeddedDockerReference reference.Named EmbeddedDockerReference reference.Named
ManifestMIMEType string ManifestMIMEType string
// The values below are NOT requests to modify the image; they provide optional context which may or may not be used. // The values below are NOT requests to modify the image; they provide optional context which may or may not be used.

View file

@ -1,5 +1,5 @@
github.com/sirupsen/logrus v1.0.0 github.com/sirupsen/logrus v1.0.0
github.com/containers/storage 47536c89fcc545a87745e1a1573addc439409165 github.com/containers/storage 9e0c323a4b425557f8310ee8d125634acd39d8f5
github.com/davecgh/go-spew 346938d642f2ec3594ed81d874461961cd0faa76 github.com/davecgh/go-spew 346938d642f2ec3594ed81d874461961cd0faa76
github.com/docker/docker-credential-helpers d68f9aeca33f5fd3f08eeae5e9d175edf4e731d1 github.com/docker/docker-credential-helpers d68f9aeca33f5fd3f08eeae5e9d175edf4e731d1
github.com/docker/distribution 5f6282db7d65e6d72ad7c2cc66310724a57be716 github.com/docker/distribution 5f6282db7d65e6d72ad7c2cc66310724a57be716
@ -36,4 +36,5 @@ github.com/tchap/go-patricia v2.2.6
github.com/opencontainers/selinux ba1aefe8057f1d0cfb8e88d0ec1dc85925ef987d github.com/opencontainers/selinux ba1aefe8057f1d0cfb8e88d0ec1dc85925ef987d
github.com/BurntSushi/toml b26d9c308763d68093482582cea63d69be07a0f0 github.com/BurntSushi/toml b26d9c308763d68093482582cea63d69be07a0f0
github.com/ostreedev/ostree-go aeb02c6b6aa2889db3ef62f7855650755befd460 github.com/ostreedev/ostree-go aeb02c6b6aa2889db3ef62f7855650755befd460
github.com/gogo/protobuf/proto fcdc5011193ff531a548e9b0301828d5a5b97fd8 github.com/gogo/protobuf fcdc5011193ff531a548e9b0301828d5a5b97fd8
github.com/pquerna/ffjson master

View file

@ -650,10 +650,21 @@ func (d *Driver) Get(id, mountLabel string) (_ string, retErr error) {
func (d *Driver) Put(id string) error { func (d *Driver) Put(id string) error {
d.locker.Lock(id) d.locker.Lock(id)
defer d.locker.Unlock(id) defer d.locker.Unlock(id)
dir := d.dir(id)
if _, err := os.Stat(dir); err != nil {
return err
}
mountpoint := path.Join(d.dir(id), "merged") mountpoint := path.Join(d.dir(id), "merged")
if count := d.ctr.Decrement(mountpoint); count > 0 { if count := d.ctr.Decrement(mountpoint); count > 0 {
return nil return nil
} }
if _, err := ioutil.ReadFile(path.Join(dir, lowerFile)); err != nil {
// If no lower, we used the diff directory, so no work to do
if os.IsNotExist(err) {
return nil
}
return err
}
if err := unix.Unmount(mountpoint, unix.MNT_DETACH); err != nil { if err := unix.Unmount(mountpoint, unix.MNT_DETACH); err != nil {
logrus.Debugf("Failed to unmount %s overlay: %s - %v", id, mountpoint, err) logrus.Debugf("Failed to unmount %s overlay: %s - %v", id, mountpoint, err)
} }

View file

@ -1888,10 +1888,16 @@ func (s *store) layersByMappedDigest(m func(ROLayerStore, digest.Digest) ([]Laye
} }
storeLayers, err := m(store, d) storeLayers, err := m(store, d)
if err != nil { if err != nil {
return nil, err if errors.Cause(err) != ErrLayerUnknown {
return nil, err
}
continue
} }
layers = append(layers, storeLayers...) layers = append(layers, storeLayers...)
} }
if len(layers) == 0 {
return nil, ErrLayerUnknown
}
return layers, nil return layers, nil
} }