-
Notifications
You must be signed in to change notification settings - Fork 98
/
pack.go
448 lines (402 loc) · 17.9 KB
/
pack.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
/*
Copyright The ORAS Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package oras
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"maps"
"regexp"
"time"
specs "github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/internal/spec"
)
const (
// MediaTypeUnknownConfig is the default config mediaType used
// - for [Pack] when PackOptions.PackImageManifest is true and
// PackOptions.ConfigDescriptor is not specified.
// - for [PackManifest] when packManifestVersion is PackManifestVersion1_0
// and PackManifestOptions.ConfigDescriptor is not specified.
MediaTypeUnknownConfig = "application/vnd.unknown.config.v1+json"
// MediaTypeUnknownArtifact is the default artifactType used for [Pack]
// when PackOptions.PackImageManifest is false and artifactType is
// not specified.
MediaTypeUnknownArtifact = "application/vnd.unknown.artifact.v1"
)
var (
// ErrInvalidDateTimeFormat is returned by [Pack] and [PackManifest] when
// "org.opencontainers.artifact.created" or "org.opencontainers.image.created"
// is provided, but its value is not in RFC 3339 format.
// Reference: https://www.rfc-editor.org/rfc/rfc3339#section-5.6
ErrInvalidDateTimeFormat = errors.New("invalid date and time format")
// ErrMissingArtifactType is returned by [PackManifest] when
// packManifestVersion is PackManifestVersion1_1 and artifactType is
// empty and the config media type is set to
// "application/vnd.oci.empty.v1+json".
ErrMissingArtifactType = errors.New("missing artifact type")
)
// PackManifestVersion represents the manifest version used for [PackManifest].
type PackManifestVersion int
const (
// PackManifestVersion1_0 represents the OCI Image Manifest defined in
// image-spec v1.0.2.
// Reference: https://github.com/opencontainers/image-spec/blob/v1.0.2/manifest.md
PackManifestVersion1_0 PackManifestVersion = 1
// PackManifestVersion1_1_RC4 represents the OCI Image Manifest defined
// in image-spec v1.1.0-rc4.
// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/manifest.md
//
// Deprecated: This constant is deprecated and not recommended for future use.
// Use [PackManifestVersion1_1] instead.
PackManifestVersion1_1_RC4 PackManifestVersion = PackManifestVersion1_1
// PackManifestVersion1_1 represents the OCI Image Manifest defined in
// image-spec v1.1.0.
// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md
PackManifestVersion1_1 PackManifestVersion = 2
)
// PackManifestOptions contains optional parameters for [PackManifest].
type PackManifestOptions struct {
// Subject is the subject of the manifest.
// This option is only valid when PackManifestVersion is
// NOT PackManifestVersion1_0.
Subject *ocispec.Descriptor
// Layers is the layers of the manifest.
Layers []ocispec.Descriptor
// ManifestAnnotations is the annotation map of the manifest. In order to
// make [PackManifest] reproducible, set the key ocispec.AnnotationCreated
// (i.e. "org.opencontainers.image.created") to a fixed value. The value
// must conform to RFC 3339.
ManifestAnnotations map[string]string
// ConfigDescriptor is a pointer to the descriptor of the config blob.
// If not nil, ConfigAnnotations will be ignored.
ConfigDescriptor *ocispec.Descriptor
// ConfigAnnotations is the annotation map of the config descriptor.
// This option is valid only when ConfigDescriptor is nil.
ConfigAnnotations map[string]string
}
// mediaTypeRegexp checks the format of media types.
// References:
// - https://github.com/opencontainers/image-spec/blob/v1.1.0/schema/defs-descriptor.json#L7
// - https://datatracker.ietf.org/doc/html/rfc6838#section-4.2
var mediaTypeRegexp = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}$`)
// PackManifest generates an OCI Image Manifest based on the given parameters
// and pushes the packed manifest to a content storage using pusher. The version
// of the manifest to be packed is determined by packManifestVersion
// (Recommended value: PackManifestVersion1_1).
//
// - If packManifestVersion is [PackManifestVersion1_1]:
// artifactType MUST NOT be empty unless opts.ConfigDescriptor is specified.
// - If packManifestVersion is [PackManifestVersion1_0]:
// if opts.ConfigDescriptor is nil, artifactType will be used as the
// config media type; if artifactType is empty,
// "application/vnd.unknown.config.v1+json" will be used.
// if opts.ConfigDescriptor is NOT nil, artifactType will be ignored.
//
// artifactType and opts.ConfigDescriptor.MediaType MUST comply with RFC 6838.
//
// Each time when PackManifest is called, if a time stamp is not specified, a new time
// stamp is generated in the manifest annotations with the key ocispec.AnnotationCreated
// (i.e. "org.opencontainers.image.created"). To make [PackManifest] reproducible,
// set the key ocispec.AnnotationCreated to a fixed value in
// opts.ManifestAnnotations. The value MUST conform to RFC 3339.
//
// If succeeded, returns a descriptor of the packed manifest.
func PackManifest(ctx context.Context, pusher content.Pusher, packManifestVersion PackManifestVersion, artifactType string, opts PackManifestOptions) (ocispec.Descriptor, error) {
switch packManifestVersion {
case PackManifestVersion1_0:
return packManifestV1_0(ctx, pusher, artifactType, opts)
case PackManifestVersion1_1:
return packManifestV1_1(ctx, pusher, artifactType, opts)
default:
return ocispec.Descriptor{}, fmt.Errorf("PackManifestVersion(%v): %w", packManifestVersion, errdef.ErrUnsupported)
}
}
// PackOptions contains optional parameters for [Pack].
//
// Deprecated: This type is deprecated and not recommended for future use.
// Use [PackManifestOptions] instead.
type PackOptions struct {
// Subject is the subject of the manifest.
Subject *ocispec.Descriptor
// ManifestAnnotations is the annotation map of the manifest.
ManifestAnnotations map[string]string
// PackImageManifest controls whether to pack an OCI Image Manifest or not.
// - If true, pack an OCI Image Manifest.
// - If false, pack an OCI Artifact Manifest (deprecated).
//
// Default value: false.
PackImageManifest bool
// ConfigDescriptor is a pointer to the descriptor of the config blob.
// If not nil, artifactType will be implied by the mediaType of the
// specified ConfigDescriptor, and ConfigAnnotations will be ignored.
// This option is valid only when PackImageManifest is true.
ConfigDescriptor *ocispec.Descriptor
// ConfigAnnotations is the annotation map of the config descriptor.
// This option is valid only when PackImageManifest is true
// and ConfigDescriptor is nil.
ConfigAnnotations map[string]string
}
// Pack packs the given blobs, generates a manifest for the pack,
// and pushes it to a content storage.
//
// When opts.PackImageManifest is true, artifactType will be used as the
// the config descriptor mediaType of the image manifest.
//
// If succeeded, returns a descriptor of the manifest.
//
// Deprecated: This method is deprecated and not recommended for future use.
// Use [PackManifest] instead.
func Pack(ctx context.Context, pusher content.Pusher, artifactType string, blobs []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) {
if opts.PackImageManifest {
return packManifestV1_1_RC2(ctx, pusher, artifactType, blobs, opts)
}
return packArtifact(ctx, pusher, artifactType, blobs, opts)
}
// packArtifact packs an Artifact manifest as defined in image-spec v1.1.0-rc2.
// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/artifact.md
func packArtifact(ctx context.Context, pusher content.Pusher, artifactType string, blobs []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) {
if artifactType == "" {
artifactType = MediaTypeUnknownArtifact
}
annotations, err := ensureAnnotationCreated(opts.ManifestAnnotations, spec.AnnotationArtifactCreated)
if err != nil {
return ocispec.Descriptor{}, err
}
manifest := spec.Artifact{
MediaType: spec.MediaTypeArtifactManifest,
ArtifactType: artifactType,
Blobs: blobs,
Subject: opts.Subject,
Annotations: annotations,
}
return pushManifest(ctx, pusher, manifest, manifest.MediaType, manifest.ArtifactType, manifest.Annotations)
}
// packManifestV1_0 packs an image manifest defined in image-spec v1.0.2.
// Reference: https://github.com/opencontainers/image-spec/blob/v1.0.2/manifest.md
func packManifestV1_0(ctx context.Context, pusher content.Pusher, artifactType string, opts PackManifestOptions) (ocispec.Descriptor, error) {
if opts.Subject != nil {
return ocispec.Descriptor{}, fmt.Errorf("subject is not supported for manifest version %v: %w", PackManifestVersion1_0, errdef.ErrUnsupported)
}
// prepare config
var configDesc ocispec.Descriptor
if opts.ConfigDescriptor != nil {
if err := validateMediaType(opts.ConfigDescriptor.MediaType); err != nil {
return ocispec.Descriptor{}, fmt.Errorf("invalid config mediaType format: %w", err)
}
configDesc = *opts.ConfigDescriptor
} else {
if artifactType == "" {
artifactType = MediaTypeUnknownConfig
} else if err := validateMediaType(artifactType); err != nil {
return ocispec.Descriptor{}, fmt.Errorf("invalid artifactType format: %w", err)
}
var err error
configDesc, err = pushCustomEmptyConfig(ctx, pusher, artifactType, opts.ConfigAnnotations)
if err != nil {
return ocispec.Descriptor{}, err
}
}
annotations, err := ensureAnnotationCreated(opts.ManifestAnnotations, ocispec.AnnotationCreated)
if err != nil {
return ocispec.Descriptor{}, err
}
if opts.Layers == nil {
opts.Layers = []ocispec.Descriptor{} // make it an empty array to prevent potential server-side bugs
}
manifest := ocispec.Manifest{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
Config: configDesc,
MediaType: ocispec.MediaTypeImageManifest,
Layers: opts.Layers,
Annotations: annotations,
}
return pushManifest(ctx, pusher, manifest, manifest.MediaType, manifest.Config.MediaType, manifest.Annotations)
}
// packManifestV1_1_RC2 packs an image manifest as defined in image-spec
// v1.1.0-rc2.
// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/manifest.md
func packManifestV1_1_RC2(ctx context.Context, pusher content.Pusher, configMediaType string, layers []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) {
if configMediaType == "" {
configMediaType = MediaTypeUnknownConfig
}
// prepare config
var configDesc ocispec.Descriptor
if opts.ConfigDescriptor != nil {
configDesc = *opts.ConfigDescriptor
} else {
var err error
configDesc, err = pushCustomEmptyConfig(ctx, pusher, configMediaType, opts.ConfigAnnotations)
if err != nil {
return ocispec.Descriptor{}, err
}
}
annotations, err := ensureAnnotationCreated(opts.ManifestAnnotations, ocispec.AnnotationCreated)
if err != nil {
return ocispec.Descriptor{}, err
}
if layers == nil {
layers = []ocispec.Descriptor{} // make it an empty array to prevent potential server-side bugs
}
manifest := ocispec.Manifest{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
Config: configDesc,
MediaType: ocispec.MediaTypeImageManifest,
Layers: layers,
Subject: opts.Subject,
Annotations: annotations,
}
return pushManifest(ctx, pusher, manifest, manifest.MediaType, manifest.Config.MediaType, manifest.Annotations)
}
// packManifestV1_1 packs an image manifest defined in image-spec v1.1.0.
// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md#guidelines-for-artifact-usage
func packManifestV1_1(ctx context.Context, pusher content.Pusher, artifactType string, opts PackManifestOptions) (ocispec.Descriptor, error) {
if artifactType == "" && (opts.ConfigDescriptor == nil || opts.ConfigDescriptor.MediaType == ocispec.MediaTypeEmptyJSON) {
// artifactType MUST be set when config.mediaType is set to the empty value
return ocispec.Descriptor{}, ErrMissingArtifactType
}
if artifactType != "" {
if err := validateMediaType(artifactType); err != nil {
return ocispec.Descriptor{}, fmt.Errorf("invalid artifactType format: %w", err)
}
}
// prepare config
var emptyBlobExists bool
var configDesc ocispec.Descriptor
if opts.ConfigDescriptor != nil {
if err := validateMediaType(opts.ConfigDescriptor.MediaType); err != nil {
return ocispec.Descriptor{}, fmt.Errorf("invalid config mediaType format: %w", err)
}
configDesc = *opts.ConfigDescriptor
} else {
// use the empty descriptor for config
configDesc = ocispec.DescriptorEmptyJSON
configDesc.Annotations = opts.ConfigAnnotations
configBytes := ocispec.DescriptorEmptyJSON.Data
// push config
if err := pushIfNotExist(ctx, pusher, configDesc, configBytes); err != nil {
return ocispec.Descriptor{}, fmt.Errorf("failed to push config: %w", err)
}
emptyBlobExists = true
}
annotations, err := ensureAnnotationCreated(opts.ManifestAnnotations, ocispec.AnnotationCreated)
if err != nil {
return ocispec.Descriptor{}, err
}
if len(opts.Layers) == 0 {
// use the empty descriptor as the single layer
layerDesc := ocispec.DescriptorEmptyJSON
layerData := ocispec.DescriptorEmptyJSON.Data
if !emptyBlobExists {
if err := pushIfNotExist(ctx, pusher, layerDesc, layerData); err != nil {
return ocispec.Descriptor{}, fmt.Errorf("failed to push layer: %w", err)
}
}
opts.Layers = []ocispec.Descriptor{layerDesc}
}
manifest := ocispec.Manifest{
Versioned: specs.Versioned{
SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
},
Config: configDesc,
MediaType: ocispec.MediaTypeImageManifest,
Layers: opts.Layers,
Subject: opts.Subject,
ArtifactType: artifactType,
Annotations: annotations,
}
return pushManifest(ctx, pusher, manifest, manifest.MediaType, manifest.ArtifactType, manifest.Annotations)
}
// pushIfNotExist pushes data described by desc if it does not exist in the
// target.
func pushIfNotExist(ctx context.Context, pusher content.Pusher, desc ocispec.Descriptor, data []byte) error {
if ros, ok := pusher.(content.ReadOnlyStorage); ok {
exists, err := ros.Exists(ctx, desc)
if err != nil {
return fmt.Errorf("failed to check existence: %s: %s: %w", desc.Digest.String(), desc.MediaType, err)
}
if exists {
return nil
}
}
if err := pusher.Push(ctx, desc, bytes.NewReader(data)); err != nil && !errors.Is(err, errdef.ErrAlreadyExists) {
return fmt.Errorf("failed to push: %s: %s: %w", desc.Digest.String(), desc.MediaType, err)
}
return nil
}
// pushManifest marshals manifest into JSON bytes and pushes it.
func pushManifest(ctx context.Context, pusher content.Pusher, manifest any, mediaType string, artifactType string, annotations map[string]string) (ocispec.Descriptor, error) {
manifestJSON, err := json.Marshal(manifest)
if err != nil {
return ocispec.Descriptor{}, fmt.Errorf("failed to marshal manifest: %w", err)
}
manifestDesc := content.NewDescriptorFromBytes(mediaType, manifestJSON)
// populate ArtifactType and Annotations of the manifest into manifestDesc
manifestDesc.ArtifactType = artifactType
manifestDesc.Annotations = annotations
// push manifest
if err := pusher.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)); err != nil && !errors.Is(err, errdef.ErrAlreadyExists) {
return ocispec.Descriptor{}, fmt.Errorf("failed to push manifest: %w", err)
}
return manifestDesc, nil
}
// pushCustomEmptyConfig generates and pushes an empty config blob.
func pushCustomEmptyConfig(ctx context.Context, pusher content.Pusher, mediaType string, annotations map[string]string) (ocispec.Descriptor, error) {
// Use an empty JSON object here, because some registries may not accept
// empty config blob.
// As of September 2022, GAR is known to return 400 on empty blob upload.
// See https://github.com/oras-project/oras-go/issues/294 for details.
configBytes := []byte("{}")
configDesc := content.NewDescriptorFromBytes(mediaType, configBytes)
configDesc.Annotations = annotations
// push config
if err := pushIfNotExist(ctx, pusher, configDesc, configBytes); err != nil {
return ocispec.Descriptor{}, fmt.Errorf("failed to push config: %w", err)
}
return configDesc, nil
}
// ensureAnnotationCreated ensures that annotationCreatedKey is in annotations,
// and that its value conforms to RFC 3339. Otherwise returns a new annotation
// map with annotationCreatedKey created.
func ensureAnnotationCreated(annotations map[string]string, annotationCreatedKey string) (map[string]string, error) {
if createdTime, ok := annotations[annotationCreatedKey]; ok {
// if annotationCreatedKey is provided, validate its format
if _, err := time.Parse(time.RFC3339, createdTime); err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidDateTimeFormat, err)
}
return annotations, nil
}
// copy the original annotation map
copied := make(map[string]string, len(annotations)+1)
maps.Copy(copied, annotations)
// set creation time in RFC 3339 format
// reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/annotations.md#pre-defined-annotation-keys
now := time.Now().UTC()
copied[annotationCreatedKey] = now.Format(time.RFC3339)
return copied, nil
}
// validateMediaType validates the format of mediaType.
func validateMediaType(mediaType string) error {
if !mediaTypeRegexp.MatchString(mediaType) {
return fmt.Errorf("%s: %w", mediaType, errdef.ErrInvalidMediaType)
}
return nil
}