mirror of https://github.com/portainer/portainer
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.
185 lines
4.1 KiB
185 lines
4.1 KiB
2 years ago
|
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
|
||
|
}
|