| package specialimage | |
| import ( | |
| "bytes" | |
| "encoding/json" | |
| "io" | |
| "os" | |
| "path/filepath" | |
| "github.com/containerd/platforms" | |
| "github.com/distribution/reference" | |
| "github.com/docker/docker/pkg/archive" | |
| "github.com/google/uuid" | |
| "github.com/opencontainers/go-digest" | |
| "github.com/opencontainers/image-spec/specs-go" | |
| ocispec "github.com/opencontainers/image-spec/specs-go/v1" | |
| ) | |
| type SingleFileLayer struct { | |
| Name string | |
| Content []byte | |
| } | |
| func MultiLayer(dir string) (*ocispec.Index, error) { | |
| return MultiLayerCustom(dir, "multilayer:latest", []SingleFileLayer{ | |
| {Name: "foo", Content: []byte("1")}, | |
| {Name: "bar", Content: []byte("2")}, | |
| {Name: "hello", Content: []byte("world")}, | |
| }) | |
| } | |
| func MultiLayerCustom(dir string, imageRef string, layers []SingleFileLayer) (*ocispec.Index, error) { | |
| var layerDescs []ocispec.Descriptor | |
| var layerDgsts []digest.Digest | |
| var layerBlobs []string | |
| for _, layer := range layers { | |
| layerDesc, err := writeLayerWithOneFile(dir, layer.Name, layer.Content) | |
| if err != nil { | |
| return nil, err | |
| } | |
| layerDescs = append(layerDescs, layerDesc) | |
| layerDgsts = append(layerDgsts, layerDesc.Digest) | |
| layerBlobs = append(layerBlobs, blobPath(layerDesc)) | |
| } | |
| configDesc, err := writeJsonBlob(dir, ocispec.MediaTypeImageConfig, ocispec.Image{ | |
| Platform: platforms.DefaultSpec(), | |
| Config: ocispec.ImageConfig{ | |
| Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}, | |
| }, | |
| RootFS: ocispec.RootFS{ | |
| Type: "layers", | |
| DiffIDs: layerDgsts, | |
| }, | |
| }) | |
| if err != nil { | |
| return nil, err | |
| } | |
| manifest := ocispec.Manifest{ | |
| MediaType: ocispec.MediaTypeImageManifest, | |
| Config: configDesc, | |
| Layers: layerDescs, | |
| } | |
| legacyManifests := []manifestItem{ | |
| { | |
| Config: blobPath(configDesc), | |
| RepoTags: []string{imageRef}, | |
| Layers: layerBlobs, | |
| }, | |
| } | |
| ref, err := reference.ParseNormalizedNamed(imageRef) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return singlePlatformImage(dir, ref, manifest, legacyManifests) | |
| } | |
| // Legacy manifest item (manifests.json) | |
| type manifestItem struct { | |
| Config string | |
| RepoTags []string | |
| Layers []string | |
| } | |
| func singlePlatformImage(dir string, ref reference.Named, manifest ocispec.Manifest, legacyManifests []manifestItem) (*ocispec.Index, error) { | |
| manifestDesc, err := writeJsonBlob(dir, ocispec.MediaTypeImageManifest, manifest) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if ref != nil { | |
| manifestDesc.Annotations = map[string]string{ | |
| "io.containerd.image.name": ref.String(), | |
| } | |
| if tagged, ok := ref.(reference.Tagged); ok { | |
| manifestDesc.Annotations[ocispec.AnnotationRefName] = tagged.Tag() | |
| } | |
| } | |
| if err := writeJson(legacyManifests, filepath.Join(dir, "manifest.json")); err != nil { | |
| return nil, err | |
| } | |
| return ociImage(dir, ref, manifestDesc) | |
| } | |
| func ociImage(dir string, ref reference.Named, target ocispec.Descriptor) (*ocispec.Index, error) { | |
| idx := ocispec.Index{ | |
| Versioned: specs.Versioned{SchemaVersion: 2}, | |
| MediaType: ocispec.MediaTypeImageIndex, | |
| Manifests: []ocispec.Descriptor{target}, | |
| } | |
| if err := writeJson(idx, filepath.Join(dir, "index.json")); err != nil { | |
| return nil, err | |
| } | |
| err := os.WriteFile(filepath.Join(dir, "oci-layout"), []byte(`{"imageLayoutVersion": "1.0.0"}`), 0o644) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return &idx, nil | |
| } | |
| func fileArchive(dir string, name string, content []byte) (io.ReadCloser, error) { | |
| tmp, err := os.MkdirTemp("", "") | |
| if err != nil { | |
| return nil, err | |
| } | |
| if err := os.WriteFile(filepath.Join(tmp, name), content, 0o644); err != nil { | |
| return nil, err | |
| } | |
| return archive.Tar(tmp, archive.Uncompressed) | |
| } | |
| func writeLayerWithOneFile(dir string, filename string, content []byte) (ocispec.Descriptor, error) { | |
| rd, err := fileArchive(dir, filename, content) | |
| if err != nil { | |
| return ocispec.Descriptor{}, err | |
| } | |
| defer rd.Close() | |
| return writeBlob(dir, ocispec.MediaTypeImageLayer, rd) | |
| } | |
| func writeJsonBlob(dir string, mt string, obj any) (ocispec.Descriptor, error) { | |
| b, err := json.Marshal(obj) | |
| if err != nil { | |
| return ocispec.Descriptor{}, err | |
| } | |
| return writeBlob(dir, mt, bytes.NewReader(b)) | |
| } | |
| func writeJson(obj any, path string) error { | |
| b, err := json.Marshal(obj) | |
| if err != nil { | |
| return err | |
| } | |
| return os.WriteFile(path, b, 0o644) | |
| } | |
| func writeBlob(dir string, mt string, rd io.Reader) (_ ocispec.Descriptor, outErr error) { | |
| digester := digest.Canonical.Digester() | |
| hashTee := io.TeeReader(rd, digester.Hash()) | |
| blobsPath := filepath.Join(dir, "blobs", "sha256") | |
| if err := os.MkdirAll(blobsPath, 0o755); err != nil { | |
| return ocispec.Descriptor{}, err | |
| } | |
| tmpPath := filepath.Join(blobsPath, uuid.New().String()) | |
| file, err := os.Create(tmpPath) | |
| if err != nil { | |
| return ocispec.Descriptor{}, err | |
| } | |
| defer func() { | |
| if outErr != nil { | |
| file.Close() | |
| os.Remove(tmpPath) | |
| } | |
| }() | |
| if _, err := io.Copy(file, hashTee); err != nil { | |
| return ocispec.Descriptor{}, err | |
| } | |
| digest := digester.Digest() | |
| stat, err := os.Stat(tmpPath) | |
| if err != nil { | |
| return ocispec.Descriptor{}, err | |
| } | |
| file.Close() | |
| if err := os.Rename(tmpPath, filepath.Join(blobsPath, digest.Encoded())); err != nil { | |
| return ocispec.Descriptor{}, err | |
| } | |
| return ocispec.Descriptor{ | |
| MediaType: mt, | |
| Digest: digest, | |
| Size: stat.Size(), | |
| }, nil | |
| } | |
| func blobPath(desc ocispec.Descriptor) string { | |
| return "blobs/sha256/" + desc.Digest.Encoded() | |
| } |