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