generate systemd: create pod template

Create a new template for generating a pod unit file. Eventually, this
allows for treating and extending pod and container generation
seprately.

The `--new` flag now also works on pods.

Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
This commit is contained in:
Valentin Rothberg
2020-06-05 18:23:12 +02:00
parent 35ae53067f
commit 8d8746adee
9 changed files with 652 additions and 209 deletions

View File

@ -26,10 +26,7 @@ Use the name of the container for the start, stop, and description in the unit f
**--new** **--new**
Create a new container via podman-run instead of starting an existing one. This option relies on container configuration files, which may not map directly to podman CLI flags; please review the generated output carefully before placing in production. Using this flag will yield unit files that do not expect containers and pods to exist. Instead, new containers and pods are created based on their configuration files. The unit files are created best effort and may need to be further edited; please review the generated files carefully before using them in production.
Since we use systemd `Type=forking` service, using this option will force the container run with the detached param `-d`.
Note: Generating systemd unit files with `--new` flag is not yet supported for pods.
**--time**, **-t**=*value* **--time**, **-t**=*value*

View File

@ -260,7 +260,7 @@ func (p *Pod) InfraContainerID() (string, error) {
// InfraContainer returns the infra container. // InfraContainer returns the infra container.
func (p *Pod) InfraContainer() (*Container, error) { func (p *Pod) InfraContainer() (*Container, error) {
if !p.HasInfraContainer() { if !p.HasInfraContainer() {
return nil, errors.New("pod has no infra container") return nil, errors.Wrap(define.ErrNoSuchCtr, "pod has no infra container")
} }
id, err := p.InfraContainerID() id, err := p.InfraContainerID()

View File

@ -34,3 +34,17 @@ Documentation=man:podman-generate-systemd(1)
Wants=network.target Wants=network.target
After=network-online.target After=network-online.target
` `
// filterPodFlags removes --pod and --pod-id-file from the specified command.
func filterPodFlags(command []string) []string {
processed := []string{}
for i := 0; i < len(command); i++ {
s := command[i]
if s == "--pod" || s == "--pod-id-file" {
i += 1
continue
}
processed = append(processed, s)
}
return processed
}

View File

@ -0,0 +1,25 @@
package generate
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestFilterPodFlags(t *testing.T) {
tests := []struct {
input []string
}{
{[]string{"podman", "pod", "create"}},
{[]string{"podman", "pod", "create", "--name", "foo"}},
{[]string{"podman", "pod", "create", "--pod-id-file", "foo"}},
{[]string{"podman", "run", "--pod", "foo"}},
}
for _, test := range tests {
processed := filterPodFlags(test.input)
assert.NotContains(t, processed, "--pod-id-file")
assert.NotContains(t, processed, "--pod")
}
}

View File

@ -33,14 +33,13 @@ type containerInfo struct {
// PIDFile of the service. Required for forking services. Must point to the // PIDFile of the service. Required for forking services. Must point to the
// PID of the associated conmon process. // PID of the associated conmon process.
PIDFile string PIDFile string
// ContainerIDFile to be used in the unit.
ContainerIDFile string
// GenerateTimestamp, if set the generated unit file has a time stamp. // GenerateTimestamp, if set the generated unit file has a time stamp.
GenerateTimestamp bool GenerateTimestamp bool
// BoundToServices are the services this service binds to. Note that this // BoundToServices are the services this service binds to. Note that this
// service runs after them. // service runs after them.
BoundToServices []string BoundToServices []string
// RequiredServices are services this service requires. Note that this
// service runs before them.
RequiredServices []string
// PodmanVersion for the header. Will be set internally. Will be auto-filled // PodmanVersion for the header. Will be set internally. Will be auto-filled
// if left empty. // if left empty.
PodmanVersion string PodmanVersion string
@ -49,16 +48,23 @@ type containerInfo struct {
Executable string Executable string
// TimeStamp at the time of creating the unit file. Will be set internally. // TimeStamp at the time of creating the unit file. Will be set internally.
TimeStamp string TimeStamp string
// New controls if a new container is created or if an existing one is started.
New bool
// CreateCommand is the full command plus arguments of the process the // CreateCommand is the full command plus arguments of the process the
// container has been created with. // container has been created with.
CreateCommand []string CreateCommand []string
// 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 is generate.EnvVariable and must not be set.
EnvVariable string EnvVariable string
// ExecStartPre of the unit.
ExecStartPre string
// ExecStart of the unit.
ExecStart string
// ExecStop of the unit.
ExecStop string
// ExecStopPost of the unit.
ExecStopPost string
// If not nil, the container is part of the pod. We can use the
// podInfo to extract the relevant data.
pod *podInfo
} }
const containerTemplate = headerTemplate + ` const containerTemplate = headerTemplate + `
@ -68,25 +74,19 @@ RefuseManualStop=yes
BindsTo={{- range $index, $value := .BoundToServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}} BindsTo={{- range $index, $value := .BoundToServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}}
After={{- range $index, $value := .BoundToServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}} After={{- range $index, $value := .BoundToServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}}
{{- end}} {{- end}}
{{- if .RequiredServices}}
Requires={{- range $index, $value := .RequiredServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}}
Before={{- range $index, $value := .RequiredServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}}
{{- end}}
[Service] [Service]
Environment={{.EnvVariable}}=%n Environment={{.EnvVariable}}=%n
Restart={{.RestartPolicy}} Restart={{.RestartPolicy}}
{{- if .New}} {{- if .ExecStartPre}}
ExecStartPre=/usr/bin/rm -f %t/%n-pid %t/%n-ctr-id ExecStartPre={{.ExecStartPre}}
ExecStart={{.RunCommand}}
ExecStop={{.Executable}} stop --ignore --cidfile %t/%n-ctr-id {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}}
ExecStopPost={{.Executable}} rm --ignore -f --cidfile %t/%n-ctr-id
PIDFile=%t/%n-pid
{{- else}}
ExecStart={{.Executable}} start {{.ContainerNameOrID}}
ExecStop={{.Executable}} stop {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}} {{.ContainerNameOrID}}
PIDFile={{.PIDFile}}
{{- end}} {{- end}}
ExecStart={{.ExecStart}}
ExecStop={{.ExecStop}}
{{- if .ExecStopPost}}
ExecStopPost={{.ExecStopPost}}
{{- end}}
PIDFile={{.PIDFile}}
KillMode=none KillMode=none
Type=forking Type=forking
@ -101,114 +101,7 @@ func ContainerUnit(ctr *libpod.Container, options entities.GenerateSystemdOption
if err != nil { if err != nil {
return "", err return "", err
} }
return createContainerSystemdUnit(info, options) return executeContainerTemplate(info, options)
}
// createContainerSystemdUnit creates a systemd unit file for a container.
func createContainerSystemdUnit(info *containerInfo, options entities.GenerateSystemdOptions) (string, error) {
if err := validateRestartPolicy(info.RestartPolicy); err != nil {
return "", err
}
// Make sure the executable is set.
if info.Executable == "" {
executable, err := os.Executable()
if err != nil {
executable = "/usr/bin/podman"
logrus.Warnf("Could not obtain podman executable location, using default %s", executable)
}
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
// *must* manually check the generated files. A container might have
// been created via a Python script, which would certainly yield an
// invalid `info.CreateCommand`. Hence, we're doing a best effort unit
// generation and don't try aiming at completeness.
if options.New {
// The create command must at least have three arguments:
// /usr/bin/podman run $IMAGE
index := 2
if info.CreateCommand[1] == "container" {
index = 3
}
if len(info.CreateCommand) < index+1 {
return "", errors.Errorf("container's create command is too short or invalid: %v", info.CreateCommand)
}
// We're hard-coding the first five arguments and append the
// CreateCommand with a stripped command and subcomand.
command := []string{
info.Executable,
"run",
"--conmon-pidfile", "%t/%n-pid",
"--cidfile", "%t/%n-ctr-id",
"--cgroups=no-conmon",
}
// Enforce detaching
//
// since we use systemd `Type=forking` service
// @see https://www.freedesktop.org/software/systemd/man/systemd.service.html#Type=
// when we generated systemd service file with the --new param,
// `ExecStart` will have `/usr/bin/podman run ...`
// if `info.CreateCommand` has no `-d` or `--detach` param,
// podman will run the container in default attached mode,
// as a result, `systemd start` will wait the `podman run` command exit until failed with timeout error.
hasDetachParam := false
for _, p := range info.CreateCommand[index:] {
if p == "--detach" || p == "-d" {
hasDetachParam = true
}
}
if !hasDetachParam {
command = append(command, "-d")
}
command = append(command, info.CreateCommand[index:]...)
info.RunCommand = strings.Join(command, " ")
info.New = true
}
if info.PodmanVersion == "" {
info.PodmanVersion = version.Version
}
if info.GenerateTimestamp {
info.TimeStamp = fmt.Sprintf("%v", time.Now().Format(time.UnixDate))
}
// Sort the slices to assure a deterministic output.
sort.Strings(info.RequiredServices)
sort.Strings(info.BoundToServices)
// Generate the template and compile it.
templ, err := template.New("systemd_service_file").Parse(containerTemplate)
if err != nil {
return "", errors.Wrap(err, "error parsing systemd service template")
}
var buf bytes.Buffer
if err := templ.Execute(&buf, info); err != nil {
return "", err
}
if !options.Files {
return buf.String(), nil
}
buf.WriteByte('\n')
cwd, err := os.Getwd()
if err != nil {
return "", errors.Wrap(err, "error getting current working directory")
}
path := filepath.Join(cwd, fmt.Sprintf("%s.service", info.ServiceName))
if err := ioutil.WriteFile(path, buf.Bytes(), 0644); err != nil {
return "", errors.Wrap(err, "error generating systemd unit")
}
return path, nil
} }
func generateContainerInfo(ctr *libpod.Container, options entities.GenerateSystemdOptions) (*containerInfo, error) { func generateContainerInfo(ctr *libpod.Container, options entities.GenerateSystemdOptions) (*containerInfo, error) {
@ -241,6 +134,7 @@ func generateContainerInfo(ctr *libpod.Container, options entities.GenerateSyste
GenerateTimestamp: true, GenerateTimestamp: true,
CreateCommand: createCommand, CreateCommand: createCommand,
} }
return &info, nil return &info, nil
} }
@ -254,3 +148,142 @@ func containerServiceName(ctr *libpod.Container, options entities.GenerateSystem
serviceName := fmt.Sprintf("%s%s%s", options.ContainerPrefix, options.Separator, nameOrID) serviceName := fmt.Sprintf("%s%s%s", options.ContainerPrefix, options.Separator, nameOrID)
return nameOrID, serviceName return nameOrID, serviceName
} }
// executeContainerTemplate executes the container template on the specified
// containerInfo. Note that the containerInfo is also post processed and
// completed, which allows for an easier unit testing.
func executeContainerTemplate(info *containerInfo, options entities.GenerateSystemdOptions) (string, error) {
if err := validateRestartPolicy(info.RestartPolicy); err != nil {
return "", err
}
// Make sure the executable is set.
if info.Executable == "" {
executable, err := os.Executable()
if err != nil {
executable = "/usr/bin/podman"
logrus.Warnf("Could not obtain podman executable location, using default %s", executable)
}
info.Executable = executable
}
info.EnvVariable = EnvVariable
info.ExecStart = "{{.Executable}} start {{.ContainerNameOrID}}"
info.ExecStop = "{{.Executable}} stop {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}} {{.ContainerNameOrID}}"
// Assemble the ExecStart command when creating a new container.
//
// Note that we cannot catch all corner cases here such that users
// *must* manually check the generated files. A container might have
// been created via a Python script, which would certainly yield an
// invalid `info.CreateCommand`. Hence, we're doing a best effort unit
// generation and don't try aiming at completeness.
if options.New {
info.PIDFile = "%t/" + info.ServiceName + ".pid"
info.ContainerIDFile = "%t/" + info.ServiceName + ".ctr-id"
// The create command must at least have three arguments:
// /usr/bin/podman run $IMAGE
index := 2
if info.CreateCommand[1] == "container" {
index = 3
}
if len(info.CreateCommand) < index+1 {
return "", errors.Errorf("container's create command is too short or invalid: %v", info.CreateCommand)
}
// We're hard-coding the first five arguments and append the
// CreateCommand with a stripped command and subcomand.
startCommand := []string{
info.Executable,
"run",
"--conmon-pidfile", "{{.PIDFile}}",
"--cidfile", "{{.ContainerIDFile}}",
"--cgroups=no-conmon",
}
// If the container is in a pod, make sure that the
// --pod-id-file is set correctly.
if info.pod != nil {
podFlags := []string{"--pod-id-file", info.pod.PodIDFile}
startCommand = append(startCommand, podFlags...)
info.CreateCommand = filterPodFlags(info.CreateCommand)
}
// Enforce detaching
//
// since we use systemd `Type=forking` service
// @see https://www.freedesktop.org/software/systemd/man/systemd.service.html#Type=
// when we generated systemd service file with the --new param,
// `ExecStart` will have `/usr/bin/podman run ...`
// if `info.CreateCommand` has no `-d` or `--detach` param,
// podman will run the container in default attached mode,
// as a result, `systemd start` will wait the `podman run` command exit until failed with timeout error.
hasDetachParam := false
for _, p := range info.CreateCommand[index:] {
if p == "--detach" || p == "-d" {
hasDetachParam = true
}
}
if !hasDetachParam {
startCommand = append(startCommand, "-d")
}
startCommand = append(startCommand, info.CreateCommand[index:]...)
info.ExecStartPre = "/usr/bin/rm -f {{.PIDFile}} {{.ContainerIDFile}}"
info.ExecStart = strings.Join(startCommand, " ")
info.ExecStop = "{{.Executable}} stop --ignore --cidfile {{.ContainerIDFile}} {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}}"
info.ExecStopPost = "{{.Executable}} rm --ignore -f --cidfile {{.ContainerIDFile}}"
}
if info.PodmanVersion == "" {
info.PodmanVersion = version.Version
}
if info.GenerateTimestamp {
info.TimeStamp = fmt.Sprintf("%v", time.Now().Format(time.UnixDate))
}
// Sort the slices to assure a deterministic output.
sort.Strings(info.BoundToServices)
// Generate the template and compile it.
//
// Note that we need a two-step generation process to allow for fields
// embedding other fields. This way we can replace `A -> B -> C` and
// make the code easier to maintain at the cost of a slightly slower
// generation. That's especially needed for embedding the PID and ID
// files in other fields which will eventually get replaced in the 2nd
// template execution.
templ, err := template.New("container_template").Parse(containerTemplate)
if err != nil {
return "", errors.Wrap(err, "error parsing systemd service template")
}
var buf bytes.Buffer
if err := templ.Execute(&buf, info); err != nil {
return "", err
}
// Now parse the generated template (i.e., buf) and execute it.
templ, err = template.New("container_template").Parse(buf.String())
if err != nil {
return "", errors.Wrap(err, "error parsing systemd service template")
}
buf = bytes.Buffer{}
if err := templ.Execute(&buf, info); err != nil {
return "", err
}
if !options.Files {
return buf.String(), nil
}
buf.WriteByte('\n')
cwd, err := os.Getwd()
if err != nil {
return "", errors.Wrap(err, "error getting current working directory")
}
path := filepath.Join(cwd, fmt.Sprintf("%s.service", info.ServiceName))
if err := ioutil.WriteFile(path, buf.Bytes(), 0644); err != nil {
return "", errors.Wrap(err, "error generating systemd unit")
}
return path, nil
}

