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