forked from TrueCloudLab/distribution
1d33874951
Go 1.13 and up enforce import paths to be versioned if a project contains a go.mod and has released v2 or up. The current v2.x branches (and releases) do not yet have a go.mod, and therefore are still allowed to be imported with a non-versioned import path (go modules add a `+incompatible` annotation in that case). However, now that this project has a `go.mod` file, incompatible import paths will not be accepted by go modules, and attempting to use code from this repository will fail. This patch uses `v3` for the import-paths (not `v2`), because changing import paths itself is a breaking change, which means that the next release should increment the "major" version to comply with SemVer (as go modules dictate). Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
287 lines
8.5 KiB
Go
287 lines
8.5 KiB
Go
package schema1
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha512"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/distribution/distribution/v3"
|
|
"github.com/distribution/distribution/v3/manifest"
|
|
"github.com/distribution/distribution/v3/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)
|
|
}
|