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  }