diff --git a/cmd/dmverity-vhd/main.go b/cmd/dmverity-vhd/main.go index 20d49f4285..f0aad0a3c0 100644 --- a/cmd/dmverity-vhd/main.go +++ b/cmd/dmverity-vhd/main.go @@ -1,17 +1,21 @@ package main import ( + "archive/tar" + "bytes" + "context" + "encoding/json" "errors" "fmt" + "io" "os" "path/filepath" + "strings" + "github.com/docker/docker/client" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/daemon" "github.com/google/go-containerregistry/pkg/v1/remote" - "github.com/google/go-containerregistry/pkg/v1/tarball" log "github.com/sirupsen/logrus" "github.com/urfave/cli" @@ -81,142 +85,302 @@ func main() { } } -func fetchImageLayers(ctx *cli.Context) (layers []v1.Layer, err error) { - image := ctx.String(imageFlag) - tarballPath := ctx.GlobalString(tarballFlag) - ref, err := name.ParseReference(image) +type LayerProcessor func(string, io.Reader) error + +func fetchImageTarball(tarballPath string) (imageReader io.ReadCloser, err error) { + if imageReader, err = os.Open(tarballPath); err != nil { + return nil, err + } + + return imageReader, err +} + +func fetchImageDocker(imageName string) (imageReader io.ReadCloser, err error) { + + dockerCtx := context.Background() + + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { - return nil, fmt.Errorf("failed to parse image reference %s: %w", image, err) + return nil, err } - dockerDaemon := ctx.GlobalBool(dockerFlag) + imageReader, err = cli.ImageSave(dockerCtx, []string{imageName}) + if err != nil { + return nil, err + } + + return imageReader, err +} - // error check to make sure docker and tarball are not both defined - if dockerDaemon && tarballPath != "" { - return nil, errors.New("cannot use both docker and tarball for image source") +func getLayerDigestsV24(configData []byte) (map[int]string, error) { + type RootFs struct { + DiffIDs []string `json:"diff_ids"` + } + type configLayerV24 struct { + RootFS RootFs `json:"rootfs"` } - // by default, using remote as source - var img v1.Image - if tarballPath != "" { - // create a tag and search the tarball for the image specified - var imageNameAndTag name.Tag - imageNameAndTag, err = name.NewTag(image) - if err != nil { - return nil, fmt.Errorf("failed to failed to create a tag to search tarball for %s: %w", image, err) + var config configLayerV24 + if err := json.Unmarshal(configData, &config); err != nil || len(config.RootFS.DiffIDs) == 0 { + return nil, errors.New("could not unmarshall json file for v24 config format") + } + + layerDigests := make(map[int]string) + for layerNumber, layerID := range config.RootFS.DiffIDs { + layerIDSplit := strings.Split(layerID, ":") + layerDigests[layerNumber] = layerIDSplit[len(layerIDSplit)-1] + } + return layerDigests, nil +} + +func getLayerDigestsV25(configData []byte) (map[int]string, error) { + type configLayerV25 []struct { + Layers []string `json:"Layers"` + } + + var config configLayerV25 + if err := json.Unmarshal(configData, &config); err != nil { + return nil, err + } + + layerDigests := make(map[int]string) + for layerNumber, layerID := range config[0].Layers { + + if !strings.HasPrefix(layerID, "blobs") { + return nil, errors.New("layer path isn't v25") } - // if only an image name is provided and not a tag, the default is "latest" - img, err = tarball.ImageFromPath(tarballPath, &imageNameAndTag) - } else if dockerDaemon { - // use the unbuffered opener by default, the tradeoff being the image will stream as needed - // so it is slower but much more memory efficient - var opts []daemon.Option - if !ctx.GlobalBool(bufferedReaderFlag) { - opt := daemon.WithUnbufferedOpener() - opts = append(opts, opt) + + layerIDSplit := strings.Split(layerID, "/") + layerDigests[layerNumber] = layerIDSplit[len(layerIDSplit)-1] + } + return layerDigests, nil +} + +func isTar(reader io.Reader) (io.Reader, bool) { + + // Wraps reader in : + // A TeeReader which copies read bytes into a separate buffer. + // A TarReader to read the header of the tar file. + var header bytes.Buffer + teeReader := io.TeeReader(reader, &header) + tarReader := tar.NewReader(teeReader) + + _, err := tarReader.Next() + + return io.MultiReader(&header, reader), err == nil +} + +func processLocalImage(imageReader io.Reader, onLayer LayerProcessor) (layerDigests map[int]string, layerIDs map[int]string, err error) { + + imageFileReader := tar.NewReader(imageReader) + layerIDs = make(map[int]string) + layerDigestCandidates := make(map[string]map[int]string) + var configPath string + for { + hdr, err := imageFileReader.Next() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return nil, nil, err } - img, err = daemon.Image(ref, opts...) - } else { - var remoteOpts []remote.Option - if ctx.IsSet(usernameFlag) && ctx.IsSet(passwordFlag) { - auth := authn.Basic{ - Username: ctx.String(usernameFlag), - Password: ctx.String(passwordFlag), + // If the file is a tar, assume it's a layer, and call the callback + imageFileReader, isTar := isTar(imageFileReader) + if isTar { + if err := onLayer(hdr.Name, imageFileReader); err != nil { + return nil, nil, err + } + } else if hdr.Name == "manifest.json" { + + type Manifest []struct { + Config string `json:"Config"` + Layers []string `json:"Layers"` + } + var manifest Manifest + + manifestData, err := io.ReadAll(imageFileReader) + if err != nil { + return nil, nil, err } - authConf, err := auth.Authorization() + + if err := json.Unmarshal(manifestData, &manifest); err != nil { + return nil, nil, err + } + + configPath = manifest[0].Config + + for layerNumber, layerID := range manifest[0].Layers { + layerIDSplit := strings.Split(layerID, ":") + layerIDs[layerNumber] = layerIDSplit[len(layerIDSplit)-1] + } + + } else { // Attempt to parse as a config file + + configData, err := io.ReadAll(imageFileReader) if err != nil { - return nil, fmt.Errorf("failed to set remote: %w", err) + return nil, nil, err + } + + // Attempt to parse as if it's an image config file, trying each version until one works + parsingFunctions := []func([]byte) (map[int]string, error){ + getLayerDigestsV25, + getLayerDigestsV24, + } + for _, parseFunc := range parsingFunctions { + layerDigestCandidate, err := parseFunc(configData) + if err == nil { + layerDigestCandidates[hdr.Name] = layerDigestCandidate + break + } } - log.Debug("using basic auth") - authOpt := remote.WithAuth(authn.FromConfig(*authConf)) - remoteOpts = append(remoteOpts, authOpt) } + } - img, err = remote.Image(ref, remoteOpts...) + layerDigests, ok := layerDigestCandidates[configPath] + if !ok { + return nil, nil, errors.New("config file either not found, or failed to parse") } + + return layerDigests, layerIDs, nil +} + +func processRemoteImage(imageName string, username string, password string, onLayer LayerProcessor) (layerDigests map[int]string, layerIDs map[int]string, err error) { + + layerDigests = make(map[int]string) + + ref, err := name.ParseReference(imageName) if err != nil { - return nil, fmt.Errorf("unable to fetch image %q, make sure it exists: %w", image, err) + return nil, nil, fmt.Errorf("failed to parse image reference %s: %w", imageName, err) } - conf, _ := img.ConfigName() - log.Debugf("Image id: %s", conf.String()) - return img.Layers() -} -var createVHDCommand = cli.Command{ - Name: "create", - Usage: "creates LCOW layer VHDs inside the output directory with dm-verity super block and merkle tree appended at the end", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: imageFlag + ",i", - Usage: "Required: container image reference", - Required: true, - }, - cli.StringFlag{ - Name: outputDirFlag + ",o", - Usage: "Required: output directory path", - Required: true, - }, - cli.StringFlag{ - Name: usernameFlag + ",u", - Usage: "Optional: custom registry username", - }, - cli.StringFlag{ - Name: passwordFlag + ",p", - Usage: "Optional: custom registry password", - }, - cli.BoolFlag{ - Name: hashDeviceVhdFlag + ",hdv", - Usage: "Optional: save hash-device as a VHD", - }, - }, - Action: func(ctx *cli.Context) error { - verbose := ctx.GlobalBool(verboseFlag) - if verbose { - log.SetLevel(log.DebugLevel) + var remoteOpts []remote.Option + if username != "" && password != "" { + + auth := authn.Basic{ + Username: username, + Password: password, } - layers, err := fetchImageLayers(ctx) + authConf, err := auth.Authorization() if err != nil { - return fmt.Errorf("failed to fetch image layers: %w", err) + return nil, nil, fmt.Errorf("failed to set remote: %w", err) } - outDir := ctx.String(outputDirFlag) - if _, err := os.Stat(outDir); os.IsNotExist(err) { - log.Debugf("creating output directory %q", outDir) - if err := os.MkdirAll(outDir, 0755); err != nil { - return fmt.Errorf("failed to create output directory %s: %w", outDir, err) - } + log.Debug("using basic auth") + authOpt := remote.WithAuth(authn.FromConfig(*authConf)) + remoteOpts = append(remoteOpts, authOpt) + } + + image, err := remote.Image(ref, remoteOpts...) + if err != nil { + return nil, nil, fmt.Errorf("unable to fetch image %q, make sure it exists: %w", imageName, err) + } + + layers, err := image.Layers() + if err != nil { + return nil, nil, fmt.Errorf("unable to fetch image layers: %w", err) + } + + for layerNumber, layer := range layers { + diffID, err := layer.DiffID() + if err != nil { + return nil, nil, fmt.Errorf("failed to read layer diff: %w", err) } - log.Debug("creating layer VHDs with dm-verity") - for layerNumber, layer := range layers { - if err := createVHD(layerNumber, layer, ctx.String(outputDirFlag), ctx.Bool(hashDeviceVhdFlag)); err != nil { - return err - } + layerDigests[layerNumber] = diffID.Hex + layerReader, err := layer.Uncompressed() + if err != nil { + return nil, nil, fmt.Errorf("failed to uncompress layer %s: %w", diffID.Hex, err) } - return nil - }, + defer layerReader.Close() + + if err = onLayer(diffID.Hex, layerReader); err != nil { + return nil, nil, err + } + } + + // For the remote case, use digests for both layer ID and layer digest + return layerDigests, layerDigests, nil } -func createVHD(layerNumber int, layer v1.Layer, outDir string, verityHashDev bool) error { - diffID, err := layer.DiffID() - if err != nil { - return fmt.Errorf("failed to read layer diff: %w", err) +func processImageLayers(ctx *cli.Context, onLayer LayerProcessor) (layerDigests map[int]string, layerIDs map[int]string, err error) { + imageName := ctx.String(imageFlag) + tarballPath := ctx.GlobalString(tarballFlag) + useDocker := ctx.GlobalBool(dockerFlag) + + if useDocker && tarballPath != "" { + return nil, nil, errors.New("cannot use both docker and tarball for image source") + } + + processLocal := func(fetcher func(string) (io.ReadCloser, error), image string) (map[int]string, map[int]string, error) { + imageReader, err := fetcher(image) + if err != nil { + return nil, nil, err + } + defer imageReader.Close() + return processLocalImage(imageReader, onLayer) + } + + if tarballPath != "" { + return processLocal(fetchImageTarball, tarballPath) + } else if useDocker { + return processLocal(fetchImageDocker, imageName) + } else { + return processRemoteImage( + imageName, + ctx.String(usernameFlag), + ctx.String(passwordFlag), + onLayer, + ) } +} - log.WithFields(log.Fields{ - "layerNumber": layerNumber, - "layerDiff": diffID.String(), - }).Debug("converting tar to layer VHD") +func moveFile(src string, dst string) error { + err := os.Rename(src, dst) - rc, err := layer.Uncompressed() + // If a simple rename didn't work, for example moving to or from a mount, + // then copy and delete the file if err != nil { - return fmt.Errorf("failed to uncompress layer %s: %w", diffID.String(), err) + sourceFile, err := os.Open(src) + if err != nil { + return err + } + defer sourceFile.Close() + + destFile, err := os.Create(dst) + if err != nil { + return err + } + defer destFile.Close() + + if _, err = io.Copy(destFile, sourceFile); err != nil { + return err + } + sourceFile.Close() + + if err = os.Remove(src); err != nil { + return err + } } - defer rc.Close() - vhdPath := filepath.Join(outDir, diffID.Hex+".vhd") + return nil +} + +func sanitiseVHDFilename(vhdFilename string) string { + return strings.TrimSuffix( + strings.ReplaceAll(vhdFilename, "/", "_"), + ".tar", + ) +} + +func createVHD(layerID string, layerReader io.Reader, verityHashDev bool, outDir string) error { + sanitisedFileName := sanitiseVHDFilename(layerID) + + // Create this file in a temp directory because at this point we don't have + // the layer digest to properly name the file, it will be moved later + vhdPath := filepath.Join(os.TempDir(), sanitisedFileName+".vhd") + out, err := os.Create(vhdPath) if err != nil { return fmt.Errorf("failed to create layer vhd file %s: %w", vhdPath, err) @@ -227,14 +391,19 @@ func createVHD(layerNumber int, layer v1.Layer, outDir string, verityHashDev boo tar2ext4.ConvertWhiteout, tar2ext4.MaximumDiskSize(maxVHDSize), } + if !verityHashDev { opts = append(opts, tar2ext4.AppendDMVerity) } - if err := tar2ext4.Convert(rc, out, opts...); err != nil { + + if err := tar2ext4.Convert(layerReader, out, opts...); err != nil { return fmt.Errorf("failed to convert tar to ext4: %w", err) } + if verityHashDev { - hashDevPath := filepath.Join(outDir, diffID.Hex+".hash-dev.vhd") + + hashDevPath := filepath.Join(outDir, sanitisedFileName+".hash-dev.vhd") + hashDev, err := os.Create(hashDevPath) if err != nil { return fmt.Errorf("failed to create hash device VHD file: %w", err) @@ -244,27 +413,33 @@ func createVHD(layerNumber int, layer v1.Layer, outDir string, verityHashDev boo if err := dmverity.ComputeAndWriteHashDevice(out, hashDev); err != nil { return err } + if err := tar2ext4.ConvertToVhd(hashDev); err != nil { return err } + fmt.Fprintf(os.Stdout, "hash device created at %s\n", hashDevPath) } if err := tar2ext4.ConvertToVhd(out); err != nil { return fmt.Errorf("failed to append VHD footer: %w", err) } - fmt.Fprintf(os.Stdout, "Layer VHD created at %s\n", vhdPath) return nil } -var rootHashVHDCommand = cli.Command{ - Name: "roothash", - Usage: "compute root hashes for each LCOW layer VHD", +var createVHDCommand = cli.Command{ + Name: "create", + Usage: "creates LCOW layer VHDs inside the output directory with dm-verity super block and merkle tree appended at the end", Flags: []cli.Flag{ cli.StringFlag{ Name: imageFlag + ",i", Usage: "Required: container image reference", Required: true, }, + cli.StringFlag{ + Name: outputDirFlag + ",o", + Usage: "Required: output directory path", + Required: true, + }, cli.StringFlag{ Name: usernameFlag + ",u", Usage: "Optional: custom registry username", @@ -273,49 +448,114 @@ var rootHashVHDCommand = cli.Command{ Name: passwordFlag + ",p", Usage: "Optional: custom registry password", }, + cli.BoolFlag{ + Name: hashDeviceVhdFlag + ",hdv", + Usage: "Optional: save hash-device as a VHD", + }, }, Action: func(ctx *cli.Context) error { verbose := ctx.GlobalBool(verboseFlag) if verbose { log.SetLevel(log.DebugLevel) } + verityHashDev := ctx.Bool(hashDeviceVhdFlag) + + outDir := ctx.String(outputDirFlag) + if _, err := os.Stat(outDir); os.IsNotExist(err) { + log.Debugf("creating output directory %q", outDir) + if err := os.MkdirAll(outDir, 0755); err != nil { + return fmt.Errorf("failed to create output directory %s: %w", outDir, err) + } + } - layers, err := fetchImageLayers(ctx) + createVHDLayer := func(layerID string, layerReader io.Reader) error { + return createVHD(layerID, layerReader, verityHashDev, outDir) + } + + log.Debug("creating layer VHDs with dm-verity") + layerDigests, layerIDs, err := processImageLayers(ctx, createVHDLayer) if err != nil { - return fmt.Errorf("failed to fetch image layers: %w", err) + return err } - log.Debugf("%d layers found", len(layers)) - convertFunc := func(layer v1.Layer) (string, error) { - rc, err := layer.Uncompressed() - if err != nil { - return "", err + // Move the VHDs to the output directory + // They can't immediately be in the output directory because they have + // temporary file names based on the layer id which isn't necessarily + // the layer digest + for layerNumber := 0; layerNumber < len(layerDigests); layerNumber++ { + layerDigest := layerDigests[layerNumber] + layerID := layerIDs[layerNumber] + sanitisedFileName := sanitiseVHDFilename(layerID) + + suffixes := []string{".vhd"} + if verityHashDev { + suffixes = append(suffixes, ".hash-dev.vhd") } - defer rc.Close() - hash, err := tar2ext4.ConvertAndComputeRootDigest(rc) - if err != nil { - return "", err + for _, srcSuffix := range suffixes { + src := filepath.Join(os.TempDir(), sanitisedFileName+srcSuffix) + if _, err := os.Stat(src); os.IsNotExist(err) { + return fmt.Errorf("layer VHD %s does not exist", src) + } + + dst := filepath.Join(outDir, layerDigest+srcSuffix) + if err := moveFile(src, dst); err != nil { + return err + } + + fmt.Fprintf(os.Stdout, "Layer VHD created at %s\n", dst) } - return hash, err + } - for layerNumber, layer := range layers { - diffID, err := layer.DiffID() - if err != nil { - return fmt.Errorf("failed to read layer diff: %w", err) - } - log.WithFields(log.Fields{ - "layerNumber": layerNumber, - "layerDiff": diffID.String(), - }).Debug("uncompressed layer") + return nil + }, +} + +var rootHashVHDCommand = cli.Command{ + Name: "roothash", + Usage: "compute root hashes for each LCOW layer VHD", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: imageFlag + ",i", + Usage: "Required: container image reference", + Required: true, + }, + cli.StringFlag{ + Name: usernameFlag + ",u", + Usage: "Optional: custom registry username", + }, + cli.StringFlag{ + Name: passwordFlag + ",p", + Usage: "Optional: custom registry password", + }, + }, + Action: func(ctx *cli.Context) error { + verbose := ctx.GlobalBool(verboseFlag) + if verbose { + log.SetLevel(log.DebugLevel) + } - hash, err := convertFunc(layer) + layerHashes := make(map[string]string) + getLayerHash := func(layerDigest string, layerReader io.Reader) error { + hash, err := tar2ext4.ConvertAndComputeRootDigest(layerReader) if err != nil { - return fmt.Errorf("failed to compute root digest: %w", err) + return err } - fmt.Fprintf(os.Stdout, "Layer %d root hash: %s\n", layerNumber, hash) + layerHashes[layerDigest] = hash + return nil } + + _, layerIDs, err := processImageLayers(ctx, getLayerHash) + if err != nil { + return err + } + + // Print the layer number to layer hash + for layerNumber := 0; layerNumber < len(layerIDs); layerNumber++ { + fmt.Fprintf(os.Stdout, "Layer %d root hash: %s\n", layerNumber, layerHashes[layerIDs[layerNumber]]) + } + return nil }, } diff --git a/go.mod b/go.mod index 302d438a45..0e93d25a89 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/containerd/protobuild v0.3.0 github.com/containerd/ttrpc v1.2.3 github.com/containerd/typeurl/v2 v2.1.1 + github.com/docker/docker v24.0.9+incompatible github.com/google/go-cmp v0.6.0 github.com/google/go-containerregistry v0.19.1 github.com/josephspurrier/goversioninfo v1.4.0 @@ -56,7 +57,6 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/docker/cli v24.0.0+incompatible // indirect github.com/docker/distribution v2.8.2+incompatible // indirect - github.com/docker/docker v24.0.9+incompatible // indirect github.com/docker/docker-credential-helpers v0.7.0 // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/README.md b/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/README.md deleted file mode 100644 index 74fc3a87c0..0000000000 --- a/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# `daemon` - -[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/daemon?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/daemon) - -The `daemon` package enables reading/writing images from/to the docker daemon. - -It is not fully fleshed out, but is useful for interoperability, see various issues: - -* https://github.com/google/go-containerregistry/issues/205 -* https://github.com/google/go-containerregistry/issues/552 -* https://github.com/google/go-containerregistry/issues/627 diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/doc.go b/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/doc.go deleted file mode 100644 index ac05d96121..0000000000 --- a/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/doc.go +++ /dev/null @@ -1,17 +0,0 @@ -// 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 daemon provides facilities for reading/writing v1.Image from/to -// a running daemon. -package daemon diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/image.go b/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/image.go deleted file mode 100644 index d2efcb372e..0000000000 --- a/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/image.go +++ /dev/null @@ -1,350 +0,0 @@ -// 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 daemon - -import ( - "bytes" - "context" - "io" - "sync" - "time" - - api "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" - - "github.com/google/go-containerregistry/pkg/name" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/tarball" - "github.com/google/go-containerregistry/pkg/v1/types" -) - -type image struct { - ref name.Reference - opener *imageOpener - tarballImage v1.Image - computed bool - id *v1.Hash - configFile *v1.ConfigFile - - once sync.Once - err error -} - -type imageOpener struct { - ref name.Reference - ctx context.Context - - buffered bool - client Client - - once sync.Once - bytes []byte - err error -} - -func (i *imageOpener) saveImage() (io.ReadCloser, error) { - return i.client.ImageSave(i.ctx, []string{i.ref.Name()}) -} - -func (i *imageOpener) bufferedOpener() (io.ReadCloser, error) { - // Store the tarball in memory and return a new reader into the bytes each time we need to access something. - i.once.Do(func() { - i.bytes, i.err = func() ([]byte, error) { - rc, err := i.saveImage() - if err != nil { - return nil, err - } - defer rc.Close() - - return io.ReadAll(rc) - }() - }) - - // Wrap the bytes in a ReadCloser so it looks like an opened file. - return io.NopCloser(bytes.NewReader(i.bytes)), i.err -} - -func (i *imageOpener) opener() tarball.Opener { - if i.buffered { - return i.bufferedOpener - } - - // To avoid storing the tarball in memory, do a save every time we need to access something. - return i.saveImage -} - -// Image provides access to an image reference from the Docker daemon, -// applying functional options to the underlying imageOpener before -// resolving the reference into a v1.Image. -func Image(ref name.Reference, options ...Option) (v1.Image, error) { - o, err := makeOptions(options...) - if err != nil { - return nil, err - } - - i := &imageOpener{ - ref: ref, - buffered: o.buffered, - client: o.client, - ctx: o.ctx, - } - - img := &image{ - ref: ref, - opener: i, - } - - // Eagerly fetch Image ID to ensure it actually exists. - // https://github.com/google/go-containerregistry/issues/1186 - id, err := img.ConfigName() - if err != nil { - return nil, err - } - img.id = &id - - return img, nil -} - -func (i *image) initialize() error { - // Don't re-initialize tarball if already initialized. - if i.tarballImage == nil { - i.once.Do(func() { - i.tarballImage, i.err = tarball.Image(i.opener.opener(), nil) - }) - } - return i.err -} - -func (i *image) compute() error { - // Don't re-compute if already computed. - if i.computed { - return nil - } - - inspect, _, err := i.opener.client.ImageInspectWithRaw(i.opener.ctx, i.ref.String()) - if err != nil { - return err - } - - configFile, err := i.computeConfigFile(inspect) - if err != nil { - return err - } - - i.configFile = configFile - i.computed = true - - return nil -} - -func (i *image) Layers() ([]v1.Layer, error) { - if err := i.initialize(); err != nil { - return nil, err - } - return i.tarballImage.Layers() -} - -func (i *image) MediaType() (types.MediaType, error) { - if err := i.initialize(); err != nil { - return "", err - } - return i.tarballImage.MediaType() -} - -func (i *image) Size() (int64, error) { - if err := i.initialize(); err != nil { - return 0, err - } - return i.tarballImage.Size() -} - -func (i *image) ConfigName() (v1.Hash, error) { - if i.id != nil { - return *i.id, nil - } - res, _, err := i.opener.client.ImageInspectWithRaw(i.opener.ctx, i.ref.String()) - if err != nil { - return v1.Hash{}, err - } - return v1.NewHash(res.ID) -} - -func (i *image) ConfigFile() (*v1.ConfigFile, error) { - if err := i.compute(); err != nil { - return nil, err - } - return i.configFile.DeepCopy(), nil -} - -func (i *image) RawConfigFile() ([]byte, error) { - if err := i.initialize(); err != nil { - return nil, err - } - - // RawConfigFile cannot be generated from "docker inspect" because Docker Engine API returns serialized data, - // and formatting information of the raw config such as indent and prefix will be lost. - return i.tarballImage.RawConfigFile() -} - -func (i *image) Digest() (v1.Hash, error) { - if err := i.initialize(); err != nil { - return v1.Hash{}, err - } - return i.tarballImage.Digest() -} - -func (i *image) Manifest() (*v1.Manifest, error) { - if err := i.initialize(); err != nil { - return nil, err - } - return i.tarballImage.Manifest() -} - -func (i *image) RawManifest() ([]byte, error) { - if err := i.initialize(); err != nil { - return nil, err - } - return i.tarballImage.RawManifest() -} - -func (i *image) LayerByDigest(h v1.Hash) (v1.Layer, error) { - if err := i.initialize(); err != nil { - return nil, err - } - return i.tarballImage.LayerByDigest(h) -} - -func (i *image) LayerByDiffID(h v1.Hash) (v1.Layer, error) { - if err := i.initialize(); err != nil { - return nil, err - } - return i.tarballImage.LayerByDiffID(h) -} - -func (i *image) configHistory(author string) ([]v1.History, error) { - historyItems, err := i.opener.client.ImageHistory(i.opener.ctx, i.ref.String()) - if err != nil { - return nil, err - } - - history := make([]v1.History, len(historyItems)) - for j, h := range historyItems { - history[j] = v1.History{ - Author: author, - Created: v1.Time{ - Time: time.Unix(h.Created, 0).UTC(), - }, - CreatedBy: h.CreatedBy, - Comment: h.Comment, - EmptyLayer: h.Size == 0, - } - } - return history, nil -} - -func (i *image) diffIDs(rootFS api.RootFS) ([]v1.Hash, error) { - diffIDs := make([]v1.Hash, len(rootFS.Layers)) - for j, l := range rootFS.Layers { - h, err := v1.NewHash(l) - if err != nil { - return nil, err - } - diffIDs[j] = h - } - return diffIDs, nil -} - -func (i *image) computeConfigFile(inspect api.ImageInspect) (*v1.ConfigFile, error) { - diffIDs, err := i.diffIDs(inspect.RootFS) - if err != nil { - return nil, err - } - - history, err := i.configHistory(inspect.Author) - if err != nil { - return nil, err - } - - created, err := time.Parse(time.RFC3339Nano, inspect.Created) - if err != nil { - return nil, err - } - - return &v1.ConfigFile{ - Architecture: inspect.Architecture, - Author: inspect.Author, - Container: inspect.Container, - Created: v1.Time{Time: created}, - DockerVersion: inspect.DockerVersion, - History: history, - OS: inspect.Os, - RootFS: v1.RootFS{ - Type: inspect.RootFS.Type, - DiffIDs: diffIDs, - }, - Config: i.computeImageConfig(inspect.Config), - OSVersion: inspect.OsVersion, - }, nil -} - -func (i *image) computeImageConfig(config *container.Config) v1.Config { - if config == nil { - return v1.Config{} - } - - c := v1.Config{ - AttachStderr: config.AttachStderr, - AttachStdin: config.AttachStdin, - AttachStdout: config.AttachStdout, - Cmd: config.Cmd, - Domainname: config.Domainname, - Entrypoint: config.Entrypoint, - Env: config.Env, - Hostname: config.Hostname, - Image: config.Image, - Labels: config.Labels, - OnBuild: config.OnBuild, - OpenStdin: config.OpenStdin, - StdinOnce: config.StdinOnce, - Tty: config.Tty, - User: config.User, - Volumes: config.Volumes, - WorkingDir: config.WorkingDir, - ArgsEscaped: config.ArgsEscaped, - NetworkDisabled: config.NetworkDisabled, - MacAddress: config.MacAddress, - StopSignal: config.StopSignal, - Shell: config.Shell, - } - - if config.Healthcheck != nil { - c.Healthcheck = &v1.HealthConfig{ - Test: config.Healthcheck.Test, - Interval: config.Healthcheck.Interval, - Timeout: config.Healthcheck.Timeout, - StartPeriod: config.Healthcheck.StartPeriod, - Retries: config.Healthcheck.Retries, - } - } - - if len(config.ExposedPorts) > 0 { - c.ExposedPorts = map[string]struct{}{} - for port := range c.ExposedPorts { - c.ExposedPorts[port] = struct{}{} - } - } - - return c -} diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/options.go b/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/options.go deleted file mode 100644 index b806463697..0000000000 --- a/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/options.go +++ /dev/null @@ -1,105 +0,0 @@ -// 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 daemon - -import ( - "context" - "io" - - "github.com/docker/docker/api/types" - api "github.com/docker/docker/api/types/image" - "github.com/docker/docker/client" -) - -// ImageOption is an alias for Option. -// Deprecated: Use Option instead. -type ImageOption Option - -// Option is a functional option for daemon operations. -type Option func(*options) - -type options struct { - ctx context.Context - client Client - buffered bool -} - -var defaultClient = func() (Client, error) { - return client.NewClientWithOpts(client.FromEnv) -} - -func makeOptions(opts ...Option) (*options, error) { - o := &options{ - buffered: true, - ctx: context.Background(), - } - for _, opt := range opts { - opt(o) - } - - if o.client == nil { - client, err := defaultClient() - if err != nil { - return nil, err - } - o.client = client - } - o.client.NegotiateAPIVersion(o.ctx) - - return o, nil -} - -// WithBufferedOpener buffers the image. -func WithBufferedOpener() Option { - return func(o *options) { - o.buffered = true - } -} - -// WithUnbufferedOpener streams the image to avoid buffering. -func WithUnbufferedOpener() Option { - return func(o *options) { - o.buffered = false - } -} - -// WithClient is a functional option to allow injecting a docker client. -// -// By default, github.com/docker/docker/client.FromEnv is used. -func WithClient(client Client) Option { - return func(o *options) { - o.client = client - } -} - -// WithContext is a functional option to pass through a context.Context. -// -// By default, context.Background() is used. -func WithContext(ctx context.Context) Option { - return func(o *options) { - o.ctx = ctx - } -} - -// Client represents the subset of a docker client that the daemon -// package uses. -type Client interface { - NegotiateAPIVersion(ctx context.Context) - ImageSave(context.Context, []string) (io.ReadCloser, error) - ImageLoad(context.Context, io.Reader, bool) (types.ImageLoadResponse, error) - ImageTag(context.Context, string, string) error - ImageInspectWithRaw(context.Context, string) (types.ImageInspect, []byte, error) - ImageHistory(context.Context, string) ([]api.HistoryResponseItem, error) -} diff --git a/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/write.go b/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/write.go deleted file mode 100644 index 3ca5b52dd2..0000000000 --- a/vendor/github.com/google/go-containerregistry/pkg/v1/daemon/write.go +++ /dev/null @@ -1,78 +0,0 @@ -// 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 daemon - -import ( - "fmt" - "io" - - "github.com/google/go-containerregistry/pkg/name" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/tarball" -) - -// Tag adds a tag to an already existent image. -func Tag(src, dest name.Tag, options ...Option) error { - o, err := makeOptions(options...) - if err != nil { - return err - } - - return o.client.ImageTag(o.ctx, src.String(), dest.String()) -} - -// Write saves the image into the daemon as the given tag. -func Write(tag name.Tag, img v1.Image, options ...Option) (string, error) { - o, err := makeOptions(options...) - if err != nil { - return "", err - } - - // If we already have this image by this image ID, we can skip loading it. - id, err := img.ConfigName() - if err != nil { - return "", fmt.Errorf("computing image ID: %w", err) - } - if resp, _, err := o.client.ImageInspectWithRaw(o.ctx, id.String()); err == nil { - want := tag.String() - - // If we already have this tag, we can skip tagging it. - for _, have := range resp.RepoTags { - if have == want { - return "", nil - } - } - - return "", o.client.ImageTag(o.ctx, id.String(), want) - } - - pr, pw := io.Pipe() - go func() { - pw.CloseWithError(tarball.Write(tag, img, pw)) - }() - - // write the image in docker save format first, then load it - resp, err := o.client.ImageLoad(o.ctx, pr, false) - if err != nil { - return "", fmt.Errorf("error loading image: %w", err) - } - defer resp.Body.Close() - b, err := io.ReadAll(resp.Body) - response := string(b) - if err != nil { - return response, fmt.Errorf("error reading load response body: %w", err) - } - return response, nil -} diff --git a/vendor/modules.txt b/vendor/modules.txt index 5d7ea860ad..d82b549eb1 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -241,7 +241,6 @@ github.com/google/go-containerregistry/pkg/compression github.com/google/go-containerregistry/pkg/logs github.com/google/go-containerregistry/pkg/name github.com/google/go-containerregistry/pkg/v1 -github.com/google/go-containerregistry/pkg/v1/daemon github.com/google/go-containerregistry/pkg/v1/empty github.com/google/go-containerregistry/pkg/v1/match github.com/google/go-containerregistry/pkg/v1/mutate