summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/podman/autoupdate.go56
-rw-r--r--cmd/podman/cliconfig/config.go13
-rw-r--r--cmd/podman/commands.go1
-rw-r--r--cmd/podman/shared/create.go59
-rw-r--r--completions/bash/podman1
-rw-r--r--contrib/systemd/auto-update/podman-auto-update.service11
-rw-r--r--contrib/systemd/auto-update/podman-auto-update.timer9
-rw-r--r--docs/source/markdown/podman-auto-update.1.md46
-rw-r--r--docs/source/markdown/podman.1.md1
-rwxr-xr-xhack/man-page-checker12
-rwxr-xr-xhack/podman-commands.sh3
-rw-r--r--libpod/container.go14
-rw-r--r--libpod/events/config.go2
-rw-r--r--libpod/options.go4
-rw-r--r--libpod/runtime_ctr.go1
-rw-r--r--libpod/runtime_pod_infra_linux.go6
-rw-r--r--pkg/adapter/autoupdate.go11
-rw-r--r--pkg/adapter/autoupdate_remote.go11
-rw-r--r--pkg/autoupdate/autoupdate.go280
-rw-r--r--pkg/autoupdate/autoupdate_test.go50
-rw-r--r--pkg/spec/createconfig.go3
-rw-r--r--pkg/specgen/create.go2
-rw-r--r--pkg/specgen/specgen.go4
-rw-r--r--pkg/systemd/generate/systemdgen.go9
-rw-r--r--pkg/systemd/generate/systemdgen_test.go7
25 files changed, 590 insertions, 26 deletions
diff --git a/cmd/podman/autoupdate.go b/cmd/podman/autoupdate.go
new file mode 100644
index 000000000..2cc1ae72e
--- /dev/null
+++ b/cmd/podman/autoupdate.go
@@ -0,0 +1,56 @@
+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",
+ Args: noSubArgs,
+ 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 79917946a..94a7b2091 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 d6018a6f4..dfa04315e 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 8968f10e8..cec837af6 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 895659fe5..8a6fc2073 100644
--- a/completions/bash/podman
+++ b/completions/bash/podman
@@ -3334,6 +3334,7 @@ _podman_podman() {
"
commands="
attach
+ auto-update
build
commit
container
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 000000000..b63f24230
--- /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
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 000000000..3e50ffa9b
--- /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
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 000000000..93ad22f76
--- /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 `podman auto-update` 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 an image ID was used, Podman 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/docs/source/markdown/podman.1.md b/docs/source/markdown/podman.1.md
index 86d246e87..5797535f7 100644
--- a/docs/source/markdown/podman.1.md
+++ b/docs/source/markdown/podman.1.md
@@ -154,6 +154,7 @@ the exit codes follow the `chroot` standard, see below:
| Command | Description |
| ------------------------------------------------ | --------------------------------------------------------------------------- |
| [podman-attach(1)](podman-attach.1.md) | Attach to a running container. |
+| [podman-auto-update(1)](podman-auto-update.1.md) | Auto update containers according to their auto-update policy |
| [podman-build(1)](podman-build.1.md) | Build a container image using a Containerfile. |
| [podman-commit(1)](podman-commit.1.md) | Create new image based on the changed container. |
| [podman-container(1)](podman-container.1.md) | Manage containers. |
diff --git a/hack/man-page-checker b/hack/man-page-checker
index 99d280539..528ff800a 100755
--- a/hack/man-page-checker
+++ b/hack/man-page-checker
@@ -49,6 +49,12 @@ for md in $(ls -1 *-*.1.md | grep -v remote);do
# podman.1.md has a two-column table; podman-*.1.md all have three.
parent=$(echo $md | sed -e 's/^\(.*\)-.*$/\1.1.md/')
+ if [[ $parent =~ "podman-auto" ]]; then
+ # podman-auto-update.1.md is special cased as it's structure differs
+ # from that of other man pages where main and sub-commands split by
+ # dashes.
+ parent="podman.1.md"
+ fi
x=3
if expr -- "$parent" : ".*-" >/dev/null; then
x=4
@@ -90,6 +96,12 @@ for md in *.1.md;do
# Get the command name, and confirm that it matches the md file name.
cmd=$(echo "$synopsis" | sed -e 's/\(.*\)\*\*.*/\1/' | tr -d \*)
md_nodash=$(basename "$md" .1.md | tr '-' ' ')
+ if [[ $md_nodash = 'podman auto update' ]]; then
+ # podman-auto-update.1.md is special cased as it's structure differs
+ # from that of other man pages where main and sub-commands split by
+ # dashes.
+ md_nodash='podman auto-update'
+ fi
if [ "$cmd" != "$md_nodash" -a "$cmd" != "podman-remote" ]; then
echo
printf "Inconsistent program name in SYNOPSIS in %s:\n" $md
diff --git a/hack/podman-commands.sh b/hack/podman-commands.sh
index 32f94fc7b..da4d446aa 100755
--- a/hack/podman-commands.sh
+++ b/hack/podman-commands.sh
@@ -38,6 +38,9 @@ function podman_man() {
# Special case: there is no podman-help man page, nor need for such.
echo "help"
+ # Auto-update differs from other commands as it's a single command, not
+ # a main and sub-command split by a dash.
+ echo "auto-update"
elif [ "$@" = "podman-image-trust" ]; then
# Special case: set and show aren't actually in a table in the man page
echo set
diff --git a/libpod/container.go b/libpod/container.go
index dbd15e55f..d83de93bb 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/events/config.go b/libpod/events/config.go
index 20c01baff..8fe551c5d 100644
--- a/libpod/events/config.go
+++ b/libpod/events/config.go
@@ -98,6 +98,8 @@ const (
// Attach ...
Attach Status = "attach"
+ // AutoUpdate ...
+ AutoUpdate Status = "auto-update"
// Checkpoint ...
Checkpoint Status = "checkpoint"
// Cleanup ...
diff --git a/libpod/options.go b/libpod/options.go
index 98de71af2..9b61d7947 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 ba2a6b93e..1f1cdc271 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 279cafa39..155ac83d9 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 000000000..01f7a29c5
--- /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 000000000..a2a82d0d4
--- /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 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)
+ }
+ }
+}
diff --git a/pkg/spec/createconfig.go b/pkg/spec/createconfig.go
index 9b2255d61..12dfed8c3 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 99a99083b..aefbe7405 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 e1dfe4dc5..7a430652a 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 4cd7745c0..eb15d4927 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 bbdccdcf8..3269405a6 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 -d --name jadda-jadda --hostname hello-world awesome-image:latest command arg1 ... argN
@@ -152,6 +157,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 --detach --name jadda-jadda --hostname hello-world awesome-image:latest command arg1 ... argN
@@ -174,6 +180,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 -d awesome-image:latest