github.com/moby/docker@v26.1.3+incompatible/internal/testutils/specialimage/multilayer.go (about) 1 package specialimage 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "io" 7 "os" 8 "path/filepath" 9 10 "github.com/containerd/containerd/platforms" 11 "github.com/distribution/reference" 12 "github.com/docker/docker/pkg/archive" 13 "github.com/google/uuid" 14 "github.com/opencontainers/go-digest" 15 "github.com/opencontainers/image-spec/specs-go" 16 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 17 ) 18 19 type SingleFileLayer struct { 20 Name string 21 Content []byte 22 } 23 24 func MultiLayer(dir string) (*ocispec.Index, error) { 25 return MultiLayerCustom(dir, "multilayer:latest", []SingleFileLayer{ 26 {Name: "foo", Content: []byte("1")}, 27 {Name: "bar", Content: []byte("2")}, 28 {Name: "hello", Content: []byte("world")}, 29 }) 30 } 31 32 func MultiLayerCustom(dir string, imageRef string, layers []SingleFileLayer) (*ocispec.Index, error) { 33 var layerDescs []ocispec.Descriptor 34 var layerDgsts []digest.Digest 35 var layerBlobs []string 36 for _, layer := range layers { 37 layerDesc, err := writeLayerWithOneFile(dir, layer.Name, layer.Content) 38 if err != nil { 39 return nil, err 40 } 41 42 layerDescs = append(layerDescs, layerDesc) 43 layerDgsts = append(layerDgsts, layerDesc.Digest) 44 layerBlobs = append(layerBlobs, blobPath(layerDesc)) 45 } 46 47 configDesc, err := writeJsonBlob(dir, ocispec.MediaTypeImageConfig, ocispec.Image{ 48 Platform: platforms.DefaultSpec(), 49 Config: ocispec.ImageConfig{ 50 Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}, 51 }, 52 RootFS: ocispec.RootFS{ 53 Type: "layers", 54 DiffIDs: layerDgsts, 55 }, 56 }) 57 if err != nil { 58 return nil, err 59 } 60 61 manifest := ocispec.Manifest{ 62 MediaType: ocispec.MediaTypeImageManifest, 63 Config: configDesc, 64 Layers: layerDescs, 65 } 66 67 legacyManifests := []manifestItem{ 68 { 69 Config: blobPath(configDesc), 70 RepoTags: []string{imageRef}, 71 Layers: layerBlobs, 72 }, 73 } 74 75 ref, err := reference.ParseNormalizedNamed(imageRef) 76 if err != nil { 77 return nil, err 78 } 79 return singlePlatformImage(dir, ref, manifest, legacyManifests) 80 } 81 82 // Legacy manifest item (manifests.json) 83 type manifestItem struct { 84 Config string 85 RepoTags []string 86 Layers []string 87 } 88 89 func singlePlatformImage(dir string, ref reference.Named, manifest ocispec.Manifest, legacyManifests []manifestItem) (*ocispec.Index, error) { 90 manifestDesc, err := writeJsonBlob(dir, ocispec.MediaTypeImageManifest, manifest) 91 if err != nil { 92 return nil, err 93 } 94 95 if ref != nil { 96 manifestDesc.Annotations = map[string]string{ 97 "io.containerd.image.name": ref.String(), 98 } 99 100 if tagged, ok := ref.(reference.Tagged); ok { 101 manifestDesc.Annotations[ocispec.AnnotationRefName] = tagged.Tag() 102 } 103 } 104 105 if err := writeJson(legacyManifests, filepath.Join(dir, "manifest.json")); err != nil { 106 return nil, err 107 } 108 109 return ociImage(dir, ref, manifestDesc) 110 } 111 112 func ociImage(dir string, ref reference.Named, target ocispec.Descriptor) (*ocispec.Index, error) { 113 idx := ocispec.Index{ 114 Versioned: specs.Versioned{SchemaVersion: 2}, 115 MediaType: ocispec.MediaTypeImageIndex, 116 Manifests: []ocispec.Descriptor{target}, 117 } 118 if err := writeJson(idx, filepath.Join(dir, "index.json")); err != nil { 119 return nil, err 120 } 121 122 err := os.WriteFile(filepath.Join(dir, "oci-layout"), []byte(`{"imageLayoutVersion": "1.0.0"}`), 0o644) 123 if err != nil { 124 return nil, err 125 } 126 127 return &idx, nil 128 } 129 130 func fileArchive(dir string, name string, content []byte) (io.ReadCloser, error) { 131 tmp, err := os.MkdirTemp("", "") 132 if err != nil { 133 return nil, err 134 } 135 136 if err := os.WriteFile(filepath.Join(tmp, name), content, 0o644); err != nil { 137 return nil, err 138 } 139 140 return archive.Tar(tmp, archive.Uncompressed) 141 } 142 143 func writeLayerWithOneFile(dir string, filename string, content []byte) (ocispec.Descriptor, error) { 144 rd, err := fileArchive(dir, filename, content) 145 if err != nil { 146 return ocispec.Descriptor{}, err 147 } 148 defer rd.Close() 149 150 return writeBlob(dir, ocispec.MediaTypeImageLayer, rd) 151 } 152 153 func writeJsonBlob(dir string, mt string, obj any) (ocispec.Descriptor, error) { 154 b, err := json.Marshal(obj) 155 if err != nil { 156 return ocispec.Descriptor{}, err 157 } 158 159 return writeBlob(dir, mt, bytes.NewReader(b)) 160 } 161 162 func writeJson(obj any, path string) error { 163 b, err := json.Marshal(obj) 164 if err != nil { 165 return err 166 } 167 168 return os.WriteFile(path, b, 0o644) 169 } 170 171 func writeBlob(dir string, mt string, rd io.Reader) (_ ocispec.Descriptor, outErr error) { 172 digester := digest.Canonical.Digester() 173 hashTee := io.TeeReader(rd, digester.Hash()) 174 175 blobsPath := filepath.Join(dir, "blobs", "sha256") 176 if err := os.MkdirAll(blobsPath, 0o755); err != nil { 177 return ocispec.Descriptor{}, err 178 } 179 180 tmpPath := filepath.Join(blobsPath, uuid.New().String()) 181 file, err := os.Create(tmpPath) 182 if err != nil { 183 return ocispec.Descriptor{}, err 184 } 185 186 defer func() { 187 if outErr != nil { 188 file.Close() 189 os.Remove(tmpPath) 190 } 191 }() 192 193 if _, err := io.Copy(file, hashTee); err != nil { 194 return ocispec.Descriptor{}, err 195 } 196 197 digest := digester.Digest() 198 199 stat, err := os.Stat(tmpPath) 200 if err != nil { 201 return ocispec.Descriptor{}, err 202 } 203 204 file.Close() 205 if err := os.Rename(tmpPath, filepath.Join(blobsPath, digest.Encoded())); err != nil { 206 return ocispec.Descriptor{}, err 207 } 208 209 return ocispec.Descriptor{ 210 MediaType: mt, 211 Digest: digest, 212 Size: stat.Size(), 213 }, nil 214 } 215 216 func blobPath(desc ocispec.Descriptor) string { 217 return "blobs/sha256/" + desc.Digest.Encoded() 218 }