github.com/hanks177/podman/v4@v4.1.3-0.20220613032544-16d90015bc83/pkg/autoupdate/autoupdate.go (about)

     1  package autoupdate
     2  
     3  import (
     4  	"context"
     5  	"os"
     6  	"sort"
     7  
     8  	"github.com/containers/common/libimage"
     9  	"github.com/containers/common/pkg/config"
    10  	"github.com/containers/image/v5/docker"
    11  	"github.com/containers/image/v5/docker/reference"
    12  	"github.com/containers/image/v5/transports/alltransports"
    13  	"github.com/hanks177/podman/v4/libpod"
    14  	"github.com/hanks177/podman/v4/libpod/define"
    15  	"github.com/hanks177/podman/v4/libpod/events"
    16  	"github.com/hanks177/podman/v4/pkg/domain/entities"
    17  	"github.com/hanks177/podman/v4/pkg/systemd"
    18  	systemdDefine "github.com/hanks177/podman/v4/pkg/systemd/define"
    19  	"github.com/coreos/go-systemd/v22/dbus"
    20  	"github.com/pkg/errors"
    21  	"github.com/sirupsen/logrus"
    22  )
    23  
    24  // Label denotes the container/pod label key to specify auto-update policies in
    25  // container labels.
    26  const Label = "io.containers.autoupdate"
    27  
    28  // Label denotes the container label key to specify authfile in
    29  // container labels.
    30  const AuthfileLabel = "io.containers.autoupdate.authfile"
    31  
    32  // Policy represents an auto-update policy.
    33  type Policy string
    34  
    35  const (
    36  	// PolicyDefault is the default policy denoting no auto updates.
    37  	PolicyDefault Policy = "disabled"
    38  	// PolicyRegistryImage is the policy to update as soon as there's a new image found.
    39  	PolicyRegistryImage = "registry"
    40  	// PolicyLocalImage is the policy to run auto-update based on a local image
    41  	PolicyLocalImage = "local"
    42  )
    43  
    44  // Map for easy lookups of supported policies.
    45  var supportedPolicies = map[string]Policy{
    46  	"":         PolicyDefault,
    47  	"disabled": PolicyDefault,
    48  	"image":    PolicyRegistryImage,
    49  	"registry": PolicyRegistryImage,
    50  	"local":    PolicyLocalImage,
    51  }
    52  
    53  // policyMapper is used for tying a container to it's autoupdate policy
    54  type policyMapper map[Policy][]*libpod.Container
    55  
    56  // LookupPolicy looks up the corresponding Policy for the specified
    57  // string. If none is found, an errors is returned including the list of
    58  // supported policies.
    59  //
    60  // Note that an empty string resolved to PolicyDefault.
    61  func LookupPolicy(s string) (Policy, error) {
    62  	policy, exists := supportedPolicies[s]
    63  	if exists {
    64  		return policy, nil
    65  	}
    66  
    67  	// Sort the keys first as maps are non-deterministic.
    68  	keys := []string{}
    69  	for k := range supportedPolicies {
    70  		if k != "" {
    71  			keys = append(keys, k)
    72  		}
    73  	}
    74  	sort.Strings(keys)
    75  
    76  	return "", errors.Errorf("invalid auto-update policy %q: valid policies are %+q", s, keys)
    77  }
    78  
    79  // ValidateImageReference checks if the specified imageName is a fully-qualified
    80  // image reference to the docker transport (without digest).  Such a reference
    81  // includes a domain, name and tag (e.g., quay.io/podman/stable:latest).  The
    82  // reference may also be prefixed with "docker://" explicitly indicating that
    83  // it's a reference to the docker transport.
    84  func ValidateImageReference(imageName string) error {
    85  	// Make sure the input image is a docker.
    86  	imageRef, err := alltransports.ParseImageName(imageName)
    87  	if err == nil && imageRef.Transport().Name() != docker.Transport.Name() {
    88  		return errors.Errorf("auto updates require the docker image transport but image is of transport %q", imageRef.Transport().Name())
    89  	} else if err != nil {
    90  		repo, err := reference.Parse(imageName)
    91  		if err != nil {
    92  			return errors.Wrap(err, "enforcing fully-qualified docker transport reference for auto updates")
    93  		}
    94  		if _, ok := repo.(reference.NamedTagged); !ok {
    95  			return errors.Errorf("auto updates require fully-qualified image references (no tag): %q", imageName)
    96  		}
    97  		if _, ok := repo.(reference.Digested); ok {
    98  			return errors.Errorf("auto updates require fully-qualified image references without digest: %q", imageName)
    99  		}
   100  	}
   101  	return nil
   102  }
   103  
   104  // AutoUpdate looks up containers with a specified auto-update policy and acts
   105  // accordingly.
   106  //
   107  // If the policy is set to PolicyRegistryImage, it checks if the image
   108  // on the remote registry is different than the local one. If the image digests
   109  // differ, it pulls the remote image and restarts the systemd unit running the
   110  // container.
   111  //
   112  // If the policy is set to PolicyLocalImage, it checks if the image
   113  // of a running container is different than the local one. If the image digests
   114  // differ, it restarts the systemd unit with the new image.
   115  //
   116  // It returns a slice of successfully restarted systemd units and a slice of
   117  // errors encountered during auto update.
   118  func AutoUpdate(ctx context.Context, runtime *libpod.Runtime, options entities.AutoUpdateOptions) ([]*entities.AutoUpdateReport, []error) {
   119  	// Create a map from `image ID -> []*Container`.
   120  	containerMap, errs := imageContainersMap(runtime)
   121  	if len(containerMap) == 0 {
   122  		return nil, errs
   123  	}
   124  
   125  	// Create a map from `image ID -> *libimage.Image` for image lookups.
   126  	listOptions := &libimage.ListImagesOptions{
   127  		Filters: []string{"readonly=false"},
   128  	}
   129  	imagesSlice, err := runtime.LibimageRuntime().ListImages(ctx, nil, listOptions)
   130  	if err != nil {
   131  		return nil, []error{err}
   132  	}
   133  	imageMap := make(map[string]*libimage.Image)
   134  	for i := range imagesSlice {
   135  		imageMap[imagesSlice[i].ID()] = imagesSlice[i]
   136  	}
   137  
   138  	// Connect to DBUS.
   139  	conn, err := systemd.ConnectToDBUS()
   140  	if err != nil {
   141  		logrus.Errorf(err.Error())
   142  		return nil, []error{err}
   143  	}
   144  	defer conn.Close()
   145  
   146  	runtime.NewSystemEvent(events.AutoUpdate)
   147  
   148  	// Update all images/container according to their auto-update policy.
   149  	var allReports []*entities.AutoUpdateReport
   150  	updatedRawImages := make(map[string]bool)
   151  	for imageID, policyMapper := range containerMap {
   152  		image, exists := imageMap[imageID]
   153  		if !exists {
   154  			errs = append(errs, errors.Errorf("container image ID %q not found in local storage", imageID))
   155  			return nil, errs
   156  		}
   157  
   158  		for _, ctr := range policyMapper[PolicyRegistryImage] {
   159  			report, err := autoUpdateRegistry(ctx, image, ctr, updatedRawImages, &options, conn, runtime)
   160  			if err != nil {
   161  				errs = append(errs, err)
   162  			}
   163  			if report != nil {
   164  				allReports = append(allReports, report)
   165  			}
   166  		}
   167  
   168  		for _, ctr := range policyMapper[PolicyLocalImage] {
   169  			report, err := autoUpdateLocally(ctx, image, ctr, &options, conn, runtime)
   170  			if err != nil {
   171  				errs = append(errs, err)
   172  			}
   173  			if report != nil {
   174  				allReports = append(allReports, report)
   175  			}
   176  		}
   177  	}
   178  
   179  	return allReports, errs
   180  }
   181  
   182  // autoUpdateRegistry updates the image/container according to the "registry" policy.
   183  func autoUpdateRegistry(ctx context.Context, image *libimage.Image, ctr *libpod.Container, updatedRawImages map[string]bool, options *entities.AutoUpdateOptions, conn *dbus.Conn, runtime *libpod.Runtime) (*entities.AutoUpdateReport, error) {
   184  	cid := ctr.ID()
   185  	rawImageName := ctr.RawImageName()
   186  	if rawImageName == "" {
   187  		return nil, errors.Errorf("registry auto-updating container %q: raw-image name is empty", cid)
   188  	}
   189  
   190  	labels := ctr.Labels()
   191  	unit, exists := labels[systemdDefine.EnvVariable]
   192  	if !exists {
   193  		return nil, errors.Errorf("auto-updating container %q: no %s label found", ctr.ID(), systemdDefine.EnvVariable)
   194  	}
   195  
   196  	report := &entities.AutoUpdateReport{
   197  		ContainerID:   cid,
   198  		ContainerName: ctr.Name(),
   199  		ImageName:     rawImageName,
   200  		Policy:        PolicyRegistryImage,
   201  		SystemdUnit:   unit,
   202  		Updated:       "failed",
   203  	}
   204  
   205  	if _, updated := updatedRawImages[rawImageName]; updated {
   206  		logrus.Infof("Auto-updating container %q using registry image %q", cid, rawImageName)
   207  		if err := restartSystemdUnit(ctx, ctr, unit, conn); err != nil {
   208  			return report, err
   209  		}
   210  		report.Updated = "true"
   211  		return report, nil
   212  	}
   213  
   214  	authfile := getAuthfilePath(ctr, options)
   215  	needsUpdate, err := newerRemoteImageAvailable(ctx, image, rawImageName, authfile)
   216  	if err != nil {
   217  		return report, errors.Wrapf(err, "registry auto-updating container %q: image check for %q failed", cid, rawImageName)
   218  	}
   219  
   220  	if !needsUpdate {
   221  		report.Updated = "false"
   222  		return report, nil
   223  	}
   224  
   225  	if options.DryRun {
   226  		report.Updated = "pending"
   227  		return report, nil
   228  	}
   229  
   230  	if _, err := updateImage(ctx, runtime, rawImageName, authfile); err != nil {
   231  		return report, errors.Wrapf(err, "registry auto-updating container %q: image update for %q failed", cid, rawImageName)
   232  	}
   233  	updatedRawImages[rawImageName] = true
   234  
   235  	logrus.Infof("Auto-updating container %q using registry image %q", cid, rawImageName)
   236  	updateErr := restartSystemdUnit(ctx, ctr, unit, conn)
   237  	if updateErr == nil {
   238  		report.Updated = "true"
   239  		return report, nil
   240  	}
   241  
   242  	if !options.Rollback {
   243  		return report, updateErr
   244  	}
   245  
   246  	// To fallback, simply retag the old image and restart the service.
   247  	if err := image.Tag(rawImageName); err != nil {
   248  		return report, errors.Wrap(err, "falling back to previous image")
   249  	}
   250  	if err := restartSystemdUnit(ctx, ctr, unit, conn); err != nil {
   251  		return report, errors.Wrap(err, "restarting unit with old image during fallback")
   252  	}
   253  
   254  	report.Updated = "rolled back"
   255  	return report, nil
   256  }
   257  
   258  // autoUpdateRegistry updates the image/container according to the "local" policy.
   259  func autoUpdateLocally(ctx context.Context, image *libimage.Image, ctr *libpod.Container, options *entities.AutoUpdateOptions, conn *dbus.Conn, runtime *libpod.Runtime) (*entities.AutoUpdateReport, error) {
   260  	cid := ctr.ID()
   261  	rawImageName := ctr.RawImageName()
   262  	if rawImageName == "" {
   263  		return nil, errors.Errorf("locally auto-updating container %q: raw-image name is empty", cid)
   264  	}
   265  
   266  	labels := ctr.Labels()
   267  	unit, exists := labels[systemdDefine.EnvVariable]
   268  	if !exists {
   269  		return nil, errors.Errorf("auto-updating container %q: no %s label found", ctr.ID(), systemdDefine.EnvVariable)
   270  	}
   271  
   272  	report := &entities.AutoUpdateReport{
   273  		ContainerID:   cid,
   274  		ContainerName: ctr.Name(),
   275  		ImageName:     rawImageName,
   276  		Policy:        PolicyLocalImage,
   277  		SystemdUnit:   unit,
   278  		Updated:       "failed",
   279  	}
   280  
   281  	needsUpdate, err := newerLocalImageAvailable(runtime, image, rawImageName)
   282  	if err != nil {
   283  		return report, errors.Wrapf(err, "locally auto-updating container %q: image check for %q failed", cid, rawImageName)
   284  	}
   285  
   286  	if !needsUpdate {
   287  		report.Updated = "false"
   288  		return report, nil
   289  	}
   290  
   291  	if options.DryRun {
   292  		report.Updated = "pending"
   293  		return report, nil
   294  	}
   295  
   296  	logrus.Infof("Auto-updating container %q using local image %q", cid, rawImageName)
   297  	updateErr := restartSystemdUnit(ctx, ctr, unit, conn)
   298  	if updateErr == nil {
   299  		report.Updated = "true"
   300  		return report, nil
   301  	}
   302  
   303  	if !options.Rollback {
   304  		return report, updateErr
   305  	}
   306  
   307  	// To fallback, simply retag the old image and restart the service.
   308  	if err := image.Tag(rawImageName); err != nil {
   309  		return report, errors.Wrap(err, "falling back to previous image")
   310  	}
   311  	if err := restartSystemdUnit(ctx, ctr, unit, conn); err != nil {
   312  		return report, errors.Wrap(err, "restarting unit with old image during fallback")
   313  	}
   314  
   315  	report.Updated = "rolled back"
   316  	return report, nil
   317  }
   318  
   319  // restartSystemdUnit restarts the systemd unit the container is running in.
   320  func restartSystemdUnit(ctx context.Context, ctr *libpod.Container, unit string, conn *dbus.Conn) error {
   321  	restartChan := make(chan string)
   322  	if _, err := conn.RestartUnitContext(ctx, unit, "replace", restartChan); err != nil {
   323  		return errors.Wrapf(err, "auto-updating container %q: restarting systemd unit %q failed", ctr.ID(), unit)
   324  	}
   325  
   326  	// Wait for the restart to finish and actually check if it was
   327  	// successful or not.
   328  	result := <-restartChan
   329  
   330  	switch result {
   331  	case "done":
   332  		logrus.Infof("Successfully restarted systemd unit %q of container %q", unit, ctr.ID())
   333  		return nil
   334  
   335  	default:
   336  		return errors.Errorf("auto-updating container %q: restarting systemd unit %q failed: expected %q but received %q", ctr.ID(), unit, "done", result)
   337  	}
   338  }
   339  
   340  // imageContainersMap generates a map[image ID] -> [containers using the image]
   341  // of all containers with a valid auto-update policy.
   342  func imageContainersMap(runtime *libpod.Runtime) (map[string]policyMapper, []error) {
   343  	allContainers, err := runtime.GetAllContainers()
   344  	if err != nil {
   345  		return nil, []error{err}
   346  	}
   347  
   348  	errors := []error{}
   349  	containerMap := make(map[string]policyMapper)
   350  	for _, ctr := range allContainers {
   351  		state, err := ctr.State()
   352  		if err != nil {
   353  			errors = append(errors, err)
   354  			continue
   355  		}
   356  		// Only update running containers.
   357  		if state != define.ContainerStateRunning {
   358  			continue
   359  		}
   360  
   361  		// Only update containers with the specific label/policy set.
   362  		labels := ctr.Labels()
   363  		value, exists := labels[Label]
   364  		if !exists {
   365  			continue
   366  		}
   367  
   368  		policy, err := LookupPolicy(value)
   369  		if err != nil {
   370  			errors = append(errors, err)
   371  			continue
   372  		}
   373  
   374  		// Skip labels not related to autoupdate
   375  		if policy == PolicyDefault {
   376  			continue
   377  		} else {
   378  			id, _ := ctr.Image()
   379  			policyMap, exists := containerMap[id]
   380  			if !exists {
   381  				policyMap = make(map[Policy][]*libpod.Container)
   382  			}
   383  			policyMap[policy] = append(policyMap[policy], ctr)
   384  			containerMap[id] = policyMap
   385  			// Now we know that `ctr` is configured for auto updates.
   386  		}
   387  	}
   388  
   389  	return containerMap, errors
   390  }
   391  
   392  // getAuthfilePath returns an authfile path, if set. The authfile label in the
   393  // container, if set, as precedence over the one set in the options.
   394  func getAuthfilePath(ctr *libpod.Container, options *entities.AutoUpdateOptions) string {
   395  	labels := ctr.Labels()
   396  	authFilePath, exists := labels[AuthfileLabel]
   397  	if exists {
   398  		return authFilePath
   399  	}
   400  	return options.Authfile
   401  }
   402  
   403  // newerRemoteImageAvailable returns true if there corresponding image on the remote
   404  // registry is newer.
   405  func newerRemoteImageAvailable(ctx context.Context, img *libimage.Image, origName string, authfile string) (bool, error) {
   406  	remoteRef, err := docker.ParseReference("//" + origName)
   407  	if err != nil {
   408  		return false, err
   409  	}
   410  	options := &libimage.HasDifferentDigestOptions{AuthFilePath: authfile}
   411  	return img.HasDifferentDigest(ctx, remoteRef, options)
   412  }
   413  
   414  // newerLocalImageAvailable returns true if the container and local image have different digests
   415  func newerLocalImageAvailable(runtime *libpod.Runtime, img *libimage.Image, rawImageName string) (bool, error) {
   416  	localImg, _, err := runtime.LibimageRuntime().LookupImage(rawImageName, nil)
   417  	if err != nil {
   418  		return false, err
   419  	}
   420  	return localImg.Digest().String() != img.Digest().String(), nil
   421  }
   422  
   423  // updateImage pulls the specified image.
   424  func updateImage(ctx context.Context, runtime *libpod.Runtime, name, authfile string) (*libimage.Image, error) {
   425  	pullOptions := &libimage.PullOptions{}
   426  	pullOptions.AuthFilePath = authfile
   427  	pullOptions.Writer = os.Stderr
   428  
   429  	pulledImages, err := runtime.LibimageRuntime().Pull(ctx, name, config.PullPolicyAlways, pullOptions)
   430  	if err != nil {
   431  		return nil, err
   432  	}
   433  	return pulledImages[0], nil
   434  }