dotcloud/docker

View on GitHub
testutil/fixtures/load/frozen.go

Summary

Maintainability
A
2 hrs
Test Coverage
package load // import "github.com/docker/docker/testutil/fixtures/load"

import (
    "bufio"
    "bytes"
    "context"
    "os"
    "os/exec"
    "path/filepath"
    "strings"
    "sync"

    "github.com/docker/docker/api/types/image"
    "github.com/docker/docker/client"
    "github.com/docker/docker/pkg/jsonmessage"
    "github.com/moby/term"
    "github.com/pkg/errors"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/codes"
    "go.opentelemetry.io/otel/trace"
)

const frozenImgDir = "/docker-frozen-images"

// FrozenImagesLinux loads the frozen image set for the integration suite
// If the images are not available locally it will download them
// TODO: This loads whatever is in the frozen image dir, regardless of what
// images were passed in. If the images need to be downloaded, then it will respect
// the passed in images
func FrozenImagesLinux(ctx context.Context, client client.APIClient, images ...string) error {
    ctx, span := otel.Tracer("").Start(ctx, "LoadFrozenImages")
    defer span.End()

    var loadImages []struct{ srcName, destName string }
    for _, img := range images {
        if !imageExists(ctx, client, img) {
            srcName := img
            // hello-world:latest gets re-tagged as hello-world:frozen
            // there are some tests that use hello-world:latest specifically so it pulls
            // the image and hello-world:frozen is used for when we just want a super
            // small image
            if img == "hello-world:frozen" {
                srcName = "hello-world:latest"
            }
            loadImages = append(loadImages, struct{ srcName, destName string }{
                srcName:  srcName,
                destName: img,
            })
        }
    }
    if len(loadImages) == 0 {
        // everything is loaded, we're done
        return nil
    }

    fi, err := os.Stat(frozenImgDir)
    if err != nil || !fi.IsDir() {
        srcImages := make([]string, 0, len(loadImages))
        for _, img := range loadImages {
            srcImages = append(srcImages, img.srcName)
        }
        if err := pullImages(ctx, client, srcImages); err != nil {
            return errors.Wrap(err, "error pulling image list")
        }
    } else {
        if err := loadFrozenImages(ctx, client); err != nil {
            return err
        }
    }

    for _, img := range loadImages {
        if img.srcName != img.destName {
            if err := client.ImageTag(ctx, img.srcName, img.destName); err != nil {
                return errors.Wrapf(err, "failed to tag %s as %s", img.srcName, img.destName)
            }
            if _, err := client.ImageRemove(ctx, img.srcName, image.RemoveOptions{}); err != nil {
                return errors.Wrapf(err, "failed to remove %s", img.srcName)
            }
        }
    }
    return nil
}

func imageExists(ctx context.Context, client client.APIClient, name string) bool {
    ctx, span := otel.Tracer("").Start(ctx, "check image exists: "+name)
    defer span.End()
    _, _, err := client.ImageInspectWithRaw(ctx, name)
    if err != nil {
        span.RecordError(err)
    }
    return err == nil
}

func loadFrozenImages(ctx context.Context, client client.APIClient) error {
    ctx, span := otel.Tracer("").Start(ctx, "load frozen images")
    defer span.End()

    tar, err := exec.LookPath("tar")
    if err != nil {
        return errors.Wrap(err, "could not find tar binary")
    }
    tarCmd := exec.Command(tar, "-cC", frozenImgDir, ".")
    out, err := tarCmd.StdoutPipe()
    if err != nil {
        return errors.Wrap(err, "error getting stdout pipe for tar command")
    }

    errBuf := bytes.NewBuffer(nil)
    tarCmd.Stderr = errBuf
    tarCmd.Start()
    defer tarCmd.Wait()

    resp, err := client.ImageLoad(ctx, out, image.LoadOptions{Quiet: true})
    if err != nil {
        return errors.Wrap(err, "failed to load frozen images")
    }
    defer resp.Body.Close()
    fd, isTerminal := term.GetFdInfo(os.Stdout)
    return jsonmessage.DisplayJSONMessagesStream(resp.Body, os.Stdout, fd, isTerminal, nil)
}

func pullImages(ctx context.Context, client client.APIClient, images []string) error {
    cwd, err := os.Getwd()
    if err != nil {
        return errors.Wrap(err, "error getting path to dockerfile")
    }
    dockerfile := os.Getenv("DOCKERFILE")
    if dockerfile == "" {
        dockerfile = "Dockerfile"
    }
    dockerfilePath := filepath.Join(filepath.Dir(filepath.Clean(cwd)), dockerfile)
    pullRefs, err := readFrozenImageList(ctx, dockerfilePath, images)
    if err != nil {
        return errors.Wrap(err, "error reading frozen image list")
    }

    var wg sync.WaitGroup
    chErr := make(chan error, len(images))
    for tag, ref := range pullRefs {
        wg.Add(1)
        go func(tag, ref string) {
            defer wg.Done()
            if err := pullTagAndRemove(ctx, client, ref, tag); err != nil {
                chErr <- err
                return
            }
        }(tag, ref)
    }
    wg.Wait()
    close(chErr)
    return <-chErr
}

func pullTagAndRemove(ctx context.Context, client client.APIClient, ref string, tag string) (retErr error) {
    ctx, span := otel.Tracer("").Start(ctx, "pull image: "+ref+" with tag: "+tag)
    defer func() {
        if retErr != nil {
            // An error here is a real error for the span, so set the span status
            span.SetStatus(codes.Error, retErr.Error())
        }
        span.End()
    }()

    resp, err := client.ImagePull(ctx, ref, image.PullOptions{})
    if err != nil {
        return errors.Wrapf(err, "failed to pull %s", ref)
    }
    defer resp.Close()
    fd, isTerminal := term.GetFdInfo(os.Stdout)
    if err := jsonmessage.DisplayJSONMessagesStream(resp, os.Stdout, fd, isTerminal, nil); err != nil {
        return err
    }

    if err := client.ImageTag(ctx, ref, tag); err != nil {
        return errors.Wrapf(err, "failed to tag %s as %s", ref, tag)
    }
    _, err = client.ImageRemove(ctx, ref, image.RemoveOptions{})
    return errors.Wrapf(err, "failed to remove %s", ref)
}

func readFrozenImageList(ctx context.Context, dockerfilePath string, images []string) (map[string]string, error) {
    f, err := os.Open(dockerfilePath)
    if err != nil {
        return nil, errors.Wrap(err, "error reading dockerfile")
    }
    defer f.Close()
    ls := make(map[string]string)

    span := trace.SpanFromContext(ctx)

    scanner := bufio.NewScanner(f)
    for scanner.Scan() {
        line := strings.Fields(scanner.Text())
        if len(line) < 3 {
            continue
        }
        if !(line[0] == "RUN" && line[1] == "./contrib/download-frozen-image-v2.sh") {
            continue
        }

        for scanner.Scan() {
            img := strings.TrimSpace(scanner.Text())
            img = strings.TrimSuffix(img, "\\")
            img = strings.TrimSpace(img)
            split := strings.Split(img, "@")
            if len(split) < 2 {
                break
            }

            for _, i := range images {
                if split[0] == i {
                    ls[i] = img
                    if span.IsRecording() {
                        span.AddEvent("found frozen image", trace.WithAttributes(attribute.String("image", i)))
                    }
                    break
                }
            }
        }
    }
    return ls, nil
}