From 6acd265306370ab5cfeaf2843bd359fe13216d92 Mon Sep 17 00:00:00 2001 From: Matthew Heon Date: Thu, 18 Mar 2021 12:45:09 -0400 Subject: 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 --- cmd/podman/common/create.go | 8 ++++++++ cmd/podman/common/create_opts.go | 1 + cmd/podman/common/specgen.go | 2 ++ docs/source/markdown/podman-create.1.md | 27 ++++++++++++++++++++++++- docs/source/markdown/podman-run.1.md | 26 +++++++++++++++++++++++- libpod/boltdb_state_internal.go | 2 +- libpod/define/errors.go | 4 ++++ libpod/in_memory_state.go | 4 ++-- pkg/api/handlers/compat/containers_start.go | 2 +- pkg/bindings/containers/types.go | 1 + pkg/bindings/containers/types_start_options.go | 16 +++++++++++++++ pkg/domain/infra/abi/containers.go | 14 +++++-------- pkg/domain/infra/abi/terminal/terminal_linux.go | 4 ++-- pkg/domain/infra/tunnel/containers.go | 2 +- pkg/specgen/generate/container_create.go | 11 ++++++++++ pkg/specgen/specgen.go | 7 +++++++ test/e2e/run_test.go | 25 +++++++++++++++++++++++ 17 files changed, 138 insertions(+), 18 deletions(-) diff --git a/cmd/podman/common/create.go b/cmd/podman/common/create.go index d1170710b..2169cdc9f 100644 --- a/cmd/podman/common/create.go +++ b/cmd/podman/common/create.go @@ -576,6 +576,14 @@ func DefineCreateFlags(cmd *cobra.Command, cf *ContainerCLIOpts) { `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" createFlags.StringVar( &cf.Restart, diff --git a/cmd/podman/common/create_opts.go b/cmd/podman/common/create_opts.go index a296ef4f1..e14918fe1 100644 --- a/cmd/podman/common/create_opts.go +++ b/cmd/podman/common/create_opts.go @@ -93,6 +93,7 @@ type ContainerCLIOpts struct { ReadOnlyTmpFS bool Restart string Replace bool + Requires []string Rm bool RootFS bool Secrets []string diff --git a/cmd/podman/common/specgen.go b/cmd/podman/common/specgen.go index d1b67d963..363a8f5f9 100644 --- a/cmd/podman/common/specgen.go +++ b/cmd/podman/common/specgen.go @@ -486,6 +486,8 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string s.ReadOnlyFilesystem = c.ReadOnly s.ConmonPidFile = c.ConmonPIDFile + s.DependencyContainers = c.Requires + // TODO // outside of specgen and oci though // defaults to true, check spec/storage diff --git a/docs/source/markdown/podman-create.1.md b/docs/source/markdown/podman-create.1.md index 9ae4ab207..9e7fa4bfd 100644 --- a/docs/source/markdown/podman-create.1.md +++ b/docs/source/markdown/podman-create.1.md @@ -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**. +#### **\-\-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 to follow when containers exit. @@ -1254,6 +1260,25 @@ $ podman create --tz=Asia/Shanghai 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 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`. ## 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)**. ## HISTORY diff --git a/docs/source/markdown/podman-run.1.md b/docs/source/markdown/podman-run.1.md index 6d9d5ba28..4019a738e 100644 --- a/docs/source/markdown/podman-run.1.md +++ b/docs/source/markdown/podman-run.1.md @@ -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**. +#### **\-\-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 to follow when containers exit. @@ -1610,6 +1616,24 @@ $ podman run --tz=Asia/Shanghai 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 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`. ## 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)**. ## HISTORY diff --git a/libpod/boltdb_state_internal.go b/libpod/boltdb_state_internal.go index d4994334f..f63876c14 100644 --- a/libpod/boltdb_state_internal.go +++ b/libpod/boltdb_state_internal.go @@ -919,7 +919,7 @@ func (s *BoltState) removeContainer(ctr *Container, pod *Pod, tx *bolt.Tx) error return err } 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 { diff --git a/libpod/define/errors.go b/libpod/define/errors.go index 2e85454b2..e19ac6a27 100644 --- a/libpod/define/errors.go +++ b/libpod/define/errors.go @@ -31,6 +31,10 @@ var ( // not exist. 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 // aliases. ErrNoAliases = errors.New("no aliases for container") diff --git a/libpod/in_memory_state.go b/libpod/in_memory_state.go index 3875878ed..df45f8e73 100644 --- a/libpod/in_memory_state.go +++ b/libpod/in_memory_state.go @@ -391,7 +391,7 @@ func (s *InMemoryState) RemoveContainer(ctr *Container) error { deps, ok := s.ctrDepends[ctr.ID()] if ok && len(deps) != 0 { 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 @@ -1497,7 +1497,7 @@ func (s *InMemoryState) RemoveContainerFromPod(pod *Pod, ctr *Container) error { deps, ok := s.ctrDepends[ctr.ID()] if ok && len(deps) != 0 { 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 diff --git a/pkg/api/handlers/compat/containers_start.go b/pkg/api/handlers/compat/containers_start.go index 391aa752d..f1ed1b2b8 100644 --- a/pkg/api/handlers/compat/containers_start.go +++ b/pkg/api/handlers/compat/containers_start.go @@ -42,7 +42,7 @@ func StartContainer(w http.ResponseWriter, r *http.Request) { utils.WriteResponse(w, http.StatusNotModified, nil) 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) return } diff --git a/pkg/bindings/containers/types.go b/pkg/bindings/containers/types.go index f63e35bf1..0d22c32f8 100644 --- a/pkg/bindings/containers/types.go +++ b/pkg/bindings/containers/types.go @@ -154,6 +154,7 @@ type RestartOptions struct { // StartOptions are optional options for starting containers type StartOptions struct { DetachKeys *string + Recursive *bool } //go:generate go run ../generator/generator.go StatsOptions diff --git a/pkg/bindings/containers/types_start_options.go b/pkg/bindings/containers/types_start_options.go index f8ba29623..d419c755c 100644 --- a/pkg/bindings/containers/types_start_options.go +++ b/pkg/bindings/containers/types_start_options.go @@ -35,3 +35,19 @@ func (o *StartOptions) GetDetachKeys() string { } 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 +} diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index 637531ee9..3fe9ef637 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -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 - 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 { 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 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 { // User manually detached // Exit cleanly immediately @@ -784,7 +784,7 @@ func (ic *ContainerEngine) ContainerStart(ctx context.Context, namesOrIds []stri RawInput: rawInput, ExitCode: 125, } - if err := ctr.Start(ctx, ctr.PodID() != ""); err != nil { + if err := ctr.Start(ctx, true); err != nil { // if lastError != nil { // 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()} if logrus.GetLevel() == logrus.DebugLevel { @@ -859,7 +855,7 @@ func (ic *ContainerEngine) ContainerRun(ctx context.Context, opts entities.Conta } if opts.Detach { // 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 report.ExitCode = define.ExitCode(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 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 // Do not perform cleanup, or wait for container exit code // Just exit immediately diff --git a/pkg/domain/infra/abi/terminal/terminal_linux.go b/pkg/domain/infra/abi/terminal/terminal_linux.go index 7a0c2907c..ab71f8f6f 100644 --- a/pkg/domain/infra/abi/terminal/terminal_linux.go +++ b/pkg/domain/infra/abi/terminal/terminal_linux.go @@ -39,7 +39,7 @@ func ExecAttachCtr(ctx context.Context, ctr *libpod.Container, execConfig *libpo // 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 // 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) 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) } - attachChan, err := ctr.StartAndAttach(ctx, streams, detachKeys, resize, recursive) + attachChan, err := ctr.StartAndAttach(ctx, streams, detachKeys, resize, true) if err != nil { return err } diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index a0f65f11f..4545d266b 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -629,7 +629,7 @@ func (ic *ContainerEngine) ContainerRun(ctx context.Context, opts entities.Conta if opts.Detach { // 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 { report.ExitCode = define.ExitCode(err) } diff --git a/pkg/specgen/generate/container_create.go b/pkg/specgen/generate/container_create.go index 1d724ffb0..ef9975021 100644 --- a/pkg/specgen/generate/container_create.go +++ b/pkg/specgen/generate/container_create.go @@ -364,6 +364,17 @@ func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen. if len(s.Secrets) != 0 { 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 } diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index c10dc5ef5..28111f96d 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -160,10 +160,17 @@ type ContainerBasicConfig struct { // to 0, 1, 2) that will be passed to the executed process. The total FDs // passed will be 3 + PreserveFDs. // set tags as `json:"-"` for not supported remote + // Optional. PreserveFDs uint `json:"-"` // Timezone is the timezone inside the container. // Local means it has the same timezone as the host machine + // Optional. 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 diff --git a/test/e2e/run_test.go b/test/e2e/run_test.go index 23930b4f7..cefe00655 100644 --- a/test/e2e/run_test.go +++ b/test/e2e/run_test.go @@ -1588,4 +1588,29 @@ WORKDIR /madethis`, BB) 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)) + }) }) -- cgit v1.2.3-54-g00ecf