mirror of https://github.com/k3d-io/k3d
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
430 lines
10 KiB
430 lines
10 KiB
1 year ago
|
// Copyright 2018 Google LLC All Rights Reserved.
|
||
|
//
|
||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
// you may not use this file except in compliance with the License.
|
||
|
// You may obtain a copy of the License at
|
||
|
//
|
||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||
|
//
|
||
|
// Unless required by applicable law or agreed to in writing, software
|
||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
// See the License for the specific language governing permissions and
|
||
|
// limitations under the License.
|
||
|
|
||
|
package tarball
|
||
|
|
||
|
import (
|
||
|
"archive/tar"
|
||
|
"bytes"
|
||
|
"encoding/json"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"os"
|
||
|
"path"
|
||
|
"path/filepath"
|
||
|
"sync"
|
||
|
|
||
|
comp "github.com/google/go-containerregistry/internal/compression"
|
||
|
"github.com/google/go-containerregistry/pkg/compression"
|
||
|
"github.com/google/go-containerregistry/pkg/name"
|
||
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||
|
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||
|
"github.com/google/go-containerregistry/pkg/v1/types"
|
||
|
)
|
||
|
|
||
|
type image struct {
|
||
|
opener Opener
|
||
|
manifest *Manifest
|
||
|
config []byte
|
||
|
imgDescriptor *Descriptor
|
||
|
|
||
|
tag *name.Tag
|
||
|
}
|
||
|
|
||
|
type uncompressedImage struct {
|
||
|
*image
|
||
|
}
|
||
|
|
||
|
type compressedImage struct {
|
||
|
*image
|
||
|
manifestLock sync.Mutex // Protects manifest
|
||
|
manifest *v1.Manifest
|
||
|
}
|
||
|
|
||
|
var _ partial.UncompressedImageCore = (*uncompressedImage)(nil)
|
||
|
var _ partial.CompressedImageCore = (*compressedImage)(nil)
|
||
|
|
||
|
// Opener is a thunk for opening a tar file.
|
||
|
type Opener func() (io.ReadCloser, error)
|
||
|
|
||
|
func pathOpener(path string) Opener {
|
||
|
return func() (io.ReadCloser, error) {
|
||
|
return os.Open(path)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// ImageFromPath returns a v1.Image from a tarball located on path.
|
||
|
func ImageFromPath(path string, tag *name.Tag) (v1.Image, error) {
|
||
|
return Image(pathOpener(path), tag)
|
||
|
}
|
||
|
|
||
|
// LoadManifest load manifest
|
||
|
func LoadManifest(opener Opener) (Manifest, error) {
|
||
|
m, err := extractFileFromTar(opener, "manifest.json")
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer m.Close()
|
||
|
|
||
|
var manifest Manifest
|
||
|
|
||
|
if err := json.NewDecoder(m).Decode(&manifest); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return manifest, nil
|
||
|
}
|
||
|
|
||
|
// Image exposes an image from the tarball at the provided path.
|
||
|
func Image(opener Opener, tag *name.Tag) (v1.Image, error) {
|
||
|
img := &image{
|
||
|
opener: opener,
|
||
|
tag: tag,
|
||
|
}
|
||
|
if err := img.loadTarDescriptorAndConfig(); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// Peek at the first layer and see if it's compressed.
|
||
|
if len(img.imgDescriptor.Layers) > 0 {
|
||
|
compressed, err := img.areLayersCompressed()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
if compressed {
|
||
|
c := compressedImage{
|
||
|
image: img,
|
||
|
}
|
||
|
return partial.CompressedToImage(&c)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
uc := uncompressedImage{
|
||
|
image: img,
|
||
|
}
|
||
|
return partial.UncompressedToImage(&uc)
|
||
|
}
|
||
|
|
||
|
func (i *image) MediaType() (types.MediaType, error) {
|
||
|
return types.DockerManifestSchema2, nil
|
||
|
}
|
||
|
|
||
|
// Descriptor stores the manifest data for a single image inside a `docker save` tarball.
|
||
|
type Descriptor struct {
|
||
|
Config string
|
||
|
RepoTags []string
|
||
|
Layers []string
|
||
|
|
||
|
// Tracks foreign layer info. Key is DiffID.
|
||
|
LayerSources map[v1.Hash]v1.Descriptor `json:",omitempty"`
|
||
|
}
|
||
|
|
||
|
// Manifest represents the manifests of all images as the `manifest.json` file in a `docker save` tarball.
|
||
|
type Manifest []Descriptor
|
||
|
|
||
|
func (m Manifest) findDescriptor(tag *name.Tag) (*Descriptor, error) {
|
||
|
if tag == nil {
|
||
|
if len(m) != 1 {
|
||
|
return nil, errors.New("tarball must contain only a single image to be used with tarball.Image")
|
||
|
}
|
||
|
return &(m)[0], nil
|
||
|
}
|
||
|
for _, img := range m {
|
||
|
for _, tagStr := range img.RepoTags {
|
||
|
repoTag, err := name.NewTag(tagStr)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// Compare the resolved names, since there are several ways to specify the same tag.
|
||
|
if repoTag.Name() == tag.Name() {
|
||
|
return &img, nil
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return nil, fmt.Errorf("tag %s not found in tarball", tag)
|
||
|
}
|
||
|
|
||
|
func (i *image) areLayersCompressed() (bool, error) {
|
||
|
if len(i.imgDescriptor.Layers) == 0 {
|
||
|
return false, errors.New("0 layers found in image")
|
||
|
}
|
||
|
layer := i.imgDescriptor.Layers[0]
|
||
|
blob, err := extractFileFromTar(i.opener, layer)
|
||
|
if err != nil {
|
||
|
return false, err
|
||
|
}
|
||
|
defer blob.Close()
|
||
|
|
||
|
cp, _, err := comp.PeekCompression(blob)
|
||
|
if err != nil {
|
||
|
return false, err
|
||
|
}
|
||
|
|
||
|
return cp != compression.None, nil
|
||
|
}
|
||
|
|
||
|
func (i *image) loadTarDescriptorAndConfig() error {
|
||
|
m, err := extractFileFromTar(i.opener, "manifest.json")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer m.Close()
|
||
|
|
||
|
if err := json.NewDecoder(m).Decode(&i.manifest); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if i.manifest == nil {
|
||
|
return errors.New("no valid manifest.json in tarball")
|
||
|
}
|
||
|
|
||
|
i.imgDescriptor, err = i.manifest.findDescriptor(i.tag)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
cfg, err := extractFileFromTar(i.opener, i.imgDescriptor.Config)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer cfg.Close()
|
||
|
|
||
|
i.config, err = io.ReadAll(cfg)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (i *image) RawConfigFile() ([]byte, error) {
|
||
|
return i.config, nil
|
||
|
}
|
||
|
|
||
|
// tarFile represents a single file inside a tar. Closing it closes the tar itself.
|
||
|
type tarFile struct {
|
||
|
io.Reader
|
||
|
io.Closer
|
||
|
}
|
||
|
|
||
|
func extractFileFromTar(opener Opener, filePath string) (io.ReadCloser, error) {
|
||
|
f, err := opener()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
close := true
|
||
|
defer func() {
|
||
|
if close {
|
||
|
f.Close()
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
tf := tar.NewReader(f)
|
||
|
for {
|
||
|
hdr, err := tf.Next()
|
||
|
if errors.Is(err, io.EOF) {
|
||
|
break
|
||
|
}
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
if hdr.Name == filePath {
|
||
|
if hdr.Typeflag == tar.TypeSymlink || hdr.Typeflag == tar.TypeLink {
|
||
|
currentDir := filepath.Dir(filePath)
|
||
|
return extractFileFromTar(opener, path.Join(currentDir, path.Clean(hdr.Linkname)))
|
||
|
}
|
||
|
close = false
|
||
|
return tarFile{
|
||
|
Reader: tf,
|
||
|
Closer: f,
|
||
|
}, nil
|
||
|
}
|
||
|
}
|
||
|
return nil, fmt.Errorf("file %s not found in tar", filePath)
|
||
|
}
|
||
|
|
||
|
// uncompressedLayerFromTarball implements partial.UncompressedLayer
|
||
|
type uncompressedLayerFromTarball struct {
|
||
|
diffID v1.Hash
|
||
|
mediaType types.MediaType
|
||
|
opener Opener
|
||
|
filePath string
|
||
|
}
|
||
|
|
||
|
// foreignUncompressedLayer implements partial.UncompressedLayer but returns
|
||
|
// a custom descriptor. This allows the foreign layer URLs to be included in
|
||
|
// the generated image manifest for uncompressed layers.
|
||
|
type foreignUncompressedLayer struct {
|
||
|
uncompressedLayerFromTarball
|
||
|
desc v1.Descriptor
|
||
|
}
|
||
|
|
||
|
func (fl *foreignUncompressedLayer) Descriptor() (*v1.Descriptor, error) {
|
||
|
return &fl.desc, nil
|
||
|
}
|
||
|
|
||
|
// DiffID implements partial.UncompressedLayer
|
||
|
func (ulft *uncompressedLayerFromTarball) DiffID() (v1.Hash, error) {
|
||
|
return ulft.diffID, nil
|
||
|
}
|
||
|
|
||
|
// Uncompressed implements partial.UncompressedLayer
|
||
|
func (ulft *uncompressedLayerFromTarball) Uncompressed() (io.ReadCloser, error) {
|
||
|
return extractFileFromTar(ulft.opener, ulft.filePath)
|
||
|
}
|
||
|
|
||
|
func (ulft *uncompressedLayerFromTarball) MediaType() (types.MediaType, error) {
|
||
|
return ulft.mediaType, nil
|
||
|
}
|
||
|
|
||
|
func (i *uncompressedImage) LayerByDiffID(h v1.Hash) (partial.UncompressedLayer, error) {
|
||
|
cfg, err := partial.ConfigFile(i)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
for idx, diffID := range cfg.RootFS.DiffIDs {
|
||
|
if diffID == h {
|
||
|
// Technically the media type should be 'application/tar' but given that our
|
||
|
// v1.Layer doesn't force consumers to care about whether the layer is compressed
|
||
|
// we should be fine returning the DockerLayer media type
|
||
|
mt := types.DockerLayer
|
||
|
if bd, ok := i.imgDescriptor.LayerSources[h]; ok {
|
||
|
// Overwrite the mediaType for foreign layers.
|
||
|
return &foreignUncompressedLayer{
|
||
|
uncompressedLayerFromTarball: uncompressedLayerFromTarball{
|
||
|
diffID: diffID,
|
||
|
mediaType: bd.MediaType,
|
||
|
opener: i.opener,
|
||
|
filePath: i.imgDescriptor.Layers[idx],
|
||
|
},
|
||
|
desc: bd,
|
||
|
}, nil
|
||
|
}
|
||
|
return &uncompressedLayerFromTarball{
|
||
|
diffID: diffID,
|
||
|
mediaType: mt,
|
||
|
opener: i.opener,
|
||
|
filePath: i.imgDescriptor.Layers[idx],
|
||
|
}, nil
|
||
|
}
|
||
|
}
|
||
|
return nil, fmt.Errorf("diff id %q not found", h)
|
||
|
}
|
||
|
|
||
|
func (c *compressedImage) Manifest() (*v1.Manifest, error) {
|
||
|
c.manifestLock.Lock()
|
||
|
defer c.manifestLock.Unlock()
|
||
|
if c.manifest != nil {
|
||
|
return c.manifest, nil
|
||
|
}
|
||
|
|
||
|
b, err := c.RawConfigFile()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
cfgHash, cfgSize, err := v1.SHA256(bytes.NewReader(b))
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
c.manifest = &v1.Manifest{
|
||
|
SchemaVersion: 2,
|
||
|
MediaType: types.DockerManifestSchema2,
|
||
|
Config: v1.Descriptor{
|
||
|
MediaType: types.DockerConfigJSON,
|
||
|
Size: cfgSize,
|
||
|
Digest: cfgHash,
|
||
|
},
|
||
|
}
|
||
|
|
||
|
for i, p := range c.imgDescriptor.Layers {
|
||
|
cfg, err := partial.ConfigFile(c)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
diffid := cfg.RootFS.DiffIDs[i]
|
||
|
if d, ok := c.imgDescriptor.LayerSources[diffid]; ok {
|
||
|
// If it's a foreign layer, just append the descriptor so we can avoid
|
||
|
// reading the entire file.
|
||
|
c.manifest.Layers = append(c.manifest.Layers, d)
|
||
|
} else {
|
||
|
l, err := extractFileFromTar(c.opener, p)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
defer l.Close()
|
||
|
sha, size, err := v1.SHA256(l)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
c.manifest.Layers = append(c.manifest.Layers, v1.Descriptor{
|
||
|
MediaType: types.DockerLayer,
|
||
|
Size: size,
|
||
|
Digest: sha,
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
return c.manifest, nil
|
||
|
}
|
||
|
|
||
|
func (c *compressedImage) RawManifest() ([]byte, error) {
|
||
|
return partial.RawManifest(c)
|
||
|
}
|
||
|
|
||
|
// compressedLayerFromTarball implements partial.CompressedLayer
|
||
|
type compressedLayerFromTarball struct {
|
||
|
desc v1.Descriptor
|
||
|
opener Opener
|
||
|
filePath string
|
||
|
}
|
||
|
|
||
|
// Digest implements partial.CompressedLayer
|
||
|
func (clft *compressedLayerFromTarball) Digest() (v1.Hash, error) {
|
||
|
return clft.desc.Digest, nil
|
||
|
}
|
||
|
|
||
|
// Compressed implements partial.CompressedLayer
|
||
|
func (clft *compressedLayerFromTarball) Compressed() (io.ReadCloser, error) {
|
||
|
return extractFileFromTar(clft.opener, clft.filePath)
|
||
|
}
|
||
|
|
||
|
// MediaType implements partial.CompressedLayer
|
||
|
func (clft *compressedLayerFromTarball) MediaType() (types.MediaType, error) {
|
||
|
return clft.desc.MediaType, nil
|
||
|
}
|
||
|
|
||
|
// Size implements partial.CompressedLayer
|
||
|
func (clft *compressedLayerFromTarball) Size() (int64, error) {
|
||
|
return clft.desc.Size, nil
|
||
|
}
|
||
|
|
||
|
func (c *compressedImage) LayerByDigest(h v1.Hash) (partial.CompressedLayer, error) {
|
||
|
m, err := c.Manifest()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
for i, l := range m.Layers {
|
||
|
if l.Digest == h {
|
||
|
fp := c.imgDescriptor.Layers[i]
|
||
|
return &compressedLayerFromTarball{
|
||
|
desc: l,
|
||
|
opener: c.opener,
|
||
|
filePath: fp,
|
||
|
}, nil
|
||
|
}
|
||
|
}
|
||
|
return nil, fmt.Errorf("blob %v not found", h)
|
||
|
}
|