aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/source/markdown/podman-generate-systemd.1.md5
-rw-r--r--libpod/pod.go2
-rw-r--r--pkg/systemd/generate/common.go14
-rw-r--r--pkg/systemd/generate/common_test.go25
-rw-r--r--pkg/systemd/generate/containers.go189
-rw-r--r--pkg/systemd/generate/containers_test.go (renamed from pkg/systemd/generate/generate_test.go)105
-rw-r--r--pkg/systemd/generate/pods.go239
-rw-r--r--pkg/systemd/generate/pods_test.go100
-rw-r--r--test/e2e/generate_systemd_test.go50
9 files changed, 586 insertions, 143 deletions
diff --git a/docs/source/markdown/podman-generate-systemd.1.md b/docs/source/markdown/podman-generate-systemd.1.md
index 72031b19b..2facd754c 100644
--- a/docs/source/markdown/podman-generate-systemd.1.md
+++ b/docs/source/markdown/podman-generate-systemd.1.md
@@ -26,10 +26,7 @@ Use the name of the container for the start, stop, and description in the unit f
**--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.
-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.
+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.
**--time**, **-t**=*value*
diff --git a/libpod/pod.go b/libpod/pod.go
index f53290876..bf0d7a397 100644
--- a/libpod/pod.go
+++ b/libpod/pod.go
@@ -260,7 +260,7 @@ func (p *Pod) InfraContainerID() (string, error) {
// InfraContainer returns the infra container.
func (p *Pod) InfraContainer() (*Container, error) {
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()
diff --git a/pkg/systemd/generate/common.go b/pkg/systemd/generate/common.go
index e809b4837..4f995be96 100644
--- a/pkg/systemd/generate/common.go
+++ b/pkg/systemd/generate/common.go
@@ -34,3 +34,17 @@ Documentation=man:podman-generate-systemd(1)
Wants=network.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
+}
diff --git a/pkg/systemd/generate/common_test.go b/pkg/systemd/generate/common_test.go
new file mode 100644
index 000000000..f53bb7828
--- /dev/null
+++ b/pkg/systemd/generate/common_test.go
@@ -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")
+ }
+}
diff --git a/pkg/systemd/generate/containers.go b/pkg/systemd/generate/containers.go
index f316d4452..dced1a3da 100644
--- a/pkg/systemd/generate/containers.go
+++ b/pkg/systemd/generate/containers.go
@@ -33,14 +33,13 @@ type containerInfo struct {
// PIDFile of the service. Required for forking services. Must point to the
// PID of the associated conmon process.
PIDFile string
+ // ContainerIDFile to be used in the unit.
+ ContainerIDFile string
// GenerateTimestamp, if set the generated unit file has a time stamp.
GenerateTimestamp bool
// BoundToServices are the services this service binds to. Note that this
// service runs after them.
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
// if left empty.
PodmanVersion string
@@ -49,16 +48,23 @@ type containerInfo struct {
Executable string
// TimeStamp at the time of creating the unit file. Will be set internally.
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
// container has been created with.
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 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 + `
@@ -68,25 +74,19 @@ RefuseManualStop=yes
BindsTo={{- range $index, $value := .BoundToServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}}
After={{- range $index, $value := .BoundToServices -}}{{if $index}} {{end}}{{ $value }}.service{{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]
Environment={{.EnvVariable}}=%n
Restart={{.RestartPolicy}}
-{{- if .New}}
-ExecStartPre=/usr/bin/rm -f %t/%n-pid %t/%n-ctr-id
-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}}
+{{- if .ExecStartPre}}
+ExecStartPre={{.ExecStartPre}}
+{{- end}}
+ExecStart={{.ExecStart}}
+ExecStop={{.ExecStop}}
+{{- if .ExecStopPost}}
+ExecStopPost={{.ExecStopPost}}
{{- end}}
+PIDFile={{.PIDFile}}
KillMode=none
Type=forking
@@ -101,11 +101,58 @@ func ContainerUnit(ctr *libpod.Container, options entities.GenerateSystemdOption
if err != nil {
return "", err
}
- return createContainerSystemdUnit(info, options)
+ return executeContainerTemplate(info, options)
+}
+
+func generateContainerInfo(ctr *libpod.Container, options entities.GenerateSystemdOptions) (*containerInfo, error) {
+ timeout := ctr.StopTimeout()
+ if options.StopTimeout != nil {
+ timeout = *options.StopTimeout
+ }
+
+ config := ctr.Config()
+ conmonPidFile := config.ConmonPidFile
+ if conmonPidFile == "" {
+ return nil, errors.Errorf("conmon PID file path is empty, try to recreate the container with --conmon-pidfile flag")
+ }
+
+ createCommand := []string{}
+ if config.CreateCommand != nil {
+ createCommand = config.CreateCommand
+ } else if options.New {
+ return nil, errors.Errorf("cannot use --new on container %q: no create command found", ctr.ID())
+ }
+
+ nameOrID, serviceName := containerServiceName(ctr, options)
+
+ info := containerInfo{
+ ServiceName: serviceName,
+ ContainerNameOrID: nameOrID,
+ RestartPolicy: options.RestartPolicy,
+ PIDFile: conmonPidFile,
+ StopTimeout: timeout,
+ GenerateTimestamp: true,
+ CreateCommand: createCommand,
+ }
+
+ return &info, nil
+}
+
+// containerServiceName returns the nameOrID and the service name of the
+// container.
+func containerServiceName(ctr *libpod.Container, options entities.GenerateSystemdOptions) (string, string) {
+ nameOrID := ctr.ID()
+ if options.Name {
+ nameOrID = ctr.Name()
+ }
+ serviceName := fmt.Sprintf("%s%s%s", options.ContainerPrefix, options.Separator, nameOrID)
+ return nameOrID, serviceName
}
-// createContainerSystemdUnit creates a systemd unit file for a container.
-func createContainerSystemdUnit(info *containerInfo, options entities.GenerateSystemdOptions) (string, error) {
+// 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
}
@@ -121,6 +168,8 @@ func createContainerSystemdUnit(info *containerInfo, options entities.GenerateSy
}
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.
//
@@ -130,6 +179,8 @@ func createContainerSystemdUnit(info *containerInfo, options entities.GenerateSy
// 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
@@ -141,13 +192,20 @@ func createContainerSystemdUnit(info *containerInfo, options entities.GenerateSy
}
// We're hard-coding the first five arguments and append the
// CreateCommand with a stripped command and subcomand.
- command := []string{
+ startCommand := []string{
info.Executable,
"run",
- "--conmon-pidfile", "%t/%n-pid",
- "--cidfile", "%t/%n-ctr-id",
+ "--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
//
@@ -165,12 +223,14 @@ func createContainerSystemdUnit(info *containerInfo, options entities.GenerateSy
}
}
if !hasDetachParam {
- command = append(command, "-d")
+ startCommand = append(startCommand, "-d")
}
+ startCommand = append(startCommand, info.CreateCommand[index:]...)
- command = append(command, info.CreateCommand[index:]...)
- info.RunCommand = strings.Join(command, " ")
- info.New = true
+ 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 == "" {
@@ -181,11 +241,17 @@ func createContainerSystemdUnit(info *containerInfo, options entities.GenerateSy
}
// 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)
+ //
+ // 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")
}
@@ -195,6 +261,17 @@ func createContainerSystemdUnit(info *containerInfo, options entities.GenerateSy
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
}
@@ -210,47 +287,3 @@ func createContainerSystemdUnit(info *containerInfo, options entities.GenerateSy
}
return path, nil
}
-
-func generateContainerInfo(ctr *libpod.Container, options entities.GenerateSystemdOptions) (*containerInfo, error) {
- timeout := ctr.StopTimeout()
- if options.StopTimeout != nil {
- timeout = *options.StopTimeout
- }
-
- config := ctr.Config()
- conmonPidFile := config.ConmonPidFile
- if conmonPidFile == "" {
- return nil, errors.Errorf("conmon PID file path is empty, try to recreate the container with --conmon-pidfile flag")
- }
-
- createCommand := []string{}
- if config.CreateCommand != nil {
- createCommand = config.CreateCommand
- } else if options.New {
- return nil, errors.Errorf("cannot use --new on container %q: no create command found", ctr.ID())
- }
-
- nameOrID, serviceName := containerServiceName(ctr, options)
-
- info := containerInfo{
- ServiceName: serviceName,
- ContainerNameOrID: nameOrID,
- RestartPolicy: options.RestartPolicy,
- PIDFile: conmonPidFile,
- StopTimeout: timeout,
- GenerateTimestamp: true,
- CreateCommand: createCommand,
- }
- return &info, nil
-}
-
-// containerServiceName returns the nameOrID and the service name of the
-// container.
-func containerServiceName(ctr *libpod.Container, options entities.GenerateSystemdOptions) (string, string) {
- nameOrID := ctr.ID()
- if options.Name {
- nameOrID = ctr.Name()
- }
- serviceName := fmt.Sprintf("%s%s%s", options.ContainerPrefix, options.Separator, nameOrID)
- return nameOrID, serviceName
-}
diff --git a/pkg/systemd/generate/generate_test.go b/pkg/systemd/generate/containers_test.go
index 11cabb463..8365ecd7a 100644
--- a/pkg/systemd/generate/generate_test.go
+++ b/pkg/systemd/generate/containers_test.go
@@ -6,7 +6,7 @@ import (
"github.com/containers/libpod/pkg/domain/entities"
)
-func TestValidateRestartPolicy(t *testing.T) {
+func TestValidateRestartPolicyContainer(t *testing.T) {
type containerInfo struct {
restart string
}
@@ -103,30 +103,30 @@ Type=forking
[Install]
WantedBy=multi-user.target default.target`
- podGoodName := `# pod-123abc.service
+ goodNameNew := `# jadda-jadda.service
# autogenerated by Podman CI
[Unit]
-Description=Podman pod-123abc.service
+Description=Podman jadda-jadda.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
+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 -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`
- goodNameNew := `# jadda-jadda.service
+ goodNameNewWithPodFile := `# jadda-jadda.service
# autogenerated by Podman CI
[Unit]
@@ -138,17 +138,16 @@ After=network-online.target
[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
-ExecStartPre=/usr/bin/rm -f %t/%n-pid %t/%n-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
-ExecStop=/usr/bin/podman stop --ignore --cidfile %t/%n-ctr-id -t 42
-ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/%n-ctr-id
-PIDFile=%t/%n-pid
+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
# autogenerated by Podman CI
@@ -161,11 +160,11 @@ After=network-online.target
[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
-ExecStartPre=/usr/bin/rm -f %t/%n-pid %t/%n-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
-ExecStop=/usr/bin/podman stop --ignore --cidfile %t/%n-ctr-id -t 42
-ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/%n-ctr-id
-PIDFile=%t/%n-pid
+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 --detach --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
@@ -184,11 +183,11 @@ After=network-online.target
[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
-ExecStartPre=/usr/bin/rm -f %t/%n-pid %t/%n-ctr-id
-ExecStart=/usr/bin/podman run --conmon-pidfile %t/%n-pid --cidfile %t/%n-ctr-id --cgroups=no-conmon -d awesome-image:latest
-ExecStop=/usr/bin/podman stop --ignore --cidfile %t/%n-ctr-id -t 10
-ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/%n-ctr-id
-PIDFile=%t/%n-pid
+ExecStartPre=/usr/bin/rm -f %t/container-639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401.pid %t/container-639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401.ctr-id
+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/container-639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401.ctr-id -t 10
+ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/container-639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401.ctr-id
+PIDFile=%t/container-639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401.pid
KillMode=none
Type=forking
@@ -199,6 +198,7 @@ WantedBy=multi-user.target default.target`
name string
info containerInfo
want string
+ new bool
wantErr bool
}{
@@ -211,9 +211,11 @@ WantedBy=multi-user.target default.target`
PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid",
StopTimeout: 10,
PodmanVersion: "CI",
+ EnvVariable: EnvVariable,
},
goodID,
false,
+ false,
},
{"good with name",
containerInfo{
@@ -224,9 +226,11 @@ WantedBy=multi-user.target default.target`
PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid",
StopTimeout: 10,
PodmanVersion: "CI",
+ EnvVariable: EnvVariable,
},
goodName,
false,
+ false,
},
{"good with name and bound to",
containerInfo{
@@ -238,22 +242,10 @@ WantedBy=multi-user.target default.target`
StopTimeout: 10,
PodmanVersion: "CI",
BoundToServices: []string{"pod", "a", "b", "c"},
+ EnvVariable: EnvVariable,
},
goodNameBoundTo,
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,
},
{"bad restart policy",
@@ -264,8 +256,10 @@ WantedBy=multi-user.target default.target`
PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid",
StopTimeout: 10,
PodmanVersion: "CI",
+ EnvVariable: EnvVariable,
},
"",
+ false,
true,
},
{"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",
StopTimeout: 42,
PodmanVersion: "CI",
- New: true,
CreateCommand: []string{"I'll get stripped", "container", "run", "--name", "jadda-jadda", "--hostname", "hello-world", "awesome-image:latest", "command", "arg1", "...", "argN"},
+ EnvVariable: EnvVariable,
},
goodNameNew,
+ true,
false,
},
{"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",
StopTimeout: 42,
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"},
+ EnvVariable: EnvVariable,
},
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,
},
{"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",
StopTimeout: 42,
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"},
+ EnvVariable: EnvVariable,
},
goodNameNewDetach,
+ true,
false,
},
{"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",
StopTimeout: 10,
PodmanVersion: "CI",
- New: true,
CreateCommand: []string{"I'll get stripped", "container", "run", "awesome-image:latest"},
+ EnvVariable: EnvVariable,
},
goodIDNew,
+ true,
false,
},
}
@@ -334,9 +351,9 @@ WantedBy=multi-user.target default.target`
t.Run(tt.name, func(t *testing.T) {
opts := entities.GenerateSystemdOptions{
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 {
t.Errorf("CreateContainerSystemdUnit() error = \n%v, wantErr \n%v", err, test.wantErr)
return
diff --git a/pkg/systemd/generate/pods.go b/pkg/systemd/generate/pods.go
index 16f324f78..355103df8 100644
--- a/pkg/systemd/generate/pods.go
+++ b/pkg/systemd/generate/pods.go
@@ -1,22 +1,101 @@
package generate
import (
+ "bytes"
"fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "sort"
"strings"
+ "text/template"
+ "time"
"github.com/containers/libpod/libpod"
"github.com/containers/libpod/pkg/domain/entities"
+ "github.com/containers/libpod/version"
"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.
// Based on the options, the return value might be the content of all units or
// the files they been written to.
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
// main service.
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
// each container.
- containerInfos := []*containerInfo{podInfo}
+ containerInfos := []*containerInfo{}
for ctr, dependencies := range graph.DependencyMap() {
// Skip the infra container as we already generated it.
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.
builder := strings.Builder{}
- for i, info := range containerInfos {
- if i > 0 {
- builder.WriteByte('\n')
- }
- out, err := createContainerSystemdUnit(info, options)
+ out, err := executePodTemplate(podInfo, options)
+ if err != nil {
+ return "", err
+ }
+ builder.WriteString(out)
+ for _, info := range containerInfos {
+ info.pod = podInfo
+ builder.WriteByte('\n')
+ out, err := executeContainerTemplate(info, options)
if err != nil {
return "", err
}
@@ -88,7 +171,7 @@ func PodUnits(pod *libpod.Pod, options entities.GenerateSystemdOptions) (string,
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
// containerInfo acts as the main service of the pod.
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")
}
- 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()
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)
- info := containerInfo{
+ info := podInfo{
ServiceName: serviceName,
- ContainerNameOrID: ctrNameOrID,
+ InfraNameOrID: ctrNameOrID,
RestartPolicy: options.RestartPolicy,
PIDFile: conmonPidFile,
StopTimeout: timeout,
@@ -128,3 +214,128 @@ func generatePodInfo(pod *libpod.Pod, options entities.GenerateSystemdOptions) (
}
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
+}
diff --git a/pkg/systemd/generate/pods_test.go b/pkg/systemd/generate/pods_test.go
new file mode 100644
index 000000000..f6e225c35
--- /dev/null
+++ b/pkg/systemd/generate/pods_test.go
@@ -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)
+ }
+ })
+ }
+}
diff --git a/test/e2e/generate_systemd_test.go b/test/e2e/generate_systemd_test.go
index 64d220776..497e8f71e 100644
--- a/test/e2e/generate_systemd_test.go
+++ b/test/e2e/generate_systemd_test.go
@@ -3,6 +3,7 @@
package integration
import (
+ "io/ioutil"
"os"
. "github.com/containers/libpod/test/utils"
@@ -191,7 +192,7 @@ var _ = Describe("Podman generate systemd", func() {
found, _ := session.GrepString("# container-foo.service")
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())
})
@@ -230,7 +231,7 @@ var _ = Describe("Podman generate systemd", func() {
session := podmanTest.Podman([]string{"generate", "systemd", "--time", "42", "--name", "--new", "foo"})
session.WaitWithDefaultTimeout()
- Expect(session.ExitCode()).To(Equal(125))
+ Expect(session.ExitCode()).To(Equal(0))
})
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")
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())
+ })
})