diff --git a/internal/image/docker_schema1.go b/internal/image/docker_schema1.go index 3ef8e144d7..d81b7eb879 100644 --- a/internal/image/docker_schema1.go +++ b/internal/image/docker_schema1.go @@ -96,6 +96,10 @@ func (m *manifestSchema1) Inspect(context.Context) (*types.ImageInspectInfo, err return m.m.Inspect(nil) } +func (m *manifestSchema1) InspectV2(context.Context) (*types.ImageInspectInfoV2, error) { + return m.m.InspectV2(nil) +} + // UpdatedImageNeedsLayerDiffIDs returns true iff UpdatedImage(options) needs InformationOnly.LayerDiffIDs. // This is a horribly specific interface, but computing InformationOnly.LayerDiffIDs can be very expensive to compute // (most importantly it forces us to download the full layers even if they are already present at the destination). diff --git a/internal/image/docker_schema2.go b/internal/image/docker_schema2.go index 23a21999aa..baf72d5a46 100644 --- a/internal/image/docker_schema2.go +++ b/internal/image/docker_schema2.go @@ -148,6 +148,20 @@ func (m *manifestSchema2) Inspect(ctx context.Context) (*types.ImageInspectInfo, } return m.m.Inspect(getter) } +func (m *manifestSchema2) InspectV2(ctx context.Context) (*types.ImageInspectInfoV2, error) { + getter := func(info types.BlobInfo) ([]byte, error) { + if info.Digest != m.ConfigInfo().Digest { + // Shouldn't ever happen + return nil, errors.New("asked for a different config blob") + } + config, err := m.ConfigBlob(ctx) + if err != nil { + return nil, err + } + return config, nil + } + return m.m.InspectV2(getter) +} // UpdatedImageNeedsLayerDiffIDs returns true iff UpdatedImage(options) needs InformationOnly.LayerDiffIDs. // This is a horribly specific interface, but computing InformationOnly.LayerDiffIDs can be very expensive to compute @@ -326,7 +340,7 @@ func (m *manifestSchema2) convertToManifestSchema1(ctx context.Context, options ID: v1ID, Parent: parentV1ID, Comment: historyEntry.Comment, - Created: historyEntry.Created, + Created: *historyEntry.Created, Author: historyEntry.Author, ThrowAway: historyEntry.EmptyLayer, } diff --git a/internal/image/manifest.go b/internal/image/manifest.go index 75e472aa74..dcb447a192 100644 --- a/internal/image/manifest.go +++ b/internal/image/manifest.go @@ -36,6 +36,7 @@ type genericManifest interface { EmbeddedDockerReferenceConflicts(ref reference.Named) bool // Inspect returns various information for (skopeo inspect) parsed from the manifest and configuration. Inspect(context.Context) (*types.ImageInspectInfo, error) + InspectV2(context.Context) (*types.ImageInspectInfoV2, error) // UpdatedImageNeedsLayerDiffIDs returns true iff UpdatedImage(options) needs InformationOnly.LayerDiffIDs. // This is a horribly specific interface, but computing InformationOnly.LayerDiffIDs can be very expensive to compute // (most importantly it forces us to download the full layers even if they are already present at the destination). diff --git a/internal/image/oci.go b/internal/image/oci.go index 4b74de3e58..8dcfbc0e69 100644 --- a/internal/image/oci.go +++ b/internal/image/oci.go @@ -130,6 +130,21 @@ func (m *manifestOCI1) Inspect(ctx context.Context) (*types.ImageInspectInfo, er return m.m.Inspect(getter) } +func (m *manifestOCI1) InspectV2(ctx context.Context) (*types.ImageInspectInfoV2, error) { + getter := func(info types.BlobInfo) ([]byte, error) { + if info.Digest != m.ConfigInfo().Digest { + // Shouldn't ever happen + return nil, errors.New("asked for a different config blob") + } + config, err := m.ConfigBlob(ctx) + if err != nil { + return nil, err + } + return config, nil + } + return m.m.InspectV2(getter) +} + // UpdatedImageNeedsLayerDiffIDs returns true iff UpdatedImage(options) needs InformationOnly.LayerDiffIDs. // This is a horribly specific interface, but computing InformationOnly.LayerDiffIDs can be very expensive to compute // (most importantly it forces us to download the full layers even if they are already present at the destination). diff --git a/manifest/common.go b/manifest/common.go index 9cf7dd3a94..867790c6a7 100644 --- a/manifest/common.go +++ b/manifest/common.go @@ -109,6 +109,19 @@ func layerInfosToStrings(infos []LayerInfo) []string { return layers } +// imgInspectLayersFromLayerInfos converts a list of layer infos, presumably obtained from a Manifest.LayerInfos() +// method call, into a format suitable for inclusion in a types.ImageInspectInfo structure. +func imgInspectLayersFromLayerInfos(infos []LayerInfo) []types.Layer { + layers := make([]types.Layer, len(infos)) + for i, info := range infos { + layers[i].MIMEType = info.MediaType + layers[i].Digest = info.Digest + layers[i].Size = info.Size + layers[i].Annotations = info.Annotations + } + return layers +} + // compressionMIMETypeSet describes a set of MIME type “variants” that represent differently-compressed // versions of “the same kind of content”. // The map key is the return value of compressiontypes.Algorithm.Name(), or mtsUncompressed; diff --git a/manifest/docker_schema1.go b/manifest/docker_schema1.go index e1f1fb9d98..9fcf74e34f 100644 --- a/manifest/docker_schema1.go +++ b/manifest/docker_schema1.go @@ -236,6 +236,39 @@ func (m *Schema1) Inspect(_ func(types.BlobInfo) ([]byte, error)) (*types.ImageI return i, nil } +func (m *Schema1) InspectV2(_ func(types.BlobInfo) ([]byte, error)) (*types.ImageInspectInfoV2, error) { + s1 := &Schema2V1Image{} + if err := json.Unmarshal([]byte(m.History[0].V1Compatibility), s1); err != nil { + return nil, err + } + i := &types.ImageInspectInfoV2{ + Tag: m.Tag, + Created: &s1.Created, + DockerVersion: s1.DockerVersion, + Architecture: s1.Architecture, + Os: s1.OS, + Layers: imgInspectLayersFromLayerInfos(m.LayerInfos()), + History: nil, + Author: s1.Author, + Size: s1.Size, + } + if s1.Config != nil { + i.Config.Env = s1.Config.Env + i.Config.Labels = s1.Config.Labels + i.Config.User = s1.Config.User + i.Config.Volumes = s1.Config.Volumes + i.Config.Entrypoint = s1.Config.Entrypoint + for key, value := range s1.Config.ExposedPorts { + exposedPorts := make(map[string]struct{}) + exposedPorts[string(key)] = value + i.Config.ExposedPorts = exposedPorts + } + i.Config.StopSignal = s1.Config.StopSignal + i.Config.WorkingDir = s1.Config.WorkingDir + } + return i, nil +} + // ToSchema2Config builds a schema2-style configuration blob using the supplied diffIDs. func (m *Schema1) ToSchema2Config(diffIDs []digest.Digest) ([]byte, error) { // Convert the schema 1 compat info into a schema 2 config, constructing some of the fields diff --git a/manifest/docker_schema2.go b/manifest/docker_schema2.go index e79d0851f2..804624d4b8 100644 --- a/manifest/docker_schema2.go +++ b/manifest/docker_schema2.go @@ -9,6 +9,7 @@ import ( "github.com/containers/image/v5/pkg/strslice" "github.com/containers/image/v5/types" "github.com/opencontainers/go-digest" + imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" ) // Schema2Descriptor is a “descriptor” in docker/distribution schema 2. @@ -151,11 +152,11 @@ type Schema2History struct { // 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"` + Parent digest.Digest `json:"parent,omitempty"` + RootFS *Schema2RootFS `json:"rootfs,omitempty"` + History []imgspecv1.History `json:"history,omitempty"` + OSVersion string `json:"os.version,omitempty"` + OSFeatures []string `json:"os.features,omitempty"` } // Schema2FromManifest creates a Schema2 manifest instance from a manifest blob. @@ -287,6 +288,45 @@ func (m *Schema2) Inspect(configGetter func(types.BlobInfo) ([]byte, error)) (*t return i, nil } +func (m *Schema2) InspectV2(configGetter func(types.BlobInfo) ([]byte, error)) (*types.ImageInspectInfoV2, 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 + } + + i := &types.ImageInspectInfoV2{ + Tag: "", + Created: &s2.Created, + DockerVersion: s2.DockerVersion, + Architecture: s2.Architecture, + Variant: s2.Variant, + Os: s2.OS, + Layers: imgInspectLayersFromLayerInfos(m.LayerInfos()), + History: s2.History, + Author: s2.Author, + Size: s2.Size, + } + if s2.Config != nil { + i.Config.Env = s2.Config.Env + i.Config.Labels = s2.Config.Labels + i.Config.User = s2.Config.User + i.Config.Volumes = s2.Config.Volumes + i.Config.Entrypoint = s2.Config.Entrypoint + for key, value := range s2.Config.ExposedPorts { + exposedPorts := make(map[string]struct{}) + exposedPorts[string(key)] = value + i.Config.ExposedPorts = exposedPorts + } + i.Config.StopSignal = s2.Config.StopSignal + i.Config.WorkingDir = s2.Config.WorkingDir + } + return i, nil +} + // ImageID computes an ID which can uniquely identify this image by its contents. func (m *Schema2) ImageID([]digest.Digest) (string, error) { if err := m.ConfigDescriptor.Digest.Validate(); err != nil { diff --git a/manifest/manifest.go b/manifest/manifest.go index 53fc866a78..b0b6d2bd96 100644 --- a/manifest/manifest.go +++ b/manifest/manifest.go @@ -87,7 +87,7 @@ type Manifest interface { // 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) - + InspectV2(configGetter func(types.BlobInfo) ([]byte, error)) (*types.ImageInspectInfoV2, 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) diff --git a/manifest/oci.go b/manifest/oci.go index fc325009ce..faeddb29df 100644 --- a/manifest/oci.go +++ b/manifest/oci.go @@ -225,6 +225,44 @@ func (m *OCI1) Inspect(configGetter func(types.BlobInfo) ([]byte, error)) (*type return i, nil } +// Inspect returns various information for (skopeo inspect) parsed from the manifest and configuration. +func (m *OCI1) InspectV2(configGetter func(types.BlobInfo) ([]byte, error)) (*types.ImageInspectInfoV2, error) { + if m.Config.MediaType != imgspecv1.MediaTypeImageConfig { + // We could return at least the layers, but that’s already available in a better format via types.Image.LayerInfos. + // Most software calling this without human intervention is going to expect the values to be realistic and relevant, + // and is probably better served by failing; we can always re-visit that later if we fail now, but + // if we started returning some data for OCI artifacts now, we couldn’t start failing in this function later. + return nil, internalManifest.NewNonImageArtifactError(m.Config.MediaType) + } + + 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 + } + d1 := &Schema2V1Image{} + if err := json.Unmarshal(config, d1); err != nil { + return nil, err + } + + i := &types.ImageInspectInfoV2{ + Tag: "", + Created: v1.Created, + DockerVersion: d1.DockerVersion, + Architecture: v1.Architecture, + Os: v1.OS, + Layers: imgInspectLayersFromLayerInfos(m.LayerInfos()), + History: v1.History, + Author: v1.Author, + Size: d1.Size, + Config: v1.Config, + } + return i, nil +} + // ImageID computes an ID which can uniquely identify this image by its contents. func (m *OCI1) ImageID([]digest.Digest) (string, error) { // The way m.Config.Digest “uniquely identifies” an image is diff --git a/types/types.go b/types/types.go index dcff8caf76..2d05bb88cc 100644 --- a/types/types.go +++ b/types/types.go @@ -402,6 +402,7 @@ type Image interface { EmbeddedDockerReferenceConflicts(ref reference.Named) bool // Inspect returns various information for (skopeo inspect) parsed from the manifest and configuration. Inspect(context.Context) (*ImageInspectInfo, error) + InspectV2(context.Context) (*ImageInspectInfoV2, error) // UpdatedImageNeedsLayerDiffIDs returns true iff UpdatedImage(options) needs InformationOnly.LayerDiffIDs. // This is a horribly specific interface, but computing InformationOnly.LayerDiffIDs can be very expensive to compute // (most importantly it forces us to download the full layers even if they are already present at the destination). @@ -468,6 +469,28 @@ type ImageInspectInfo struct { Env []string } +type Layer struct { + MIMEType string + Digest digest.Digest + Size int64 + Annotations map[string]string +} + +type ImageInspectInfoV2 struct { + Tag string + Created *time.Time + DockerVersion string + Labels map[string]string + Architecture string + Variant string + Os string + Layers []Layer + History []v1.History + Author string + Size int64 + Config v1.ImageConfig +} + // DockerAuthConfig contains authorization information for connecting to a registry. // the value of Username and Password can be empty for accessing the registry anonymously type DockerAuthConfig struct {