288 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			Go
		
	
	
			
		
		
	
	
			288 lines
		
	
	
		
			8.5 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/manifest"
 | 
						|
	"github.com/docker/distribution/reference"
 | 
						|
	"github.com/docker/libtrust"
 | 
						|
	"github.com/opencontainers/go-digest"
 | 
						|
)
 | 
						|
 | 
						|
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, fmt.Errorf("number of descriptors and number of layers in rootfs must match: len(%v) != len(%v)", img.RootFS.DiffIDs, mb.descriptors)
 | 
						|
	}
 | 
						|
 | 
						|
	// 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 {
 | 
						|
	descriptor := d.Descriptor()
 | 
						|
 | 
						|
	if err := descriptor.Digest.Validate(); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	mb.descriptors = append(mb.descriptors, 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)
 | 
						|
}
 |