2ff77c00ba
Add schema2 manifest implementation. Add a schema2 builder that creates a schema2 manifest from descriptors and a configuration. It will add the configuration to the blob store if necessary. Rename the original schema1 manifest builder to ReferenceBuilder, and create a ConfigBuilder variant that can build a schema1 manifest from an image configuration and set of descriptors. This will be used to translate schema2 manifests to the schema1 format for backward compatibliity, by adding the descriptors from the existing schema2 manifest to the schema1 builder. It will also be used by engine-side push code to create schema1 manifests from the new-style image configration, when necessary to push a schema1 manifest. Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
278 lines
8.2 KiB
Go
278 lines
8.2 KiB
Go
package schema1
|
|
|
|
import (
|
|
"crypto/sha512"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/docker/distribution"
|
|
"github.com/docker/distribution/context"
|
|
"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
|
|
// name is the name provided to NewConfigManifestBuilder
|
|
name string
|
|
// tag is the tag provided to NewConfigManifestBuilder
|
|
tag string
|
|
// 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, name, tag string, configJSON []byte) distribution.ManifestBuilder {
|
|
return &configManifestBuilder{
|
|
bs: bs,
|
|
pk: pk,
|
|
configJSON: configJSON,
|
|
name: name,
|
|
tag: tag,
|
|
}
|
|
}
|
|
|
|
// 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"`
|
|
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,
|
|
}
|
|
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)
|
|
|
|
mfst := Manifest{
|
|
Versioned: manifest.Versioned{
|
|
SchemaVersion: 1,
|
|
},
|
|
Name: mb.name,
|
|
Tag: mb.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)
|
|
}
|