diff options
-rw-r--r-- | Makefile | 22 | ||||
-rw-r--r-- | cmd/podman/generate/systemd.go | 12 | ||||
-rw-r--r-- | cmd/podman/machine/start.go | 2 | ||||
-rw-r--r-- | contrib/msi/podman.wxs | 4 | ||||
-rw-r--r-- | docs/source/markdown/podman-generate-systemd.1.md | 16 | ||||
-rw-r--r-- | pkg/api/handlers/compat/images.go | 4 | ||||
-rw-r--r-- | pkg/api/handlers/libpod/generate.go | 28 | ||||
-rw-r--r-- | pkg/api/server/register_generate.go | 21 | ||||
-rw-r--r-- | pkg/bindings/generate/types.go | 6 | ||||
-rw-r--r-- | pkg/bindings/generate/types_systemd_options.go | 45 | ||||
-rw-r--r-- | pkg/domain/entities/generate.go | 6 | ||||
-rw-r--r-- | pkg/domain/infra/tunnel/generate.go | 12 | ||||
-rw-r--r-- | pkg/machine/wsl/machine.go | 163 | ||||
-rw-r--r-- | pkg/machine/wsl/util_windows.go | 19 | ||||
-rw-r--r-- | pkg/systemd/generate/containers.go | 23 | ||||
-rw-r--r-- | pkg/systemd/generate/containers_test.go | 188 | ||||
-rw-r--r-- | pkg/systemd/generate/pods.go | 20 | ||||
-rw-r--r-- | pkg/systemd/generate/pods_test.go | 203 | ||||
-rw-r--r-- | test/apiv2/20-containers.at | 26 | ||||
-rw-r--r-- | test/e2e/generate_systemd_test.go | 92 |
20 files changed, 895 insertions, 17 deletions
@@ -186,6 +186,13 @@ ifdef HOMEBREW_PREFIX endif endif +# win-sshproxy is checked out manually to keep from pulling in gvisor and it's transitive +# dependencies. This is only used for the Windows installer task (podman.msi), which must +# include this lightweight helper binary. +# +GV_GITURL=git://github.com/containers/gvisor-tap-vsock.git +GV_SHA=e943b1806d94d387c4c38d96719432d50a84bbd0 + ### ### Primary entry-point targets ### @@ -695,7 +702,7 @@ podman-remote-release-%.zip: test/version/version ## Build podman-remote for %=$ .PHONY: podman.msi podman.msi: test/version/version ## Build podman-remote, package for installation on Windows $(MAKE) podman-v$(RELEASE_NUMBER).msi -podman-v$(RELEASE_NUMBER).msi: podman-remote-windows podman-remote-windows-docs podman-winpath +podman-v$(RELEASE_NUMBER).msi: podman-remote-windows podman-remote-windows-docs podman-winpath win-sshproxy $(eval DOCFILE := docs/build/remote/windows) find $(DOCFILE) -print | \ wixl-heat --var var.ManSourceDir --component-group ManFiles \ @@ -704,6 +711,19 @@ podman-v$(RELEASE_NUMBER).msi: podman-remote-windows podman-remote-windows-docs wixl -D VERSION=$(call err_if_empty,RELEASE_VERSION) -D ManSourceDir=$(DOCFILE) \ -o $@ contrib/msi/podman.wxs $(DOCFILE)/pages.wsx --arch x64 +# Checks out and builds win-sshproxy helper. See comment on GV_GITURL declaration +.PHONY: win-sshproxy +win-sshproxy: test/version/version + rm -rf tmp-gv; mkdir tmp-gv + (cd tmp-gv; \ + git init; \ + git remote add origin $(GV_GITURL); \ + git fetch --depth 1 origin $(GV_SHA); \ + git checkout FETCH_HEAD; make win-sshproxy) + mkdir -p bin/windows/ + cp tmp-gv/bin/win-sshproxy.exe bin/windows/ + rm -rf tmp-gv + .PHONY: package package: ## Build rpm packages ## TODO(ssbarnea): make version number predictable, it should not change diff --git a/cmd/podman/generate/systemd.go b/cmd/podman/generate/systemd.go index 5ad42ebc0..0dab6299d 100644 --- a/cmd/podman/generate/systemd.go +++ b/cmd/podman/generate/systemd.go @@ -25,6 +25,9 @@ const ( restartPolicyFlagName = "restart-policy" restartSecFlagName = "restart-sec" newFlagName = "new" + wantsFlagName = "wants" + afterFlagName = "after" + requiresFlagName = "requires" ) var ( @@ -97,6 +100,15 @@ func init() { flags.StringVar(&format, formatFlagName, "", "Print the created units in specified format (json)") _ = systemdCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(nil)) + flags.StringArrayVar(&systemdOptions.Wants, wantsFlagName, nil, "Add (weak) requirement dependencies to the generated unit file") + _ = systemdCmd.RegisterFlagCompletionFunc(wantsFlagName, completion.AutocompleteNone) + + flags.StringArrayVar(&systemdOptions.After, afterFlagName, nil, "Add dependencies order to the generated unit file") + _ = systemdCmd.RegisterFlagCompletionFunc(afterFlagName, completion.AutocompleteNone) + + flags.StringArrayVar(&systemdOptions.Requires, requiresFlagName, nil, "Similar to wants, but declares stronger requirement dependencies") + _ = systemdCmd.RegisterFlagCompletionFunc(requiresFlagName, completion.AutocompleteNone) + flags.SetNormalizeFunc(utils.TimeoutAliasFlags) } diff --git a/cmd/podman/machine/start.go b/cmd/podman/machine/start.go index 0bcf32cd5..16faa25ef 100644 --- a/cmd/podman/machine/start.go +++ b/cmd/podman/machine/start.go @@ -1,3 +1,4 @@ +//go:build amd64 || arm64 // +build amd64 arm64 package machine @@ -64,5 +65,6 @@ func start(cmd *cobra.Command, args []string) error { if err := vm.Start(vmName, machine.StartOptions{}); err != nil { return err } + fmt.Printf("Machine %q started successfully\n", vmName) return nil } diff --git a/contrib/msi/podman.wxs b/contrib/msi/podman.wxs index c2826fc19..c4ba623c0 100644 --- a/contrib/msi/podman.wxs +++ b/contrib/msi/podman.wxs @@ -29,6 +29,9 @@ <Component Id="WinPathExecutable" Guid="00F5B731-D4A6-4B69-87B0-EA4EBAB89F95" Win64="Yes"> <File Id="8F507E28-A61D-4E64-A92B-B5A00F023AE8" Name="winpath.exe" Source="bin/windows/winpath.exe" KeyPath="yes"/> </Component> + <Component Id="WinSshProxyExecutable" Guid="0DA730AB-2F97-40E8-A8FC-356E88EAA4D2" Win64="Yes"> + <File Id="4A2AD125-34E7-4BD8-BE28-B2A9A5EDBEB5" Name="win-sshproxy.exe" Source="bin/windows/win-sshproxy.exe" KeyPath="yes"/> + </Component> </Directory> </Directory> </Directory> @@ -41,6 +44,7 @@ <ComponentRef Id="INSTALLDIR_Component"/> <ComponentRef Id="MainExecutable"/> <ComponentRef Id="WinPathExecutable"/> + <ComponentRef Id="WinSshProxyExecutable"/> <ComponentGroupRef Id="ManFiles"/> </Feature> diff --git a/docs/source/markdown/podman-generate-systemd.1.md b/docs/source/markdown/podman-generate-systemd.1.md index 363d042ae..d051092f8 100644 --- a/docs/source/markdown/podman-generate-systemd.1.md +++ b/docs/source/markdown/podman-generate-systemd.1.md @@ -68,6 +68,22 @@ Set the systemd unit name prefix for pods. The default is *pod*. Set the systemd unit name separator between the name/id of a container/pod and the prefix. The default is *-*. +#### **--wants**=*dependency_name* + +Add the systemd unit wants (`Wants=`) option, that this service is (weak) dependent on. This option may be specified more than once. This option does not influence the order in which services are started or stopped. + +User-defined dependencies will be appended to the generated unit file, but any existing options such as needed or defined by default (e.g. `online.target`) will **not** be removed or overridden. + +#### **--after**=*dependency_name* + +Add the systemd unit after (`After=`) option, that ordering dependencies between the list of dependencies and this service. This option may be specified more than once. + +User-defined dependencies will be appended to the generated unit file, but any existing options such as needed or defined by default (e.g. `online.target`) will **not** be removed or overridden. + +#### **--requires**=*dependency_name* + +Set the systemd unit requires (`Requires=`) option. Similar to wants, but declares a stronger requirement dependency. + #### **--template** Add template specifiers to run multiple services from the systemd unit file. diff --git a/pkg/api/handlers/compat/images.go b/pkg/api/handlers/compat/images.go index 97fa4ddad..23a9b12a3 100644 --- a/pkg/api/handlers/compat/images.go +++ b/pkg/api/handlers/compat/images.go @@ -138,7 +138,9 @@ func CommitContainer(w http.ResponseWriter, r *http.Request) { options.Message = query.Comment options.Author = query.Author options.Pause = query.Pause - options.Changes = strings.Fields(query.Changes) + if query.Changes != "" { + options.Changes = strings.Split(query.Changes, ",") + } ctr, err := runtime.LookupContainer(query.Container) if err != nil { utils.Error(w, "Something went wrong.", http.StatusNotFound, err) diff --git a/pkg/api/handlers/libpod/generate.go b/pkg/api/handlers/libpod/generate.go index aadb5ad52..9b62a1388 100644 --- a/pkg/api/handlers/libpod/generate.go +++ b/pkg/api/handlers/libpod/generate.go @@ -17,17 +17,20 @@ func GenerateSystemd(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) query := struct { - Name bool `schema:"useName"` - New bool `schema:"new"` - NoHeader bool `schema:"noHeader"` - TemplateUnitFile bool `schema:"templateUnitFile"` - RestartPolicy *string `schema:"restartPolicy"` - RestartSec uint `schema:"restartSec"` - StopTimeout uint `schema:"stopTimeout"` - StartTimeout uint `schema:"startTimeout"` - ContainerPrefix string `schema:"containerPrefix"` - PodPrefix string `schema:"podPrefix"` - Separator string `schema:"separator"` + Name bool `schema:"useName"` + New bool `schema:"new"` + NoHeader bool `schema:"noHeader"` + TemplateUnitFile bool `schema:"templateUnitFile"` + RestartPolicy *string `schema:"restartPolicy"` + RestartSec uint `schema:"restartSec"` + StopTimeout uint `schema:"stopTimeout"` + StartTimeout uint `schema:"startTimeout"` + ContainerPrefix string `schema:"containerPrefix"` + PodPrefix string `schema:"podPrefix"` + Separator string `schema:"separator"` + Wants []string `schema:"wants"` + After []string `schema:"after"` + Requires []string `schema:"requires"` }{ StartTimeout: 0, StopTimeout: util.DefaultContainerConfig().Engine.StopTimeout, @@ -55,6 +58,9 @@ func GenerateSystemd(w http.ResponseWriter, r *http.Request) { PodPrefix: query.PodPrefix, Separator: query.Separator, RestartSec: &query.RestartSec, + Wants: query.Wants, + After: query.After, + Requires: query.Requires, } report, err := containerEngine.GenerateSystemd(r.Context(), utils.GetName(r), options) diff --git a/pkg/api/server/register_generate.go b/pkg/api/server/register_generate.go index 47057959c..6b7f0cfe7 100644 --- a/pkg/api/server/register_generate.go +++ b/pkg/api/server/register_generate.go @@ -72,6 +72,27 @@ func (s *APIServer) registerGenerateHandlers(r *mux.Router) error { // type: integer // default: 0 // description: Configures the time to sleep before restarting a service. + // - in: query + // name: wants + // type: array + // items: + // type: string + // default: [] + // description: Systemd Wants list for the container or pods. + // - in: query + // name: after + // type: array + // items: + // type: string + // default: [] + // description: Systemd After list for the container or pods. + // - in: query + // name: requires + // type: array + // items: + // type: string + // default: [] + // description: Systemd Requires list for the container or pods. // produces: // - application/json // responses: diff --git a/pkg/bindings/generate/types.go b/pkg/bindings/generate/types.go index ce560c547..25c398c8b 100644 --- a/pkg/bindings/generate/types.go +++ b/pkg/bindings/generate/types.go @@ -32,4 +32,10 @@ type SystemdOptions struct { PodPrefix *string // Separator - systemd unit name separator between name/id and prefix Separator *string + // Wants - systemd wants list for the container or pods + Wants *[]string + // After - systemd after list for the container or pods + After *[]string + // Requires - systemd requires list for the container or pods + Requires *[]string } diff --git a/pkg/bindings/generate/types_systemd_options.go b/pkg/bindings/generate/types_systemd_options.go index 960e45e50..4d436945b 100644 --- a/pkg/bindings/generate/types_systemd_options.go +++ b/pkg/bindings/generate/types_systemd_options.go @@ -181,3 +181,48 @@ func (o *SystemdOptions) GetSeparator() string { } return *o.Separator } + +// WithWants set field Wants to given value +func (o *SystemdOptions) WithWants(value []string) *SystemdOptions { + o.Wants = &value + return o +} + +// GetWants returns value of field Wants +func (o *SystemdOptions) GetWants() []string { + if o.Wants == nil { + var z []string + return z + } + return *o.Wants +} + +// WithAfter set field After to given value +func (o *SystemdOptions) WithAfter(value []string) *SystemdOptions { + o.After = &value + return o +} + +// GetAfter returns value of field After +func (o *SystemdOptions) GetAfter() []string { + if o.After == nil { + var z []string + return z + } + return *o.After +} + +// WithRequires set field Requires to given value +func (o *SystemdOptions) WithRequires(value []string) *SystemdOptions { + o.Requires = &value + return o +} + +// GetRequires returns value of field Requires +func (o *SystemdOptions) GetRequires() []string { + if o.Requires == nil { + var z []string + return z + } + return *o.Requires +} diff --git a/pkg/domain/entities/generate.go b/pkg/domain/entities/generate.go index e431a70af..73dd64ecd 100644 --- a/pkg/domain/entities/generate.go +++ b/pkg/domain/entities/generate.go @@ -26,6 +26,12 @@ type GenerateSystemdOptions struct { NoHeader bool // TemplateUnitFile - make use of %i and %I to differentiate between the different instances of the unit TemplateUnitFile bool + // Wants - systemd wants list for the container or pods + Wants []string + // After - systemd after list for the container or pods + After []string + // Requires - systemd requires list for the container or pods + Requires []string } // GenerateSystemdReport diff --git a/pkg/domain/infra/tunnel/generate.go b/pkg/domain/infra/tunnel/generate.go index 49b66e908..235d478ec 100644 --- a/pkg/domain/infra/tunnel/generate.go +++ b/pkg/domain/infra/tunnel/generate.go @@ -8,7 +8,17 @@ import ( ) func (ic *ContainerEngine) GenerateSystemd(ctx context.Context, nameOrID string, opts entities.GenerateSystemdOptions) (*entities.GenerateSystemdReport, error) { - options := new(generate.SystemdOptions).WithUseName(opts.Name).WithContainerPrefix(opts.ContainerPrefix).WithNew(opts.New).WithNoHeader(opts.NoHeader).WithTemplateUnitFile(opts.TemplateUnitFile).WithPodPrefix(opts.PodPrefix).WithSeparator(opts.Separator) + options := new( + generate.SystemdOptions). + WithUseName(opts.Name). + WithContainerPrefix(opts.ContainerPrefix). + WithNew(opts.New).WithNoHeader(opts.NoHeader). + WithTemplateUnitFile(opts.TemplateUnitFile). + WithPodPrefix(opts.PodPrefix). + WithSeparator(opts.Separator). + WithWants(opts.Wants). + WithAfter(opts.After). + WithRequires(opts.Requires) if opts.StartTimeout != nil { options.WithStartTimeout(*opts.StartTimeout) diff --git a/pkg/machine/wsl/machine.go b/pkg/machine/wsl/machine.go index 6cab855d3..c7d857954 100644 --- a/pkg/machine/wsl/machine.go +++ b/pkg/machine/wsl/machine.go @@ -1,4 +1,3 @@ -//go:build windows // +build windows package wsl @@ -143,6 +142,11 @@ http://docs.microsoft.com/en-us/windows/wsl/install\ ` +const ( + winSShProxy = "win-sshproxy.exe" + winSshProxyTid = "win-sshproxy.tid" +) + type Provider struct{} type MachineVM struct { @@ -705,8 +709,6 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error { return errors.Errorf("%q is already running", name) } - fmt.Println("Starting machine...") - dist := toDist(name) err := runCmdPassThrough("wsl", "-d", dist, "/root/bootstrap") @@ -714,9 +716,107 @@ func (v *MachineVM) Start(name string, _ machine.StartOptions) error { return errors.Wrap(err, "WSL bootstrap script failed") } + globalName, pipeName, err := launchWinProxy(v) + if err != nil { + fmt.Fprintln(os.Stderr, "API forwarding for Docker API clients is not available due to the following startup failures.") + fmt.Fprintf(os.Stderr, "\t%s\n", err.Error()) + fmt.Fprintln(os.Stderr, "\nPodman clients are still able to connect.") + } else { + fmt.Printf("API forwarding listening on: %s\n", pipeName) + if globalName { + fmt.Printf("\nDocker API clients default to this address. You do not need to set DOCKER_HOST.\n") + } else { + fmt.Printf("\nAnother process was listening on the default Docker API pipe address.\n") + fmt.Printf("You can still connect Docker API clients by setting DOCKER HOST using the\n") + fmt.Printf("following powershell command in your terminal session:\n") + fmt.Printf("\n\t$Env:DOCKER_HOST = '%s'\n", pipeName) + fmt.Printf("\nOr in a classic CMD prompt:\n") + fmt.Printf("\n\tset DOCKER_HOST = '%s'\n", pipeName) + fmt.Printf("\nAlternatively terminate the other process and restart podman machine.\n") + } + } + return markStart(name) } +func launchWinProxy(v *MachineVM) (bool, string, error) { + globalName := true + pipeName := "docker_engine" + if !pipeAvailable(pipeName) { + pipeName = toDist(v.Name) + globalName = false + if !pipeAvailable(pipeName) { + return globalName, "", errors.Errorf("could not start api proxy since expected pipe is not available: %s", pipeName) + } + } + fullPipeName := "npipe:////./pipe/" + pipeName + + exe, err := os.Executable() + if err != nil { + return globalName, "", err + } + + exe, err = filepath.EvalSymlinks(exe) + if err != nil { + return globalName, "", err + } + + command := filepath.Join(filepath.Dir(exe), winSShProxy) + stateDir, err := getWinProxyStateDir(v) + if err != nil { + return globalName, "", err + } + + dest := fmt.Sprintf("ssh://root@localhost:%d/run/podman/podman.sock", v.Port) + cmd := exec.Command(command, v.Name, stateDir, fullPipeName, dest, v.IdentityPath) + if err := cmd.Start(); err != nil { + return globalName, "", err + } + + return globalName, fullPipeName, waitPipeExists(pipeName, 30, func() error { + active, exitCode := getProcessState(cmd.Process.Pid) + if !active { + return errors.Errorf("win-sshproxy.exe failed to start, exit code: %d (see windows event logs)", exitCode) + } + + return nil + }) +} + +func getWinProxyStateDir(v *MachineVM) (string, error) { + dir, err := machine.GetDataDir(vmtype) + if err != nil { + return "", err + } + stateDir := filepath.Join(dir, v.Name) + if err = os.MkdirAll(stateDir, 0755); err != nil { + return "", err + } + + return stateDir, nil +} + +func pipeAvailable(pipeName string) bool { + _, err := os.Stat(`\\.\pipe\` + pipeName) + return os.IsNotExist(err) +} + +func waitPipeExists(pipeName string, retries int, checkFailure func() error) error { + var err error + for i := 0; i < retries; i++ { + _, err = os.Stat(`\\.\pipe\` + pipeName) + if err == nil { + break + } + if fail := checkFailure(); fail != nil { + return fail + } + time.Sleep(100 * time.Millisecond) + } + + return err +} + func isWSLInstalled() bool { cmd := exec.Command("wsl", "--status") out, err := cmd.StdoutPipe() @@ -817,6 +917,10 @@ func (v *MachineVM) Stop(name string, _ machine.StopOptions) error { return errors.Errorf("%q is not running", v.Name) } + if err := stopWinProxy(v); err != nil { + fmt.Fprintf(os.Stderr, "Could not stop API forwarding service (win-sshproxy.exe): %s\n", err.Error()) + } + cmd := exec.Command("wsl", "-d", dist, "sh") cmd.Stdin = strings.NewReader(waitTerm) if err = cmd.Start(); err != nil { @@ -840,6 +944,59 @@ func (v *MachineVM) Stop(name string, _ machine.StopOptions) error { return nil } +func stopWinProxy(v *MachineVM) error { + pid, tid, tidFile, err := readWinProxyTid(v) + if err != nil { + return err + } + + proc, err := os.FindProcess(int(pid)) + if err != nil { + return nil + } + sendQuit(tid) + _ = waitTimeout(proc, 20*time.Second) + _ = os.Remove(tidFile) + + return nil +} + +func waitTimeout(proc *os.Process, timeout time.Duration) bool { + done := make(chan bool) + go func() { + proc.Wait() + done <- true + }() + ret := false + select { + case <-time.After(timeout): + proc.Kill() + <-done + case <-done: + ret = true + break + } + + return ret +} + +func readWinProxyTid(v *MachineVM) (uint32, uint32, string, error) { + stateDir, err := getWinProxyStateDir(v) + if err != nil { + return 0, 0, "", err + } + + tidFile := filepath.Join(stateDir, winSshProxyTid) + contents, err := ioutil.ReadFile(tidFile) + if err != nil { + return 0, 0, "", err + } + + var pid, tid uint32 + fmt.Sscanf(string(contents), "%d:%d", &pid, &tid) + return pid, tid, tidFile, nil +} + //nolint:cyclop func (v *MachineVM) Remove(name string, opts machine.RemoveOptions) (string, func() error, error) { var files []string diff --git a/pkg/machine/wsl/util_windows.go b/pkg/machine/wsl/util_windows.go index 95e4c9894..b5c28e015 100644 --- a/pkg/machine/wsl/util_windows.go +++ b/pkg/machine/wsl/util_windows.go @@ -67,6 +67,7 @@ const ( TOKEN_QUERY = 0x0008 SE_PRIVILEGE_ENABLED = 0x00000002 SE_ERR_ACCESSDENIED = 0x05 + WM_QUIT = 0x12 ) func winVersionAtLeast(major uint, minor uint, build uint) bool { @@ -279,6 +280,18 @@ func obtainShutdownPrivilege() error { return nil } +func getProcessState(pid int) (active bool, exitCode int) { + const da = syscall.STANDARD_RIGHTS_READ | syscall.PROCESS_QUERY_INFORMATION | syscall.SYNCHRONIZE + handle, err := syscall.OpenProcess(da, false, uint32(pid)) + if err != nil { + return false, int(syscall.ERROR_PROC_NOT_FOUND) + } + + var code uint32 + syscall.GetExitCodeProcess(handle, &code) + return code == 259, int(code) +} + func addRunOnceRegistryEntry(command string) error { k, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\RunOnce`, registry.WRITE) if err != nil { @@ -336,3 +349,9 @@ func buildCommandArgs(elevate bool) string { } return strings.Join(args, " ") } + +func sendQuit(tid uint32) { + user32 := syscall.NewLazyDLL("user32.dll") + postMessage := user32.NewProc("PostThreadMessageW") + postMessage.Call(uintptr(tid), WM_QUIT, 0, 0) +} diff --git a/pkg/systemd/generate/containers.go b/pkg/systemd/generate/containers.go index fd5c247f3..ea829c810 100644 --- a/pkg/systemd/generate/containers.go +++ b/pkg/systemd/generate/containers.go @@ -94,6 +94,13 @@ type containerInfo struct { RunRoot string // Add %i and %I to description and execute parts IdentifySpecifier bool + // Wants are the list of services that this service is (weak) dependent on. This + // option does not influence the order in which services are started or stopped. + Wants []string + // After ordering dependencies between the list of services and this service. + After []string + // Similar to Wants, but declares a stronger requirement dependency. + Requires []string } const containerTemplate = headerTemplate + ` @@ -101,6 +108,19 @@ const containerTemplate = headerTemplate + ` BindsTo={{{{- range $index, $value := .BoundToServices -}}}}{{{{if $index}}}} {{{{end}}}}{{{{ $value }}}}.service{{{{end}}}} After={{{{- range $index, $value := .BoundToServices -}}}}{{{{if $index}}}} {{{{end}}}}{{{{ $value }}}}.service{{{{end}}}} {{{{- end}}}} +{{{{- if or .Wants .After .Requires }}}} + +# User-defined dependencies +{{{{- end}}}} +{{{{- if .Wants}}}} +Wants={{{{- range $index, $value := .Wants }}}}{{{{ if $index}}}} {{{{end}}}}{{{{ $value }}}}{{{{end}}}} +{{{{- end}}}} +{{{{- if .After}}}} +After={{{{- range $index, $value := .After }}}}{{{{ if $index}}}} {{{{end}}}}{{{{ $value }}}}{{{{end}}}} +{{{{- end}}}} +{{{{- if .Requires}}}} +Requires={{{{- range $index, $value := .Requires }}}}{{{{ if $index}}}} {{{{end}}}}{{{{ $value }}}}{{{{end}}}} +{{{{- end}}}} [Service] Environment={{{{.EnvVariable}}}}=%n{{{{- if (eq .IdentifySpecifier true) }}}}-%i{{{{- end}}}} @@ -201,6 +221,9 @@ func generateContainerInfo(ctr *libpod.Container, options entities.GenerateSyste CreateCommand: createCommand, RunRoot: runRoot, containerEnv: envs, + Wants: options.Wants, + After: options.After, + Requires: options.Requires, } return &info, nil diff --git a/pkg/systemd/generate/containers_test.go b/pkg/systemd/generate/containers_test.go index 45bb5173a..2f653a4b9 100644 --- a/pkg/systemd/generate/containers_test.go +++ b/pkg/systemd/generate/containers_test.go @@ -91,6 +91,116 @@ Type=forking WantedBy=default.target ` + goodNameCustomWants := `# container-foobar.service +# autogenerated by Podman CI + +[Unit] +Description=Podman container-foobar.service +Documentation=man:podman-generate-systemd(1) +Wants=network-online.target +After=network-online.target +RequiresMountsFor=/var/run/containers/storage + +# User-defined dependencies +Wants=a.service b.service c.target + +[Service] +Environment=PODMAN_SYSTEMD_UNIT=%n +Restart=on-failure +TimeoutStopSec=70 +ExecStart=/usr/bin/podman start foobar +ExecStop=/usr/bin/podman stop -t 10 foobar +ExecStopPost=/usr/bin/podman stop -t 10 foobar +PIDFile=/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid +Type=forking + +[Install] +WantedBy=default.target +` + + goodNameCustomAfter := `# container-foobar.service +# autogenerated by Podman CI + +[Unit] +Description=Podman container-foobar.service +Documentation=man:podman-generate-systemd(1) +Wants=network-online.target +After=network-online.target +RequiresMountsFor=/var/run/containers/storage + +# User-defined dependencies +After=a.service b.service c.target + +[Service] +Environment=PODMAN_SYSTEMD_UNIT=%n +Restart=on-failure +TimeoutStopSec=70 +ExecStart=/usr/bin/podman start foobar +ExecStop=/usr/bin/podman stop -t 10 foobar +ExecStopPost=/usr/bin/podman stop -t 10 foobar +PIDFile=/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid +Type=forking + +[Install] +WantedBy=default.target +` + + goodNameCustomRequires := `# container-foobar.service +# autogenerated by Podman CI + +[Unit] +Description=Podman container-foobar.service +Documentation=man:podman-generate-systemd(1) +Wants=network-online.target +After=network-online.target +RequiresMountsFor=/var/run/containers/storage + +# User-defined dependencies +Requires=a.service b.service c.target + +[Service] +Environment=PODMAN_SYSTEMD_UNIT=%n +Restart=on-failure +TimeoutStopSec=70 +ExecStart=/usr/bin/podman start foobar +ExecStop=/usr/bin/podman stop -t 10 foobar +ExecStopPost=/usr/bin/podman stop -t 10 foobar +PIDFile=/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid +Type=forking + +[Install] +WantedBy=default.target +` + + goodNameCustomDependencies := `# container-foobar.service +# autogenerated by Podman CI + +[Unit] +Description=Podman container-foobar.service +Documentation=man:podman-generate-systemd(1) +Wants=network-online.target +After=network-online.target +RequiresMountsFor=/var/run/containers/storage + +# User-defined dependencies +Wants=a.service b.service c.target +After=a.service b.service c.target +Requires=a.service b.service c.target + +[Service] +Environment=PODMAN_SYSTEMD_UNIT=%n +Restart=on-failure +TimeoutStopSec=70 +ExecStart=/usr/bin/podman start foobar +ExecStop=/usr/bin/podman stop -t 10 foobar +ExecStopPost=/usr/bin/podman stop -t 10 foobar +PIDFile=/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid +Type=forking + +[Install] +WantedBy=default.target +` + goodNameBoundTo := `# container-foobar.service # autogenerated by Podman CI @@ -613,6 +723,84 @@ WantedBy=default.target false, false, }, + {"good with name and wants", + containerInfo{ + Executable: "/usr/bin/podman", + ServiceName: "container-foobar", + ContainerNameOrID: "foobar", + PIDFile: "/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", + StopTimeout: 10, + PodmanVersion: "CI", + Wants: []string{"a.service", "b.service", "c.target"}, + EnvVariable: define.EnvVariable, + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", + }, + goodNameCustomWants, + false, + false, + false, + false, + }, + {"good with name and after", + containerInfo{ + Executable: "/usr/bin/podman", + ServiceName: "container-foobar", + ContainerNameOrID: "foobar", + PIDFile: "/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", + StopTimeout: 10, + PodmanVersion: "CI", + After: []string{"a.service", "b.service", "c.target"}, + EnvVariable: define.EnvVariable, + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", + }, + goodNameCustomAfter, + false, + false, + false, + false, + }, + {"good with name and requires", + containerInfo{ + Executable: "/usr/bin/podman", + ServiceName: "container-foobar", + ContainerNameOrID: "foobar", + PIDFile: "/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", + StopTimeout: 10, + PodmanVersion: "CI", + Requires: []string{"a.service", "b.service", "c.target"}, + EnvVariable: define.EnvVariable, + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", + }, + goodNameCustomRequires, + false, + false, + false, + false, + }, + {"good with name and dependencies", + containerInfo{ + Executable: "/usr/bin/podman", + ServiceName: "container-foobar", + ContainerNameOrID: "foobar", + PIDFile: "/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", + StopTimeout: 10, + PodmanVersion: "CI", + Wants: []string{"a.service", "b.service", "c.target"}, + After: []string{"a.service", "b.service", "c.target"}, + Requires: []string{"a.service", "b.service", "c.target"}, + EnvVariable: define.EnvVariable, + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", + }, + goodNameCustomDependencies, + false, + false, + false, + false, + }, {"good with name and bound to", containerInfo{ Executable: "/usr/bin/podman", diff --git a/pkg/systemd/generate/pods.go b/pkg/systemd/generate/pods.go index 17e1dc5a2..003c23e77 100644 --- a/pkg/systemd/generate/pods.go +++ b/pkg/systemd/generate/pods.go @@ -83,10 +83,30 @@ type podInfo struct { RunRoot string // Add %i and %I to description and execute parts - this should not be used IdentifySpecifier bool + // Wants are the list of services that this service is (weak) dependent on. This + // option does not influence the order in which services are started or stopped. + Wants []string + // After ordering dependencies between the list of services and this service. + After []string + // Similar to Wants, but declares a stronger requirement dependency. + Requires []string } const podTemplate = headerTemplate + `Requires={{{{- range $index, $value := .RequiredServices -}}}}{{{{if $index}}}} {{{{end}}}}{{{{ $value }}}}.service{{{{end}}}} Before={{{{- range $index, $value := .RequiredServices -}}}}{{{{if $index}}}} {{{{end}}}}{{{{ $value }}}}.service{{{{end}}}} +{{{{- if or .Wants .After .Requires }}}} + +# User-defined dependencies +{{{{- end}}}} +{{{{- if .Wants}}}} +Wants={{{{- range $index, $value := .Wants }}}}{{{{ if $index}}}} {{{{end}}}}{{{{ $value }}}}{{{{end}}}} +{{{{- end}}}} +{{{{- if .After}}}} +After={{{{- range $index, $value := .After }}}}{{{{ if $index}}}} {{{{end}}}}{{{{ $value }}}}{{{{end}}}} +{{{{- end}}}} +{{{{- if .Requires}}}} +Requires={{{{- range $index, $value := .Requires }}}}{{{{ if $index}}}} {{{{end}}}}{{{{ $value }}}}{{{{end}}}} +{{{{- end}}}} [Service] Environment={{{{.EnvVariable}}}}=%n diff --git a/pkg/systemd/generate/pods_test.go b/pkg/systemd/generate/pods_test.go index 6c84c8895..b37e0825b 100644 --- a/pkg/systemd/generate/pods_test.go +++ b/pkg/systemd/generate/pods_test.go @@ -67,6 +67,121 @@ WantedBy=default.target podGood := serviceInfo + headerInfo + podContent podGoodNoHeaderInfo := serviceInfo + podContent + podGoodCustomWants := `# pod-123abc.service +# autogenerated by Podman CI + +[Unit] +Description=Podman pod-123abc.service +Documentation=man:podman-generate-systemd(1) +Wants=network-online.target +After=network-online.target +RequiresMountsFor=/var/run/containers/storage +Requires=container-1.service container-2.service +Before=container-1.service container-2.service + +# User-defined dependencies +Wants=a.service b.service c.target + +[Service] +Environment=PODMAN_SYSTEMD_UNIT=%n +Restart=on-failure +TimeoutStopSec=102 +ExecStart=/usr/bin/podman start jadda-jadda-infra +ExecStop=/usr/bin/podman stop -t 42 jadda-jadda-infra +ExecStopPost=/usr/bin/podman stop -t 42 jadda-jadda-infra +PIDFile=/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid +Type=forking + +[Install] +WantedBy=default.target +` + podGoodCustomAfter := `# pod-123abc.service +# autogenerated by Podman CI + +[Unit] +Description=Podman pod-123abc.service +Documentation=man:podman-generate-systemd(1) +Wants=network-online.target +After=network-online.target +RequiresMountsFor=/var/run/containers/storage +Requires=container-1.service container-2.service +Before=container-1.service container-2.service + +# User-defined dependencies +After=a.service b.service c.target + +[Service] +Environment=PODMAN_SYSTEMD_UNIT=%n +Restart=on-failure +TimeoutStopSec=102 +ExecStart=/usr/bin/podman start jadda-jadda-infra +ExecStop=/usr/bin/podman stop -t 42 jadda-jadda-infra +ExecStopPost=/usr/bin/podman stop -t 42 jadda-jadda-infra +PIDFile=/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid +Type=forking + +[Install] +WantedBy=default.target +` + podGoodCustomRequires := `# pod-123abc.service +# autogenerated by Podman CI + +[Unit] +Description=Podman pod-123abc.service +Documentation=man:podman-generate-systemd(1) +Wants=network-online.target +After=network-online.target +RequiresMountsFor=/var/run/containers/storage +Requires=container-1.service container-2.service +Before=container-1.service container-2.service + +# User-defined dependencies +Requires=a.service b.service c.target + +[Service] +Environment=PODMAN_SYSTEMD_UNIT=%n +Restart=on-failure +TimeoutStopSec=102 +ExecStart=/usr/bin/podman start jadda-jadda-infra +ExecStop=/usr/bin/podman stop -t 42 jadda-jadda-infra +ExecStopPost=/usr/bin/podman stop -t 42 jadda-jadda-infra +PIDFile=/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid +Type=forking + +[Install] +WantedBy=default.target +` + podGoodCustomDependencies := `# pod-123abc.service +# autogenerated by Podman CI + +[Unit] +Description=Podman pod-123abc.service +Documentation=man:podman-generate-systemd(1) +Wants=network-online.target +After=network-online.target +RequiresMountsFor=/var/run/containers/storage +Requires=container-1.service container-2.service +Before=container-1.service container-2.service + +# User-defined dependencies +Wants=a.service b.service c.target +After=a.service b.service c.target +Requires=a.service b.service c.target + +[Service] +Environment=PODMAN_SYSTEMD_UNIT=%n +Restart=on-failure +TimeoutStopSec=102 +ExecStart=/usr/bin/podman start jadda-jadda-infra +ExecStop=/usr/bin/podman stop -t 42 jadda-jadda-infra +ExecStopPost=/usr/bin/podman stop -t 42 jadda-jadda-infra +PIDFile=/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid +Type=forking + +[Install] +WantedBy=default.target +` + podGoodRestartSec := `# pod-123abc.service # autogenerated by Podman CI @@ -232,6 +347,94 @@ WantedBy=default.target false, false, }, + {"pod", + podInfo{ + Executable: "/usr/bin/podman", + ServiceName: "pod-123abc", + InfraNameOrID: "jadda-jadda-infra", + PIDFile: "/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", + StopTimeout: 42, + PodmanVersion: "CI", + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", + RequiredServices: []string{"container-1", "container-2"}, + Wants: []string{"a.service", "b.service", "c.target"}, + CreateCommand: []string{ + "podman", "pod", "create", "--name", "foo", "--wants", "a.service", + "--wants", "b.service", "--wants", "c.target", "bar=arg with space"}, + }, + podGoodCustomWants, + false, + false, + false, + }, + {"pod", + podInfo{ + Executable: "/usr/bin/podman", + ServiceName: "pod-123abc", + InfraNameOrID: "jadda-jadda-infra", + PIDFile: "/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", + StopTimeout: 42, + PodmanVersion: "CI", + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", + RequiredServices: []string{"container-1", "container-2"}, + After: []string{"a.service", "b.service", "c.target"}, + CreateCommand: []string{ + "podman", "pod", "create", "--name", "foo", "--after", "a.service", + "--after", "b.service", "--after", "c.target", "bar=arg with space"}, + }, + podGoodCustomAfter, + false, + false, + false, + }, + {"pod", + podInfo{ + Executable: "/usr/bin/podman", + ServiceName: "pod-123abc", + InfraNameOrID: "jadda-jadda-infra", + PIDFile: "/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", + StopTimeout: 42, + PodmanVersion: "CI", + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", + RequiredServices: []string{"container-1", "container-2"}, + Requires: []string{"a.service", "b.service", "c.target"}, + CreateCommand: []string{ + "podman", "pod", "create", "--name", "foo", "--requires", "a.service", + "--requires", "b.service", "--requires", "c.target", "bar=arg with space"}, + }, + podGoodCustomRequires, + false, + false, + false, + }, + {"pod", + podInfo{ + Executable: "/usr/bin/podman", + ServiceName: "pod-123abc", + InfraNameOrID: "jadda-jadda-infra", + PIDFile: "/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", + StopTimeout: 42, + PodmanVersion: "CI", + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", + RequiredServices: []string{"container-1", "container-2"}, + Wants: []string{"a.service", "b.service", "c.target"}, + After: []string{"a.service", "b.service", "c.target"}, + Requires: []string{"a.service", "b.service", "c.target"}, + CreateCommand: []string{ + "podman", "pod", "create", "--name", "foo", "--wants", "a.service", + "--wants", "b.service", "--wants", "c.target", "--after", "a.service", + "--after", "b.service", "--after", "c.target", "--requires", "a.service", + "--requires", "b.service", "--requires", "c.target", "bar=arg with space"}, + }, + podGoodCustomDependencies, + false, + false, + false, + }, {"pod restartSec", podInfo{ Executable: "/usr/bin/podman", diff --git a/test/apiv2/20-containers.at b/test/apiv2/20-containers.at index e5b9eeef3..45c040fbc 100644 --- a/test/apiv2/20-containers.at +++ b/test/apiv2/20-containers.at @@ -420,3 +420,29 @@ t GET containers/$cid/json 200 \ .HostConfig.Binds[0]~/tmp:/mnt:.* \ t DELETE containers/$cid?v=true 204 + +# test apiv2 create/commit +t POST containers/create \ + Image=$IMAGE \ + Entrypoint='["echo"]' \ + Cmd='["param1","param2"]' \ + 201 \ + .Id~[0-9a-f]\\{64\\} +cid=$(jq -r '.Id' <<<"$output") + +# No such container +t POST "commit?container=nonesuch" 404 + +cparam="repo=newrepo&tag=v3&comment=abcd&author=eric" +cparam="$cparam&format=docker&changes=CMD=/bin/bar,EXPOSE=9090" +t POST "commit?container=${cid:0:12}&$cparam" 201 \ + .Id~[0-9a-f]\\{64\\} +iid=$(jq -r '.Id' <<<"$output") +t GET images/$iid/json 200 \ + .RepoTags[0]=docker.io/library/newrepo:v3 \ + .Config.ExposedPorts~.*"9090/tcp" \ + .Config.Cmd~.*"/bin/bar" \ + .Comment="abcd" + +t DELETE containers/$cid 204 +t DELETE images/docker.io/library/newrepo:v3?force=false 200 diff --git a/test/e2e/generate_systemd_test.go b/test/e2e/generate_systemd_test.go index 976048886..55b9a8037 100644 --- a/test/e2e/generate_systemd_test.go +++ b/test/e2e/generate_systemd_test.go @@ -159,6 +159,50 @@ var _ = Describe("Podman generate systemd", func() { Expect(session.OutputToString()).To(ContainSubstring("podman stop -t 5")) }) + It("podman generate systemd with user-defined dependencies", func() { + n := podmanTest.Podman([]string{"run", "--name", "nginx", "-dt", nginx}) + n.WaitWithDefaultTimeout() + Expect(n).Should(Exit(0)) + + session := podmanTest.Podman([]string{"generate", "systemd", "--wants", "foobar.service", "nginx"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + // The generated systemd unit should contain the User-defined Wants option + Expect(session.OutputToString()).To(ContainSubstring("# User-defined dependencies")) + Expect(session.OutputToString()).To(ContainSubstring("Wants=foobar.service")) + + session = podmanTest.Podman([]string{"generate", "systemd", "--after", "foobar.service", "nginx"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + // The generated systemd unit should contain the User-defined After option + Expect(session.OutputToString()).To(ContainSubstring("# User-defined dependencies")) + Expect(session.OutputToString()).To(ContainSubstring("After=foobar.service")) + + session = podmanTest.Podman([]string{"generate", "systemd", "--requires", "foobar.service", "nginx"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + // The generated systemd unit should contain the User-defined Requires option + Expect(session.OutputToString()).To(ContainSubstring("# User-defined dependencies")) + Expect(session.OutputToString()).To(ContainSubstring("Requires=foobar.service")) + + session = podmanTest.Podman([]string{ + "generate", "systemd", + "--wants", "foobar.service", "--wants", "barfoo.service", + "--after", "foobar.service", "--after", "barfoo.service", + "--requires", "foobar.service", "--requires", "barfoo.service", "nginx"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + // The generated systemd unit should contain the User-defined Want, After, Requires options + Expect(session.OutputToString()).To(ContainSubstring("# User-defined dependencies")) + Expect(session.OutputToString()).To(ContainSubstring("Wants=foobar.service barfoo.service")) + Expect(session.OutputToString()).To(ContainSubstring("After=foobar.service barfoo.service")) + Expect(session.OutputToString()).To(ContainSubstring("Requires=foobar.service barfoo.service")) + }) + It("podman generate systemd pod --name", func() { n := podmanTest.Podman([]string{"pod", "create", "--name", "foo"}) n.WaitWithDefaultTimeout() @@ -213,6 +257,54 @@ var _ = Describe("Podman generate systemd", func() { Expect(session.OutputToString()).To(ContainSubstring("/container-foo-1.service")) }) + It("podman generate systemd pod with user-defined dependencies", func() { + n := podmanTest.Podman([]string{"pod", "create", "--name", "foo"}) + n.WaitWithDefaultTimeout() + Expect(n).Should(Exit(0)) + + n = podmanTest.Podman([]string{"create", "--pod", "foo", "--name", "foo-1", "alpine", "top"}) + n.WaitWithDefaultTimeout() + Expect(n).Should(Exit(0)) + + session := podmanTest.Podman([]string{"generate", "systemd", "--name", "--wants", "foobar.service", "foo"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + // The generated systemd unit should contain the User-defined Wants option + Expect(session.OutputToString()).To(ContainSubstring("# User-defined dependencies")) + Expect(session.OutputToString()).To(ContainSubstring("Wants=foobar.service")) + + session = podmanTest.Podman([]string{"generate", "systemd", "--name", "--after", "foobar.service", "foo"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + // The generated systemd unit should contain the User-defined After option + Expect(session.OutputToString()).To(ContainSubstring("# User-defined dependencies")) + Expect(session.OutputToString()).To(ContainSubstring("After=foobar.service")) + + session = podmanTest.Podman([]string{"generate", "systemd", "--name", "--requires", "foobar.service", "foo"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + // The generated systemd unit should contain the User-defined Requires option + Expect(session.OutputToString()).To(ContainSubstring("# User-defined dependencies")) + Expect(session.OutputToString()).To(ContainSubstring("Requires=foobar.service")) + + session = podmanTest.Podman([]string{ + "generate", "systemd", "--name", + "--wants", "foobar.service", "--wants", "barfoo.service", + "--after", "foobar.service", "--after", "barfoo.service", + "--requires", "foobar.service", "--requires", "barfoo.service", "foo"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + // The generated systemd unit should contain the User-defined Want, After, Requires options + Expect(session.OutputToString()).To(ContainSubstring("# User-defined dependencies")) + Expect(session.OutputToString()).To(ContainSubstring("Wants=foobar.service barfoo.service")) + Expect(session.OutputToString()).To(ContainSubstring("After=foobar.service barfoo.service")) + Expect(session.OutputToString()).To(ContainSubstring("Requires=foobar.service barfoo.service")) + }) + It("podman generate systemd --new --name foo", func() { n := podmanTest.Podman([]string{"create", "--name", "foo", "alpine", "top"}) n.WaitWithDefaultTimeout() |