From 42a8ff96c9951d39855a0796e8d3de9451bfc4c5 Mon Sep 17 00:00:00 2001 From: Mike Sul Date: Wed, 9 Oct 2024 16:10:29 +0200 Subject: [PATCH] publish: Build OCI compliant app manifest Encapsulate the compose app manifest creation functionality in a separate file and object. Use the empty descriptor as a value of the app manifest config field as the OCI image specification instructs. Signed-off-by: Mike Sul --- internal/app_manifest_builder.go | 98 ++++++++++++++++++++++++++++++++ internal/publish.go | 27 ++------- 2 files changed, 104 insertions(+), 21 deletions(-) create mode 100644 internal/app_manifest_builder.go diff --git a/internal/app_manifest_builder.go b/internal/app_manifest_builder.go new file mode 100644 index 0000000..0f2b524 --- /dev/null +++ b/internal/app_manifest_builder.go @@ -0,0 +1,98 @@ +//go:build publish + +package internal + +import ( + "context" + "encoding/json" + "github.com/docker/distribution" + "github.com/docker/distribution/manifest" + "github.com/docker/distribution/manifest/ocischema" + v1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +type ( + AppManifest struct { + ocischema.Manifest + // ArtifactType is the IANA media type of the artifact this schema refers to. + ArtifactType string `json:"artifactType,omitempty"` + // This field breaks the OCI image specification. It should be removed once all devices switch to version >= v93 + Manifests []distribution.Descriptor `json:"manifests,omitempty"` + } + ManifestBuilder struct { + bs distribution.BlobService + manifest AppManifest + } +) + +var ( + AppManifestTemplate = AppManifest{ + Manifest: ocischema.Manifest{ + Versioned: manifest.Versioned{ + SchemaVersion: 2, + MediaType: v1.MediaTypeImageManifest, + }, + // Set the empty descriptor for the config as the specification guides + // https://github.com/opencontainers/image-spec/blob/main/manifest.md#guidance-for-an-empty-descriptor + Config: distribution.Descriptor{ + MediaType: v1.DescriptorEmptyJSON.MediaType, + Digest: v1.DescriptorEmptyJSON.Digest, + Size: v1.DescriptorEmptyJSON.Size, + }, + Annotations: map[string]string{"compose-app": "v1"}, + }, + ArtifactType: "application/vnd.fio+compose-app", + } +) + +func NewManifestBuilder(bs distribution.BlobService) distribution.ManifestBuilder { + return &ManifestBuilder{ + bs: bs, + manifest: AppManifestTemplate, + } +} + +func (mb *ManifestBuilder) Build(ctx context.Context) (distribution.Manifest, error) { + _, err := mb.bs.Stat(ctx, mb.manifest.Config.Digest) + switch err { + case nil: + // Config blob is present in the blob store + return fromStruct(mb.manifest) + case distribution.ErrBlobUnknown: + // nop + default: + return nil, err + } + // Add config to the blob store + _, err = mb.bs.Put(ctx, mb.manifest.Config.MediaType, v1.DescriptorEmptyJSON.Data) + if err != nil { + return nil, err + } + return fromStruct(mb.manifest) +} + +// AppendReference adds a reference to the current ManifestBuilder. +func (mb *ManifestBuilder) AppendReference(d distribution.Describable) error { + mb.manifest.Layers = append(mb.manifest.Layers, d.Descriptor()) + return nil +} + +// References returns the current references added to this builder. +func (mb *ManifestBuilder) References() []distribution.Descriptor { + return mb.manifest.Layers +} + +func (mb *ManifestBuilder) SetLayerMetaManifests(manifests []distribution.Descriptor) { + mb.manifest.Manifests = manifests +} + +func fromStruct(m AppManifest) (*ocischema.DeserializedManifest, error) { + canonical, err := json.MarshalIndent(&m, "", " ") + + dm := ocischema.DeserializedManifest{} + err = dm.UnmarshalJSON(canonical) + if err != nil { + return nil, err + } + return &dm, err +} diff --git a/internal/publish.go b/internal/publish.go index 7c819cf..a82bc03 100644 --- a/internal/publish.go +++ b/internal/publish.go @@ -8,7 +8,6 @@ import ( "compress/gzip" "context" "crypto/sha256" - "encoding/json" "errors" "fmt" "gopkg.in/yaml.v3" @@ -274,7 +273,7 @@ func CreateApp(ctx context.Context, config map[string]interface{}, target string } fmt.Println(" |-> app blob: ", desc.Digest.String()) - mb := ocischema.NewManifestBuilder(blobStore, []byte{}, map[string]string{"compose-app": "v1"}) + mb := NewManifestBuilder(blobStore) if err := mb.AppendReference(desc); err != nil { return "", err } @@ -291,6 +290,10 @@ func CreateApp(ctx context.Context, config map[string]interface{}, target string } } + if layerManifests != nil { + mb.(*ManifestBuilder).SetLayerMetaManifests(layerManifests) + } + manifest, err := mb.Build(ctx) if err != nil { return "", err @@ -300,29 +303,11 @@ func CreateApp(ctx context.Context, config map[string]interface{}, target string if !ok { return "", fmt.Errorf("invalid manifest type, expected *ocischema.DeserializedManifest, got: %T", manifest) } - b, err := man.MarshalJSON() + _, b, err := man.Payload() if err != nil { return "", err } - if layerManifests != nil { - manMap := make(map[string]interface{}) - err = json.Unmarshal(b, &manMap) - if err != nil { - return "", err - } - - manMap["manifests"] = layerManifests - b, err = json.MarshalIndent(manMap, "", " ") - if err != nil { - return "", err - } - err = man.UnmarshalJSON(b) - if err != nil { - return "", err - } - } - fmt.Printf(" |-> manifest size: %d\n", len(b)) // TODO: this check is needed in order to overcome the aklite's check on the maximum manifest size (2048) // Once the new version of aklite is deployed (max manifest size = 16K) then this check can be removed or MaxArchNumb increased