summaryrefslogtreecommitdiff
path: root/pkg/systemd/generate
diff options
context:
space:
mode:
authorValentin Rothberg <rothberg@redhat.com>2020-06-05 18:23:12 +0200
committerValentin Rothberg <rothberg@redhat.com>2020-06-11 11:01:13 +0200
commit8d8746adeeab8a39ccedb5b06fe8d0a785a97190 (patch)
treeafa9891205369f8d36043ad6236939720c012a2a /pkg/systemd/generate
parent35ae53067f01c0194dc13513656e57293de95004 (diff)
downloadpodman-8d8746adeeab8a39ccedb5b06fe8d0a785a97190.tar.gz
podman-8d8746adeeab8a39ccedb5b06fe8d0a785a97190.tar.bz2
podman-8d8746adeeab8a39ccedb5b06fe8d0a785a97190.zip
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>
Diffstat (limited to 'pkg/systemd/generate')
-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
6 files changed, 536 insertions, 136 deletions
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)
+ }
+ })
+ }
+}