diff --git a/content/storage.go b/content/storage.go index 971142cb..47c95d87 100644 --- a/content/storage.go +++ b/content/storage.go @@ -31,7 +31,7 @@ type Fetcher interface { // Pusher pushes content. type Pusher interface { // Push pushes the content, matching the expected descriptor. - // Reader is perferred to Writer so that the suitable buffer size can be + // Reader is preferred to Writer so that the suitable buffer size can be // chosen by the underlying implementation. Furthermore, the implementation // can also do reflection on the Reader for more advanced I/O optimization. Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error diff --git a/pack.go b/pack.go index 872afa51..c5686242 100644 --- a/pack.go +++ b/pack.go @@ -31,55 +31,117 @@ import ( ) const ( - // MediaTypeUnknownConfig is the default mediaType used when no - // config media type is specified. + // MediaTypeUnknownConfig is the default mediaType used for [Pack] when + // PackOptions.PackImageManifest is true and PackOptions.PackManifestType + // is PackManifestTypeImageV1_1_0_RC2 and PackOptions.ConfigDescriptor + // is not specified. MediaTypeUnknownConfig = "application/vnd.unknown.config.v1+json" - // MediaTypeUnknownArtifact is the default artifactType used when no - // artifact type is specified. + + // MediaTypeUnknownArtifact is the default artifactType used for [Pack] + // when PackOptions.PackImageManifest is false and artifactType is + // not specified. MediaTypeUnknownArtifact = "application/vnd.unknown.artifact.v1" ) -// ErrInvalidDateTimeFormat is returned by Pack() when -// AnnotationArtifactCreated or AnnotationCreated is provided, but its value -// is not in RFC 3339 format. -// Reference: https://www.rfc-editor.org/rfc/rfc3339#section-5.6 -var ErrInvalidDateTimeFormat = errors.New("invalid date and time format") +// PackManifestType represents the manifest type used for [Pack]. +type PackManifestType int + +const ( + // PackManifestTypeImageV1_1_0_RC2 represents the OCI Image Manifest type + // defined in image-spec v1.1.0-rc2. + // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/manifest.md + // + // Deprecated: This type is deprecated and not recommended for future use. + // Use PackManifestTypeImageV1_1_0_RC4 instead. + PackManifestTypeImageV1_1_0_RC2 PackManifestType = 0 + + // PackManifestTypeImageV1_1_0_RC4 represents the OCI Image Manifest type + // defined since image-spec v1.1.0-rc4. + // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/manifest.md + PackManifestTypeImageV1_1_0_RC4 PackManifestType = 1 +) + +var ( + // ErrInvalidDateTimeFormat is returned by [Pack] when + // AnnotationArtifactCreated or AnnotationCreated 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 [Pack] when artifactType is not + // specified and the config media type is set to + // "application/vnd.oci.empty.v1+json". + ErrMissingArtifactType = errors.New("missing artifact type") +) -// PackOptions contains parameters for [oras.Pack]. +// PackOptions contains parameters for [Pack]. 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 image manifest or not. - // - If true, pack an image manifest; artifactType will be used as the - // the config descriptor mediaType of the image manifest. - // - If false, pack an artifact manifest. - // Default: false. + // 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. + // Recommended value: true (See DefaultPackOptions). PackImageManifest bool + + // PackManifestType controls which type of manifest to pack. + // This option is valid only when PackImageManifest is true. + // + // Default value: PackManifestTypeImageV1_1_0_RC2 (deprecated). + // Recommended value: PackManifestTypeImageV1_1_0_RC4 (See DefaultPackOptions). + PackManifestType PackManifestType + // 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 } +// DefaultPackOptions provides the default PackOptions. +// Note that the default options are subject to change in the future. +var DefaultPackOptions PackOptions = PackOptions{ + PackImageManifest: true, + PackManifestType: PackManifestTypeImageV1_1_0_RC4, +} + // 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 opts.PackImageManifest is true and opts.PackManifestType is +// [PackManifestTypeImageV1_1_0_RC2], +// artifactType will be used as the the config media type of the image +// manifest when opts.ConfigDescriptor is not specified. +// - If opts.PackImageManifest is true and opts.PackManifestType is +// [PackManifestTypeImageV1_1_0_RC4], +// [ErrMissingArtifactType] will be returned when none of artifactType and +// opts.ConfigDescriptor is specified. +// // If succeeded, returns a descriptor of the manifest. func Pack(ctx context.Context, pusher content.Pusher, artifactType string, blobs []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) { - if opts.PackImageManifest { - return packImage(ctx, pusher, artifactType, blobs, opts) + if !opts.PackImageManifest { + return packArtifact(ctx, pusher, artifactType, blobs, opts) + } + + switch opts.PackManifestType { + case PackManifestTypeImageV1_1_0_RC2: + return packImageRC2(ctx, pusher, artifactType, blobs, opts) + case PackManifestTypeImageV1_1_0_RC4: + return packImageRC4(ctx, pusher, artifactType, blobs, opts) + default: + return ocispec.Descriptor{}, fmt.Errorf("PackManifestType(%v): %w", opts.PackManifestType, errdef.ErrUnsupported) } - return packArtifact(ctx, pusher, artifactType, blobs, opts) } // packArtifact packs the given blobs, generates an artifact manifest for the @@ -101,28 +163,14 @@ func packArtifact(ctx context.Context, pusher content.Pusher, artifactType strin Subject: opts.Subject, Annotations: annotations, } - manifestJSON, err := json.Marshal(manifest) - if err != nil { - return ocispec.Descriptor{}, fmt.Errorf("failed to marshal manifest: %w", err) - } - manifestDesc := content.NewDescriptorFromBytes(spec.MediaTypeArtifactManifest, manifestJSON) - // populate ArtifactType and Annotations of the manifest into manifestDesc - manifestDesc.ArtifactType = manifest.ArtifactType - manifestDesc.Annotations = manifest.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 + return pushManifest(ctx, pusher, manifest, manifest.MediaType, manifest.ArtifactType, manifest.Annotations) } -// packImage packs the given blobs, generates an image manifest for the pack, -// and pushes it to a content storage. artifactType will be used as the config -// descriptor mediaType of the image manifest. +// packImageRC2 packs the given blobs, generates an image manifest for the +// pack, and pushes it to a content storage. // If succeeded, returns a descriptor of the manifest. -func packImage(ctx context.Context, pusher content.Pusher, configMediaType string, layers []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) { +// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/manifest.md +func packImageRC2(ctx context.Context, pusher content.Pusher, configMediaType string, layers []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) { if configMediaType == "" { configMediaType = MediaTypeUnknownConfig } @@ -139,7 +187,7 @@ func packImage(ctx context.Context, pusher content.Pusher, configMediaType strin configDesc = content.NewDescriptorFromBytes(configMediaType, configBytes) configDesc.Annotations = opts.ConfigAnnotations // push config - if err := pusher.Push(ctx, configDesc, bytes.NewReader(configBytes)); err != nil && !errors.Is(err, errdef.ErrAlreadyExists) { + if err := pushIfNotExist(ctx, pusher, configDesc, configBytes); err != nil { return ocispec.Descriptor{}, fmt.Errorf("failed to push config: %w", err) } } @@ -161,20 +209,100 @@ func packImage(ctx context.Context, pusher content.Pusher, configMediaType strin Subject: opts.Subject, Annotations: annotations, } + return pushManifest(ctx, pusher, manifest, manifest.MediaType, manifest.Config.MediaType, manifest.Annotations) +} + +// packImageRC4 packs the given blobs, generates an image manifest for the pack, +// and pushes it to a content storage. +// If succeeded, returns a descriptor of the manifest. +// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/manifest.md#guidelines-for-artifact-usage +func packImageRC4(ctx context.Context, pusher content.Pusher, artifactType string, layers []ocispec.Descriptor, opts PackOptions) (ocispec.Descriptor, error) { + var emptyBlobExists bool + var configDesc ocispec.Descriptor + if opts.ConfigDescriptor != nil { + 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 + } + if artifactType == "" { + if configDesc.MediaType == ocispec.MediaTypeEmptyJSON { + // artifactType MUST be set when config.mediaType is set to the empty value + return ocispec.Descriptor{}, ErrMissingArtifactType + } + artifactType = configDesc.MediaType + } + + annotations, err := ensureAnnotationCreated(opts.ManifestAnnotations, ocispec.AnnotationCreated) + if err != nil { + return ocispec.Descriptor{}, err + } + if len(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) + } + } + 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: 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(ocispec.MediaTypeImageManifest, manifestJSON) + manifestDesc := content.NewDescriptorFromBytes(mediaType, manifestJSON) // populate ArtifactType and Annotations of the manifest into manifestDesc - manifestDesc.ArtifactType = manifest.Config.MediaType - manifestDesc.Annotations = manifest.Annotations - + 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 } diff --git a/pack_test.go b/pack_test.go index 505f1b79..01c2b02b 100644 --- a/pack_test.go +++ b/pack_test.go @@ -30,10 +30,11 @@ import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/content/memory" + "oras.land/oras-go/v2/errdef" "oras.land/oras-go/v2/internal/spec" ) -func Test_Pack_Default(t *testing.T) { +func Test_Pack_Artifact_NoOption(t *testing.T) { s := memory.New() // prepare test content @@ -50,7 +51,7 @@ func Test_Pack_Default(t *testing.T) { t.Fatal("Oras.Pack() error =", err) } - // test blobs + // verify blobs var manifest spec.Artifact rc, err := s.Fetch(ctx, manifestDesc) if err != nil { @@ -66,17 +67,17 @@ func Test_Pack_Default(t *testing.T) { t.Errorf("Store.Fetch() = %v, want %v", manifest.Blobs, blobs) } - // test media type + // verify media type if got := manifest.MediaType; got != spec.MediaTypeArtifactManifest { t.Fatalf("got media type = %s, want %s", got, spec.MediaTypeArtifactManifest) } - // test artifact type + // verify artifact type if got := manifest.ArtifactType; got != artifactType { t.Fatalf("got artifact type = %s, want %s", got, artifactType) } - // test created time annotation + // verify created time annotation createdTime, ok := manifest.Annotations[spec.AnnotationArtifactCreated] if !ok { t.Errorf("Annotation %s = %v, want %v", spec.AnnotationArtifactCreated, ok, true) @@ -85,9 +86,19 @@ func Test_Pack_Default(t *testing.T) { if err != nil { t.Errorf("error parsing created time: %s, error = %v", createdTime, err) } + + // verify descriptor artifact type + if want := manifest.ArtifactType; !reflect.DeepEqual(manifestDesc.ArtifactType, want) { + t.Errorf("got descriptor artifactType = %v, want %v", manifestDesc.ArtifactType, want) + } + + // verify descriptor annotations + if want := manifest.Annotations; !reflect.DeepEqual(manifestDesc.Annotations, want) { + t.Errorf("got descriptor annotations = %v, want %v", manifestDesc.Annotations, want) + } } -func Test_Pack_WithOptions(t *testing.T) { +func Test_Pack_Artifact_WithOptions(t *testing.T) { s := memory.New() // prepare test content @@ -108,12 +119,18 @@ func Test_Pack_WithOptions(t *testing.T) { ArtifactType: artifactType, Annotations: annotations, } + configBytes := []byte("{}") + configDesc := content.NewDescriptorFromBytes("testconfig", configBytes) + configAnnotations := map[string]string{"foo": "bar"} // test Pack ctx := context.Background() opts := PackOptions{ Subject: &subjectDesc, ManifestAnnotations: annotations, + ConfigDescriptor: &configDesc, // should not work + ConfigAnnotations: configAnnotations, // should not work + PackManifestType: PackManifestTypeImageV1_1_0_RC4, // should not work } manifestDesc, err := Pack(ctx, s, artifactType, blobs, opts) if err != nil { @@ -132,7 +149,7 @@ func Test_Pack_WithOptions(t *testing.T) { t.Fatal("failed to marshal manifest:", err) } - // test manifest + // verify manifest rc, err := s.Fetch(ctx, manifestDesc) if err != nil { t.Fatal("Store.Fetch() error =", err) @@ -148,9 +165,17 @@ func Test_Pack_WithOptions(t *testing.T) { if !bytes.Equal(got, expectedManifestBytes) { t.Errorf("Store.Fetch() = %v, want %v", got, expectedManifestBytes) } + + // verify descriptor + expectedManifestDesc := content.NewDescriptorFromBytes(expectedManifest.MediaType, expectedManifestBytes) + expectedManifestDesc.ArtifactType = expectedManifest.ArtifactType + expectedManifestDesc.Annotations = expectedManifest.Annotations + if !reflect.DeepEqual(manifestDesc, expectedManifestDesc) { + t.Errorf("Pack() = %v, want %v", manifestDesc, expectedManifestDesc) + } } -func Test_Pack_NoBlob(t *testing.T) { +func Test_Pack_Artifact_NoBlob(t *testing.T) { s := memory.New() // test Pack @@ -173,14 +198,14 @@ func Test_Pack_NoBlob(t *testing.T) { t.Fatal("Store.Fetch().Close() error =", err) } - // test blobs + // verify blobs var expectedBlobs []ocispec.Descriptor if !reflect.DeepEqual(manifest.Blobs, expectedBlobs) { t.Errorf("Store.Fetch() = %v, want %v", manifest.Blobs, expectedBlobs) } } -func Test_Pack_NoArtifactType(t *testing.T) { +func Test_Pack_Artifact_NoArtifactType(t *testing.T) { s := memory.New() ctx := context.Background() @@ -201,7 +226,7 @@ func Test_Pack_NoArtifactType(t *testing.T) { t.Fatal("Store.Fetch().Close() error =", err) } - // test artifact type + // verify artifact type if manifestDesc.ArtifactType != MediaTypeUnknownArtifact { t.Fatalf("got artifact type = %s, want %s", manifestDesc.ArtifactType, MediaTypeUnknownArtifact) } @@ -210,7 +235,7 @@ func Test_Pack_NoArtifactType(t *testing.T) { } } -func Test_Pack_InvalidDateTimeFormat(t *testing.T) { +func Test_Pack_Artifact_InvalidDateTimeFormat(t *testing.T) { s := memory.New() ctx := context.Background() @@ -221,12 +246,12 @@ func Test_Pack_InvalidDateTimeFormat(t *testing.T) { } artifactType := "application/vnd.test" _, err := Pack(ctx, s, artifactType, nil, opts) - if err == nil || !errors.Is(err, ErrInvalidDateTimeFormat) { - t.Errorf("Oras.Pack() error = %v, wantErr = %v", err, ErrInvalidDateTimeFormat) + if wantErr := ErrInvalidDateTimeFormat; !errors.Is(err, wantErr) { + t.Errorf("Oras.Pack() error = %v, wantErr = %v", err, wantErr) } } -func Test_Pack_Image(t *testing.T) { +func Test_Pack_ImageRC2(t *testing.T) { s := memory.New() // prepare test content @@ -255,13 +280,13 @@ func Test_Pack_Image(t *testing.T) { t.Fatal("Store.Fetch().Close() error =", err) } - // test media type + // verify media type got := manifest.MediaType if got != ocispec.MediaTypeImageManifest { t.Fatalf("got media type = %s, want %s", got, ocispec.MediaTypeImageManifest) } - // test config + // verify config expectedConfigBytes := []byte("{}") expectedConfig := ocispec.Descriptor{ MediaType: artifactType, @@ -272,12 +297,12 @@ func Test_Pack_Image(t *testing.T) { t.Errorf("got config = %v, want %v", manifest.Config, expectedConfig) } - // test layers + // verify layers if !reflect.DeepEqual(manifest.Layers, layers) { t.Errorf("got layers = %v, want %v", manifest.Layers, layers) } - // test created time annotation + // verify created time annotation createdTime, ok := manifest.Annotations[ocispec.AnnotationCreated] if !ok { t.Errorf("Annotation %s = %v, want %v", ocispec.AnnotationCreated, ok, true) @@ -286,9 +311,14 @@ func Test_Pack_Image(t *testing.T) { if err != nil { t.Errorf("error parsing created time: %s, error = %v", createdTime, err) } + + // verify descriptor annotations + if want := manifest.Annotations; !reflect.DeepEqual(manifestDesc.Annotations, want) { + t.Errorf("got descriptor annotations = %v, want %v", manifestDesc.Annotations, want) + } } -func Test_Pack_Image_WithOptions(t *testing.T) { +func Test_Pack_ImageRC2_WithOptions(t *testing.T) { s := memory.New() // prepare test content @@ -355,9 +385,18 @@ func Test_Pack_Image_WithOptions(t *testing.T) { t.Errorf("Store.Fetch() = %v, want %v", string(got), string(expectedManifestBytes)) } + // verify descriptor + expectedManifestDesc := content.NewDescriptorFromBytes(expectedManifest.MediaType, expectedManifestBytes) + expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType + expectedManifestDesc.Annotations = expectedManifest.Annotations + if !reflect.DeepEqual(manifestDesc, expectedManifestDesc) { + t.Errorf("Pack() = %v, want %v", manifestDesc, expectedManifestDesc) + } + // test Pack without ConfigDescriptor opts = PackOptions{ PackImageManifest: true, + PackManifestType: PackManifestTypeImageV1_1_0_RC2, Subject: &subjectDesc, ConfigAnnotations: configAnnotations, ManifestAnnotations: annotations, @@ -399,9 +438,17 @@ func Test_Pack_Image_WithOptions(t *testing.T) { if !bytes.Equal(got, expectedManifestBytes) { t.Errorf("Store.Fetch() = %v, want %v", string(got), string(expectedManifestBytes)) } + + // verify descriptor + expectedManifestDesc = content.NewDescriptorFromBytes(expectedManifest.MediaType, expectedManifestBytes) + expectedManifestDesc.ArtifactType = expectedManifest.Config.MediaType + expectedManifestDesc.Annotations = expectedManifest.Annotations + if !reflect.DeepEqual(manifestDesc, expectedManifestDesc) { + t.Errorf("Pack() = %v, want %v", manifestDesc, expectedManifestDesc) + } } -func Test_Pack_Image_NoArtifactType(t *testing.T) { +func Test_Pack_ImageRC2_NoArtifactType(t *testing.T) { s := memory.New() ctx := context.Background() @@ -422,7 +469,7 @@ func Test_Pack_Image_NoArtifactType(t *testing.T) { t.Fatal("Store.Fetch().Close() error =", err) } - // test artifact type and config media type + // verify artifact type and config media type if manifestDesc.ArtifactType != MediaTypeUnknownConfig { t.Fatalf("got artifact type = %s, want %s", manifestDesc.ArtifactType, MediaTypeUnknownConfig) } @@ -431,7 +478,7 @@ func Test_Pack_Image_NoArtifactType(t *testing.T) { } } -func Test_Pack_Image_NoLayer(t *testing.T) { +func Test_Pack_ImageRC2_NoLayer(t *testing.T) { s := memory.New() // test Pack @@ -453,14 +500,14 @@ func Test_Pack_Image_NoLayer(t *testing.T) { t.Fatal("Store.Fetch().Close() error =", err) } - // test layers + // verify layers expectedLayers := []ocispec.Descriptor{} if !reflect.DeepEqual(manifest.Layers, expectedLayers) { t.Errorf("got layers = %v, want %v", manifest.Layers, expectedLayers) } } -func Test_Pack_Image_InvalidDateTimeFormat(t *testing.T) { +func Test_Pack_ImageRC2_InvalidDateTimeFormat(t *testing.T) { s := memory.New() ctx := context.Background() @@ -471,7 +518,414 @@ func Test_Pack_Image_InvalidDateTimeFormat(t *testing.T) { }, } _, err := Pack(ctx, s, "", nil, opts) - if err == nil || !errors.Is(err, ErrInvalidDateTimeFormat) { - t.Errorf("Oras.Pack() error = %v, wantErr = %v", err, ErrInvalidDateTimeFormat) + if wantErr := ErrInvalidDateTimeFormat; !errors.Is(err, wantErr) { + t.Errorf("Oras.Pack() error = %v, wantErr = %v", err, wantErr) + } +} + +func Test_Pack_ImageRC4(t *testing.T) { + s := memory.New() + + // prepare test content + layers := []ocispec.Descriptor{ + content.NewDescriptorFromBytes("test", []byte("hello world")), + content.NewDescriptorFromBytes("test", []byte("goodbye world")), + } + + // verify Pack + ctx := context.Background() + artifactType := "application/vnd.test" + opts := PackOptions{ + PackImageManifest: true, + PackManifestType: PackManifestTypeImageV1_1_0_RC4, + } + manifestDesc, err := Pack(ctx, s, artifactType, layers, opts) + if err != nil { + t.Fatal("Oras.Pack() error =", err) + } + + // verify manifest + var manifest ocispec.Manifest + rc, err := s.Fetch(ctx, manifestDesc) + if err != nil { + t.Fatal("Store.Fetch() error =", err) + } + if err := json.NewDecoder(rc).Decode(&manifest); err != nil { + t.Fatal("error decoding manifest, error =", err) + } + if err := rc.Close(); err != nil { + t.Fatal("Store.Fetch().Close() error =", err) + } + + // verify media type + got := manifest.MediaType + if got != ocispec.MediaTypeImageManifest { + t.Fatalf("got media type = %s, want %s", got, ocispec.MediaTypeImageManifest) + } + + // verify config + expectedConfig := ocispec.DescriptorEmptyJSON + if !reflect.DeepEqual(manifest.Config, expectedConfig) { + t.Errorf("got config = %v, want %v", manifest.Config, expectedConfig) + } + + // verify layers + if !reflect.DeepEqual(manifest.Layers, layers) { + t.Errorf("got layers = %v, want %v", manifest.Layers, layers) + } + + // verify created time annotation + createdTime, ok := manifest.Annotations[ocispec.AnnotationCreated] + if !ok { + t.Errorf("Annotation %s = %v, want %v", ocispec.AnnotationCreated, ok, true) + } + _, err = time.Parse(time.RFC3339, createdTime) + if err != nil { + t.Errorf("error parsing created time: %s, error = %v", createdTime, err) + } + + // verify artifact type + if !reflect.DeepEqual(manifest.ArtifactType, artifactType) { + t.Errorf("got artifactType = %v, want %v", manifest.ArtifactType, artifactType) + } + + // verify descriptor artifact type + if want := manifest.ArtifactType; !reflect.DeepEqual(manifestDesc.ArtifactType, want) { + t.Errorf("got descriptor artifactType = %v, want %v", manifestDesc.ArtifactType, want) + } + + // verify descriptor annotations + if want := manifest.Annotations; !reflect.DeepEqual(manifestDesc.Annotations, want) { + t.Errorf("got descriptor annotations = %v, want %v", manifestDesc.Annotations, want) + } +} + +func Test_Pack_ImageRC4_WithOptions(t *testing.T) { + s := memory.New() + + // prepare test content + layers := []ocispec.Descriptor{ + content.NewDescriptorFromBytes("test", []byte("hello world")), + content.NewDescriptorFromBytes("test", []byte("goodbye world")), + } + configBytes := []byte("config") + configDesc := content.NewDescriptorFromBytes("testconfig", configBytes) + configAnnotations := map[string]string{"foo": "bar"} + annotations := map[string]string{ + ocispec.AnnotationCreated: "2000-01-01T00:00:00Z", + } + artifactType := "application/vnd.test" + subjectManifest := []byte(`{"layers":[]}`) + subjectDesc := ocispec.Descriptor{ + MediaType: ocispec.MediaTypeImageManifest, + Digest: digest.FromBytes(subjectManifest), + Size: int64(len(subjectManifest)), + } + + // test Pack with ConfigDescriptor + ctx := context.Background() + opts := PackOptions{ + PackImageManifest: true, + PackManifestType: PackManifestTypeImageV1_1_0_RC4, + Subject: &subjectDesc, + ConfigDescriptor: &configDesc, + ConfigAnnotations: configAnnotations, + ManifestAnnotations: annotations, + } + manifestDesc, err := Pack(ctx, s, artifactType, layers, opts) + if err != nil { + t.Fatal("Oras.Pack() error =", err) + } + + expectedManifest := ocispec.Manifest{ + Versioned: specs.Versioned{ + SchemaVersion: 2, // historical value. does not pertain to OCI or docker version + }, + MediaType: ocispec.MediaTypeImageManifest, + ArtifactType: artifactType, + Subject: &subjectDesc, + Config: configDesc, + Layers: layers, + Annotations: annotations, + } + expectedManifestBytes, err := json.Marshal(expectedManifest) + if err != nil { + t.Fatal("failed to marshal manifest:", err) + } + + rc, err := s.Fetch(ctx, manifestDesc) + if err != nil { + t.Fatal("Store.Fetch() error =", err) + } + got, err := io.ReadAll(rc) + if err != nil { + t.Fatal("Store.Fetch().Read() error =", err) + } + err = rc.Close() + if err != nil { + t.Error("Store.Fetch().Close() error =", err) + } + if !bytes.Equal(got, expectedManifestBytes) { + t.Errorf("Store.Fetch() = %v, want %v", string(got), string(expectedManifestBytes)) + } + + // verify descriptor + expectedManifestDesc := content.NewDescriptorFromBytes(expectedManifest.MediaType, expectedManifestBytes) + expectedManifestDesc.ArtifactType = expectedManifest.ArtifactType + expectedManifestDesc.Annotations = expectedManifest.Annotations + if !reflect.DeepEqual(manifestDesc, expectedManifestDesc) { + t.Errorf("Pack() = %v, want %v", manifestDesc, expectedManifestDesc) + } + + // test Pack with ConfigDescriptor, but without artifactType + manifestDesc, err = Pack(ctx, s, "", layers, opts) + if err != nil { + t.Fatal("Oras.Pack() error =", err) + } + + expectedManifest = ocispec.Manifest{ + Versioned: specs.Versioned{ + SchemaVersion: 2, // historical value. does not pertain to OCI or docker version + }, + MediaType: ocispec.MediaTypeImageManifest, + ArtifactType: configDesc.MediaType, + Subject: &subjectDesc, + Config: configDesc, + Layers: layers, + Annotations: annotations, + } + expectedManifestBytes, err = json.Marshal(expectedManifest) + if err != nil { + t.Fatal("failed to marshal manifest:", err) + } + + rc, err = s.Fetch(ctx, manifestDesc) + if err != nil { + t.Fatal("Store.Fetch() error =", err) + } + got, err = io.ReadAll(rc) + if err != nil { + t.Fatal("Store.Fetch().Read() error =", err) + } + err = rc.Close() + if err != nil { + t.Error("Store.Fetch().Close() error =", err) + } + if !bytes.Equal(got, expectedManifestBytes) { + t.Errorf("Store.Fetch() = %v, want %v", string(got), string(expectedManifestBytes)) + } + + // verify descriptor + expectedManifestDesc = content.NewDescriptorFromBytes(expectedManifest.MediaType, expectedManifestBytes) + expectedManifestDesc.ArtifactType = expectedManifest.ArtifactType + expectedManifestDesc.Annotations = expectedManifest.Annotations + if !reflect.DeepEqual(manifestDesc, expectedManifestDesc) { + t.Errorf("Pack() = %v, want %v", manifestDesc, expectedManifestDesc) + } + + // test Pack without ConfigDescriptor + opts = PackOptions{ + PackImageManifest: true, + PackManifestType: PackManifestTypeImageV1_1_0_RC4, + Subject: &subjectDesc, + ConfigAnnotations: configAnnotations, + ManifestAnnotations: annotations, + } + manifestDesc, err = Pack(ctx, s, artifactType, layers, opts) + if err != nil { + t.Fatal("Oras.Pack() error =", err) + } + + expectedConfigDesc := ocispec.DescriptorEmptyJSON + expectedConfigDesc.Annotations = configAnnotations + expectedManifest = ocispec.Manifest{ + Versioned: specs.Versioned{ + SchemaVersion: 2, // historical value. does not pertain to OCI or docker version + }, + MediaType: ocispec.MediaTypeImageManifest, + ArtifactType: artifactType, + Subject: &subjectDesc, + Config: expectedConfigDesc, + Layers: layers, + Annotations: annotations, + } + expectedManifestBytes, err = json.Marshal(expectedManifest) + if err != nil { + t.Fatal("failed to marshal manifest:", err) + } + + rc, err = s.Fetch(ctx, manifestDesc) + if err != nil { + t.Fatal("Store.Fetch() error =", err) + } + got, err = io.ReadAll(rc) + if err != nil { + t.Fatal("Store.Fetch().Read() error =", err) + } + err = rc.Close() + if err != nil { + t.Error("Store.Fetch().Close() error =", err) + } + if !bytes.Equal(got, expectedManifestBytes) { + t.Errorf("Store.Fetch() = %v, want %v", string(got), string(expectedManifestBytes)) + } + + // verify descriptor + expectedManifestDesc = content.NewDescriptorFromBytes(expectedManifest.MediaType, expectedManifestBytes) + expectedManifestDesc.ArtifactType = expectedManifest.ArtifactType + expectedManifestDesc.Annotations = expectedManifest.Annotations + if !reflect.DeepEqual(manifestDesc, expectedManifestDesc) { + t.Errorf("Pack() = %v, want %v", manifestDesc, expectedManifestDesc) + } +} + +func Test_Pack_ImageRC4_NoArtifactType(t *testing.T) { + s := memory.New() + + ctx := context.Background() + opts := PackOptions{ + PackImageManifest: true, + PackManifestType: PackManifestTypeImageV1_1_0_RC4, + } + _, err := Pack(ctx, s, "", nil, opts) + if wantErr := ErrMissingArtifactType; !errors.Is(err, wantErr) { + t.Errorf("Oras.Pack() error = %v, wantErr = %v", err, wantErr) + } +} + +func Test_Pack_ImageRC4_NoLayer(t *testing.T) { + s := memory.New() + + // test Pack + ctx := context.Background() + opts := PackOptions{ + PackImageManifest: true, + PackManifestType: PackManifestTypeImageV1_1_0_RC4, + } + manifestDesc, err := Pack(ctx, s, "test", nil, opts) + if err != nil { + t.Fatal("Oras.Pack() error =", err) + } + + var manifest ocispec.Manifest + rc, err := s.Fetch(ctx, manifestDesc) + if err != nil { + t.Fatal("Store.Fetch() error =", err) + } + if err := json.NewDecoder(rc).Decode(&manifest); err != nil { + t.Fatal("error decoding manifest, error =", err) + } + if err := rc.Close(); err != nil { + t.Fatal("Store.Fetch().Close() error =", err) + } + + // verify layers + expectedLayers := []ocispec.Descriptor{ocispec.DescriptorEmptyJSON} + if !reflect.DeepEqual(manifest.Layers, expectedLayers) { + t.Errorf("got layers = %v, want %v", manifest.Layers, expectedLayers) + } +} + +func Test_Pack_ImageRC4_InvalidDateTimeFormat(t *testing.T) { + s := memory.New() + + ctx := context.Background() + opts := PackOptions{ + PackImageManifest: true, + PackManifestType: PackManifestTypeImageV1_1_0_RC4, + ManifestAnnotations: map[string]string{ + ocispec.AnnotationCreated: "2000/01/01 00:00:00", + }, + } + _, err := Pack(ctx, s, "test", nil, opts) + if wantErr := ErrInvalidDateTimeFormat; !errors.Is(err, wantErr) { + t.Errorf("Oras.Pack() error = %v, wantErr = %v", err, wantErr) + } +} + +func Test_Pack_DefaultPackOptions(t *testing.T) { + s := memory.New() + + // prepare test content + layers := []ocispec.Descriptor{ + content.NewDescriptorFromBytes("test", []byte("hello world")), + content.NewDescriptorFromBytes("test", []byte("goodbye world")), + } + + // verify Pack + ctx := context.Background() + artifactType := "application/vnd.test" + manifestDesc, err := Pack(ctx, s, artifactType, layers, DefaultPackOptions) + if err != nil { + t.Fatal("Oras.Pack() error =", err) + } + + // verify manifest + var manifest ocispec.Manifest + rc, err := s.Fetch(ctx, manifestDesc) + if err != nil { + t.Fatal("Store.Fetch() error =", err) + } + if err := json.NewDecoder(rc).Decode(&manifest); err != nil { + t.Fatal("error decoding manifest, error =", err) + } + if err := rc.Close(); err != nil { + t.Fatal("Store.Fetch().Close() error =", err) + } + + // verify media type + got := manifest.MediaType + if got != ocispec.MediaTypeImageManifest { + t.Fatalf("got media type = %s, want %s", got, ocispec.MediaTypeImageManifest) + } + + // verify config + expectedConfig := ocispec.DescriptorEmptyJSON + if !reflect.DeepEqual(manifest.Config, expectedConfig) { + t.Errorf("got config = %v, want %v", manifest.Config, expectedConfig) + } + + // verify layers + if !reflect.DeepEqual(manifest.Layers, layers) { + t.Errorf("got layers = %v, want %v", manifest.Layers, layers) + } + + // verify created time annotation + createdTime, ok := manifest.Annotations[ocispec.AnnotationCreated] + if !ok { + t.Errorf("Annotation %s = %v, want %v", ocispec.AnnotationCreated, ok, true) + } + _, err = time.Parse(time.RFC3339, createdTime) + if err != nil { + t.Errorf("error parsing created time: %s, error = %v", createdTime, err) + } + + // verify artifact type + if !reflect.DeepEqual(manifest.ArtifactType, artifactType) { + t.Errorf("got artifactType = %v, want %v", manifest.ArtifactType, artifactType) + } + + // verify descriptor artifact type + if want := manifest.ArtifactType; !reflect.DeepEqual(manifestDesc.ArtifactType, want) { + t.Errorf("got descriptor artifactType = %v, want %v", manifestDesc.ArtifactType, want) + } + + // verify descriptor annotations + if want := manifest.Annotations; !reflect.DeepEqual(manifestDesc.Annotations, want) { + t.Errorf("got descriptor annotations = %v, want %v", manifestDesc.Annotations, want) + } +} + +func Test_Pack_UnsupportedPackManifestType(t *testing.T) { + s := memory.New() + + ctx := context.Background() + opts := PackOptions{ + PackImageManifest: true, + PackManifestType: -1, + } + _, err := Pack(ctx, s, "", nil, opts) + if wantErr := errdef.ErrUnsupported; !errors.Is(err, wantErr) { + t.Errorf("Oras.Pack() error = %v, wantErr = %v", err, wantErr) } }