Skip to content

Commit

Permalink
auto updates
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
vrothberg committed Mar 13, 2020
1 parent 3d86aff commit c002336
Show file tree
Hide file tree
Showing 22 changed files with 571 additions and 27 deletions.
55 changes: 55 additions & 0 deletions 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
}
13 changes: 9 additions & 4 deletions cmd/podman/cliconfig/config.go
Expand Up @@ -54,6 +54,10 @@ type AttachValues struct {
SigProxy bool
}

type AutoUpdateValues struct {
PodmanCommand
}

type ImagesValues struct {
PodmanCommand
All bool
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions cmd/podman/commands.go
Expand Up @@ -11,6 +11,7 @@ const remoteclient = false
// Commands that the local client implements
func getMainCommands() []*cobra.Command {
rootCommands := []*cobra.Command{
_autoUpdateCommand,
_cpCommand,
_playCommand,
_loginCommand,
Expand Down
59 changes: 45 additions & 14 deletions cmd/podman/shared/create.go
Expand Up @@ -18,13 +18,15 @@ 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"
ns "github.com/containers/libpod/pkg/namespaces"
"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"
Expand Down Expand Up @@ -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
Expand All @@ -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")
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions completions/bash/podman
Expand Up @@ -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`
Expand Down
11 changes: 11 additions & 0 deletions 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
9 changes: 9 additions & 0 deletions 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
46 changes: 46 additions & 0 deletions 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)
1 change: 0 additions & 1 deletion go.mod
Expand Up @@ -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
Expand Down
14 changes: 13 additions & 1 deletion libpod/container.go
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions libpod/options.go
Expand Up @@ -593,15 +593,15 @@ 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
}

ctr.config.RootfsImageID = imageID
ctr.config.RootfsImageName = imageName

ctr.config.RawImageName = rawImageName
return nil
}
}
Expand Down
1 change: 1 addition & 0 deletions libpod/runtime_ctr.go
Expand Up @@ -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)
}

Expand Down
6 changes: 3 additions & 3 deletions libpod/runtime_pod_infra_linux.go
Expand Up @@ -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")
Expand Down Expand Up @@ -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())

Expand All @@ -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)
}

0 comments on commit c002336

Please sign in to comment.