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 98f160d
Show file tree
Hide file tree
Showing 5 changed files with 463 additions and 133 deletions.
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 %t/%n-ctr-id {{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 98f160d

Please sign in to comment.