mirror of https://github.com/portainer/portainer
				
				
				
			
		
			
				
	
	
		
			305 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Go
		
	
	
			
		
		
	
	
			305 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Go
		
	
	
package images
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"slices"
 | 
						|
	"strings"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/docker/docker/api/types"
 | 
						|
	"github.com/docker/docker/api/types/container"
 | 
						|
	"github.com/docker/docker/api/types/filters"
 | 
						|
	portainer "github.com/portainer/portainer/api"
 | 
						|
	consts "github.com/portainer/portainer/api/docker/consts"
 | 
						|
 | 
						|
	"github.com/opencontainers/go-digest"
 | 
						|
	"github.com/patrickmn/go-cache"
 | 
						|
	"github.com/pkg/errors"
 | 
						|
	"github.com/rs/zerolog/log"
 | 
						|
)
 | 
						|
 | 
						|
// Status constants
 | 
						|
const (
 | 
						|
	Processing = Status("processing")
 | 
						|
	Outdated   = Status("outdated")
 | 
						|
	Updated    = Status("updated")
 | 
						|
	Skipped    = Status("skipped")
 | 
						|
	Preparing  = Status("preparing")
 | 
						|
	Error      = Status("error")
 | 
						|
)
 | 
						|
 | 
						|
var (
 | 
						|
	statusCache       = cache.New(24*time.Hour, 24*time.Hour)
 | 
						|
	remoteDigestCache = cache.New(5*time.Second, 5*time.Second)
 | 
						|
	swarmID2NameCache = cache.New(5*time.Second, 5*time.Second)
 | 
						|
)
 | 
						|
 | 
						|
// Status holds Docker image  analysis
 | 
						|
type Status string
 | 
						|
 | 
						|
