Skip to content

Commit

Permalink
publish: Build OCI compliant app manifest
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
mike-sul committed Oct 11, 2024
1 parent ed9b76c commit 71fdb75
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 21 deletions.
98 changes: 98 additions & 0 deletions internal/app_manifest_builder.go
Original file line number Diff line number Diff line change
@@ -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
}
27 changes: 6 additions & 21 deletions internal/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"compress/gzip"
"context"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"gopkg.in/yaml.v3"
Expand Down Expand Up @@ -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
}
Expand All @@ -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
Expand All @@ -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
Expand Down

0 comments on commit 71fdb75

Please sign in to comment.