Add --requires flag to podman run/create

Podman has, for a long time, had an internal concept of
dependency management, used mainly to ensure that pod infra
containers are started before any other container in the pod. We
also have the ability to recursively start these dependencies,
which we use to ensure that `podman start` on a container in a
pod will not fail because the infra container is stopped. We have
not, however, exposed these via the command line until now.

Add a `--requires` flag to `podman run` and `podman create` to
allow users to manually specify dependency containers. These
containers must be running before the container will start. Also,
make recursive starting with `podman start` default so we can
start these containers and their dependencies easily.

Fixes #9250

Signed-off-by: Matthew Heon <matthew.heon@pm.me>
This commit is contained in:
Matthew Heon
2021-03-18 12:45:09 -04:00
parent f7ad9fbd9e
commit 6acd265306
17 changed files with 138 additions and 18 deletions

View File

@ -576,6 +576,14 @@ func DefineCreateFlags(cmd *cobra.Command, cf *ContainerCLIOpts) {
`If a container with the same name exists, replace it`, `If a container with the same name exists, replace it`,
) )
requiresFlagName := "requires"
createFlags.StringSliceVar(
&cf.Requires,
requiresFlagName, []string{},
"Add one or more requirement containers that must be started before this container will start",
)
_ = cmd.RegisterFlagCompletionFunc(requiresFlagName, AutocompleteContainers)
restartFlagName := "restart" restartFlagName := "restart"
createFlags.StringVar( createFlags.StringVar(
&cf.Restart, &cf.Restart,

View File

@ -93,6 +93,7 @@ type ContainerCLIOpts struct {
ReadOnlyTmpFS bool ReadOnlyTmpFS bool
Restart string Restart string
Replace bool Replace bool
Requires []string
Rm bool Rm bool
RootFS bool RootFS bool
Secrets []string Secrets []string

View File

@ -486,6 +486,8 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string
s.ReadOnlyFilesystem = c.ReadOnly s.ReadOnlyFilesystem = c.ReadOnly
s.ConmonPidFile = c.ConmonPIDFile s.ConmonPidFile = c.ConmonPIDFile
s.DependencyContainers = c.Requires
// TODO // TODO
// outside of specgen and oci though // outside of specgen and oci though
// defaults to true, check spec/storage // defaults to true, check spec/storage

View File

@ -781,6 +781,12 @@ If container is running in --read-only mode, then mount a read-write tmpfs on /r
If another container with the same name already exists, replace and remove it. The default is **false**. If another container with the same name already exists, replace and remove it. The default is **false**.
#### **\-\-requires**=**container**
Specify one or more requirements.
A requirement is a dependency container that will be started before this container.
Containers can be specified by name or ID, with multiple containers being separated by commas.
#### **\-\-restart**=*policy* #### **\-\-restart**=*policy*
Restart policy to follow when containers exit. Restart policy to follow when containers exit.
@ -1254,6 +1260,25 @@ $ podman create --tz=Asia/Shanghai alpine date
$ podman create --tz=US/Eastern alpine date $ podman create --tz=US/Eastern alpine date
``` ```
### Adding dependency containers
Podman will make sure the first container, container1, is running before the second container (container2) is started.
```
$ podman create --name container1 -t -i fedora bash
$ podman create --name container2 --requires container1 -t -i fedora bash
$ podman start --attach container2
```
Multiple containers can be required.
```
$ podman create --name container1 -t -i fedora bash
$ podman create --name container2 -t -i fedora bash
$ podman create --name container3 --requires container1,container2 -t -i fedora bash
$ podman start --attach container3
```
### Rootless Containers ### Rootless Containers
Podman runs as a non root user on most systems. This feature requires that a new enough version of shadow-utils Podman runs as a non root user on most systems. This feature requires that a new enough version of shadow-utils
@ -1301,7 +1326,7 @@ b
NOTE: Use the environment variable `TMPDIR` to change the temporary storage location of downloaded container images. Podman defaults to use `/var/tmp`. NOTE: Use the environment variable `TMPDIR` to change the temporary storage location of downloaded container images. Podman defaults to use `/var/tmp`.
## SEE ALSO ## SEE ALSO
**podman**(1), **podman-secret**(1), **podman-save**(1), **podman-ps**(1), **podman-attach**(1), **podman-pod-create**(1), **podman-port**(1), **podman-kill**(1), **podman-stop**(1), **podman**(1), **podman-secret**(1), **podman-save**(1), **podman-ps**(1), **podman-attach**(1), **podman-pod-create**(1), **podman-port**(1), **podman-start*(1), **podman-kill**(1), **podman-stop**(1),
**podman-generate-systemd**(1) **podman-rm**(1), **subgid**(5), **subuid**(5), **containers.conf**(5), **systemd.unit**(5), **setsebool**(8), **slirp4netns**(1), **fuse-overlayfs**(1), **proc**(5)**. **podman-generate-systemd**(1) **podman-rm**(1), **subgid**(5), **subuid**(5), **containers.conf**(5), **systemd.unit**(5), **setsebool**(8), **slirp4netns**(1), **fuse-overlayfs**(1), **proc**(5)**.
## HISTORY ## HISTORY

View File

@ -825,6 +825,12 @@ If container is running in **\-\-read-only** mode, then mount a read-write tmpfs
If another container with the same name already exists, replace and remove it. The default is **false**. If another container with the same name already exists, replace and remove it. The default is **false**.
#### **\-\-requires**=**container**
Specify one or more requirements.
A requirement is a dependency container that will be started before this container.
Containers can be specified by name or ID, with multiple containers being separated by commas.
#### **\-\-restart**=*policy* #### **\-\-restart**=*policy*
Restart policy to follow when containers exit. Restart policy to follow when containers exit.
@ -1610,6 +1616,24 @@ $ podman run --tz=Asia/Shanghai alpine date
$ podman run --tz=US/Eastern alpine date $ podman run --tz=US/Eastern alpine date
``` ```
### Adding dependency containers
The first container, container1, is not started initially, but must be running before container2 will start.
The `podman run` command will start the container automatically before starting container2.
```
$ podman create --name container1 -t -i fedora bash
$ podman run --name container2 --requires container1 -t -i fedora bash
```
Multiple containers can be required.
```
$ podman create --name container1 -t -i fedora bash
$ podman create --name container2 -t -i fedora bash
$ podman run --name container3 --requires container1,container2 -t -i fedora bash
```
### Rootless Containers ### Rootless Containers
Podman runs as a non root user on most systems. This feature requires that a new enough version of **shadow-utils** Podman runs as a non root user on most systems. This feature requires that a new enough version of **shadow-utils**
@ -1655,7 +1679,7 @@ b
NOTE: Use the environment variable `TMPDIR` to change the temporary storage location of downloaded container images. Podman defaults to use `/var/tmp`. NOTE: Use the environment variable `TMPDIR` to change the temporary storage location of downloaded container images. Podman defaults to use `/var/tmp`.
## SEE ALSO ## SEE ALSO
**podman**(1), **podman-save**(1), **podman-ps**(1), **podman-attach**(1), **podman-pod-create**(1), **podman-port**(1), **podman-kill**(1), **podman-stop**(1), **podman**(1), **podman-save**(1), **podman-ps**(1), **podman-attach**(1), **podman-pod-create**(1), **podman-port**(1), **podman-start**(1), **podman-kill**(1), **podman-stop**(1),
**podman-generate-systemd**(1) **podman-rm**(1), **subgid**(5), **subuid**(5), **containers.conf**(5), **systemd.unit**(5), **setsebool**(8), **slirp4netns**(1), **fuse-overlayfs**(1), **proc**(5)**. **podman-generate-systemd**(1) **podman-rm**(1), **subgid**(5), **subuid**(5), **containers.conf**(5), **systemd.unit**(5), **setsebool**(8), **slirp4netns**(1), **fuse-overlayfs**(1), **proc**(5)**.
## HISTORY ## HISTORY

View File

@ -919,7 +919,7 @@ func (s *BoltState) removeContainer(ctr *Container, pod *Pod, tx *bolt.Tx) error
return err return err
} }
if len(deps) != 0 { if len(deps) != 0 {
return errors.Wrapf(define.ErrCtrExists, "container %s is a dependency of the following containers: %s", ctr.ID(), strings.Join(deps, ", ")) return errors.Wrapf(define.ErrDepExists, "container %s is a dependency of the following containers: %s", ctr.ID(), strings.Join(deps, ", "))
} }
if err := ctrBucket.DeleteBucket(ctrID); err != nil { if err := ctrBucket.DeleteBucket(ctrID); err != nil {

View File

@ -31,6 +31,10 @@ var (
// not exist. // not exist.
ErrNoSuchExecSession = errors.New("no such exec session") ErrNoSuchExecSession = errors.New("no such exec session")
// ErrDepExists indicates that the current object has dependencies and
// cannot be removed before them.
ErrDepExists = errors.New("dependency exists")
// ErrNoAliases indicates that the container does not have any network // ErrNoAliases indicates that the container does not have any network
// aliases. // aliases.
ErrNoAliases = errors.New("no aliases for container") ErrNoAliases = errors.New("no aliases for container")

View File

@ -391,7 +391,7 @@ func (s *InMemoryState) RemoveContainer(ctr *Container) error {
deps, ok := s.ctrDepends[ctr.ID()] deps, ok := s.ctrDepends[ctr.ID()]
if ok && len(deps) != 0 { if ok && len(deps) != 0 {
depsStr := strings.Join(deps, ", ") depsStr := strings.Join(deps, ", ")
return errors.Wrapf(define.ErrCtrExists, "the following containers depend on container %s: %s", ctr.ID(), depsStr) return errors.Wrapf(define.ErrDepExists, "the following containers depend on container %s: %s", ctr.ID(), depsStr)
} }
// Ensure we don't have active exec sessions // Ensure we don't have active exec sessions
@ -1497,7 +1497,7 @@ func (s *InMemoryState) RemoveContainerFromPod(pod *Pod, ctr *Container) error {
deps, ok := s.ctrDepends[ctr.ID()] deps, ok := s.ctrDepends[ctr.ID()]
if ok && len(deps) != 0 { if ok && len(deps) != 0 {
depsStr := strings.Join(deps, ", ") depsStr := strings.Join(deps, ", ")
return errors.Wrapf(define.ErrCtrExists, "the following containers depend on container %s: %s", ctr.ID(), depsStr) return errors.Wrapf(define.ErrDepExists, "the following containers depend on container %s: %s", ctr.ID(), depsStr)
} }
// Ensure we don't have active exec sessions // Ensure we don't have active exec sessions

View File

@ -42,7 +42,7 @@ func StartContainer(w http.ResponseWriter, r *http.Request) {
utils.WriteResponse(w, http.StatusNotModified, nil) utils.WriteResponse(w, http.StatusNotModified, nil)
return return
} }
if err := con.Start(r.Context(), len(con.PodID()) > 0); err != nil { if err := con.Start(r.Context(), true); err != nil {
utils.InternalServerError(w, err) utils.InternalServerError(w, err)
return return
} }

View File

@ -154,6 +154,7 @@ type RestartOptions struct {
// StartOptions are optional options for starting containers // StartOptions are optional options for starting containers
type StartOptions struct { type StartOptions struct {
DetachKeys *string DetachKeys *string
Recursive *bool
} }
//go:generate go run ../generator/generator.go StatsOptions //go:generate go run ../generator/generator.go StatsOptions

View File

@ -35,3 +35,19 @@ func (o *StartOptions) GetDetachKeys() string {
} }
return *o.DetachKeys return *o.DetachKeys
} }
// WithRecursive
func (o *StartOptions) WithRecursive(value bool) *StartOptions {
v := &value
o.Recursive = v
return o
}
// GetRecursive
func (o *StartOptions) GetRecursive() bool {
var recursive bool
if o.Recursive == nil {
return recursive
}
return *o.Recursive
}

View File

@ -585,7 +585,7 @@ func (ic *ContainerEngine) ContainerAttach(ctx context.Context, nameOrID string,
} }
// If the container is in a pod, also set to recursively start dependencies // If the container is in a pod, also set to recursively start dependencies
err = terminal.StartAttachCtr(ctx, ctr, options.Stdout, options.Stderr, options.Stdin, options.DetachKeys, options.SigProxy, false, ctr.PodID() != "") err = terminal.StartAttachCtr(ctx, ctr, options.Stdout, options.Stderr, options.Stdin, options.DetachKeys, options.SigProxy, false)
if err != nil && errors.Cause(err) != define.ErrDetach { if err != nil && errors.Cause(err) != define.ErrDetach {
return errors.Wrapf(err, "error attaching to container %s", ctr.ID()) return errors.Wrapf(err, "error attaching to container %s", ctr.ID())
} }
@ -708,7 +708,7 @@ func (ic *ContainerEngine) ContainerStart(ctx context.Context, namesOrIds []stri
ctrRunning := ctrState == define.ContainerStateRunning ctrRunning := ctrState == define.ContainerStateRunning
if options.Attach { if options.Attach {
err = terminal.StartAttachCtr(ctx, ctr, options.Stdout, options.Stderr, options.Stdin, options.DetachKeys, options.SigProxy, !ctrRunning, ctr.PodID() != "") err = terminal.StartAttachCtr(ctx, ctr, options.Stdout, options.Stderr, options.Stdin, options.DetachKeys, options.SigProxy, !ctrRunning)
if errors.Cause(err) == define.ErrDetach { if errors.Cause(err) == define.ErrDetach {
// User manually detached // User manually detached
// Exit cleanly immediately // Exit cleanly immediately
@ -784,7 +784,7 @@ func (ic *ContainerEngine) ContainerStart(ctx context.Context, namesOrIds []stri
RawInput: rawInput, RawInput: rawInput,
ExitCode: 125, ExitCode: 125,
} }
if err := ctr.Start(ctx, ctr.PodID() != ""); err != nil { if err := ctr.Start(ctx, true); err != nil {
// if lastError != nil { // if lastError != nil {
// fmt.Fprintln(os.Stderr, lastError) // fmt.Fprintln(os.Stderr, lastError)
// } // }
@ -845,10 +845,6 @@ func (ic *ContainerEngine) ContainerRun(ctx context.Context, opts entities.Conta
} }
} }
var joinPod bool
if len(ctr.PodID()) > 0 {
joinPod = true
}
report := entities.ContainerRunReport{Id: ctr.ID()} report := entities.ContainerRunReport{Id: ctr.ID()}
if logrus.GetLevel() == logrus.DebugLevel { if logrus.GetLevel() == logrus.DebugLevel {
@ -859,7 +855,7 @@ func (ic *ContainerEngine) ContainerRun(ctx context.Context, opts entities.Conta
} }
if opts.Detach { if opts.Detach {
// if the container was created as part of a pod, also start its dependencies, if any. // if the container was created as part of a pod, also start its dependencies, if any.
if err := ctr.Start(ctx, joinPod); err != nil { if err := ctr.Start(ctx, true); err != nil {
// This means the command did not exist // This means the command did not exist
report.ExitCode = define.ExitCode(err) report.ExitCode = define.ExitCode(err)
return &report, err return &report, err
@ -869,7 +865,7 @@ func (ic *ContainerEngine) ContainerRun(ctx context.Context, opts entities.Conta
} }
// if the container was created as part of a pod, also start its dependencies, if any. // if the container was created as part of a pod, also start its dependencies, if any.
if err := terminal.StartAttachCtr(ctx, ctr, opts.OutputStream, opts.ErrorStream, opts.InputStream, opts.DetachKeys, opts.SigProxy, true, joinPod); err != nil { if err := terminal.StartAttachCtr(ctx, ctr, opts.OutputStream, opts.ErrorStream, opts.InputStream, opts.DetachKeys, opts.SigProxy, true); err != nil {
// We've manually detached from the container // We've manually detached from the container
// Do not perform cleanup, or wait for container exit code // Do not perform cleanup, or wait for container exit code
// Just exit immediately // Just exit immediately

View File

@ -39,7 +39,7 @@ func ExecAttachCtr(ctx context.Context, ctr *libpod.Container, execConfig *libpo
// StartAttachCtr starts and (if required) attaches to a container // StartAttachCtr starts and (if required) attaches to a container
// if you change the signature of this function from os.File to io.Writer, it will trigger a downstream // if you change the signature of this function from os.File to io.Writer, it will trigger a downstream
// error. we may need to just lint disable this one. // error. we may need to just lint disable this one.
func StartAttachCtr(ctx context.Context, ctr *libpod.Container, stdout, stderr, stdin *os.File, detachKeys string, sigProxy bool, startContainer bool, recursive bool) error { //nolint-interfacer func StartAttachCtr(ctx context.Context, ctr *libpod.Container, stdout, stderr, stdin *os.File, detachKeys string, sigProxy bool, startContainer bool) error { //nolint-interfacer
resize := make(chan define.TerminalSize) resize := make(chan define.TerminalSize)
haveTerminal := terminal.IsTerminal(int(os.Stdin.Fd())) haveTerminal := terminal.IsTerminal(int(os.Stdin.Fd()))
@ -88,7 +88,7 @@ func StartAttachCtr(ctx context.Context, ctr *libpod.Container, stdout, stderr,
return ctr.Attach(streams, detachKeys, resize) return ctr.Attach(streams, detachKeys, resize)
} }
attachChan, err := ctr.StartAndAttach(ctx, streams, detachKeys, resize, recursive) attachChan, err := ctr.StartAndAttach(ctx, streams, detachKeys, resize, true)
if err != nil { if err != nil {
return err return err
} }

View File

@ -629,7 +629,7 @@ func (ic *ContainerEngine) ContainerRun(ctx context.Context, opts entities.Conta
if opts.Detach { if opts.Detach {
// Detach and return early // Detach and return early
err := containers.Start(ic.ClientCtx, con.ID, nil) err := containers.Start(ic.ClientCtx, con.ID, new(containers.StartOptions).WithRecursive(true))
if err != nil { if err != nil {
report.ExitCode = define.ExitCode(err) report.ExitCode = define.ExitCode(err)
} }

View File

@ -364,6 +364,17 @@ func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen.
if len(s.Secrets) != 0 { if len(s.Secrets) != 0 {
options = append(options, libpod.WithSecrets(s.Secrets)) options = append(options, libpod.WithSecrets(s.Secrets))
} }
if len(s.DependencyContainers) > 0 {
deps := make([]*libpod.Container, 0, len(s.DependencyContainers))
for _, ctr := range s.DependencyContainers {
depCtr, err := rt.LookupContainer(ctr)
if err != nil {
return nil, errors.Wrapf(err, "%q is not a valid container, cannot be used as a dependency", ctr)
}
deps = append(deps, depCtr)
}
options = append(options, libpod.WithDependencyCtrs(deps))
}
return options, nil return options, nil
} }

View File

@ -160,10 +160,17 @@ type ContainerBasicConfig struct {
// to 0, 1, 2) that will be passed to the executed process. The total FDs // to 0, 1, 2) that will be passed to the executed process. The total FDs
// passed will be 3 + PreserveFDs. // passed will be 3 + PreserveFDs.
// set tags as `json:"-"` for not supported remote // set tags as `json:"-"` for not supported remote
// Optional.
PreserveFDs uint `json:"-"` PreserveFDs uint `json:"-"`
// Timezone is the timezone inside the container. // Timezone is the timezone inside the container.
// Local means it has the same timezone as the host machine // Local means it has the same timezone as the host machine
// Optional.
Timezone string `json:"timezone,omitempty"` Timezone string `json:"timezone,omitempty"`
// DependencyContainers is an array of containers this container
// depends on. Dependency containers must be started before this
// container. Dependencies can be specified by name or full/partial ID.
// Optional.
DependencyContainers []string `json:"dependencyContainers,omitempty"`
} }
// ContainerStorageConfig contains information on the storage configuration of a // ContainerStorageConfig contains information on the storage configuration of a

View File

@ -1588,4 +1588,29 @@ WORKDIR /madethis`, BB)
Expect(session.OutputToString()).To(ContainSubstring("mysecret")) Expect(session.OutputToString()).To(ContainSubstring("mysecret"))
}) })
It("podman run --requires", func() {
depName := "ctr1"
depContainer := podmanTest.Podman([]string{"create", "--name", depName, ALPINE, "top"})
depContainer.WaitWithDefaultTimeout()
Expect(depContainer.ExitCode()).To(Equal(0))
mainName := "ctr2"
mainContainer := podmanTest.Podman([]string{"run", "--name", mainName, "--requires", depName, "-d", ALPINE, "top"})
mainContainer.WaitWithDefaultTimeout()
Expect(mainContainer.ExitCode()).To(Equal(0))
stop := podmanTest.Podman([]string{"stop", "--all"})
stop.WaitWithDefaultTimeout()
Expect(stop.ExitCode()).To(Equal(0))
start := podmanTest.Podman([]string{"start", mainName})
start.WaitWithDefaultTimeout()
Expect(start.ExitCode()).To(Equal(0))
running := podmanTest.Podman([]string{"ps", "-q"})
running.WaitWithDefaultTimeout()
Expect(running.ExitCode()).To(Equal(0))
Expect(len(running.OutputToStringArray())).To(Equal(2))
})
}) })