func (c *DigestClient) ContainersImageStatus(ctx context.Context, containers []types.Container, endpoint *portainer.Endpoint) Status {
 | 
						|
	cli, err := c.clientFactory.CreateClient(endpoint, "", nil)
 | 
						|
	if err != nil {
 | 
						|
		log.Error().Err(err).Msg("cannot create docker client")
 | 
						|
 | 
						|
		return Error
 | 
						|
	}
 | 
						|
 | 
						|
	statuses := make([]Status, len(containers))
 | 
						|
	for i, ct := range containers {
 | 
						|
		var nodeName string
 | 
						|
		if swarmNodeId := ct.Labels[consts.SwarmNodeIDLabel]; swarmNodeId != "" {
 | 
						|
			if swarmNodeName, ok := swarmID2NameCache.Get(swarmNodeId); ok {
 | 
						|
				nodeName, _ = swarmNodeName.(string)
 | 
						|
			} else {
 | 
						|
				node, _, err := cli.NodeInspectWithRaw(ctx, ct.Labels[consts.SwarmNodeIDLabel])
 | 
						|
				if err != nil {
 | 
						|
					return Error
 | 
						|
				}
 | 
						|
 | 
						|
				nodeName = node.Description.Hostname
 | 
						|
				swarmID2NameCache.Set(swarmNodeId, nodeName, 0)
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		s, err := c.ContainerImageStatus(ctx, ct.ID, endpoint, nodeName)
 | 
						|
		if err != nil {
 | 
						|
			statuses[i] = Error
 | 
						|
			log.Warn().Str("containerId", ct.ID).Err(err).Msg("error when fetching image status for container")
 | 
						|
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		statuses[i] = s
 | 
						|
 | 
						|
		if s == Outdated || s == Processing {
 | 
						|
			break
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return FigureOut(statuses)
 | 
						|
}
 | 
						|
 | 
						|
func FigureOut(statuses []Status) Status {
 | 
						|
	if allMatch(statuses, Skipped) {
 | 
						|
		return Skipped
 | 
						|
	}
 | 
						|
 | 
						|
	if allMatch(statuses, Preparing) {
 | 
						|
		return Preparing
 | 
						|
	}
 | 
						|
 | 
						|
	if contains(statuses, Outdated) {
 | 
						|
		return Outdated
 | 
						|
	} else if contains(statuses, Processing) {
 | 
						|
		return Processing
 | 
						|
	} else if contains(statuses, Error) {
 | 
						|
		return Error
 | 
						|
	}
 | 
						|
 | 
						|
	return Updated
 | 
						|
}
 | 
						|
 | 
						|
func (c *DigestClient) ContainerImageStatus(ctx context.Context, containerID string, endpoint *portainer.Endpoint, nodeName string) (Status, error) {
 | 
						|
	cli, err := c.clientFactory.CreateClient(endpoint, nodeName, nil)
 | 
						|
	if err != nil {
 | 
						|
		log.Warn().Str("swarmNodeId", nodeName).Msg("Cannot create new docker client.")
 | 
						|
	}
 | 
						|
 | 
						|
	container, err := cli.ContainerInspect(ctx, containerID)
 | 
						|
	if err != nil {
 | 
						|
		log.Warn().Err(err).Str("containerID", containerID).Msg("Inspect container error.")
 | 
						|
		return Skipped, nil
 | 
						|
	}
 | 
						|
 | 
						|
	var imageID string
 | 
						|
	if strings.Contains(container.Image, "sha256") {
 | 
						|
		imageID = container.Image[strings.Index(container.Image, "sha256"):]
 | 
						|
	}
 | 
						|
 | 
						|
	if imageID == "" {
 | 
						|
		return Skipped, nil
 | 
						|
	}
 | 
						|
 | 
						|
	digs := make([]digest.Digest, 0)
 | 
						|
	images := make([]*Image, 0)
 | 
						|
	if i, err := ParseImage(ParseImageOptions{Name: container.Config.Image}); err == nil {
 | 
						|
		images = append(images, &i)
 | 
						|
	}
 | 
						|
 | 
						|
	imageInspect, _, err := cli.ImageInspectWithRaw(ctx, imageID)
 | 
						|
	if err != nil {
 | 
						|
		log.Debug().Str("imageID", imageID).Msg("inspect failed")
 | 
						|
		return Error, err
 | 
						|
	}
 | 
						|
 | 
						|
	if len(imageInspect.RepoDigests) > 0 {
 | 
						|
		digs = append(digs, ParseRepoDigests(imageInspect.RepoDigests)...)
 | 
						|
	}
 | 
						|
 | 
						|
	if len(imageInspect.RepoTags) > 0 {
 | 
						|
		images = append(images, ParseRepoTags(imageInspect.RepoTags)...)
 | 
						|
	}
 | 
						|
 | 
						|
	s, err := c.checkStatus(images, digs)
 | 
						|
	if err != nil {
 | 
						|
		log.Debug().Str("image", container.Image).Err(err).Msg("fetching a certain image status")
 | 
						|
		return Error, err
 | 
						|
	}
 | 
						|
 | 
						|
	statusCache.Set(imageID, s, 0)
 | 
						|
 | 
						|
	return s, err
 | 
						|
}
 | 
						|
 | 
						|
func (c *DigestClient) ServiceImageStatus(ctx context.Context, serviceID string, endpoint *portainer.Endpoint) (Status, error) {
 | 
						|
	cli, err := c.clientFactory.CreateClient(endpoint, "", nil)
 | 
						|
	if err != nil {
 | 
						|
		return Error, nil
 | 
						|
	}
 | 
						|
 | 
						|
	containers, err := cli.ContainerList(ctx, container.ListOptions{
 | 
						|
		All:     true,
 | 
						|
		Filters: filters.NewArgs(filters.Arg("label", consts.SwarmServiceIDLabel+"="+serviceID)),
 | 
						|
	})
 | 
						|
	if err != nil {
 | 
						|
		log.Warn().Err(err).Str("serviceID", serviceID).Msg("cannot list container for the service")
 | 
						|
		return Error, err
 | 
						|
	}
 | 
						|
 | 
						|
	nonExistedOrStoppedContainers := make([]types.Container, 0)
 | 
						|
	for _, container := range containers {
 | 
						|
		if container.State == "exited" || container.State == "stopped" {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		// When there is a container with the state "Created" under the service, it
 | 
						|
		// indicates that the Docker Swarm is replacing the existing task with
 | 
						|
		// a new task. At the moment, the state of the new task is "Created", and
 | 
						|
		// the state of the old task is "Running".
 | 
						|
		// Until the new task runs up, the image status should be set "Preparing"
 | 
						|
		if container.State == "created" {
 | 
						|
			return Preparing, nil
 | 
						|
		}
 | 
						|
		nonExistedOrStoppedContainers = append(nonExistedOrStoppedContainers, container)
 | 
						|
	}
 | 
						|
 | 
						|
	if len(nonExistedOrStoppedContainers) == 0 {
 | 
						|
		return Preparing, nil
 | 
						|
	}
 | 
						|
 | 
						|
	return c.ContainersImageStatus(ctx, nonExistedOrStoppedContainers, endpoint), nil
 | 
						|
}
 | 
						|
 | 
						|
func (c *DigestClient) checkStatus(images []*Image, digests []digest.Digest) (Status, error) {
 | 
						|
	if digests == nil {
 | 
						|
		digests = make([]digest.Digest, 0)
 | 
						|
	}
 | 
						|
 | 
						|
	for _, img := range images {
 | 
						|
		if img.Digest != "" && !slices.Contains(digests, img.Digest) {
 | 
						|
			log.Info().Str("localDigest", img.Domain).Msg("incoming local digest is not nil")
 | 
						|
			digests = append([]digest.Digest{img.Digest}, digests...)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if len(digests) == 0 {
 | 
						|
		return Skipped, nil
 | 
						|
	}
 | 
						|
 | 
						|
	var imageStatus Status
 | 
						|
 | 
						|
	for _, img := range images {
 | 
						|
		var remoteDigest digest.Digest
 | 
						|
		var err error
 | 
						|
		if rd, ok := remoteDigestCache.Get(img.FullName()); ok {
 | 
						|
			remoteDigest, _ = rd.(digest.Digest)
 | 
						|
		}
 | 
						|
		if remoteDigest == "" {
 | 
						|
			remoteDigest, err = c.RemoteDigest(*img)
 | 
						|
			if err != nil {
 | 
						|
				log.Error().Str("image", img.String()).Msg("error when fetch remote digest for image")
 | 
						|
				return Error, err
 | 
						|
			}
 | 
						|
		}
 | 
						|
		remoteDigestCache.Set(img.FullName(), remoteDigest, 0)
 | 
						|
 | 
						|
		log.Debug().Str("image", img.FullName()).Stringer("remote_digest", remoteDigest).
 | 
						|
			Int("local_digest_size", len(digests)).
 | 
						|
			Msg("Digests")
 | 
						|
 | 
						|
		// final locals vs remote one
 | 
						|
		for _, dig := range digests {
 | 
						|
			log.Debug().
 | 
						|
				Str("image", img.FullName()).
 | 
						|
				Stringer("remote_digest", remoteDigest).
 | 
						|
				Stringer("local_digest", dig).
 | 
						|
				Msg("Comparing")
 | 
						|
 | 
						|
			if dig == remoteDigest {
 | 
						|
				log.Debug().Str("image", img.FullName()).
 | 
						|
					Stringer("remote_digest", remoteDigest).
 | 
						|
					Stringer("local_digest", dig).
 | 
						|
					Msg("Found a match")
 | 
						|
				return Updated, nil
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	imageStatus = Outdated
 | 
						|
 | 
						|
	return imageStatus, nil
 | 
						|
}
 | 
						|
 | 
						|
func CachedResourceImageStatus(resourceID string) (Status, error) {
 | 
						|
	if s, ok := statusCache.Get(resourceID); ok {
 | 
						|
		return s.(Status), nil
 | 
						|
	}
 | 
						|
 | 
						|
	return "", errors.Errorf("no image found in cache: %s", resourceID)
 | 
						|
}
 | 
						|
 | 
						|
func CacheResourceImageStatus(resourceID string, status Status) {
 | 
						|
	statusCache.Set(resourceID, status, 0)
 | 
						|
}
 | 
						|
 | 
						|
func CachedImageDigest(resourceID string) (Status, error) {
 | 
						|
	if s, ok := statusCache.Get(resourceID); ok {
 | 
						|
		return s.(Status), nil
 | 
						|
	}
 | 
						|
 | 
						|
	return "", errors.Errorf("no image found in cache: %s", resourceID)
 | 
						|
}
 | 
						|
 | 
						|
func EvictImageStatus(resourceID string) {
 | 
						|
	statusCache.Delete(resourceID)
 | 
						|
}
 | 
						|
 | 
						|
func contains(statuses []Status, status Status) bool {
 | 
						|
	if len(statuses) == 0 {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
 | 
						|
	for _, s := range statuses {
 | 
						|
		if s == status {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return false
 | 
						|
}
 | 
						|
 | 
						|
func allMatch(statuses []Status, status Status) bool {
 | 
						|
	if len(statuses) == 0 {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
 | 
						|
	for _, s := range statuses {
 | 
						|
		if s != status {
 | 
						|
			return false
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return true
 | 
						|
}
 |