diff options
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/systemd/generate/common.go | 14 | ||||
-rw-r--r-- | pkg/systemd/generate/common_test.go | 25 | ||||
-rw-r--r-- | pkg/systemd/generate/containers.go | 189 | ||||
-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.go | 239 | ||||
-rw-r--r-- | pkg/systemd/generate/pods_test.go | 100 |
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) + } + }) + } +} |