Skip to content

Commit 8716d02

Browse files
committed
Add schema1 package
1 parent 44e4d6e commit 8716d02

File tree

25 files changed

+4124
-1
lines changed

25 files changed

+4124
-1
lines changed

pkg/image/registryclient/v2/go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ go 1.24.0
55
require (
66
github.com/distribution/distribution/v3 v3.0.0
77
github.com/distribution/reference v0.6.0
8+
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7
89
github.com/google/go-cmp v0.7.0
910
github.com/google/uuid v1.6.0
1011
github.com/klauspost/compress v1.18.0
1112
github.com/opencontainers/go-digest v1.0.0
1213
github.com/opencontainers/image-spec v1.1.1
1314
github.com/openshift/library-go v0.0.0-20250711143941-47604345e7ea
15+
github.com/sirupsen/logrus v1.9.3
1416
golang.org/x/time v0.12.0
1517
k8s.io/client-go v0.33.2
1618
k8s.io/klog/v2 v2.130.1
@@ -36,7 +38,6 @@ require (
3638
github.com/prometheus/client_model v0.6.2 // indirect
3739
github.com/prometheus/common v0.65.0 // indirect
3840
github.com/prometheus/procfs v0.17.0 // indirect
39-
github.com/sirupsen/logrus v1.9.3 // indirect
4041
github.com/x448/float16 v0.8.4 // indirect
4142
golang.org/x/net v0.40.0 // indirect
4243
golang.org/x/oauth2 v0.30.0 // indirect

pkg/image/registryclient/v2/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
1717
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
1818
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
1919
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
20+
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
21+
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
2022
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
2123
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
2224
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
package schema1
2+
3+
import (
4+
"context"
5+
"crypto/sha512"
6+
"encoding/json"
7+
"errors"
8+
"fmt"
9+
"time"
10+
11+
"github.com/distribution/distribution/v3"
12+
"github.com/distribution/distribution/v3/manifest"
13+
"github.com/distribution/reference"
14+
"github.com/docker/libtrust"
15+
"github.com/opencontainers/go-digest"
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+
// ref contains the name and optional tag provided to NewConfigManifestBuilder.
43+
ref reference.Named
44+
// descriptors is the set of descriptors referencing the layers.
45+
descriptors []distribution.Descriptor
46+
// emptyTarDigest is set to a valid digest if an empty tar has been
47+
// put in the blob store; otherwise it is empty.
48+
emptyTarDigest digest.Digest
49+
}
50+
51+
// NewConfigManifestBuilder is used to build new manifests for the current
52+
// schema version from an image configuration and a set of descriptors.
53+
// It takes a BlobService so that it can add an empty tar to the blob store
54+
// if the resulting manifest needs empty layers.
55+
//
56+
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015,
57+
// use Docker Image Manifest v2, Schema 2, or the OCI Image Specification v1.
58+
// This package should not be used for purposes other than backward compatibility.
59+
func NewConfigManifestBuilder(bs distribution.BlobService, pk libtrust.PrivateKey, ref reference.Named, configJSON []byte) *ConfigManifestBuilder {
60+
return &ConfigManifestBuilder{
61+
bs: bs,
62+
pk: pk,
63+
configJSON: configJSON,
64+
ref: ref,
65+
}
66+
}
67+
68+
// Build produces a final manifest from the given references.
69+
//
70+
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
71+
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
72+
func (mb *ConfigManifestBuilder) Build(ctx context.Context) (m distribution.Manifest, err error) {
73+
type imageRootFS struct {
74+
Type string `json:"type"`
75+
DiffIDs []diffID `json:"diff_ids,omitempty"`
76+
BaseLayer string `json:"base_layer,omitempty"`
77+
}
78+
79+
type imageHistory struct {
80+
Created time.Time `json:"created"`
81+
Author string `json:"author,omitempty"`
82+
CreatedBy string `json:"created_by,omitempty"`
83+
Comment string `json:"comment,omitempty"`
84+
EmptyLayer bool `json:"empty_layer,omitempty"`
85+
}
86+
87+
type imageConfig struct {
88+
RootFS *imageRootFS `json:"rootfs,omitempty"`
89+
History []imageHistory `json:"history,omitempty"`
90+
Architecture string `json:"architecture,omitempty"`
91+
}
92+
93+
var img imageConfig
94+
95+
if err := json.Unmarshal(mb.configJSON, &img); err != nil {
96+
return nil, err
97+
}
98+
99+
if len(img.History) == 0 {
100+
return nil, errors.New("empty history when trying to create schema1 manifest")
101+
}
102+
103+
if len(img.RootFS.DiffIDs) != len(mb.descriptors) {
104+
return nil, fmt.Errorf("number of descriptors and number of layers in rootfs must match: len(%v) != len(%v)", img.RootFS.DiffIDs, mb.descriptors)
105+
}
106+
107+
// Generate IDs for each layer
108+
// For non-top-level layers, create fake V1Compatibility strings that
109+
// fit the format and don't collide with anything else, but don't
110+
// result in runnable images on their own.
111+
type v1Compatibility struct {
112+
ID string `json:"id"`
113+
Parent string `json:"parent,omitempty"`
114+
Comment string `json:"comment,omitempty"`
115+
Created time.Time `json:"created"`
116+
ContainerConfig struct {
117+
Cmd []string
118+
} `json:"container_config,omitempty"`
119+
Author string `json:"author,omitempty"`
120+
ThrowAway bool `json:"throwaway,omitempty"`
121+
}
122+
123+
fsLayerList := make([]FSLayer, len(img.History))
124+
history := make([]History, len(img.History))
125+
126+
parent := ""
127+
layerCounter := 0
128+
for i, h := range img.History[:len(img.History)-1] {
129+
var blobsum digest.Digest
130+
if h.EmptyLayer {
131+
if blobsum, err = mb.emptyTar(ctx); err != nil {
132+
return nil, err
133+
}
134+
} else {
135+
if len(img.RootFS.DiffIDs) <= layerCounter {
136+
return nil, errors.New("too many non-empty layers in History section")
137+
}
138+
blobsum = mb.descriptors[layerCounter].Digest
139+
layerCounter++
140+
}
141+
142+
v1ID := digest.FromBytes([]byte(blobsum.Encoded() + " " + parent)).Encoded()
143+
144+
if i == 0 && img.RootFS.BaseLayer != "" {
145+
// windows-only baselayer setup
146+
baseID := sha512.Sum384([]byte(img.RootFS.BaseLayer))
147+
parent = fmt.Sprintf("%x", baseID[:32])
148+
}
149+
150+
v1Compat := v1Compatibility{
151+
ID: v1ID,
152+
Parent: parent,
153+
Comment: h.Comment,
154+
Created: h.Created,
155+
Author: h.Author,
156+
}
157+
v1Compat.ContainerConfig.Cmd = []string{img.History[i].CreatedBy}
158+
if h.EmptyLayer {
159+
v1Compat.ThrowAway = true
160+
}
161+
jsonBytes, err := json.Marshal(&v1Compat)
162+
if err != nil {
163+
return nil, err
164+
}
165+
166+
reversedIndex := len(img.History) - i - 1
167+
history[reversedIndex].V1Compatibility = string(jsonBytes)
168+
fsLayerList[reversedIndex] = FSLayer{BlobSum: blobsum}
169+
170+
parent = v1ID
171+
}
172+
173+
latestHistory := img.History[len(img.History)-1]
174+
175+
var blobsum digest.Digest
176+
if latestHistory.EmptyLayer {
177+
if blobsum, err = mb.emptyTar(ctx); err != nil {
178+
return nil, err
179+
}
180+
} else {
181+
if len(img.RootFS.DiffIDs) <= layerCounter {
182+
return nil, errors.New("too many non-empty layers in History section")
183+
}
184+
blobsum = mb.descriptors[layerCounter].Digest
185+
}
186+
187+
fsLayerList[0] = FSLayer{BlobSum: blobsum}
188+
dgst := digest.FromBytes([]byte(blobsum.Encoded() + " " + parent + " " + string(mb.configJSON)))
189+
190+
// Top-level v1compatibility string should be a modified version of the
191+
// image config.
192+
transformedConfig, err := MakeV1ConfigFromConfig(mb.configJSON, dgst.Encoded(), parent, latestHistory.EmptyLayer)
193+
if err != nil {
194+
return nil, err
195+
}
196+
197+
history[0].V1Compatibility = string(transformedConfig)
198+
199+
tag := ""
200+
if tagged, isTagged := mb.ref.(reference.Tagged); isTagged {
201+
tag = tagged.Tag()
202+
}
203+
204+
mfst := Manifest{
205+
Versioned: manifest.Versioned{
206+
SchemaVersion: 1,
207+
},
208+
Name: mb.ref.Name(),
209+
Tag: tag,
210+
Architecture: img.Architecture,
211+
FSLayers: fsLayerList,
212+
History: history,
213+
}
214+
215+
return Sign(&mfst, mb.pk)
216+
}
217+
218+
// emptyTar pushes a compressed empty tar to the blob store if one doesn't
219+
// already exist, and returns its blobsum.
220+
func (mb *ConfigManifestBuilder) emptyTar(ctx context.Context) (digest.Digest, error) {
221+
if mb.emptyTarDigest != "" {
222+
// Already put an empty tar
223+
return mb.emptyTarDigest, nil
224+
}
225+
226+
descriptor, err := mb.bs.Stat(ctx, digestSHA256GzippedEmptyTar)
227+
switch err {
228+
case nil:
229+
mb.emptyTarDigest = descriptor.Digest
230+
return descriptor.Digest, nil
231+
case distribution.ErrBlobUnknown:
232+
// nop
233+
default:
234+
return "", err
235+
}
236+
237+
// Add gzipped empty tar to the blob store
238+
descriptor, err = mb.bs.Put(ctx, "", gzippedEmptyTar)
239+
if err != nil {
240+
return "", err
241+
}
242+
243+
mb.emptyTarDigest = descriptor.Digest
244+
245+
return descriptor.Digest, nil
246+
}
247+
248+
// AppendReference adds a reference to the current ManifestBuilder.
249+
//
250+
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
251+
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
252+
func (mb *ConfigManifestBuilder) AppendReference(descriptor distribution.Descriptor) error {
253+
if err := descriptor.Digest.Validate(); err != nil {
254+
return err
255+
}
256+
257+
mb.descriptors = append(mb.descriptors, descriptor)
258+
return nil
259+
}
260+
261+
// References returns the current references added to this builder.
262+
//
263+
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
264+
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
265+
func (mb *ConfigManifestBuilder) References() []distribution.Descriptor {
266+
return mb.descriptors
267+
}
268+
269+
// MakeV1ConfigFromConfig creates an legacy V1 image config from image config JSON.
270+
//
271+
// Deprecated: Docker Image Manifest v2, Schema 1 is deprecated since 2015.
272+
// Use Docker Image Manifest v2, Schema 2, or the OCI Image Specification.
273+
func MakeV1ConfigFromConfig(configJSON []byte, v1ID, parentV1ID string, throwaway bool) ([]byte, error) {
274+
// Top-level v1compatibility string should be a modified version of the
275+
// image config.
276+
var configAsMap map[string]*json.RawMessage
277+
if err := json.Unmarshal(configJSON, &configAsMap); err != nil {
278+
return nil, err
279+
}
280+
281+
// Delete fields that didn't exist in old manifest
282+
delete(configAsMap, "rootfs")
283+
delete(configAsMap, "history")
284+
configAsMap["id"] = rawJSON(v1ID)
285+
if parentV1ID != "" {
286+
configAsMap["parent"] = rawJSON(parentV1ID)
287+
}
288+
if throwaway {
289+
configAsMap["throwaway"] = rawJSON(true)
290+
}
291+
292+
return json.Marshal(configAsMap)
293+
}
294+
295+
func rawJSON(value interface{}) *json.RawMessage {
296+
jsonval, err := json.Marshal(value)
297+
if err != nil {
298+
return nil
299+
}
300+
return (*json.RawMessage)(&jsonval)
301+
}

0 commit comments

Comments
 (0)