View File

@ -6,7 +6,7 @@ import (
"github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/domain/entities"
) )
func TestValidateRestartPolicy(t *testing.T) { func TestValidateRestartPolicyContainer(t *testing.T) {
type containerInfo struct { type containerInfo struct {
restart string restart string
} }
@ -100,29 +100,6 @@ PIDFile=/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635
KillMode=none KillMode=none
Type=forking Type=forking
[Install]
WantedBy=multi-user.target default.target`
podGoodName := `# pod-123abc.service
# autogenerated by Podman CI
[Unit]
Description=Podman pod-123abc.service
Documentation=man:podman-generate-systemd(1)
Wants=network.target
After=network-online.target
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
PIDFile=/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid
KillMode=none
Type=forking
[Install] [Install]
WantedBy=multi-user.target default.target` WantedBy=multi-user.target default.target`
@ -138,17 +115,39 @@ After=network-online.target
[Service] [Service]
Environment=PODMAN_SYSTEMD_UNIT=%n Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always Restart=always
ExecStartPre=/usr/bin/rm -f %t/%n-pid %t/%n-ctr-id ExecStartPre=/usr/bin/rm -f %t/jadda-jadda.pid %t/jadda-jadda.ctr-id
ExecStart=/usr/bin/podman run --conmon-pidfile %t/%n-pid --cidfile %t/%n-ctr-id --cgroups=no-conmon -d --name jadda-jadda --hostname hello-world awesome-image:latest command arg1 ... argN ExecStart=/usr/bin/podman run --conmon-pidfile %t/jadda-jadda.pid --cidfile %t/jadda-jadda.ctr-id --cgroups=no-conmon -d --name jadda-jadda --hostname hello-world awesome-image:latest command arg1 ... argN
ExecStop=/usr/bin/podman stop --ignore --cidfile %t/%n-ctr-id -t 42 ExecStop=/usr/bin/podman stop --ignore --cidfile %t/jadda-jadda.ctr-id -t 42
ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/%n-ctr-id ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/jadda-jadda.ctr-id
PIDFile=%t/%n-pid PIDFile=%t/jadda-jadda.pid
KillMode=none KillMode=none
Type=forking Type=forking
[Install] [Install]
WantedBy=multi-user.target default.target` WantedBy=multi-user.target default.target`
goodNameNewWithPodFile := `# jadda-jadda.service
# autogenerated by Podman CI
[Unit]
Description=Podman jadda-jadda.service
Documentation=man:podman-generate-systemd(1)
Wants=network.target
After=network-online.target
[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
ExecStartPre=/usr/bin/rm -f %t/jadda-jadda.pid %t/jadda-jadda.ctr-id
ExecStart=/usr/bin/podman run --conmon-pidfile %t/jadda-jadda.pid --cidfile %t/jadda-jadda.ctr-id --cgroups=no-conmon --pod-id-file /tmp/pod-foobar.pod-id-file -d --name jadda-jadda --hostname hello-world awesome-image:latest command arg1 ... argN
ExecStop=/usr/bin/podman stop --ignore --cidfile %t/jadda-jadda.ctr-id -t 42
ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/jadda-jadda.ctr-id
PIDFile=%t/jadda-jadda.pid
KillMode=none
Type=forking
[Install]
WantedBy=multi-user.target default.target`
goodNameNewDetach := `# jadda-jadda.service goodNameNewDetach := `# jadda-jadda.service
# autogenerated by Podman CI # autogenerated by Podman CI
@ -161,11 +160,11 @@ After=network-online.target
[Service] [Service]
Environment=PODMAN_SYSTEMD_UNIT=%n Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always Restart=always
ExecStartPre=/usr/bin/rm -f %t/%n-pid %t/%n-ctr-id ExecStartPre=/usr/bin/rm -f %t/jadda-jadda.pid %t/jadda-jadda.ctr-id
ExecStart=/usr/bin/podman run --conmon-pidfile %t/%n-pid --cidfile %t/%n-ctr-id --cgroups=no-conmon --detach --name jadda-jadda --hostname hello-world awesome-image:latest command arg1 ... argN ExecStart=/usr/bin/podman run --conmon-pidfile %t/jadda-jadda.pid --cidfile %t/jadda-jadda.ctr-id --cgroups=no-conmon --detach --name jadda-jadda --hostname hello-world awesome-image:latest command arg1 ... argN
ExecStop=/usr/bin/podman stop --ignore --cidfile %t/%n-ctr-id -t 42 ExecStop=/usr/bin/podman stop --ignore --cidfile %t/jadda-jadda.ctr-id -t 42
ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/%n-ctr-id ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/jadda-jadda.ctr-id
PIDFile=%t/%n-pid PIDFile=%t/jadda-jadda.pid
KillMode=none KillMode=none
Type=forking Type=forking
@ -184,11 +183,11 @@ After=network-online.target
[Service] [Service]
Environment=PODMAN_SYSTEMD_UNIT=%n Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always Restart=always
ExecStartPre=/usr/bin/rm -f %t/%n-pid %t/%n-ctr-id ExecStartPre=/usr/bin/rm -f %t/container-639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401.pid %t/container-639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401.ctr-id
ExecStart=/usr/bin/podman run --conmon-pidfile %t/%n-pid --cidfile %t/%n-ctr-id --cgroups=no-conmon -d awesome-image:latest ExecStart=/usr/bin/podman run --conmon-pidfile %t/container-639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401.pid --cidfile %t/container-639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401.ctr-id --cgroups=no-conmon -d awesome-image:latest
ExecStop=/usr/bin/podman stop --ignore --cidfile %t/%n-ctr-id -t 10 ExecStop=/usr/bin/podman stop --ignore --cidfile %t/container-639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401.ctr-id -t 10
ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/%n-ctr-id ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/container-639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401.ctr-id
PIDFile=%t/%n-pid PIDFile=%t/container-639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401.pid
KillMode=none KillMode=none
Type=forking Type=forking
@ -199,6 +198,7 @@ WantedBy=multi-user.target default.target`
name string name string
info containerInfo info containerInfo
want string want string
new bool
wantErr bool wantErr bool
}{ }{
@ -211,9 +211,11 @@ WantedBy=multi-user.target default.target`
PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid",
StopTimeout: 10, StopTimeout: 10,
PodmanVersion: "CI", PodmanVersion: "CI",
EnvVariable: EnvVariable,
}, },
goodID, goodID,
false, false,
false,
}, },
{"good with name", {"good with name",
containerInfo{ containerInfo{
@ -224,9 +226,11 @@ WantedBy=multi-user.target default.target`
PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid",
StopTimeout: 10, StopTimeout: 10,
PodmanVersion: "CI", PodmanVersion: "CI",
EnvVariable: EnvVariable,
}, },
goodName, goodName,
false, false,
false,
}, },
{"good with name and bound to", {"good with name and bound to",
containerInfo{ containerInfo{
@ -238,22 +242,10 @@ WantedBy=multi-user.target default.target`
StopTimeout: 10, StopTimeout: 10,
PodmanVersion: "CI", PodmanVersion: "CI",
BoundToServices: []string{"pod", "a", "b", "c"}, BoundToServices: []string{"pod", "a", "b", "c"},
EnvVariable: EnvVariable,
}, },
goodNameBoundTo, goodNameBoundTo,
false, false,
},
{"pod",
containerInfo{
Executable: "/usr/bin/podman",
ServiceName: "pod-123abc",
ContainerNameOrID: "jadda-jadda-infra",
RestartPolicy: "always",
PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid",
StopTimeout: 10,
PodmanVersion: "CI",
RequiredServices: []string{"container-1", "container-2"},
},
podGoodName,
false, false,
}, },
{"bad restart policy", {"bad restart policy",
@ -264,8 +256,10 @@ WantedBy=multi-user.target default.target`
PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid",
StopTimeout: 10, StopTimeout: 10,
PodmanVersion: "CI", PodmanVersion: "CI",
EnvVariable: EnvVariable,
}, },
"", "",
false,
true, true,
}, },
{"good with name and generic", {"good with name and generic",
@ -277,10 +271,11 @@ WantedBy=multi-user.target default.target`
PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid",
StopTimeout: 42, StopTimeout: 42,
PodmanVersion: "CI", PodmanVersion: "CI",
New: true,
CreateCommand: []string{"I'll get stripped", "container", "run", "--name", "jadda-jadda", "--hostname", "hello-world", "awesome-image:latest", "command", "arg1", "...", "argN"}, CreateCommand: []string{"I'll get stripped", "container", "run", "--name", "jadda-jadda", "--hostname", "hello-world", "awesome-image:latest", "command", "arg1", "...", "argN"},
EnvVariable: EnvVariable,
}, },
goodNameNew, goodNameNew,
true,
false, false,
}, },
{"good with explicit short detach param", {"good with explicit short detach param",
@ -292,10 +287,30 @@ WantedBy=multi-user.target default.target`
PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid",
StopTimeout: 42, StopTimeout: 42,
PodmanVersion: "CI", PodmanVersion: "CI",
New: true,
CreateCommand: []string{"I'll get stripped", "container", "run", "-d", "--name", "jadda-jadda", "--hostname", "hello-world", "awesome-image:latest", "command", "arg1", "...", "argN"}, CreateCommand: []string{"I'll get stripped", "container", "run", "-d", "--name", "jadda-jadda", "--hostname", "hello-world", "awesome-image:latest", "command", "arg1", "...", "argN"},
EnvVariable: EnvVariable,
}, },
goodNameNew, goodNameNew,
true,
false,
},
{"good with explicit short detach param and podInfo",
containerInfo{
Executable: "/usr/bin/podman",
ServiceName: "jadda-jadda",
ContainerNameOrID: "jadda-jadda",
RestartPolicy: "always",
PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid",
StopTimeout: 42,
PodmanVersion: "CI",
CreateCommand: []string{"I'll get stripped", "container", "run", "-d", "--name", "jadda-jadda", "--hostname", "hello-world", "awesome-image:latest", "command", "arg1", "...", "argN"},
EnvVariable: EnvVariable,
pod: &podInfo{
PodIDFile: "/tmp/pod-foobar.pod-id-file",
},
},
goodNameNewWithPodFile,
true,
false, false,
}, },
{"good with explicit full detach param", {"good with explicit full detach param",
@ -307,10 +322,11 @@ WantedBy=multi-user.target default.target`
PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid",
StopTimeout: 42, StopTimeout: 42,
PodmanVersion: "CI", PodmanVersion: "CI",
New: true,
CreateCommand: []string{"I'll get stripped", "container", "run", "--detach", "--name", "jadda-jadda", "--hostname", "hello-world", "awesome-image:latest", "command", "arg1", "...", "argN"}, CreateCommand: []string{"I'll get stripped", "container", "run", "--detach", "--name", "jadda-jadda", "--hostname", "hello-world", "awesome-image:latest", "command", "arg1", "...", "argN"},
EnvVariable: EnvVariable,
}, },
goodNameNewDetach, goodNameNewDetach,
true,
false, false,
}, },
{"good with id and no param", {"good with id and no param",
@ -322,10 +338,11 @@ WantedBy=multi-user.target default.target`
PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid",
StopTimeout: 10, StopTimeout: 10,
PodmanVersion: "CI", PodmanVersion: "CI",
New: true,
CreateCommand: []string{"I'll get stripped", "container", "run", "awesome-image:latest"}, CreateCommand: []string{"I'll get stripped", "container", "run", "awesome-image:latest"},
EnvVariable: EnvVariable,
}, },
goodIDNew, goodIDNew,
true,
false, false,
}, },
} }
@ -334,9 +351,9 @@ WantedBy=multi-user.target default.target`
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
opts := entities.GenerateSystemdOptions{ opts := entities.GenerateSystemdOptions{
Files: false, Files: false,
New: test.info.New, New: test.new,
} }
got, err := createContainerSystemdUnit(&test.info, opts) got, err := executeContainerTemplate(&test.info, opts)
if (err != nil) != test.wantErr { if (err != nil) != test.wantErr {
t.Errorf("CreateContainerSystemdUnit() error = \n%v, wantErr \n%v", err, test.wantErr) t.Errorf("CreateContainerSystemdUnit() error = \n%v, wantErr \n%v", err, test.wantErr)
return return

View File

@ -1,22 +1,101 @@
package generate package generate
import ( import (
"bytes"
"fmt" "fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings" "strings"
"text/template"
"time"
"github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod"
"github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/domain/entities"
"github.com/containers/libpod/version"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus"
) )
// podInfo contains data required for generating a pod's systemd
// unit file.
type podInfo struct {
// ServiceName of the systemd service.
ServiceName string
// Name or ID of the infra container.
InfraNameOrID string
// StopTimeout sets the timeout Podman waits before killing the container
// during service stop.
StopTimeout uint
// RestartPolicy of the systemd unit (e.g., no, on-failure, always).
RestartPolicy string
// PIDFile of the service. Required for forking services. Must point to the
// PID of the associated conmon process.
PIDFile string
// PodIDFile of the unit.
PodIDFile string
// GenerateTimestamp, if set the generated unit file has a time stamp.
GenerateTimestamp bool
// RequiredServices are services this service requires. Note that this
// service runs before them.
RequiredServices []string
// PodmanVersion for the header. Will be set internally. Will be auto-filled
// if left empty.
PodmanVersion string
// Executable is the path to the podman executable. Will be auto-filled if
// left empty.
Executable string
// TimeStamp at the time of creating the unit file. Will be set internally.
TimeStamp string
// CreateCommand is the full command plus arguments of the process the
// container has been created with.
CreateCommand []string
// PodCreateCommand - a post-processed variant of CreateCommand to use
// when creating the pod.
PodCreateCommand string
// EnvVariable is generate.EnvVariable and must not be set.
EnvVariable string
// ExecStartPre1 of the unit.
ExecStartPre1 string
// ExecStartPre2 of the unit.
ExecStartPre2 string
// ExecStart of the unit.
ExecStart string
// ExecStop of the unit.
ExecStop string
// ExecStopPost of the unit.
ExecStopPost 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}}
[Service]
Environment={{.EnvVariable}}=%n
Restart={{.RestartPolicy}}
{{- if .ExecStartPre1}}
ExecStartPre={{.ExecStartPre1}}
{{- end}}
{{- if .ExecStartPre2}}
ExecStartPre={{.ExecStartPre2}}
{{- end}}
ExecStart={{.ExecStart}}
ExecStop={{.ExecStop}}
{{- if .ExecStopPost}}
ExecStopPost={{.ExecStopPost}}
{{- end}}
PIDFile={{.PIDFile}}
KillMode=none
Type=forking
[Install]
WantedBy=multi-user.target default.target`
// PodUnits generates systemd units for the specified pod and its containers. // PodUnits generates systemd units for the specified pod and its containers.
// Based on the options, the return value might be the content of all units or // Based on the options, the return value might be the content of all units or
// the files they been written to. // the files they been written to.
func PodUnits(pod *libpod.Pod, options entities.GenerateSystemdOptions) (string, error) { func PodUnits(pod *libpod.Pod, options entities.GenerateSystemdOptions) (string, error) {
if options.New {
return "", errors.New("--new is not supported for pods")
}
// Error out if the pod has no infra container, which we require to be the // Error out if the pod has no infra container, which we require to be the
// main service. // main service.
if !pod.HasInfraContainer() { if !pod.HasInfraContainer() {
@ -48,7 +127,7 @@ func PodUnits(pod *libpod.Pod, options entities.GenerateSystemdOptions) (string,
// Traverse the dependency graph and create systemdgen.containerInfo's for // Traverse the dependency graph and create systemdgen.containerInfo's for
// each container. // each container.
containerInfos := []*containerInfo{podInfo} containerInfos := []*containerInfo{}
for ctr, dependencies := range graph.DependencyMap() { for ctr, dependencies := range graph.DependencyMap() {
// Skip the infra container as we already generated it. // Skip the infra container as we already generated it.
if ctr.ID() == infraID { if ctr.ID() == infraID {
@ -74,11 +153,15 @@ func PodUnits(pod *libpod.Pod, options entities.GenerateSystemdOptions) (string,
// Now generate the systemd service for all containers. // Now generate the systemd service for all containers.
builder := strings.Builder{} builder := strings.Builder{}
for i, info := range containerInfos { out, err := executePodTemplate(podInfo, options)
if i > 0 { if err != nil {
builder.WriteByte('\n') return "", err
} }
out, err := createContainerSystemdUnit(info, options) builder.WriteString(out)
for _, info := range containerInfos {
info.pod = podInfo
builder.WriteByte('\n')
out, err := executeContainerTemplate(info, options)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -88,7 +171,7 @@ func PodUnits(pod *libpod.Pod, options entities.GenerateSystemdOptions) (string,
return builder.String(), nil return builder.String(), nil
} }
func generatePodInfo(pod *libpod.Pod, options entities.GenerateSystemdOptions) (*containerInfo, error) { func generatePodInfo(pod *libpod.Pod, options entities.GenerateSystemdOptions) (*podInfo, error) {
// Generate a systemdgen.containerInfo for the infra container. This // Generate a systemdgen.containerInfo for the infra container. This
// containerInfo acts as the main service of the pod. // containerInfo acts as the main service of the pod.
infraCtr, err := pod.InfraContainer() infraCtr, err := pod.InfraContainer()
@ -107,7 +190,10 @@ func generatePodInfo(pod *libpod.Pod, options entities.GenerateSystemdOptions) (
return nil, errors.Errorf("conmon PID file path is empty, try to recreate the container with --conmon-pidfile flag") return nil, errors.Errorf("conmon PID file path is empty, try to recreate the container with --conmon-pidfile flag")
} }
createCommand := []string{} createCommand := pod.CreateCommand()
if options.New && len(createCommand) == 0 {
return nil, errors.Errorf("cannot use --new on pod %q: no create command found", pod.ID())
}
nameOrID := pod.ID() nameOrID := pod.ID()
ctrNameOrID := infraCtr.ID() ctrNameOrID := infraCtr.ID()
@ -117,9 +203,9 @@ func generatePodInfo(pod *libpod.Pod, options entities.GenerateSystemdOptions) (
} }
serviceName := fmt.Sprintf("%s%s%s", options.PodPrefix, options.Separator, nameOrID) serviceName := fmt.Sprintf("%s%s%s", options.PodPrefix, options.Separator, nameOrID)
info := containerInfo{ info := podInfo{
ServiceName: serviceName, ServiceName: serviceName,
ContainerNameOrID: ctrNameOrID, InfraNameOrID: ctrNameOrID,
RestartPolicy: options.RestartPolicy, RestartPolicy: options.RestartPolicy,
PIDFile: conmonPidFile, PIDFile: conmonPidFile,
StopTimeout: timeout, StopTimeout: timeout,
@ -128,3 +214,128 @@ func generatePodInfo(pod *libpod.Pod, options entities.GenerateSystemdOptions) (
} }
return &info, nil return &info, nil
} }
// executePodTemplate executes the pod template on the specified podInfo. Note
// that the podInfo is also post processed and completed, which allows for an
// easier unit testing.
func executePodTemplate(info *podInfo, options entities.GenerateSystemdOptions) (string, error) {
if err := validateRestartPolicy(info.RestartPolicy); err != nil {
return "", err
}
// Make sure the executable is set.
if info.Executable == "" {
executable, err := os.Executable()
if err != nil {
executable = "/usr/bin/podman"
logrus.Warnf("Could not obtain podman executable location, using default %s", executable)
}
info.Executable = executable
}
info.EnvVariable = EnvVariable
info.ExecStart = "{{.Executable}} start {{.InfraNameOrID}}"
info.ExecStop = "{{.Executable}} stop {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}} {{.InfraNameOrID}}"
// Assemble the ExecStart command when creating a new pod.
//
// Note that we cannot catch all corner cases here such that users
// *must* manually check the generated files. A pod might have been
// created via a Python script, which would certainly yield an invalid
// `info.CreateCommand`. Hence, we're doing a best effort unit
// generation and don't try aiming at completeness.
if options.New {
info.PIDFile = "%t/" + info.ServiceName + ".pid"
info.PodIDFile = "%t/" + info.ServiceName + ".pod-id"
podCreateIndex := 0
var podRootArgs, podCreateArgs []string
switch len(info.CreateCommand) {
case 0, 1, 2:
return "", errors.Errorf("pod does not appear to be created via `podman pod create`: %v", info.CreateCommand)
default:
// Make sure that pod was created with `pod create` and
// not something else, such as `run --pod new`.
for i := 1; i < len(info.CreateCommand); i++ {
if info.CreateCommand[i-1] == "pod" && info.CreateCommand[i] == "create" {
podCreateIndex = i
break
}
}
if podCreateIndex == 0 {
return "", errors.Errorf("pod does not appear to be created via `podman pod create`: %v", info.CreateCommand)
}
podRootArgs = info.CreateCommand[1 : podCreateIndex-2]
podCreateArgs = filterPodFlags(info.CreateCommand[podCreateIndex+1:])
}
// We're hard-coding the first five arguments and append the
// CreateCommand with a stripped command and subcomand.
startCommand := []string{info.Executable}
startCommand = append(startCommand, podRootArgs...)
startCommand = append(startCommand,
[]string{"pod", "create",
"--infra-conmon-pidfile", "{{.PIDFile}}",
"--pod-id-file", "{{.PodIDFile}}"}...)
startCommand = append(startCommand, podCreateArgs...)
info.ExecStartPre1 = "/usr/bin/rm -f {{.PIDFile}} {{.PodIDFile}}"
info.ExecStartPre2 = strings.Join(startCommand, " ")
info.ExecStart = "{{.Executable}} pod start --pod-id-file {{.PodIDFile}}"
info.ExecStop = "{{.Executable}} pod stop --ignore --pod-id-file {{.PodIDFile}} {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}}"
info.ExecStopPost = "{{.Executable}} pod rm --ignore -f --pod-id-file {{.PodIDFile}}"
}
if info.PodmanVersion == "" {
info.PodmanVersion = version.Version
}
if info.GenerateTimestamp {
info.TimeStamp = fmt.Sprintf("%v", time.Now().Format(time.UnixDate))
}
// Sort the slices to assure a deterministic output.
sort.Strings(info.RequiredServices)
// Generate the template and compile it.
//
// Note that we need a two-step generation process to allow for fields
// embedding other fields. This way we can replace `A -> B -> C` and
// make the code easier to maintain at the cost of a slightly slower
// generation. That's especially needed for embedding the PID and ID
// files in other fields which will eventually get replaced in the 2nd
// template execution.
templ, err := template.New("pod_template").Parse(podTemplate)
if err != nil {
return "", errors.Wrap(err, "error parsing systemd service template")
}
var buf bytes.Buffer
if err := templ.Execute(&buf, info); err != nil {
return "", err
}
// Now parse the generated template (i.e., buf) and execute it.
templ, err = template.New("pod_template").Parse(buf.String())
if err != nil {
return "", errors.Wrap(err, "error parsing systemd service template")
}
buf = bytes.Buffer{}
if err := templ.Execute(&buf, info); err != nil {
return "", err
}
if !options.Files {
return buf.String(), nil
}
buf.WriteByte('\n')
cwd, err := os.Getwd()
if err != nil {
return "", errors.Wrap(err, "error getting current working directory")
}
path := filepath.Join(cwd, fmt.Sprintf("%s.service", info.ServiceName))
if err := ioutil.WriteFile(path, buf.Bytes(), 0644); err != nil {
return "", errors.Wrap(err, "error generating systemd unit")
}
return path, nil
}

View File

@ -0,0 +1,100 @@
package generate
import (
"testing"
"github.com/containers/libpod/pkg/domain/entities"
)
func TestValidateRestartPolicyPod(t *testing.T) {
type podInfo struct {
restart string
}
tests := []struct {
name string
podInfo podInfo
wantErr bool
}{
{"good-on", podInfo{restart: "no"}, false},
{"good-on-success", podInfo{restart: "on-success"}, false},
{"good-on-failure", podInfo{restart: "on-failure"}, false},
{"good-on-abnormal", podInfo{restart: "on-abnormal"}, false},
{"good-on-watchdog", podInfo{restart: "on-watchdog"}, false},
{"good-on-abort", podInfo{restart: "on-abort"}, false},
{"good-always", podInfo{restart: "always"}, false},
{"fail", podInfo{restart: "foobar"}, true},
{"failblank", podInfo{restart: ""}, true},
}
for _, tt := range tests {
test := tt
t.Run(tt.name, func(t *testing.T) {
if err := validateRestartPolicy(test.podInfo.restart); (err != nil) != test.wantErr {
t.Errorf("ValidateRestartPolicy() error = %v, wantErr %v", err, test.wantErr)
}
})
}
}
func TestCreatePodSystemdUnit(t *testing.T) {
podGoodName := `# pod-123abc.service
# autogenerated by Podman CI
[Unit]
Description=Podman pod-123abc.service
Documentation=man:podman-generate-systemd(1)
Wants=network.target
After=network-online.target
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
PIDFile=/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid
KillMode=none
Type=forking
[Install]
WantedBy=multi-user.target default.target`
tests := []struct {
name string
info podInfo
want string
wantErr bool
}{
{"pod",
podInfo{
Executable: "/usr/bin/podman",
ServiceName: "pod-123abc",
InfraNameOrID: "jadda-jadda-infra",
RestartPolicy: "always",
PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid",
StopTimeout: 10,
PodmanVersion: "CI",
RequiredServices: []string{"container-1", "container-2"},
},
podGoodName,
false,
},
}
for _, tt := range tests {
test := tt
t.Run(tt.name, func(t *testing.T) {
opts := entities.GenerateSystemdOptions{
Files: false,
}
got, err := executePodTemplate(&test.info, opts)
if (err != nil) != test.wantErr {
t.Errorf("CreatePodSystemdUnit() error = \n%v, wantErr \n%v", err, test.wantErr)
return
}
if got != test.want {
t.Errorf("CreatePodSystemdUnit() = \n%v\n---------> want\n%v", got, test.want)
}
})
}
}

View File

@ -3,6 +3,7 @@
package integration package integration
import ( import (
"io/ioutil"
"os" "os"
. "github.com/containers/libpod/test/utils" . "github.com/containers/libpod/test/utils"
@ -191,7 +192,7 @@ var _ = Describe("Podman generate systemd", func() {
found, _ := session.GrepString("# container-foo.service") found, _ := session.GrepString("# container-foo.service")
Expect(found).To(BeTrue()) Expect(found).To(BeTrue())
found, _ = session.GrepString("stop --ignore --cidfile %t/%n-ctr-id -t 42") found, _ = session.GrepString("stop --ignore --cidfile %t/container-foo.ctr-id -t 42")
Expect(found).To(BeTrue()) Expect(found).To(BeTrue())
}) })
@ -230,7 +231,7 @@ var _ = Describe("Podman generate systemd", func() {
session := podmanTest.Podman([]string{"generate", "systemd", "--time", "42", "--name", "--new", "foo"}) session := podmanTest.Podman([]string{"generate", "systemd", "--time", "42", "--name", "--new", "foo"})
session.WaitWithDefaultTimeout() session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(125)) Expect(session.ExitCode()).To(Equal(0))
}) })
It("podman generate systemd --container-prefix con", func() { It("podman generate systemd --container-prefix con", func() {
@ -325,4 +326,49 @@ var _ = Describe("Podman generate systemd", func() {
found, _ = session.GrepString("BindsTo=p_foo.service") found, _ = session.GrepString("BindsTo=p_foo.service")
Expect(found).To(BeTrue()) Expect(found).To(BeTrue())
}) })
It("podman generate systemd pod with containers --new", func() {
tmpDir, err := ioutil.TempDir("", "")
Expect(err).To(BeNil())
tmpFile := tmpDir + "podID"
defer os.RemoveAll(tmpDir)
n := podmanTest.Podman([]string{"pod", "create", "--pod-id-file", tmpFile, "--name", "foo"})
n.WaitWithDefaultTimeout()
Expect(n.ExitCode()).To(Equal(0))
n = podmanTest.Podman([]string{"create", "--pod", "foo", "--name", "foo-1", "alpine", "top"})
n.WaitWithDefaultTimeout()
Expect(n.ExitCode()).To(Equal(0))
n = podmanTest.Podman([]string{"create", "--pod", "foo", "--name", "foo-2", "alpine", "top"})
n.WaitWithDefaultTimeout()
Expect(n.ExitCode()).To(Equal(0))
session := podmanTest.Podman([]string{"generate", "systemd", "--new", "--name", "foo"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
// Grepping the output (in addition to unit tests)
found, _ := session.GrepString("# pod-foo.service")
Expect(found).To(BeTrue())
found, _ = session.GrepString("Requires=container-foo-1.service container-foo-2.service")
Expect(found).To(BeTrue())
found, _ = session.GrepString("BindsTo=pod-foo.service")
Expect(found).To(BeTrue())
found, _ = session.GrepString("pod create --infra-conmon-pidfile %t/pod-foo.pid --pod-id-file %t/pod-foo.pod-id --name foo")
Expect(found).To(BeTrue())
found, _ = session.GrepString("ExecStartPre=/usr/bin/rm -f %t/pod-foo.pid %t/pod-foo.pod-id")
Expect(found).To(BeTrue())
found, _ = session.GrepString("pod stop --ignore --pod-id-file %t/pod-foo.pod-id -t 10")
Expect(found).To(BeTrue())
found, _ = session.GrepString("pod rm --ignore -f --pod-id-file %t/pod-foo.pod-id")
Expect(found).To(BeTrue())
})
}) })