Merge pull request #5480 from vrothberg/auto-updates

auto update containers in systemd units
This commit is contained in:
OpenShift Merge Robot
2020-03-18 18:27:33 +01:00
committed by GitHub
27 changed files with 641 additions and 66 deletions

56
cmd/podman/autoupdate.go Normal file
View File

@ -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
}

View File

@ -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 {

View File

@ -11,6 +11,7 @@ const remoteclient = false
// Commands that the local client implements
func getMainCommands() []*cobra.Command {
rootCommands := []*cobra.Command{
_autoUpdateCommand,
_cpCommand,
_playCommand,
_loginCommand,

View File

@ -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

View File

@ -3334,6 +3334,7 @@ _podman_podman() {
"
commands="
attach
auto-update
build
commit
container

View File

@ -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

View File

@ -0,0 +1,9 @@
[Unit]
Description=Podman auto-update timer
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target

View File

@ -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)

View File

@ -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. |

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -98,6 +98,8 @@ const (
// Attach ...
Attach Status = "attach"
// AutoUpdate ...
AutoUpdate Status = "auto-update"
// Checkpoint ...
Checkpoint Status = "checkpoint"
// Cleanup ...

View File

@ -4,50 +4,14 @@ import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"github.com/containers/libpod/pkg/rootless"
"github.com/coreos/go-systemd/v22/dbus"
godbus "github.com/godbus/dbus/v5"
"github.com/containers/libpod/pkg/systemd"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
func dbusAuthRootlessConnection(createBus func(opts ...godbus.ConnOption) (*godbus.Conn, error)) (*godbus.Conn, error) {
conn, err := createBus()
if err != nil {
return nil, err
}
methods := []godbus.Auth{godbus.AuthExternal(strconv.Itoa(rootless.GetRootlessUID()))}
err = conn.Auth(methods)
if err != nil {
conn.Close()
return nil, err
}
return conn, nil
}
func newRootlessConnection() (*dbus.Conn, error) {
return dbus.NewConnection(func() (*godbus.Conn, error) {
return dbusAuthRootlessConnection(func(opts ...godbus.ConnOption) (*godbus.Conn, error) {
path := filepath.Join(os.Getenv("XDG_RUNTIME_DIR"), "systemd/private")
return godbus.Dial(fmt.Sprintf("unix:path=%s", path))
})
})
}
func getConnection() (*dbus.Conn, error) {
if rootless.IsRootless() {
return newRootlessConnection()
}
return dbus.NewSystemdConnection()
}
// createTimer systemd timers for healthchecks of a container
func (c *Container) createTimer() error {
if c.disableHealthCheckSystemd() {
@ -64,7 +28,7 @@ func (c *Container) createTimer() error {
}
cmd = append(cmd, "--unit", c.ID(), fmt.Sprintf("--on-unit-inactive=%s", c.HealthCheckConfig().Interval.String()), "--timer-property=AccuracySec=1s", podman, "healthcheck", "run", c.ID())
conn, err := getConnection()
conn, err := systemd.ConnectToDBUS()
if err != nil {
return errors.Wrapf(err, "unable to get systemd connection to add healthchecks")
}
@ -83,7 +47,7 @@ func (c *Container) startTimer() error {
if c.disableHealthCheckSystemd() {
return nil
}
conn, err := getConnection()
conn, err := systemd.ConnectToDBUS()
if err != nil {
return errors.Wrapf(err, "unable to get systemd connection to start healthchecks")
}
@ -98,7 +62,7 @@ func (c *Container) removeTimer() error {
if c.disableHealthCheckSystemd() {
return nil
}
conn, err := getConnection()
conn, err := systemd.ConnectToDBUS()
if err != nil {
return errors.Wrapf(err, "unable to get systemd connection to remove healthchecks")
}

View File

@ -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
}
}

View File

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

View File

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

11
pkg/adapter/autoupdate.go Normal file
View File

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

View File

@ -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}
}

View File

@ -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
}

View File

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

View File

@ -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))

View File

@ -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 {

View File

@ -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.

47
pkg/systemd/dbus.go Normal file
View File

@ -0,0 +1,47 @@
package systemd
import (
"fmt"
"os"
"path/filepath"
"strconv"
"github.com/containers/libpod/pkg/rootless"
"github.com/coreos/go-systemd/v22/dbus"
godbus "github.com/godbus/dbus/v5"
)
func dbusAuthRootlessConnection(createBus func(opts ...godbus.ConnOption) (*godbus.Conn, error)) (*godbus.Conn, error) {
conn, err := createBus()
if err != nil {
return nil, err
}
methods := []godbus.Auth{godbus.AuthExternal(strconv.Itoa(rootless.GetRootlessUID()))}
err = conn.Auth(methods)
if err != nil {
conn.Close()
return nil, err
}
return conn, nil
}
func newRootlessConnection() (*dbus.Conn, error) {
return dbus.NewConnection(func() (*godbus.Conn, error) {
return dbusAuthRootlessConnection(func(opts ...godbus.ConnOption) (*godbus.Conn, error) {
path := filepath.Join(os.Getenv("XDG_RUNTIME_DIR"), "systemd/private")
return godbus.Dial(fmt.Sprintf("unix:path=%s", path))
})
})
}
// ConnectToDBUS returns a DBUS connection. It works both as root and non-root
// users.
func ConnectToDBUS() (*dbus.Conn, error) {
if rootless.IsRootless() {
return newRootlessConnection()
}
return dbus.NewSystemdConnection()
}

View File

@ -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

View File

@ -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