93d76247f2
When we push a schema1 manifest, we encode history information from the image JSON into v1Compatibility strings for the respective layers. The "author" field was not being set in these v1Compatibility strings, so if a parent layer had an author set, it would not be preserved after pushing through a schema1 manifest and repulling, so the image ID would change after the pull. This change preserves the authorship information for parent layers so that the image ID does not change. Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
283 lines
8.3 KiB
Go
283 lines
8.3 KiB
Go
package schema1
|
|
|
|
import (
|
|
"crypto/sha512"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/docker/distribution"
|
|
"github.com/docker/distribution/context"
|
|
"github.com/docker/distribution/reference"
|
|
"github.com/docker/libtrust"
|
|
|
|
"github.com/docker/distribution/digest"
|
|
"github.com/docker/distribution/manifest"
|
|
)
|
|
|
|
type diffID digest.Digest
|
|
|
|
// gzippedEmptyTar is a gzip-compressed version of an empty tar file
|
|
// (1024 NULL bytes)
|
|
var gzippedEmptyTar = []byte{
|
|
31, 139, 8, 0, 0, 9, 110, 136, 0, 255, 98, 24, 5, 163, 96, 20, 140, 88,
|
|
0, 8, 0, 0, 255, 255, 46, 175, 181, 239, 0, 4, 0, 0,
|
|
}
|
|
|
|
// digestSHA256GzippedEmptyTar is the canonical sha256 digest of
|
|
// gzippedEmptyTar
|
|
const digestSHA256GzippedEmptyTar = digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")
|
|
|
|
// configManifestBuilder is a type for constructing manifests from an image
|
|
// configuration and generic descriptors.
|
|
type configManifestBuilder struct {
|
|
// bs is a BlobService used to create empty layer tars in the
|
|
// blob store if necessary.
|
|
bs distribution.BlobService
|
|
// pk is the libtrust private key used to sign the final manifest.
|
|
pk libtrust.PrivateKey
|
|
// configJSON is configuration supplied when the ManifestBuilder was
|
|
// created.
|
|
configJSON []byte
|
|
// ref contains the name and optional tag provided to NewConfigManifestBuilder.
|
|
ref reference.Named
|
|
// descriptors is the set of descriptors referencing the layers.
|
|
descriptors []distribution.Descriptor
|
|
// emptyTarDigest is set to a valid digest if an empty tar has been
|
|
// put in the blob store; otherwise it is empty.
|
|
emptyTarDigest digest.Digest
|
|
}
|
|
|
|
// NewConfigManifestBuilder is used to build new manifests for the current
|
|
// schema version from an image configuration and a set of descriptors.
|
|
// It takes a BlobService so that it can add an empty tar to the blob store
|
|
// if the resulting manifest needs empty layers.
|
|
func NewConfigManifestBuilder(bs distribution.BlobService, pk libtrust.PrivateKey, ref reference.Named, configJSON []byte) distribution.ManifestBuilder {
|
|
return &configManifestBuilder{
|
|
bs: bs,
|
|
pk: pk,
|
|
configJSON: configJSON,
|
|
ref: ref,
|
|
}
|
|
}
|
|
|
|
// Build produces a final manifest from the given references
|
|
func (mb *configManifestBuilder) Build(ctx context.Context) (m distribution.Manifest, err error) {
|
|
type imageRootFS struct {
|
|
Type string `json:"type"`
|
|
DiffIDs []diffID `json:"diff_ids,omitempty"`
|
|
BaseLayer string `json:"base_layer,omitempty"`
|
|
}
|
|
|
|
type imageHistory struct {
|
|
Created time.Time `json:"created"`
|
|
Author string `json:"author,omitempty"`
|
|
CreatedBy string `json:"created_by,omitempty"`
|
|
Comment string `json:"comment,omitempty"`
|
|
EmptyLayer bool `json:"empty_layer,omitempty"`
|
|
}
|
|
|
|
type imageConfig struct {
|
|
RootFS *imageRootFS `json:"rootfs,omitempty"`
|
|
History []imageHistory `json:"history,omitempty"`
|
|
Architecture string `json:"architecture,omitempty"`
|
|
}
|
|
|
|
var img imageConfig
|
|
|
|
if err := json.Unmarshal(mb.configJSON, &img); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(img.History) == 0 {
|
|
return nil, errors.New("empty history when trying to create schema1 manifest")
|
|
}
|
|
|
|
if len(img.RootFS.DiffIDs) != len(mb.descriptors) {
|
|
return nil, errors.New("number of descriptors and number of layers in rootfs must match")
|
|
}
|
|
|
|
// Generate IDs for each layer
|
|
// For non-top-level layers, create fake V1Compatibility strings that
|
|
// fit the format and don't collide with anything else, but don't
|
|
// result in runnable images on their own.
|
|
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"`
|
|
}
|
|
|
|
fsLayerList := make([]FSLayer, len(img.History))
|
|
history := make([]History, len(img.History))
|
|
|
|
parent := ""
|
|
layerCounter := 0
|
|
for i, h := range img.History[:len(img.History)-1] {
|
|
var blobsum digest.Digest
|
|
if h.EmptyLayer {
|
|
if blobsum, err = mb.emptyTar(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
if len(img.RootFS.DiffIDs) <= layerCounter {
|
|
return nil, errors.New("too many non-empty layers in History section")
|
|
}
|
|
blobsum = mb.descriptors[layerCounter].Digest
|
|
layerCounter++
|
|
}
|
|
|
|
v1ID := digest.FromBytes([]byte(blobsum.Hex() + " " + parent)).Hex()
|
|
|
|
if i == 0 && img.RootFS.BaseLayer != "" {
|
|
// windows-only baselayer setup
|
|
baseID := sha512.Sum384([]byte(img.RootFS.BaseLayer))
|
|
parent = fmt.Sprintf("%x", baseID[:32])
|
|
}
|
|
|
|
v1Compatibility := v1Compatibility{
|
|
ID: v1ID,
|
|
Parent: parent,
|
|
Comment: h.Comment,
|
|
Created: h.Created,
|
|
Author: h.Author,
|
|
}
|
|
v1Compatibility.ContainerConfig.Cmd = []string{img.History[i].CreatedBy}
|
|
if h.EmptyLayer {
|
|
v1Compatibility.ThrowAway = true
|
|
}
|
|
jsonBytes, err := json.Marshal(&v1Compatibility)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
reversedIndex := len(img.History) - i - 1
|
|
history[reversedIndex].V1Compatibility = string(jsonBytes)
|
|
fsLayerList[reversedIndex] = FSLayer{BlobSum: blobsum}
|
|
|
|
parent = v1ID
|
|
}
|
|
|
|
latestHistory := img.History[len(img.History)-1]
|
|
|
|
var blobsum digest.Digest
|
|
if latestHistory.EmptyLayer {
|
|
if blobsum, err = mb.emptyTar(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
if len(img.RootFS.DiffIDs) <= layerCounter {
|
|
return nil, errors.New("too many non-empty layers in History section")
|
|
}
|
|
blobsum = mb.descriptors[layerCounter].Digest
|
|
}
|
|
|
|
fsLayerList[0] = FSLayer{BlobSum: blobsum}
|
|
dgst := digest.FromBytes([]byte(blobsum.Hex() + " " + parent + " " + string(mb.configJSON)))
|
|
|
|
// Top-level v1compatibility string should be a modified version of the
|
|
// image config.
|
|
transformedConfig, err := MakeV1ConfigFromConfig(mb.configJSON, dgst.Hex(), parent, latestHistory.EmptyLayer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
history[0].V1Compatibility = string(transformedConfig)
|
|
|
|
tag := ""
|
|
if tagged, isTagged := mb.ref.(reference.Tagged); isTagged {
|
|
tag = tagged.Tag()
|
|
}
|
|
|
|
mfst := Manifest{
|
|
Versioned: manifest.Versioned{
|
|
SchemaVersion: 1,
|
|
},
|
|
Name: mb.ref.Name(),
|
|
Tag: tag,
|
|
Architecture: img.Architecture,
|
|
FSLayers: fsLayerList,
|
|
History: history,
|
|
}
|
|
|
|
return Sign(&mfst, mb.pk)
|
|
}
|
|
|
|
// emptyTar pushes a compressed empty tar to the blob store if one doesn't
|
|
// already exist, and returns its blobsum.
|
|
func (mb *configManifestBuilder) emptyTar(ctx context.Context) (digest.Digest, error) {
|
|
if mb.emptyTarDigest != "" {
|
|
// Already put an empty tar
|
|
return mb.emptyTarDigest, nil
|
|
}
|
|
|
|
descriptor, err := mb.bs.Stat(ctx, digestSHA256GzippedEmptyTar)
|
|
switch err {
|
|
case nil:
|
|
mb.emptyTarDigest = descriptor.Digest
|
|
return descriptor.Digest, nil
|
|
case distribution.ErrBlobUnknown:
|
|
// nop
|
|
default:
|
|
return "", err
|
|
}
|
|
|
|
// Add gzipped empty tar to the blob store
|
|
descriptor, err = mb.bs.Put(ctx, "", gzippedEmptyTar)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
mb.emptyTarDigest = descriptor.Digest
|
|
|
|
return descriptor.Digest, nil
|
|
}
|
|
|
|
// AppendReference adds a reference to the current ManifestBuilder
|
|
func (mb *configManifestBuilder) AppendReference(d distribution.Describable) error {
|
|
// todo: verification here?
|
|
mb.descriptors = append(mb.descriptors, d.Descriptor())
|
|
return nil
|
|
}
|
|
|
|
// References returns the current references added to this builder
|
|
func (mb *configManifestBuilder) References() []distribution.Descriptor {
|
|
return mb.descriptors
|
|
}
|
|
|
|
// MakeV1ConfigFromConfig creates an legacy V1 image config from image config JSON
|
|
func MakeV1ConfigFromConfig(configJSON []byte, v1ID, parentV1ID string, throwaway bool) ([]byte, error) {
|
|
// Top-level v1compatibility string should be a modified version of the
|
|
// image config.
|
|
var configAsMap map[string]*json.RawMessage
|
|
if err := json.Unmarshal(configJSON, &configAsMap); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Delete fields that didn't exist in old manifest
|
|
delete(configAsMap, "rootfs")
|
|
delete(configAsMap, "history")
|
|
configAsMap["id"] = rawJSON(v1ID)
|
|
if parentV1ID != "" {
|
|
configAsMap["parent"] = rawJSON(parentV1ID)
|
|
}
|
|
if throwaway {
|
|
configAsMap["throwaway"] = rawJSON(true)
|
|
}
|
|
|
|
return json.Marshal(configAsMap)
|
|
}
|
|
|
|
func rawJSON(value interface{}) *json.RawMessage {
|
|
jsonval, err := json.Marshal(value)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return (*json.RawMessage)(&jsonval)
|
|
}
|