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