blob: d2bcd673862967fef2dadc6c4e29b73d1f103306 [file] [log] [blame]
Jan Schär954c4b32025-07-18 09:46:25 +02001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
4package main
5
6import (
7 "crypto/sha256"
8 "encoding/json"
9 "flag"
10 "fmt"
11 "log"
12 "os"
13 "path/filepath"
14
15 "github.com/opencontainers/go-digest"
16 ocispec "github.com/opencontainers/image-spec/specs-go"
17 ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1"
18
19 "source.monogon.dev/osbase/oci"
20)
21
22var (
23 outPath = flag.String("out", "", "Output OCI Image Layout directory path")
24)
25
26func addImage(outPath string, path string, haveBlob map[digest.Digest]bool) (*ocispecv1.Descriptor, error) {
27 index, err := oci.ReadLayoutIndex(path)
28 if err != nil {
29 return nil, err
30 }
31 if len(index.Manifest.Manifests) == 0 {
32 return nil, fmt.Errorf("index.json contains no manifests")
33 }
34 if len(index.Manifest.Manifests) != 1 {
35 return nil, fmt.Errorf("index.json files containing multiple manifests are not supported")
36 }
37 manifestDescriptor := &index.Manifest.Manifests[0]
38
39 image, err := oci.AsImage(index.Ref(manifestDescriptor))
40 if err != nil {
41 return nil, err
42 }
43
44 // Create symlinks to blobs
45 descriptors := []ocispecv1.Descriptor{*manifestDescriptor, image.Manifest.Config}
46 descriptors = append(descriptors, image.Manifest.Layers...)
47 for _, descriptor := range descriptors {
48 if haveBlob[descriptor.Digest] {
49 continue
50 }
51 haveBlob[descriptor.Digest] = true
52
53 algorithm, encoded, err := oci.ParseDigest(string(descriptor.Digest))
54 if err != nil {
55 return nil, fmt.Errorf("failed to parse digest: %w", err)
56 }
57 srcPath := filepath.Join(path, "blobs", algorithm, encoded)
58 destDir := filepath.Join(outPath, "blobs", algorithm)
59 destPath := filepath.Join(outPath, "blobs", algorithm, encoded)
60 relPath, err := filepath.Rel(destDir, srcPath)
61 if err != nil {
62 return nil, err
63 }
64 err = os.Symlink(relPath, destPath)
65 if err != nil {
66 return nil, err
67 }
68 }
69
70 return manifestDescriptor, nil
71}
72
73func main() {
74 var images []string
75 flag.Func("image", "OCI image path", func(path string) error {
76 images = append(images, path)
77 return nil
78 })
79 flag.Parse()
80
81 // Create blobs directory.
82 blobsPath := filepath.Join(*outPath, "blobs", "sha256")
83 err := os.MkdirAll(blobsPath, 0755)
84 if err != nil {
85 log.Fatal(err)
86 }
87
88 haveBlob := make(map[digest.Digest]bool)
89 index := ocispecv1.Index{
90 Versioned: ocispec.Versioned{SchemaVersion: 2},
91 MediaType: ocispecv1.MediaTypeImageIndex,
92 Manifests: []ocispecv1.Descriptor{},
93 }
94 for _, path := range images {
95 descriptor, err := addImage(*outPath, path, haveBlob)
96 if err != nil {
97 log.Fatalf("Failed to add image %q: %v", path, err)
98 }
99 index.Manifests = append(index.Manifests, *descriptor)
100 }
101
102 // Write the index manifest.
103 indexBytes, err := json.MarshalIndent(index, "", "\t")
104 if err != nil {
105 log.Fatalf("Failed to marshal index manifest: %v", err)
106 }
107 indexBytes = append(indexBytes, '\n')
108 indexHash := fmt.Sprintf("%x", sha256.Sum256(indexBytes))
109 err = os.WriteFile(filepath.Join(blobsPath, indexHash), indexBytes, 0644)
110 if err != nil {
111 log.Fatalf("Failed to write index manifest: %v", err)
112 }
113
114 // Write the entry-point index.
115 topIndex := ocispecv1.Index{
116 Versioned: ocispec.Versioned{SchemaVersion: 2},
117 MediaType: ocispecv1.MediaTypeImageIndex,
118 Manifests: []ocispecv1.Descriptor{{
119 MediaType: ocispecv1.MediaTypeImageIndex,
120 Digest: digest.NewDigestFromEncoded(digest.SHA256, indexHash),
121 Size: int64(len(indexBytes)),
122 }},
123 }
124 topIndexBytes, err := json.MarshalIndent(topIndex, "", "\t")
125 if err != nil {
126 log.Fatalf("Failed to marshal entry-point index: %v", err)
127 }
128 topIndexBytes = append(topIndexBytes, '\n')
129 err = os.WriteFile(filepath.Join(*outPath, "index.json"), topIndexBytes, 0644)
130 if err != nil {
131 log.Fatalf("Failed to write entry-point index: %v", err)
132 }
133
134 // Write the oci-layout marker file.
135 err = os.WriteFile(
136 filepath.Join(*outPath, "oci-layout"),
137 []byte(`{"imageLayoutVersion": "1.0.0"}`+"\n"),
138 0644,
139 )
140 if err != nil {
141 log.Fatalf("Failed to write oci-layout file: %v", err)
142 }
143}