From c002336036c13785781c0dc44a8330f05107cc0a Mon Sep 17 00:00:00 2001 From: Valentin Rothberg Date: Tue, 10 Mar 2020 08:32:19 +0100 Subject: [PATCH] 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 --- cmd/podman/autoupdate.go | 55 ++++ cmd/podman/cliconfig/config.go | 13 +- cmd/podman/commands.go | 1 + cmd/podman/shared/create.go | 59 +++- completions/bash/podman | 4 + .../auto-update/podman-auto-update.service | 11 + .../auto-update/podman-auto-update.timer | 9 + docs/source/markdown/podman-auto-update.1.md | 46 +++ go.mod | 1 - libpod/container.go | 14 +- libpod/options.go | 4 +- libpod/runtime_ctr.go | 1 + libpod/runtime_pod_infra_linux.go | 6 +- pkg/adapter/autoupdate.go | 11 + pkg/adapter/autoupdate_remote.go | 11 + pkg/autoupdate/autoupdate.go | 279 ++++++++++++++++++ pkg/autoupdate/autoupdate_test.go | 50 ++++ pkg/spec/createconfig.go | 3 +- pkg/specgen/create.go | 2 +- pkg/specgen/specgen.go | 4 + pkg/systemd/generate/systemdgen.go | 9 + pkg/systemd/generate/systemdgen_test.go | 5 + 22 files changed, 571 insertions(+), 27 deletions(-) create mode 100644 cmd/podman/autoupdate.go create mode 100644 contrib/systemd/auto-update/podman-auto-update.service create mode 100644 contrib/systemd/auto-update/podman-auto-update.timer create mode 100644 docs/source/markdown/podman-auto-update.1.md create mode 100644 pkg/adapter/autoupdate.go create mode 100644 pkg/adapter/autoupdate_remote.go create mode 100644 pkg/autoupdate/autoupdate.go create mode 100644 pkg/autoupdate/autoupdate_test.go diff --git a/cmd/podman/autoupdate.go b/cmd/podman/autoupdate.go new file mode 100644 index 000000000000..8b66837ffd60 --- /dev/null +++ b/cmd/podman/autoupdate.go @@ -0,0 +1,55 @@ +package main + +import ( + "fmt" + + "github.com/containers/libpod/cmd/podman/cliconfig" + "github.com/containers/libpod/pkg/adapter" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + autoUpdateCommand cliconfig.AutoUpdateValues + autoUpdateDescription = `Auto update containers according to their auto-update policy. + +Auto-update policies are specified with the "io.containers.autoupdate" label.` + _autoUpdateCommand = &cobra.Command{ + Use: "auto-update [flags]", + Short: "Auto update containers according to their auto-update policy", + Long: autoUpdateDescription, + RunE: func(cmd *cobra.Command, args []string) error { + restartCommand.InputArgs = args + restartCommand.GlobalFlags = MainGlobalOpts + return autoUpdateCmd(&restartCommand) + }, + Example: `podman auto-update`, + } +) + +func init() { + autoUpdateCommand.Command = _autoUpdateCommand + autoUpdateCommand.SetHelpTemplate(HelpTemplate()) + autoUpdateCommand.SetUsageTemplate(UsageTemplate()) +} + +func autoUpdateCmd(c *cliconfig.RestartValues) error { + runtime, err := adapter.GetRuntime(getContext(), &c.PodmanCommand) + if err != nil { + return errors.Wrapf(err, "error creating libpod runtime") + } + defer runtime.DeferredShutdown(false) + + units, failures := runtime.AutoUpdate() + for _, unit := range units { + fmt.Println(unit) + } + var finalErr error + if len(failures) > 0 { + finalErr = failures[0] + for _, e := range failures[1:] { + finalErr = errors.Errorf("%v\n%v", finalErr, e) + } + } + return finalErr +} diff --git a/cmd/podman/cliconfig/config.go b/cmd/podman/cliconfig/config.go index 79917946a1db..94a7b2091b8c 100644 --- a/cmd/podman/cliconfig/config.go +++ b/cmd/podman/cliconfig/config.go @@ -54,6 +54,10 @@ type AttachValues struct { SigProxy bool } +type AutoUpdateValues struct { + PodmanCommand +} + type ImagesValues struct { PodmanCommand All bool @@ -470,10 +474,11 @@ type RefreshValues struct { type RestartValues struct { PodmanCommand - All bool - Latest bool - Running bool - Timeout uint + All bool + AutoUpdate bool + Latest bool + Running bool + Timeout uint } type RestoreValues struct { diff --git a/cmd/podman/commands.go b/cmd/podman/commands.go index d6018a6f4d4d..dfa04315ea98 100644 --- a/cmd/podman/commands.go +++ b/cmd/podman/commands.go @@ -11,6 +11,7 @@ const remoteclient = false // Commands that the local client implements func getMainCommands() []*cobra.Command { rootCommands := []*cobra.Command{ + _autoUpdateCommand, _cpCommand, _playCommand, _loginCommand, diff --git a/cmd/podman/shared/create.go b/cmd/podman/shared/create.go index 11b2bd027f86..98af27c17261 100644 --- a/cmd/podman/shared/create.go +++ b/cmd/podman/shared/create.go @@ -18,6 +18,7 @@ import ( "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/image" ann "github.com/containers/libpod/pkg/annotations" + "github.com/containers/libpod/pkg/autoupdate" envLib "github.com/containers/libpod/pkg/env" "github.com/containers/libpod/pkg/errorhandling" "github.com/containers/libpod/pkg/inspect" @@ -25,6 +26,7 @@ import ( "github.com/containers/libpod/pkg/rootless" "github.com/containers/libpod/pkg/seccomp" cc "github.com/containers/libpod/pkg/spec" + systemdGen "github.com/containers/libpod/pkg/systemd/generate" "github.com/containers/libpod/pkg/util" "github.com/docker/go-connections/nat" "github.com/docker/go-units" @@ -69,6 +71,7 @@ func CreateContainer(ctx context.Context, c *GenericCLIResults, runtime *libpod. } imageName := "" + rawImageName := "" var imageData *inspect.ImageData = nil // Set the storage if there is no rootfs specified @@ -78,9 +81,8 @@ func CreateContainer(ctx context.Context, c *GenericCLIResults, runtime *libpod. writer = os.Stderr } - name := "" if len(c.InputArgs) != 0 { - name = c.InputArgs[0] + rawImageName = c.InputArgs[0] } else { return nil, nil, errors.Errorf("error, image name not provided") } @@ -97,7 +99,7 @@ func CreateContainer(ctx context.Context, c *GenericCLIResults, runtime *libpod. ArchitectureChoice: overrideArch, } - newImage, err := runtime.ImageRuntime().New(ctx, name, rtc.SignaturePolicyPath, c.String("authfile"), writer, &dockerRegistryOptions, image.SigningOptions{}, nil, pullType) + newImage, err := runtime.ImageRuntime().New(ctx, rawImageName, rtc.SignaturePolicyPath, c.String("authfile"), writer, &dockerRegistryOptions, image.SigningOptions{}, nil, pullType) if err != nil { return nil, nil, err } @@ -174,11 +176,32 @@ func CreateContainer(ctx context.Context, c *GenericCLIResults, runtime *libpod. } } - createConfig, err := ParseCreateOpts(ctx, c, runtime, imageName, imageData) + createConfig, err := ParseCreateOpts(ctx, c, runtime, imageName, rawImageName, imageData) if err != nil { return nil, nil, err } + // (VR): Ideally we perform the checks _before_ pulling the image but that + // would require some bigger code refactoring of `ParseCreateOpts` and the + // logic here. But as the creation code will be consolidated in the future + // and given auto updates are experimental, we can live with that for now. + // In the end, the user may only need to correct the policy or the raw image + // name. + autoUpdatePolicy, autoUpdatePolicySpecified := createConfig.Labels[autoupdate.Label] + if autoUpdatePolicySpecified { + if _, err := autoupdate.LookupPolicy(autoUpdatePolicy); err != nil { + return nil, nil, err + } + // Now we need to make sure we're having a fully-qualified image reference. + if rootfs != "" { + return nil, nil, errors.Errorf("auto updates do not work with --rootfs") + } + // Make sure the input image is a docker. + if err := autoupdate.ValidateImageReference(rawImageName); err != nil { + return nil, nil, err + } + } + // Because parseCreateOpts does derive anything from the image, we add health check // at this point. The rest is done by WithOptions. createConfig.HealthCheck = healthCheck @@ -270,7 +293,7 @@ func configurePod(c *GenericCLIResults, runtime *libpod.Runtime, namespaces map[ // Parses CLI options related to container creation into a config which can be // parsed into an OCI runtime spec -func ParseCreateOpts(ctx context.Context, c *GenericCLIResults, runtime *libpod.Runtime, imageName string, data *inspect.ImageData) (*cc.CreateConfig, error) { +func ParseCreateOpts(ctx context.Context, c *GenericCLIResults, runtime *libpod.Runtime, imageName string, rawImageName string, data *inspect.ImageData) (*cc.CreateConfig, error) { var ( inputCommand, command []string memoryLimit, memoryReservation, memorySwap, memoryKernel int64 @@ -481,12 +504,15 @@ func ParseCreateOpts(ctx context.Context, c *GenericCLIResults, runtime *libpod. "container": "podman", } + // First transform the os env into a map. We need it for the labels later in + // any case. + osEnv, err := envLib.ParseSlice(os.Environ()) + if err != nil { + return nil, errors.Wrap(err, "error parsing host environment variables") + } + // Start with env-host if c.Bool("env-host") { - osEnv, err := envLib.ParseSlice(os.Environ()) - if err != nil { - return nil, errors.Wrap(err, "error parsing host environment variables") - } env = envLib.Join(env, osEnv) } @@ -534,6 +560,10 @@ func ParseCreateOpts(ctx context.Context, c *GenericCLIResults, runtime *libpod. } } + if systemdUnit, exists := osEnv[systemdGen.EnvVariable]; exists { + labels[systemdGen.EnvVariable] = systemdUnit + } + // ANNOTATIONS annotations := make(map[string]string) @@ -764,11 +794,12 @@ func ParseCreateOpts(ctx context.Context, c *GenericCLIResults, runtime *libpod. Entrypoint: entrypoint, Env: env, // ExposedPorts: ports, - Init: c.Bool("init"), - InitPath: c.String("init-path"), - Image: imageName, - ImageID: imageID, - Interactive: c.Bool("interactive"), + Init: c.Bool("init"), + InitPath: c.String("init-path"), + Image: imageName, + RawImageName: rawImageName, + ImageID: imageID, + Interactive: c.Bool("interactive"), // IP6Address: c.String("ipv6"), // Not implemented yet - needs CNI support for static v6 Labels: labels, // LinkLocalIP: c.StringSlice("link-local-ip"), // Not implemented yet diff --git a/completions/bash/podman b/completions/bash/podman index 895659fe580b..a8a26cd6498b 100644 --- a/completions/bash/podman +++ b/completions/bash/podman @@ -12,6 +12,10 @@ __podman_q() { podman ${host:+-H "$host"} ${config:+--config "$config"} 2>/dev/null "$@" } +__podman_auto-update() { + +} + # __podman_containers returns a list of containers. Additional options to # `podman ps` may be specified in order to filter the list, e.g. # `__podman_containers --filter status=running` diff --git a/contrib/systemd/auto-update/podman-auto-update.service b/contrib/systemd/auto-update/podman-auto-update.service new file mode 100644 index 000000000000..21bc2b601f6a --- /dev/null +++ b/contrib/systemd/auto-update/podman-auto-update.service @@ -0,0 +1,11 @@ +[Unit] +Description=Podman auto-update service +Documentation=man:podman-auto-update(1) +Wants=network.target +After=network-online.target + +[Service] +ExecStart=/usr/bin/podman auto-update + +[Install] +WantedBy=multi-user.target default.target \ No newline at end of file diff --git a/contrib/systemd/auto-update/podman-auto-update.timer b/contrib/systemd/auto-update/podman-auto-update.timer new file mode 100644 index 000000000000..3094ce8a18cb --- /dev/null +++ b/contrib/systemd/auto-update/podman-auto-update.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Podman auto-update timer + +[Timer] +OnCalendar=daily +Persistent=true + +[Install] +WantedBy=timers.target \ No newline at end of file diff --git a/docs/source/markdown/podman-auto-update.1.md b/docs/source/markdown/podman-auto-update.1.md new file mode 100644 index 000000000000..bc9bcd1a3feb --- /dev/null +++ b/docs/source/markdown/podman-auto-update.1.md @@ -0,0 +1,46 @@ +% podman-auto-update(1) + +## NAME +podman\-auto-update - Auto update containers according to their auto-update policy + +## SYNOPSIS +**podman auto-update** + +## DESCRIPTION +`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. +An image is considered 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 systemd unit executing the container. + +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 (e.g., quay.io/podman/stable:latest) 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. + +## EXAMPLES + +``` +# Start a container +$ podman run -d busybox:latest top +bc219740a210455fa27deacc96d50a9e20516492f1417507c13ce1533dbdcd9d + +# Generate a systemd unit for this container +$ podman generate systemd --new --files bc219740a210455fa27deacc96d50a9e20516492f1417507c13ce1533dbdcd9d +/home/user/containers/libpod/container-bc219740a210455fa27deacc96d50a9e20516492f1417507c13ce1533dbdcd9d.service + +# Load the new systemd unit and start it +$ mv ./container-bc219740a210455fa27deacc96d50a9e20516492f1417507c13ce1533dbdcd9d.service ~/.config/systemd/user +$ systemctl --user daemon-reload +$ systemctl --user start container-bc219740a210455fa27deacc96d50a9e20516492f1417507c13ce1533dbdcd9d.service + +# Auto-update the container +$ podman auto-update +container-bc219740a210455fa27deacc96d50a9e20516492f1417507c13ce1533dbdcd9d.service +``` + +## SEE ALSO +podman(1), podman-generate-systemd(1), podman-run(1), systemd.unit(5) diff --git a/go.mod b/go.mod index 01bb997e8687..bd1d219326f5 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,6 @@ require ( github.com/containers/image/v5 v5.2.1 github.com/containers/psgo v1.4.0 github.com/containers/storage v1.16.2 - github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e github.com/coreos/go-systemd/v22 v22.0.0 github.com/cri-o/ocicni v0.1.1-0.20190920040751-deac903fd99b github.com/cyphar/filepath-securejoin v0.2.2 diff --git a/libpod/container.go b/libpod/container.go index dbd15e55f850..d83de93bbcae 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -239,6 +239,12 @@ type ContainerConfig struct { // container has been created with. CreateCommand []string `json:"CreateCommand,omitempty"` + // RawImageName is the raw and unprocessed name of the image when creating + // the container (as specified by the user). May or may not be set. One + // use case to store this data are auto-updates where we need the _exact_ + // name and not some normalized instance of it. + RawImageName string `json:"RawImageName,omitempty"` + // TODO consider breaking these subsections up into smaller structs // UID/GID mappings used by the storage @@ -503,11 +509,17 @@ func (c *Container) Namespace() string { return c.config.Namespace } -// Image returns the ID and name of the image used as the container's rootfs +// Image returns the ID and name of the image used as the container's rootfs. func (c *Container) Image() (string, string) { return c.config.RootfsImageID, c.config.RootfsImageName } +// RawImageName returns the unprocessed and not-normalized user-specified image +// name. +func (c *Container) RawImageName() string { + return c.config.RawImageName +} + // ShmDir returns the sources path to be mounted on /dev/shm in container func (c *Container) ShmDir() string { return c.config.ShmDir diff --git a/libpod/options.go b/libpod/options.go index 98de71af2566..9b61d7947f1c 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -593,7 +593,7 @@ func WithUser(user string) CtrCreateOption { // other configuration from the image will be added to the config. // TODO: Replace image name and ID with a libpod.Image struct when that is // finished. -func WithRootFSFromImage(imageID string, imageName string) CtrCreateOption { +func WithRootFSFromImage(imageID, imageName, rawImageName string) CtrCreateOption { return func(ctr *Container) error { if ctr.valid { return define.ErrCtrFinalized @@ -601,7 +601,7 @@ func WithRootFSFromImage(imageID string, imageName string) CtrCreateOption { ctr.config.RootfsImageID = imageID ctr.config.RootfsImageName = imageName - + ctr.config.RawImageName = rawImageName return nil } } diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go index ba2a6b93ed2c..1f1cdc271b2d 100644 --- a/libpod/runtime_ctr.go +++ b/libpod/runtime_ctr.go @@ -131,6 +131,7 @@ func (r *Runtime) newContainer(ctx context.Context, rSpec *spec.Spec, options .. return nil, errors.Wrapf(err, "error running container create option") } } + return r.setupContainer(ctx, ctr) } diff --git a/libpod/runtime_pod_infra_linux.go b/libpod/runtime_pod_infra_linux.go index 27735a9b26a9..2d2ee5602e28 100644 --- a/libpod/runtime_pod_infra_linux.go +++ b/libpod/runtime_pod_infra_linux.go @@ -23,7 +23,7 @@ const ( IDTruncLength = 12 ) -func (r *Runtime) makeInfraContainer(ctx context.Context, p *Pod, imgName, imgID string, config *v1.ImageConfig) (*Container, error) { +func (r *Runtime) makeInfraContainer(ctx context.Context, p *Pod, imgName, rawImageName, imgID string, config *v1.ImageConfig) (*Container, error) { // Set up generator for infra container defaults g, err := generate.New("linux") @@ -127,7 +127,7 @@ func (r *Runtime) makeInfraContainer(ctx context.Context, p *Pod, imgName, imgID containerName := p.ID()[:IDTruncLength] + "-infra" options = append(options, r.WithPod(p)) - options = append(options, WithRootFSFromImage(imgID, imgName)) + options = append(options, WithRootFSFromImage(imgID, imgName, rawImageName)) options = append(options, WithName(containerName)) options = append(options, withIsInfra()) @@ -154,5 +154,5 @@ func (r *Runtime) createInfraContainer(ctx context.Context, p *Pod) (*Container, imageName := newImage.Names()[0] imageID := data.ID - return r.makeInfraContainer(ctx, p, imageName, imageID, data.Config) + return r.makeInfraContainer(ctx, p, imageName, r.config.InfraImage, imageID, data.Config) } diff --git a/pkg/adapter/autoupdate.go b/pkg/adapter/autoupdate.go new file mode 100644 index 000000000000..01f7a29c5b65 --- /dev/null +++ b/pkg/adapter/autoupdate.go @@ -0,0 +1,11 @@ +// +build !remoteclient + +package adapter + +import ( + "github.com/containers/libpod/pkg/autoupdate" +) + +func (r *LocalRuntime) AutoUpdate() ([]string, []error) { + return autoupdate.AutoUpdate(r.Runtime) +} diff --git a/pkg/adapter/autoupdate_remote.go b/pkg/adapter/autoupdate_remote.go new file mode 100644 index 000000000000..a2a82d0d4df1 --- /dev/null +++ b/pkg/adapter/autoupdate_remote.go @@ -0,0 +1,11 @@ +// +build remoteclient + +package adapter + +import ( + "github.com/containers/libpod/libpod/define" +) + +func (r *LocalRuntime) AutoUpdate() ([]string, []error) { + return nil, []error{define.ErrNotImplemented} +} diff --git a/pkg/autoupdate/autoupdate.go b/pkg/autoupdate/autoupdate.go new file mode 100644 index 000000000000..83dcc9d1374e --- /dev/null +++ b/pkg/autoupdate/autoupdate.go @@ -0,0 +1,279 @@ +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 + } + 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, + ®istryOpts, + 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 000000000000..7a5da5bb049c --- /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) + } + } +} diff --git a/pkg/spec/createconfig.go b/pkg/spec/createconfig.go index 9b2255d61487..12dfed8c30a1 100644 --- a/pkg/spec/createconfig.go +++ b/pkg/spec/createconfig.go @@ -144,6 +144,7 @@ type CreateConfig struct { InitPath string //init-path Image string ImageID string + RawImageName string BuiltinImgVolumes map[string]struct{} // volumes defined in the image config ImageVolumeType string // how to handle the image volume, either bind, tmpfs, or ignore Interactive bool //interactive @@ -348,7 +349,7 @@ func (c *CreateConfig) getContainerCreateOptions(runtime *libpod.Runtime, pod *l options = append(options, nsOpts...) // Gather up the options for NewContainer which consist of With... funcs - options = append(options, libpod.WithRootFSFromImage(c.ImageID, c.Image)) + options = append(options, libpod.WithRootFSFromImage(c.ImageID, c.Image, c.RawImageName)) options = append(options, libpod.WithConmonPidFile(c.ConmonPidFile)) options = append(options, libpod.WithLabels(c.Labels)) options = append(options, libpod.WithShmSize(c.Resources.ShmSize)) diff --git a/pkg/specgen/create.go b/pkg/specgen/create.go index 99a99083b55f..aefbe74056be 100644 --- a/pkg/specgen/create.go +++ b/pkg/specgen/create.go @@ -36,7 +36,7 @@ func (s *SpecGenerator) MakeContainer(rt *libpod.Runtime) (*libpod.Container, er return nil, err } - options = append(options, libpod.WithRootFSFromImage(newImage.ID(), s.Image)) + options = append(options, libpod.WithRootFSFromImage(newImage.ID(), s.Image, s.RawImageName)) runtimeSpec, err := s.toOCISpec(rt, newImage) if err != nil { diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index e1dfe4dc599f..7a430652a374 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -143,6 +143,10 @@ type ContainerStorageConfig struct { // Conflicts with Rootfs. // At least one of Image or Rootfs must be specified. Image string `json:"image"` + // RawImageName is the unprocessed and not-normalized user-specified image + // name. One use case for having this data at hand are auto-updates where + // the _exact_ user input is needed in order to look-up the correct image. + RawImageName string `json:"raw_image_name,omitempty"` // Rootfs is the path to a directory that will be used as the // container's root filesystem. No modification will be made to the // directory, it will be directly mounted into the container as root. diff --git a/pkg/systemd/generate/systemdgen.go b/pkg/systemd/generate/systemdgen.go index 4410e13958c4..3d298c0bd53e 100644 --- a/pkg/systemd/generate/systemdgen.go +++ b/pkg/systemd/generate/systemdgen.go @@ -16,6 +16,10 @@ import ( "github.com/sirupsen/logrus" ) +// EnvVariable "PODMAN_SYSTEMD_UNIT" is set in all generated systemd units and +// is set to the unit's (unique) name. +const EnvVariable = "PODMAN_SYSTEMD_UNIT" + // ContainerInfo contains data required for generating a container's systemd // unit file. type ContainerInfo struct { @@ -57,6 +61,8 @@ type ContainerInfo struct { // RunCommand is a post-processed variant of CreateCommand and used for // the ExecStart field in generic unit files. RunCommand string + // EnvVariable is generate.EnvVariable and must not be set. + EnvVariable string } var restartPolicies = []string{"no", "on-success", "on-failure", "on-abnormal", "on-watchdog", "on-abort", "always"} @@ -94,6 +100,7 @@ Before={{- range $index, $value := .RequiredServices -}}{{if $index}} {{end}}{{ {{- end}} [Service] +Environment={{.EnvVariable}}=%n Restart={{.RestartPolicy}} {{- if .New}} ExecStartPre=/usr/bin/rm -f %t/%n-pid %t/%n-cid @@ -138,6 +145,8 @@ func CreateContainerSystemdUnit(info *ContainerInfo, opts Options) (string, erro info.Executable = executable } + info.EnvVariable = EnvVariable + // Assemble the ExecStart command when creating a new container. // // Note that we cannot catch all corner cases here such that users diff --git a/pkg/systemd/generate/systemdgen_test.go b/pkg/systemd/generate/systemdgen_test.go index 3749d89ce1f3..7b15ecde3649 100644 --- a/pkg/systemd/generate/systemdgen_test.go +++ b/pkg/systemd/generate/systemdgen_test.go @@ -44,6 +44,7 @@ Wants=network.target After=network-online.target [Service] +Environment=PODMAN_SYSTEMD_UNIT=%n Restart=always ExecStart=/usr/bin/podman start 639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401 ExecStop=/usr/bin/podman stop -t 10 639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401 @@ -64,6 +65,7 @@ Wants=network.target After=network-online.target [Service] +Environment=PODMAN_SYSTEMD_UNIT=%n Restart=always ExecStart=/usr/bin/podman start foobar ExecStop=/usr/bin/podman stop -t 10 foobar @@ -88,6 +90,7 @@ BindsTo=a.service b.service c.service pod.service After=a.service b.service c.service pod.service [Service] +Environment=PODMAN_SYSTEMD_UNIT=%n Restart=always ExecStart=/usr/bin/podman start foobar ExecStop=/usr/bin/podman stop -t 10 foobar @@ -110,6 +113,7 @@ Requires=container-1.service container-2.service Before=container-1.service container-2.service [Service] +Environment=PODMAN_SYSTEMD_UNIT=%n Restart=always ExecStart=/usr/bin/podman start jadda-jadda-infra ExecStop=/usr/bin/podman stop -t 10 jadda-jadda-infra @@ -130,6 +134,7 @@ Wants=network.target After=network-online.target [Service] +Environment=PODMAN_SYSTEMD_UNIT=%n Restart=always ExecStartPre=/usr/bin/rm -f %t/%n-pid %t/%n-cid ExecStart=/usr/bin/podman run --conmon-pidfile %t/%n-pid --cidfile %t/%n-cid --cgroups=no-conmon --name jadda-jadda --hostname hello-world awesome-image:latest command arg1 ... argN