Skip to content

Commit

Permalink
images: Add docker pull support
Browse files Browse the repository at this point in the history
Add support to pull LVH images directly from the commandline so that
users who don't need to customize the image can directly pull the
required images directly rather than having to run another script or
manually pull & decompress the images locally.

Example usage:

$ lvh images pull quay.io/lvh-images/kind:5.4-main

Signed-off-by: Joe Stringer <[email protected]>
  • Loading branch information
joestringer committed Jan 9, 2024
1 parent ec4df02 commit ea09cb7
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 29 deletions.
2 changes: 1 addition & 1 deletion cmd/lvh/images/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ func ImagesCommand() *cobra.Command {
Short: "Build VM images",
}

ret.AddCommand(BuildCmd(), ExampleCmd())
ret.AddCommand(BuildCmd(), ExampleCmd(), PullCmd())
return ret
}
45 changes: 45 additions & 0 deletions cmd/lvh/images/pull.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package images

import (
"context"
"fmt"
"os"

"github.com/cilium/little-vm-helper/pkg/images"
"github.com/spf13/cobra"
)

var (
dirName string
cache bool
)

func PullCmd() *cobra.Command {

cmd := &cobra.Command{
Use: "pull <URL>",
Short: "Pull an image from an OCI repository",
Args: cobra.MinimumNArgs(1),
ArgAliases: []string{"imageURL"},
RunE: func(cmd *cobra.Command, args []string) error {
if err := os.MkdirAll(dirName, 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %w", dirName, err)
}

_, err := images.PullImage(context.Background(), images.PullConf{
Image: args[0],
TargetDir: dirName,
Cache: cache,
})
return err
},
}

cmd.Flags().StringVar(&dirName, "dir", "_data", "directory to keep the images (images will be saved in images in <dir>/images)")
cmd.Flags().BoolVar(&cache, "cache", false, "cache a compressed version of the image")

return cmd
}
145 changes: 145 additions & 0 deletions pkg/images/pull.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package images

import (
"archive/tar"
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
)

type PullConf struct {
Image string
TargetDir string
Cache bool
}

type PullResult struct {
Images []string
}

// PullImage pulls an OCI image from a remote repository and decompresses it
// into a local directory.
func PullImage(ctx context.Context, conf PullConf) (*PullResult, error) {
result := &PullResult{
Images: make([]string, 0, 1),
}

cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return nil, fmt.Errorf("cannot establish client for image %s: %w", conf.Image, err)
}
defer cli.Close()

remotePullReader, err := cli.ImagePull(ctx, conf.Image, types.ImagePullOptions{})
if err != nil {
return nil, fmt.Errorf("cannot pull image %s: %w", conf.Image, err)
}
defer remotePullReader.Close()

// Complete the image pull
// TODO: Take the output and overwrite each line in stdout for pretty
// status output instead of spamming the terminal with json
io.Copy(os.Stdout, remotePullReader)

resp, err := cli.ContainerCreate(ctx, &container.Config{
Image: conf.Image,
Tty: false,
}, nil, nil, nil, "")
if err != nil {
return nil, fmt.Errorf("cannot create container from %s: %w", conf.Image, err)
}
defer func() {
err := cli.ContainerRemove(ctx, resp.ID, types.ContainerRemoveOptions{
Force: true,
})
if err != nil {
fmt.Fprintf(os.Stderr, "could not clean up container %s: %s\n", resp.ID, err)
}
}()

ctrImagesPath := filepath.Join("/", "data", "images")
imageReader, _, err := cli.CopyFromContainer(ctx, resp.ID, ctrImagesPath)
if err != nil {
return nil, fmt.Errorf("unable to locate images inside %s: %w", conf.Image, err)
}
defer imageReader.Close()

tarReader := tar.NewReader(imageReader)
for {
hdr, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, fmt.Errorf("failed to read tar header in %s: %w", conf.Image, err)
}

image, err := handleTarObject(ctx, tarReader, hdr, conf, resp.ID)
if err != nil {
return nil, err
}
if image != "" {
result.Images = append(result.Images, image)
}
}

return result, nil
}

func handleTarObject(ctx context.Context, tr *tar.Reader, hdr *tar.Header, conf PullConf, containerID string) (string, error) {
image := ""

dstPath := filepath.Join(conf.TargetDir, hdr.Name)
switch hdr.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(dstPath, 0755); err != nil {
return image, fmt.Errorf("failed to create directory %s: %w", dstPath, err)
}
case tar.TypeReg:
compressed := strings.HasSuffix(dstPath, ".zst")
if compressed {
image = strings.TrimSuffix(dstPath, ".zst")
cmd := exec.CommandContext(ctx, "zstd", "-d", "-", "-o", image)
cmd.Stdin = tr

if _, err := cmd.Output(); err != nil {
var e *exec.ExitError
if errors.As(err, &e) {
fmt.Fprintf(os.Stderr, string(e.Stderr))
}
return image, fmt.Errorf("failed during zst decompression of %s: %w", hdr.Name, err)
}
}
if conf.Cache || !compressed {
dst, err := os.Create(dstPath)
if err != nil {
return image, fmt.Errorf("failed to create file %s: %w", dstPath, err)
}
defer dst.Close()

n, err := io.CopyN(dst, tr, hdr.Size)
if err != nil {
return image, fmt.Errorf("failed to copy %s from container %s: %w", dstPath, containerID, err)
}
if n != hdr.Size {
return image, fmt.Errorf("tar header reports file %s size %d, but only %d bytes were pulled", hdr.Name, hdr.Size, n)
}
}
default:
return image, fmt.Errorf("unexpected tar header type %d", hdr.Typeflag)
}

return image, nil
}
28 changes: 0 additions & 28 deletions scripts/pull_image.sh

This file was deleted.

0 comments on commit ea09cb7

Please sign in to comment.