github.com/npaton/distribution@v2.3.1-rc.0+incompatible/manifest/schema1/config_builder.go (about)

     1  package schema1
     2  
     3  import (
     4  	"crypto/sha512"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"time"
     9  
    10  	"github.com/docker/distribution"
    11  	"github.com/docker/distribution/context"
    12  	"github.com/docker/distribution/reference"
    13  	"github.com/docker/libtrust"
    14  
    15  	"github.com/docker/distribution/digest"
    16  	"github.com/docker/distribution/manifest"
    17  )
    18  
    19  type diffID digest.Digest
    20  
    21  // gzippedEmptyTar is a gzip-compressed version of an empty tar file
    22  // (1024 NULL bytes)
    23  var gzippedEmptyTar = []byte{
    24  	31, 139, 8, 0, 0, 9, 110, 136, 0, 255, 98, 24, 5, 163, 96, 20, 140, 88,
    25  	0, 8, 0, 0, 255, 255, 46, 175, 181, 239, 0, 4, 0, 0,
    26  }
    27  
    28  // digestSHA256GzippedEmptyTar is the canonical sha256 digest of
    29  // gzippedEmptyTar
    30  const digestSHA256GzippedEmptyTar = digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")
    31  
    32  // configManifestBuilder is a type for constructing manifests from an image
    33  // configuration and generic descriptors.
    34  type configManifestBuilder struct {
    35  	// bs is a BlobService used to create empty layer tars in the
    36  	// blob store if necessary.
    37  	bs distribution.BlobService
    38  	// pk is the libtrust private key used to sign the final manifest.
    39  	pk libtrust.PrivateKey
    40  	// configJSON is configuration supplied when the ManifestBuilder was
    41  	// created.
    42  	configJSON []byte
    43  	// ref contains the name and optional tag provided to NewConfigManifestBuilder.
    44  	ref reference.Named
    45  	// descriptors is the set of descriptors referencing the layers.
    46  	descriptors []distribution.Descriptor
    47  	// emptyTarDigest is set to a valid digest if an empty tar has been
    48  	// put in the blob store; otherwise it is empty.
    49  	emptyTarDigest digest.Digest
    50  }
    51  
    52  // NewConfigManifestBuilder is used to build new manifests for the current
    53  // schema version from an image configuration and a set of descriptors.
    54  // It takes a BlobService so that it can add an empty tar to the blob store
    55  // if the resulting manifest needs empty layers.
    56  func NewConfigManifestBuilder(bs distribution.BlobService, pk libtrust.PrivateKey, ref reference.Named, configJSON []byte) distribution.ManifestBuilder {
    57  	return &configManifestBuilder{
    58  		bs:         bs,
    59  		pk:         pk,
    60  		configJSON: configJSON,
    61  		ref:        ref,
    62  	}
    63  }
    64  
    65  // Build produces a final manifest from the given references
    66  func (mb *configManifestBuilder) Build(ctx context.Context) (m distribution.Manifest, err error) {
    67  	type imageRootFS struct {
    68  		Type      string   `json:"type"`
    69  		DiffIDs   []diffID `json:"diff_ids,omitempty"`
    70  		BaseLayer string   `json:"base_layer,omitempty"`
    71  	}
    72  
    73  	type imageHistory struct {
    74  		Created    time.Time `json:"created"`
    75  		Author     string    `json:"author,omitempty"`
    76  		CreatedBy  string    `json:"created_by,omitempty"`
    77  		Comment    string    `json:"comment,omitempty"`
    78  		EmptyLayer bool      `json:"empty_layer,omitempty"`
    79  	}
    80  
    81  	type imageConfig struct {
    82  		RootFS       *imageRootFS   `json:"rootfs,omitempty"`
    83  		History      []imageHistory `json:"history,omitempty"`
    84  		Architecture string         `json:"architecture,omitempty"`
    85  	}
    86  
    87  	var img imageConfig
    88  
    89  	if err := json.Unmarshal(mb.configJSON, &img); err != nil {
    90  		return nil, err
    91  	}
    92  
    93  	if len(img.History) == 0 {
    94  		return nil, errors.New("empty history when trying to create schema1 manifest")
    95  	}
    96  
    97  	if len(img.RootFS.DiffIDs) != len(mb.descriptors) {
    98  		return nil, errors.New("number of descriptors and number of layers in rootfs must match")
    99  	}
   100  
   101  	// Generate IDs for each layer
   102  	// For non-top-level layers, create fake V1Compatibility strings that
   103  	// fit the format and don't collide with anything else, but don't
   104  	// result in runnable images on their own.
   105  	type v1Compatibility struct {
   106  		ID              string    `json:"id"`
   107  		Parent          string    `json:"parent,omitempty"`
   108  		Comment         string    `json:"comment,omitempty"`
   109  		Created         time.Time `json:"created"`
   110  		ContainerConfig struct {
   111  			Cmd []string
   112  		} `json:"container_config,omitempty"`
   113  		ThrowAway bool `json:"throwaway,omitempty"`
   114  	}
   115  
   116  	fsLayerList := make([]FSLayer, len(img.History))
   117  	history := make([]History, len(img.History))
   118  
   119  	parent := ""
   120  	layerCounter := 0
   121  	for i, h := range img.History[:len(img.History)-1] {
   122  		var blobsum digest.Digest
   123  		if h.EmptyLayer {
   124  			if blobsum, err = mb.emptyTar(ctx); err != nil {
   125  				return nil, err
   126  			}
   127  		} else {
   128  			if len(img.RootFS.DiffIDs) <= layerCounter {
   129  				return nil, errors.New("too many non-empty layers in History section")
   130  			}
   131  			blobsum = mb.descriptors[layerCounter].Digest
   132  			layerCounter++
   133  		}
   134  
   135  		v1ID := digest.FromBytes([]byte(blobsum.Hex() + " " + parent)).Hex()
   136  
   137  		if i == 0 && img.RootFS.BaseLayer != "" {
   138  			// windows-only baselayer setup
   139  			baseID := sha512.Sum384([]byte(img.RootFS.BaseLayer))
   140  			parent = fmt.Sprintf("%x", baseID[:32])
   141  		}
   142  
   143  		v1Compatibility := v1Compatibility{
   144  			ID:      v1ID,
   145  			Parent:  parent,
   146  			Comment: h.Comment,
   147  			Created: h.Created,
   148  		}
   149  		v1Compatibility.ContainerConfig.Cmd = []string{img.History[i].CreatedBy}
   150  		if h.EmptyLayer {
   151  			v1Compatibility.ThrowAway = true
   152  		}
   153  		jsonBytes, err := json.Marshal(&v1Compatibility)
   154  		if err != nil {
   155  			return nil, err
   156  		}
   157  
   158  		reversedIndex := len(img.History) - i - 1
   159  		history[reversedIndex].V1Compatibility = string(jsonBytes)
   160  		fsLayerList[reversedIndex] = FSLayer{BlobSum: blobsum}
   161  
   162  		parent = v1ID
   163  	}
   164  
   165  	latestHistory := img.History[len(img.History)-1]
   166  
   167  	var blobsum digest.Digest
   168  	if latestHistory.EmptyLayer {
   169  		if blobsum, err = mb.emptyTar(ctx); err != nil {
   170  			return nil, err
   171  		}
   172  	} else {
   173  		if len(img.RootFS.DiffIDs) <= layerCounter {
   174  			return nil, errors.New("too many non-empty layers in History section")
   175  		}
   176  		blobsum = mb.descriptors[layerCounter].Digest
   177  	}
   178  
   179  	fsLayerList[0] = FSLayer{BlobSum: blobsum}
   180  	dgst := digest.FromBytes([]byte(blobsum.Hex() + " " + parent + " " + string(mb.configJSON)))
   181  
   182  	// Top-level v1compatibility string should be a modified version of the
   183  	// image config.
   184  	transformedConfig, err := MakeV1ConfigFromConfig(mb.configJSON, dgst.Hex(), parent, latestHistory.EmptyLayer)
   185  	if err != nil {
   186  		return nil, err
   187  	}
   188  
   189  	history[0].V1Compatibility = string(transformedConfig)
   190  
   191  	tag := ""
   192  	if tagged, isTagged := mb.ref.(reference.Tagged); isTagged {
   193  		tag = tagged.Tag()
   194  	}
   195  
   196  	mfst := Manifest{
   197  		Versioned: manifest.Versioned{
   198  			SchemaVersion: 1,
   199  		},
   200  		Name:         mb.ref.Name(),
   201  		Tag:          tag,
   202  		Architecture: img.Architecture,
   203  		FSLayers:     fsLayerList,
   204  		History:      history,
   205  	}
   206  
   207  	return Sign(&mfst, mb.pk)
   208  }
   209  
   210  // emptyTar pushes a compressed empty tar to the blob store if one doesn't
   211  // already exist, and returns its blobsum.
   212  func (mb *configManifestBuilder) emptyTar(ctx context.Context) (digest.Digest, error) {
   213  	if mb.emptyTarDigest != "" {
   214  		// Already put an empty tar
   215  		return mb.emptyTarDigest, nil
   216  	}
   217  
   218  	descriptor, err := mb.bs.Stat(ctx, digestSHA256GzippedEmptyTar)
   219  	switch err {
   220  	case nil:
   221  		mb.emptyTarDigest = descriptor.Digest
   222  		return descriptor.Digest, nil
   223  	case distribution.ErrBlobUnknown:
   224  		// nop
   225  	default:
   226  		return "", err
   227  	}
   228  
   229  	// Add gzipped empty tar to the blob store
   230  	descriptor, err = mb.bs.Put(ctx, "", gzippedEmptyTar)
   231  	if err != nil {
   232  		return "", err
   233  	}
   234  
   235  	mb.emptyTarDigest = descriptor.Digest
   236  
   237  	return descriptor.Digest, nil
   238  }
   239  
   240  // AppendReference adds a reference to the current ManifestBuilder
   241  func (mb *configManifestBuilder) AppendReference(d distribution.Describable) error {
   242  	// todo: verification here?
   243  	mb.descriptors = append(mb.descriptors, d.Descriptor())
   244  	return nil
   245  }
   246  
   247  // References returns the current references added to this builder
   248  func (mb *configManifestBuilder) References() []distribution.Descriptor {
   249  	return mb.descriptors
   250  }
   251  
   252  // MakeV1ConfigFromConfig creates an legacy V1 image config from image config JSON
   253  func MakeV1ConfigFromConfig(configJSON []byte, v1ID, parentV1ID string, throwaway bool) ([]byte, error) {
   254  	// Top-level v1compatibility string should be a modified version of the
   255  	// image config.
   256  	var configAsMap map[string]*json.RawMessage
   257  	if err := json.Unmarshal(configJSON, &configAsMap); err != nil {
   258  		return nil, err
   259  	}
   260  
   261  	// Delete fields that didn't exist in old manifest
   262  	delete(configAsMap, "rootfs")
   263  	delete(configAsMap, "history")
   264  	configAsMap["id"] = rawJSON(v1ID)
   265  	if parentV1ID != "" {
   266  		configAsMap["parent"] = rawJSON(parentV1ID)
   267  	}
   268  	if throwaway {
   269  		configAsMap["throwaway"] = rawJSON(true)
   270  	}
   271  
   272  	return json.Marshal(configAsMap)
   273  }
   274  
   275  func rawJSON(value interface{}) *json.RawMessage {
   276  	jsonval, err := json.Marshal(value)
   277  	if err != nil {
   278  		return nil
   279  	}
   280  	return (*json.RawMessage)(&jsonval)
   281  }