summaryrefslogtreecommitdiff
path: root/pkg/autoupdate
diff options
context:
space:
mode:
authorValentin Rothberg <rothberg@redhat.com>2020-03-10 08:32:19 +0100
committerValentin Rothberg <rothberg@redhat.com>2020-03-17 17:18:56 +0100
commitf4e873c4e10502dd0a7fb14cc2fd87b12760a318 (patch)
tree13f9cb9c6ec95ef9522f0b8ee7cf590d2ed4ec7c /pkg/autoupdate
parenta255d7986a1e6795d448979f7662464be4558324 (diff)
downloadpodman-f4e873c4e10502dd0a7fb14cc2fd87b12760a318.tar.gz
podman-f4e873c4e10502dd0a7fb14cc2fd87b12760a318.tar.bz2
podman-f4e873c4e10502dd0a7fb14cc2fd87b12760a318.zip
auto updates
Add support to auto-update containers running in systemd units as generated with `podman generate systemd --new`. `podman auto-update` looks up containers with a specified "io.containers.autoupdate" label (i.e., the auto-update policy). If the label is present and set to "image", Podman reaches out to the corresponding registry to check if the image has been updated. We consider an image to be updated if the digest in the local storage is different than the one of the remote image. If an image must be updated, Podman pulls it down and restarts the container. Note that the restarting sequence relies on systemd. At container-creation time, Podman looks up the "PODMAN_SYSTEMD_UNIT" environment variables and stores it verbatim in the container's label. This variable is now set by all systemd units generated by `podman-generate-systemd` and is set to `%n` (i.e., the name of systemd unit starting the container). This data is then being used in the auto-update sequence to instruct systemd (via DBUS) to restart the unit and hence to restart the container. Note that this implementation of auto-updates relies on systemd and requires a fully-qualified image reference to be used to create the container. This enforcement is necessary to know which image to actually check and pull. If we used an image ID, we would not know which image to check/pull anymore. Fixes: #3575 Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
Diffstat (limited to 'pkg/autoupdate')
-rw-r--r--pkg/autoupdate/autoupdate.go280
-rw-r--r--pkg/autoupdate/autoupdate_test.go50
2 files changed, 330 insertions, 0 deletions
diff --git a/pkg/autoupdate/autoupdate.go b/pkg/autoupdate/autoupdate.go
new file mode 100644
index 000000000..7c243eb00
--- /dev/null
+++ b/pkg/autoupdate/autoupdate.go
@@ -0,0 +1,280 @@
+package autoupdate
+
+import (
+ "context"
+ "os"
+ "sort"
+
+ "github.com/containers/image/v5/docker"
+ "github.com/containers/image/v5/docker/reference"
+ "github.com/containers/image/v5/manifest"
+ "github.com/containers/image/v5/transports/alltransports"
+ "github.com/containers/libpod/libpod"
+ "github.com/containers/libpod/libpod/define"
+ "github.com/containers/libpod/libpod/image"
+ "github.com/containers/libpod/pkg/systemd"
+ systemdGen "github.com/containers/libpod/pkg/systemd/generate"
+ "github.com/containers/libpod/pkg/util"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+)
+
+// Label denotes the container/pod label key to specify auto-update policies in
+// container labels.
+const Label = "io.containers.autoupdate"
+
+// Policy represents an auto-update policy.
+type Policy string
+
+const (
+ // PolicyDefault is the default policy denoting no auto updates.
+ PolicyDefault Policy = "disabled"
+ // PolicyNewImage is the policy to update as soon as there's a new image found.
+ PolicyNewImage = "image"
+)
+
+// Map for easy lookups of supported policies.
+var supportedPolicies = map[string]Policy{
+ "": PolicyDefault,
+ "disabled": PolicyDefault,
+ "image": PolicyNewImage,
+}
+
+// LookupPolicy looksup the corresponding Policy for the specified
+// string. If none is found, an errors is returned including the list of
+// supported policies.
+//
+// Note that an empty string resolved to PolicyDefault.
+func LookupPolicy(s string) (Policy, error) {
+ policy, exists := supportedPolicies[s]
+ if exists {
+ return policy, nil
+ }
+
+ // Sort the keys first as maps are non-deterministic.
+ keys := []string{}
+ for k := range supportedPolicies {
+ if k != "" {
+ keys = append(keys, k)
+ }
+ }
+ sort.Strings(keys)
+
+ return "", errors.Errorf("invalid auto-update policy %q: valid policies are %+q", s, keys)
+}
+
+// ValidateImageReference checks if the specified imageName is a fully-qualified
+// image reference to the docker transport (without digest). Such a reference
+// includes a domain, name and tag (e.g., quay.io/podman/stable:latest). The
+// reference may also be prefixed with "docker://" explicitly indicating that
+// it's a reference to the docker transport.
+func ValidateImageReference(imageName string) error {
+ // Make sure the input image is a docker.
+ imageRef, err := alltransports.ParseImageName(imageName)
+ if err == nil && imageRef.Transport().Name() != docker.Transport.Name() {
+ return errors.Errorf("auto updates require the docker image transport but image is of transport %q", imageRef.Transport().Name())
+ } else if err != nil {
+ repo, err := reference.Parse(imageName)
+ if err != nil {
+ return errors.Wrap(err, "error enforcing fully-qualified docker transport reference for auto updates")
+ }
+ if _, ok := repo.(reference.NamedTagged); !ok {
+ return errors.Errorf("auto updates require fully-qualified image references (no tag): %q", imageName)
+ }
+ if _, ok := repo.(reference.Digested); ok {
+ return errors.Errorf("auto updates require fully-qualified image references without digest: %q", imageName)
+ }
+ }
+ return nil
+}
+
+// AutoUpdate looks up containers with a specified auto-update policy and acts
+// accordingly. If the policy is set to PolicyNewImage, it checks if the image
+// on the remote registry is different than the local one. If the image digests
+// differ, it pulls the remote image and restarts the systemd unit running the
+// container.
+//
+// It returns a slice of successfully restarted systemd units and a slice of
+// errors encountered during auto update.
+func AutoUpdate(runtime *libpod.Runtime) ([]string, []error) {
+ // Create a map from `image ID -> []*Container`.
+ containerMap, errs := imageContainersMap(runtime)
+ if len(containerMap) == 0 {
+ return nil, errs
+ }
+
+ // Create a map from `image ID -> *image.Image` for image lookups.
+ imagesSlice, err := runtime.ImageRuntime().GetImages()
+ if err != nil {
+ return nil, []error{err}
+ }
+ imageMap := make(map[string]*image.Image)
+ for i := range imagesSlice {
+ imageMap[imagesSlice[i].ID()] = imagesSlice[i]
+ }
+
+ // Connect to DBUS.
+ conn, err := systemd.ConnectToDBUS()
+ if err != nil {
+ logrus.Errorf(err.Error())
+ return nil, []error{err}
+ }
+ defer conn.Close()
+
+ // Update images.
+ containersToRestart := []*libpod.Container{}
+ updatedRawImages := make(map[string]bool)
+ for imageID, containers := range containerMap {
+ image, exists := imageMap[imageID]
+ if !exists {
+ errs = append(errs, errors.Errorf("container image ID %q not found in local storage", imageID))
+ return nil, errs
+ }
+ // Now we have to check if the image of any containers must be updated.
+ // Note that the image ID is NOT enough for this check as a given image
+ // may have multiple tags.
+ for i, ctr := range containers {
+ rawImageName := ctr.RawImageName()
+ if rawImageName == "" {
+ errs = append(errs, errors.Errorf("error auto-updating container %q: raw-image name is empty", ctr.ID()))
+ }
+ needsUpdate, err := newerImageAvailable(runtime, image, rawImageName)
+ if err != nil {
+ errs = append(errs, errors.Wrapf(err, "error auto-updating container %q: image check for %q failed", ctr.ID(), rawImageName))
+ continue
+ }
+ if !needsUpdate {
+ continue
+ }
+ logrus.Infof("Auto-updating container %q using image %q", ctr.ID(), rawImageName)
+ if _, updated := updatedRawImages[rawImageName]; !updated {
+ _, err = updateImage(runtime, rawImageName)
+ if err != nil {
+ errs = append(errs, errors.Wrapf(err, "error auto-updating container %q: image update for %q failed", ctr.ID(), rawImageName))
+ continue
+ }
+ updatedRawImages[rawImageName] = true
+ }
+ containersToRestart = append(containersToRestart, containers[i])
+ }
+ }
+
+ // Restart containers.
+ updatedUnits := []string{}
+ for _, ctr := range containersToRestart {
+ labels := ctr.Labels()
+ unit, exists := labels[systemdGen.EnvVariable]
+ if !exists {
+ // Shouldn't happen but let's be sure of it.
+ errs = append(errs, errors.Errorf("error auto-updating container %q: no %s label found", ctr.ID(), systemdGen.EnvVariable))
+ continue
+ }
+ _, err := conn.RestartUnit(unit, "replace", nil)
+ if err != nil {
+ errs = append(errs, errors.Wrapf(err, "error auto-updating container %q: restarting systemd unit %q failed", ctr.ID(), unit))
+ continue
+ }
+ logrus.Infof("Successfully restarted systemd unit %q", unit)
+ updatedUnits = append(updatedUnits, unit)
+ }
+
+ return updatedUnits, errs
+}
+
+// imageContainersMap generates a map[image ID] -> [containers using the image]
+// of all containers with a valid auto-update policy.
+func imageContainersMap(runtime *libpod.Runtime) (map[string][]*libpod.Container, []error) {
+ allContainers, err := runtime.GetAllContainers()
+ if err != nil {
+ return nil, []error{err}
+ }
+
+ errors := []error{}
+ imageMap := make(map[string][]*libpod.Container)
+ for i, ctr := range allContainers {
+ state, err := ctr.State()
+ if err != nil {
+ errors = append(errors, err)
+ continue
+ }
+ // Only update running containers.
+ if state != define.ContainerStateRunning {
+ continue
+ }
+ // Only update containers with the specific label/policy set.
+ labels := ctr.Labels()
+ if value, exists := labels[Label]; exists {
+ policy, err := LookupPolicy(value)
+ if err != nil {
+ errors = append(errors, err)
+ continue
+ }
+ if policy != PolicyNewImage {
+ continue
+ }
+ }
+ // Now we know that `ctr` is configured for auto updates.
+ id, _ := ctr.Image()
+ imageMap[id] = append(imageMap[id], allContainers[i])
+ }
+
+ return imageMap, errors
+}
+
+// newerImageAvailable returns true if there corresponding image on the remote
+// registry is newer.
+func newerImageAvailable(runtime *libpod.Runtime, img *image.Image, origName string) (bool, error) {
+ remoteRef, err := docker.ParseReference("//" + origName)
+ if err != nil {
+ return false, err
+ }
+
+ remoteImg, err := remoteRef.NewImage(context.Background(), runtime.SystemContext())
+ if err != nil {
+ return false, err
+ }
+
+ rawManifest, _, err := remoteImg.Manifest(context.Background())
+ if err != nil {
+ return false, err
+ }
+
+ remoteDigest, err := manifest.Digest(rawManifest)
+ if err != nil {
+ return false, err
+ }
+
+ return img.Digest().String() != remoteDigest.String(), nil
+}
+
+// updateImage pulls the specified image.
+func updateImage(runtime *libpod.Runtime, name string) (*image.Image, error) {
+ sys := runtime.SystemContext()
+ registryOpts := image.DockerRegistryOptions{}
+ signaturePolicyPath := ""
+ authFilePath := ""
+
+ if sys != nil {
+ registryOpts.OSChoice = sys.OSChoice
+ registryOpts.ArchitectureChoice = sys.OSChoice
+ registryOpts.DockerCertPath = sys.DockerCertPath
+
+ signaturePolicyPath = sys.SignaturePolicyPath
+ authFilePath = sys.AuthFilePath
+ }
+
+ newImage, err := runtime.ImageRuntime().New(context.Background(),
+ docker.Transport.Name()+"://"+name,
+ signaturePolicyPath,
+ authFilePath,
+ os.Stderr,
+ &registryOpts,
+ image.SigningOptions{},
+ nil,
+ util.PullImageAlways,
+ )
+ if err != nil {
+ return nil, err
+ }
+ return newImage, nil
+}
diff --git a/pkg/autoupdate/autoupdate_test.go b/pkg/autoupdate/autoupdate_test.go
new file mode 100644
index 000000000..7a5da5bb0
--- /dev/null
+++ b/pkg/autoupdate/autoupdate_test.go
@@ -0,0 +1,50 @@
+package autoupdate
+
+import (
+ "testing"
+)
+
+func TestValidateImageReference(t *testing.T) {
+ tests := []struct {
+ input string
+ valid bool
+ }{
+ { // Fully-qualified reference
+ input: "quay.io/foo/bar:tag",
+ valid: true,
+ },
+ { // Fully-qualified reference in transport notation
+ input: "docker://quay.io/foo/bar:tag",
+ valid: true,
+ },
+ { // Fully-qualified reference but with digest
+ input: "quay.io/foo/bar@sha256:c9b1b535fdd91a9855fb7f82348177e5f019329a58c53c47272962dd60f71fc9",
+ valid: false,
+ },
+ { // Reference with missing tag
+ input: "quay.io/foo/bar",
+ valid: false,
+ },
+ { // Short name
+ input: "alpine",
+ valid: false,
+ },
+ { // Short name with repo
+ input: "library/alpine",
+ valid: false,
+ },
+ { // Wrong transport
+ input: "docker-archive:/some/path.tar",
+ valid: false,
+ },
+ }
+
+ for _, test := range tests {
+ err := ValidateImageReference(test.input)
+ if test.valid && err != nil {
+ t.Fatalf("parsing %q should have succeeded: %v", test.input, err)
+ } else if !test.valid && err == nil {
+ t.Fatalf("parsing %q should have failed", test.input)
+ }
+ }
+}