github.com/containers/libpod@v1.9.4-0.20220419124438-4284fd425507/pkg/autoupdate/autoupdate.go (about)

     1  package autoupdate
     2  
     3  import (
     4  	"context"
     5  	"os"
     6  	"sort"
     7  
     8  	"github.com/containers/image/v5/docker"
     9  	"github.com/containers/image/v5/docker/reference"
    10  	"github.com/containers/image/v5/manifest"
    11  	"github.com/containers/image/v5/transports/alltransports"
    12  	"github.com/containers/libpod/libpod"
    13  	"github.com/containers/libpod/libpod/define"
    14  	"github.com/containers/libpod/libpod/image"
    15  	"github.com/containers/libpod/pkg/systemd"
    16  	systemdGen "github.com/containers/libpod/pkg/systemd/generate"
    17  	"github.com/containers/libpod/pkg/util"
    18  	"github.com/pkg/errors"
    19  	"github.com/sirupsen/logrus"
    20  )
    21  
    22  // Label denotes the container/pod label key to specify auto-update policies in
    23  // container labels.
    24  const Label = "io.containers.autoupdate"
    25  
    26  // Policy represents an auto-update policy.
    27  type Policy string
    28  
    29  const (
    30  	// PolicyDefault is the default policy denoting no auto updates.
    31  	PolicyDefault Policy = "disabled"
    32  	// PolicyNewImage is the policy to update as soon as there's a new image found.
    33  	PolicyNewImage = "image"
    34  )
    35  
    36  // Map for easy lookups of supported policies.
    37  var supportedPolicies = map[string]Policy{
    38  	"":         PolicyDefault,
    39  	"disabled": PolicyDefault,
    40  	"image":    PolicyNewImage,
    41  }
    42  
    43  // LookupPolicy looksup the corresponding Policy for the specified
    44  // string. If none is found, an errors is returned including the list of
    45  // supported policies.
    46  //
    47  // Note that an empty string resolved to PolicyDefault.
    48  func LookupPolicy(s string) (Policy, error) {
    49  	policy, exists := supportedPolicies[s]
    50  	if exists {
    51  		return policy, nil
    52  	}
    53  
    54  	// Sort the keys first as maps are non-deterministic.
    55  	keys := []string{}
    56  	for k := range supportedPolicies {
    57  		if k != "" {
    58  			keys = append(keys, k)
    59  		}
    60  	}
    61  	sort.Strings(keys)
    62  
    63  	return "", errors.Errorf("invalid auto-update policy %q: valid policies are %+q", s, keys)
    64  }
    65  
    66  // ValidateImageReference checks if the specified imageName is a fully-qualified
    67  // image reference to the docker transport (without digest).  Such a reference
    68  // includes a domain, name and tag (e.g., quay.io/podman/stable:latest).  The
    69  // reference may also be prefixed with "docker://" explicitly indicating that
    70  // it's a reference to the docker transport.
    71  func ValidateImageReference(imageName string) error {
    72  	// Make sure the input image is a docker.
    73  	imageRef, err := alltransports.ParseImageName(imageName)
    74  	if err == nil && imageRef.Transport().Name() != docker.Transport.Name() {
    75  		return errors.Errorf("auto updates require the docker image transport but image is of transport %q", imageRef.Transport().Name())
    76  	} else if err != nil {
    77  		repo, err := reference.Parse(imageName)
    78  		if err != nil {
    79  			return errors.Wrap(err, "error enforcing fully-qualified docker transport reference for auto updates")
    80  		}
    81  		if _, ok := repo.(reference.NamedTagged); !ok {
    82  			return errors.Errorf("auto updates require fully-qualified image references (no tag): %q", imageName)
    83  		}
    84  		if _, ok := repo.(reference.Digested); ok {
    85  			return errors.Errorf("auto updates require fully-qualified image references without digest: %q", imageName)
    86  		}
    87  	}
    88  	return nil
    89  }
    90  
    91  // AutoUpdate looks up containers with a specified auto-update policy and acts
    92  // accordingly.  If the policy is set to PolicyNewImage, it checks if the image
    93  // on the remote registry is different than the local one. If the image digests
    94  // differ, it pulls the remote image and restarts the systemd unit running the
    95  // container.
    96  //
    97  // It returns a slice of successfully restarted systemd units and a slice of
    98  // errors encountered during auto update.
    99  func AutoUpdate(runtime *libpod.Runtime) ([]string, []error) {
   100  	// Create a map from `image ID -> []*Container`.
   101  	containerMap, errs := imageContainersMap(runtime)
   102  	if len(containerMap) == 0 {
   103  		return nil, errs
   104  	}
   105  
   106  	// Create a map from `image ID -> *image.Image` for image lookups.
   107  	imagesSlice, err := runtime.ImageRuntime().GetImages()
   108  	if err != nil {
   109  		return nil, []error{err}
   110  	}
   111  	imageMap := make(map[string]*image.Image)
   112  	for i := range imagesSlice {
   113  		imageMap[imagesSlice[i].ID()] = imagesSlice[i]
   114  	}
   115  
   116  	// Connect to DBUS.
   117  	conn, err := systemd.ConnectToDBUS()
   118  	if err != nil {
   119  		logrus.Errorf(err.Error())
   120  		return nil, []error{err}
   121  	}
   122  	defer conn.Close()
   123  
   124  	// Update images.
   125  	containersToRestart := []*libpod.Container{}
   126  	updatedRawImages := make(map[string]bool)
   127  	for imageID, containers := range containerMap {
   128  		image, exists := imageMap[imageID]
   129  		if !exists {
   130  			errs = append(errs, errors.Errorf("container image ID %q not found in local storage", imageID))
   131  			return nil, errs
   132  		}
   133  		// Now we have to check if the image of any containers must be updated.
   134  		// Note that the image ID is NOT enough for this check as a given image
   135  		// may have multiple tags.
   136  		for i, ctr := range containers {
   137  			rawImageName := ctr.RawImageName()
   138  			if rawImageName == "" {
   139  				errs = append(errs, errors.Errorf("error auto-updating container %q: raw-image name is empty", ctr.ID()))
   140  			}
   141  			needsUpdate, err := newerImageAvailable(runtime, image, rawImageName)
   142  			if err != nil {
   143  				errs = append(errs, errors.Wrapf(err, "error auto-updating container %q: image check for %q failed", ctr.ID(), rawImageName))
   144  				continue
   145  			}
   146  			if !needsUpdate {
   147  				continue
   148  			}
   149  			logrus.Infof("Auto-updating container %q using image %q", ctr.ID(), rawImageName)
   150  			if _, updated := updatedRawImages[rawImageName]; !updated {
   151  				_, err = updateImage(runtime, rawImageName)
   152  				if err != nil {
   153  					errs = append(errs, errors.Wrapf(err, "error auto-updating container %q: image update for %q failed", ctr.ID(), rawImageName))
   154  					continue
   155  				}
   156  				updatedRawImages[rawImageName] = true
   157  			}
   158  			containersToRestart = append(containersToRestart, containers[i])
   159  		}
   160  	}
   161  
   162  	// Restart containers.
   163  	updatedUnits := []string{}
   164  	for _, ctr := range containersToRestart {
   165  		labels := ctr.Labels()
   166  		unit, exists := labels[systemdGen.EnvVariable]
   167  		if !exists {
   168  			// Shouldn't happen but let's be sure of it.
   169  			errs = append(errs, errors.Errorf("error auto-updating container %q: no %s label found", ctr.ID(), systemdGen.EnvVariable))
   170  			continue
   171  		}
   172  		_, err := conn.RestartUnit(unit, "replace", nil)
   173  		if err != nil {
   174  			errs = append(errs, errors.Wrapf(err, "error auto-updating container %q: restarting systemd unit %q failed", ctr.ID(), unit))
   175  			continue
   176  		}
   177  		logrus.Infof("Successfully restarted systemd unit %q", unit)
   178  		updatedUnits = append(updatedUnits, unit)
   179  	}
   180  
   181  	return updatedUnits, errs
   182  }
   183  
   184  // imageContainersMap generates a map[image ID] -> [containers using the image]
   185  // of all containers with a valid auto-update policy.
   186  func imageContainersMap(runtime *libpod.Runtime) (map[string][]*libpod.Container, []error) {
   187  	allContainers, err := runtime.GetAllContainers()
   188  	if err != nil {
   189  		return nil, []error{err}
   190  	}
   191  
   192  	errors := []error{}
   193  	imageMap := make(map[string][]*libpod.Container)
   194  	for i, ctr := range allContainers {
   195  		state, err := ctr.State()
   196  		if err != nil {
   197  			errors = append(errors, err)
   198  			continue
   199  		}
   200  		// Only update running containers.
   201  		if state != define.ContainerStateRunning {
   202  			continue
   203  		}
   204  		// Only update containers with the specific label/policy set.
   205  		labels := ctr.Labels()
   206  		if value, exists := labels[Label]; exists {
   207  			policy, err := LookupPolicy(value)
   208  			if err != nil {
   209  				errors = append(errors, err)
   210  				continue
   211  			}
   212  			if policy != PolicyNewImage {
   213  				continue
   214  			}
   215  		}
   216  		// Now we know that `ctr` is configured for auto updates.
   217  		id, _ := ctr.Image()
   218  		imageMap[id] = append(imageMap[id], allContainers[i])
   219  	}
   220  
   221  	return imageMap, errors
   222  }
   223  
   224  // newerImageAvailable returns true if there corresponding image on the remote
   225  // registry is newer.
   226  func newerImageAvailable(runtime *libpod.Runtime, img *image.Image, origName string) (bool, error) {
   227  	remoteRef, err := docker.ParseReference("//" + origName)
   228  	if err != nil {
   229  		return false, err
   230  	}
   231  
   232  	remoteImg, err := remoteRef.NewImage(context.Background(), runtime.SystemContext())
   233  	if err != nil {
   234  		return false, err
   235  	}
   236  
   237  	rawManifest, _, err := remoteImg.Manifest(context.Background())
   238  	if err != nil {
   239  		return false, err
   240  	}
   241  
   242  	remoteDigest, err := manifest.Digest(rawManifest)
   243  	if err != nil {
   244  		return false, err
   245  	}
   246  
   247  	return img.Digest().String() != remoteDigest.String(), nil
   248  }
   249  
   250  // updateImage pulls the specified image.
   251  func updateImage(runtime *libpod.Runtime, name string) (*image.Image, error) {
   252  	sys := runtime.SystemContext()
   253  	registryOpts := image.DockerRegistryOptions{}
   254  	signaturePolicyPath := ""
   255  	authFilePath := ""
   256  
   257  	if sys != nil {
   258  		registryOpts.OSChoice = sys.OSChoice
   259  		registryOpts.ArchitectureChoice = sys.OSChoice
   260  		registryOpts.DockerCertPath = sys.DockerCertPath
   261  
   262  		signaturePolicyPath = sys.SignaturePolicyPath
   263  		authFilePath = sys.AuthFilePath
   264  	}
   265  
   266  	newImage, err := runtime.ImageRuntime().New(context.Background(),
   267  		docker.Transport.Name()+"://"+name,
   268  		signaturePolicyPath,
   269  		authFilePath,
   270  		os.Stderr,
   271  		&registryOpts,
   272  		image.SigningOptions{},
   273  		nil,
   274  		util.PullImageAlways,
   275  	)
   276  	if err != nil {
   277  		return nil, err
   278  	}
   279  	return newImage, nil
   280  }