portainer/portainer

View on GitHub
api/docker/images/digest.go

Summary

Maintainability
A
1 hr
Test Coverage
package images

import (
    "context"
    "strings"
    "time"

    dockerclient "github.com/portainer/portainer/api/docker/client"

    "github.com/containers/image/v5/docker"
    imagetypes "github.com/containers/image/v5/types"
    "github.com/docker/docker/api/types"
    "github.com/opencontainers/go-digest"
    "github.com/pkg/errors"
    "github.com/rs/zerolog/log"
)

// Options holds docker registry object options
type Options struct {
    Auth    imagetypes.DockerAuthConfig
    Timeout time.Duration
}

type DigestClient struct {
    clientFactory  *dockerclient.ClientFactory
    opts           Options
    sysCtx         *imagetypes.SystemContext
    registryClient *RegistryClient
}

func NewClientWithRegistry(registryClient *RegistryClient, clientFactory *dockerclient.ClientFactory) *DigestClient {
    return &DigestClient{
        clientFactory:  clientFactory,
        registryClient: registryClient,
    }
}

func (c *DigestClient) RemoteDigest(image Image) (digest.Digest, error) {
    ctx, cancel := c.timeoutContext()
    defer cancel()
    // Docker references with both a tag and digest are currently not supported
    if image.Tag != "" && image.Digest != "" {
        err := image.trimDigest()
        if err != nil {
            return "", err
        }
    }

    rmRef, err := ParseReference(image.String())
    if err != nil {
        return "", errors.Wrap(err, "Cannot parse the image reference")
    }

    sysCtx := c.sysCtx
    if c.registryClient != nil {
        username, password, err := c.registryClient.RegistryAuth(image)
        if err != nil {
            log.Info().Str("image up to date indicator", image.String()).Msg("No environment registry credentials found, using anonymous access")
        } else {
            sysCtx = &imagetypes.SystemContext{
                DockerAuthConfig: &imagetypes.DockerAuthConfig{
                    Username: username,
                    Password: password,
                },
            }
        }
    }

    // Retrieve remote digest through HEAD request
    rmDigest, err := docker.GetDigest(ctx, sysCtx, rmRef)
    if err != nil {
        // fallback to public registry for hub
        if image.HubLink != "" {
            rmDigest, err = docker.GetDigest(ctx, c.sysCtx, rmRef)
            if err == nil {
                return rmDigest, nil
            }
        }

        log.Debug().Err(err).Msg("get remote digest error")

        return "", errors.Wrap(err, "Cannot get image digest from HEAD request")
    }

    return rmDigest, nil
}

func ParseLocalImage(inspect types.ImageInspect) (*Image, error) {
    if IsLocalImage(inspect) || IsDanglingImage(inspect) {
        return nil, errors.New("the image is not regular")
    }

    fromRepoDigests, err := ParseImage(ParseImageOptions{
        // including image name but no tag
        Name: inspect.RepoDigests[0],
    })
    if err != nil {
        return nil, err
    }

    if IsNoTagImage(inspect) {
        return &fromRepoDigests, nil
    }

    fromRepoTags, err := ParseImage(ParseImageOptions{
        Name: inspect.RepoTags[0],
    })
    if err != nil {
        return nil, err
    }

    fromRepoDigests.Tag = fromRepoTags.Tag

    return &fromRepoDigests, nil
}

func ParseRepoDigests(repoDigests []string) []digest.Digest {
    digests := make([]digest.Digest, 0)
    for _, repoDigest := range repoDigests {
        d := ParseRepoDigest(repoDigest)
        if d == "" {
            continue
        }

        digests = append(digests, d)
    }

    return digests
}

func ParseRepoTags(repoTags []string) []*Image {
    images := make([]*Image, 0)
    for _, repoTag := range repoTags {
        image := ParseRepoTag(repoTag)
        if image != nil {
            images = append(images, image)
        }
    }

    return images
}

func ParseRepoDigest(repoDigest string) digest.Digest {
    if !strings.ContainsAny(repoDigest, "@") {
        return ""
    }

    d, err := digest.Parse(strings.Split(repoDigest, "@")[1])
    if err != nil {
        log.Warn().Msgf("Skip invalid repo digest item: %s [error: %v]", repoDigest, err)

        return ""
    }

    return d
}

func ParseRepoTag(repoTag string) *Image {
    if repoTag == "" {
        return nil
    }

    image, err := ParseImage(ParseImageOptions{
        Name: repoTag,
    })
    if err != nil {
        log.Warn().Err(err).Str("repoTag", repoTag).Msg("RepoTag cannot be parsed.")

        return nil
    }

    return &image
}

func (c *DigestClient) timeoutContext() (context.Context, context.CancelFunc) {
    ctx := context.Background()
    var cancel context.CancelFunc = func() {}

    if c.opts.Timeout > 0 {
        ctx, cancel = context.WithTimeout(ctx, c.opts.Timeout)
    }

    return ctx, cancel
}