diff options
Diffstat (limited to 'pkg/systemd/generate/pods.go')
-rw-r--r-- | pkg/systemd/generate/pods.go | 341 |
1 files changed, 341 insertions, 0 deletions
diff --git a/pkg/systemd/generate/pods.go b/pkg/systemd/generate/pods.go new file mode 100644 index 000000000..5cfd5ab0a --- /dev/null +++ b/pkg/systemd/generate/pods.go @@ -0,0 +1,341 @@ +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) { + // Error out if the pod has no infra container, which we require to be the + // main service. + if !pod.HasInfraContainer() { + return "", errors.Errorf("error generating systemd unit files: Pod %q has no infra container", pod.Name()) + } + + podInfo, err := generatePodInfo(pod, options) + if err != nil { + return "", err + } + + infraID, err := pod.InfraContainerID() + if err != nil { + return "", err + } + + // Compute the container-dependency graph for the Pod. + containers, err := pod.AllContainers() + if err != nil { + return "", err + } + if len(containers) == 0 { + return "", errors.Errorf("error generating systemd unit files: Pod %q has no containers", pod.Name()) + } + graph, err := libpod.BuildContainerGraph(containers) + if err != nil { + return "", err + } + + // Traverse the dependency graph and create systemdgen.containerInfo's for + // each container. + containerInfos := []*containerInfo{} + for ctr, dependencies := range graph.DependencyMap() { + // Skip the infra container as we already generated it. + if ctr.ID() == infraID { + continue + } + ctrInfo, err := generateContainerInfo(ctr, options) + if err != nil { + return "", err + } + // Now add the container's dependencies and at the container as a + // required service of the infra container. + for _, dep := range dependencies { + if dep.ID() == infraID { + ctrInfo.BoundToServices = append(ctrInfo.BoundToServices, podInfo.ServiceName) + } else { + _, serviceName := containerServiceName(dep, options) + ctrInfo.BoundToServices = append(ctrInfo.BoundToServices, serviceName) + } + } + podInfo.RequiredServices = append(podInfo.RequiredServices, ctrInfo.ServiceName) + containerInfos = append(containerInfos, ctrInfo) + } + + // Now generate the systemd service for all containers. + builder := strings.Builder{} + 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 + } + builder.WriteString(out) + } + + return builder.String(), nil +} + +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() + if err != nil { + return nil, errors.Wrap(err, "could not find infra container") + } + + timeout := infraCtr.StopTimeout() + if options.StopTimeout != nil { + timeout = *options.StopTimeout + } + + config := infraCtr.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 := 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() + if options.Name { + nameOrID = pod.Name() + ctrNameOrID = infraCtr.Name() + } + serviceName := fmt.Sprintf("%s%s%s", options.PodPrefix, options.Separator, nameOrID) + + info := podInfo{ + ServiceName: serviceName, + InfraNameOrID: ctrNameOrID, + RestartPolicy: options.RestartPolicy, + PIDFile: conmonPidFile, + StopTimeout: timeout, + GenerateTimestamp: true, + CreateCommand: createCommand, + } + 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 +} |