blob: d2bcd673862967fef2dadc6c4e29b73d1f103306 [file] [log] [blame]
// Copyright The Monogon Project Authors.
// SPDX-License-Identifier: Apache-2.0
package main
import (
"crypto/sha256"
"encoding/json"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go"
ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1"
"source.monogon.dev/osbase/oci"
)
var (
outPath = flag.String("out", "", "Output OCI Image Layout directory path")
)
func addImage(outPath string, path string, haveBlob map[digest.Digest]bool) (*ocispecv1.Descriptor, error) {
index, err := oci.ReadLayoutIndex(path)
if err != nil {
return nil, err
}
if len(index.Manifest.Manifests) == 0 {
return nil, fmt.Errorf("index.json contains no manifests")
}
if len(index.Manifest.Manifests) != 1 {
return nil, fmt.Errorf("index.json files containing multiple manifests are not supported")
}
manifestDescriptor := &index.Manifest.Manifests[0]
image, err := oci.AsImage(index.Ref(manifestDescriptor))
if err != nil {
return nil, err
}
// Create symlinks to blobs
descriptors := []ocispecv1.Descriptor{*manifestDescriptor, image.Manifest.Config}
descriptors = append(descriptors, image.Manifest.Layers...)
for _, descriptor := range descriptors {
if haveBlob[descriptor.Digest] {
continue
}
haveBlob[descriptor.Digest] = true
algorithm, encoded, err := oci.ParseDigest(string(descriptor.Digest))
if err != nil {
return nil, fmt.Errorf("failed to parse digest: %w", err)
}
srcPath := filepath.Join(path, "blobs", algorithm, encoded)
destDir := filepath.Join(outPath, "blobs", algorithm)
destPath := filepath.Join(outPath, "blobs", algorithm, encoded)
relPath, err := filepath.Rel(destDir, srcPath)
if err != nil {
return nil, err
}
err = os.Symlink(relPath, destPath)
if err != nil {
return nil, err
}
}
return manifestDescriptor, nil
}
func main() {
var images []string
flag.Func("image", "OCI image path", func(path string) error {
images = append(images, path)
return nil
})
flag.Parse()
// Create blobs directory.
blobsPath := filepath.Join(*outPath, "blobs", "sha256")
err := os.MkdirAll(blobsPath, 0755)
if err != nil {
log.Fatal(err)
}
haveBlob := make(map[digest.Digest]bool)
index := ocispecv1.Index{
Versioned: ocispec.Versioned{SchemaVersion: 2},
MediaType: ocispecv1.MediaTypeImageIndex,
Manifests: []ocispecv1.Descriptor{},
}
for _, path := range images {
descriptor, err := addImage(*outPath, path, haveBlob)
if err != nil {
log.Fatalf("Failed to add image %q: %v", path, err)
}
index.Manifests = append(index.Manifests, *descriptor)
}
// Write the index manifest.
indexBytes, err := json.MarshalIndent(index, "", "\t")
if err != nil {
log.Fatalf("Failed to marshal index manifest: %v", err)
}
indexBytes = append(indexBytes, '\n')
indexHash := fmt.Sprintf("%x", sha256.Sum256(indexBytes))
err = os.WriteFile(filepath.Join(blobsPath, indexHash), indexBytes, 0644)
if err != nil {
log.Fatalf("Failed to write index manifest: %v", err)
}
// Write the entry-point index.
topIndex := ocispecv1.Index{
Versioned: ocispec.Versioned{SchemaVersion: 2},
MediaType: ocispecv1.MediaTypeImageIndex,
Manifests: []ocispecv1.Descriptor{{
MediaType: ocispecv1.MediaTypeImageIndex,
Digest: digest.NewDigestFromEncoded(digest.SHA256, indexHash),
Size: int64(len(indexBytes)),
}},
}
topIndexBytes, err := json.MarshalIndent(topIndex, "", "\t")
if err != nil {
log.Fatalf("Failed to marshal entry-point index: %v", err)
}
topIndexBytes = append(topIndexBytes, '\n')
err = os.WriteFile(filepath.Join(*outPath, "index.json"), topIndexBytes, 0644)
if err != nil {
log.Fatalf("Failed to write entry-point index: %v", err)
}
// Write the oci-layout marker file.
err = os.WriteFile(
filepath.Join(*outPath, "oci-layout"),
[]byte(`{"imageLayoutVersion": "1.0.0"}`+"\n"),
0644,
)
if err != nil {
log.Fatalf("Failed to write oci-layout file: %v", err)
}
}