Skip to content

Commit

Permalink
generate systemd: create pod template
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
vrothberg committed Jun 9, 2020
1 parent fd1a799 commit 0862783
Show file tree
Hide file tree
Showing 7 changed files with 511 additions and 140 deletions.
5 changes: 1 addition & 4 deletions docs/source/markdown/podman-generate-systemd.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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*

Expand Down
2 changes: 1 addition & 1 deletion libpod/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
197 changes: 122 additions & 75 deletions pkg/systemd/generate/containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 + `
Expand All @@ -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
Expand All @@ -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)
}

// createContainerSystemdUnit creates a systemd unit file for a container.
func createContainerSystemdUnit(info *containerInfo, options entities.GenerateSystemdOptions) (string, error) {
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
}

// 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
}
Expand All @@ -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.
//
Expand All @@ -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
Expand All @@ -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
//
Expand All @@ -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 == "" {
Expand All @@ -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")
}
Expand All @@ -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
}
Expand All @@ -211,46 +288,16 @@ 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()
// 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)
}
serviceName := fmt.Sprintf("%s%s%s", options.ContainerPrefix, options.Separator, nameOrID)
return nameOrID, serviceName
return processed
}
Loading

0 comments on commit 0862783

Please sign in to comment.