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 }