github.com/containers/podman/v2@v2.2.2-0.20210501105131-c1e07d070c4c/pkg/systemd/generate/pods.go (about)

     1  package generate
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"os"
     7  	"sort"
     8  	"strings"
     9  	"text/template"
    10  	"time"
    11  
    12  	"github.com/containers/podman/v2/libpod"
    13  	"github.com/containers/podman/v2/pkg/domain/entities"
    14  	"github.com/containers/podman/v2/version"
    15  	"github.com/pkg/errors"
    16  	"github.com/sirupsen/logrus"
    17  )
    18  
    19  // podInfo contains data required for generating a pod's systemd
    20  // unit file.
    21  type podInfo struct {
    22  	// ServiceName of the systemd service.
    23  	ServiceName string
    24  	// Name or ID of the infra container.
    25  	InfraNameOrID string
    26  	// StopTimeout sets the timeout Podman waits before killing the container
    27  	// during service stop.
    28  	StopTimeout uint
    29  	// RestartPolicy of the systemd unit (e.g., no, on-failure, always).
    30  	RestartPolicy string
    31  	// PIDFile of the service. Required for forking services. Must point to the
    32  	// PID of the associated conmon process.
    33  	PIDFile string
    34  	// PodIDFile of the unit.
    35  	PodIDFile string
    36  	// GenerateTimestamp, if set the generated unit file has a time stamp.
    37  	GenerateTimestamp bool
    38  	// RequiredServices are services this service requires. Note that this
    39  	// service runs before them.
    40  	RequiredServices []string
    41  	// PodmanVersion for the header. Will be set internally. Will be auto-filled
    42  	// if left empty.
    43  	PodmanVersion string
    44  	// Executable is the path to the podman executable. Will be auto-filled if
    45  	// left empty.
    46  	Executable string
    47  	// TimeStamp at the time of creating the unit file. Will be set internally.
    48  	TimeStamp string
    49  	// CreateCommand is the full command plus arguments of the process the
    50  	// container has been created with.
    51  	CreateCommand []string
    52  	// PodCreateCommand - a post-processed variant of CreateCommand to use
    53  	// when creating the pod.
    54  	PodCreateCommand string
    55  	// EnvVariable is generate.EnvVariable and must not be set.
    56  	EnvVariable string
    57  	// ExecStartPre1 of the unit.
    58  	ExecStartPre1 string
    59  	// ExecStartPre2 of the unit.
    60  	ExecStartPre2 string
    61  	// ExecStart of the unit.
    62  	ExecStart string
    63  	// ExecStop of the unit.
    64  	ExecStop string
    65  	// ExecStopPost of the unit.
    66  	ExecStopPost string
    67  }
    68  
    69  const podTemplate = headerTemplate + `Requires={{- range $index, $value := .RequiredServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}}
    70  Before={{- range $index, $value := .RequiredServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}}
    71  
    72  [Service]
    73  Environment={{.EnvVariable}}=%n
    74  Restart={{.RestartPolicy}}
    75  {{- if .ExecStartPre1}}
    76  ExecStartPre={{.ExecStartPre1}}
    77  {{- end}}
    78  {{- if .ExecStartPre2}}
    79  ExecStartPre={{.ExecStartPre2}}
    80  {{- end}}
    81  ExecStart={{.ExecStart}}
    82  ExecStop={{.ExecStop}}
    83  ExecStopPost={{.ExecStopPost}}
    84  PIDFile={{.PIDFile}}
    85  KillMode=none
    86  Type=forking
    87  
    88  [Install]
    89  WantedBy=multi-user.target default.target
    90  `
    91  
    92  // PodUnits generates systemd units for the specified pod and its containers.
    93  // Based on the options, the return value might be the content of all units or
    94  // the files they been written to.
    95  func PodUnits(pod *libpod.Pod, options entities.GenerateSystemdOptions) (map[string]string, error) {
    96  	// Error out if the pod has no infra container, which we require to be the
    97  	// main service.
    98  	if !pod.HasInfraContainer() {
    99  		return nil, errors.Errorf("error generating systemd unit files: Pod %q has no infra container", pod.Name())
   100  	}
   101  
   102  	podInfo, err := generatePodInfo(pod, options)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  
   107  	infraID, err := pod.InfraContainerID()
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  
   112  	// Compute the container-dependency graph for the Pod.
   113  	containers, err := pod.AllContainers()
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  	if len(containers) == 0 {
   118  		return nil, errors.Errorf("error generating systemd unit files: Pod %q has no containers", pod.Name())
   119  	}
   120  	graph, err := libpod.BuildContainerGraph(containers)
   121  	if err != nil {
   122  		return nil, err
   123  	}
   124  
   125  	// Traverse the dependency graph and create systemdgen.containerInfo's for
   126  	// each container.
   127  	containerInfos := []*containerInfo{}
   128  	for ctr, dependencies := range graph.DependencyMap() {
   129  		// Skip the infra container as we already generated it.
   130  		if ctr.ID() == infraID {
   131  			continue
   132  		}
   133  		ctrInfo, err := generateContainerInfo(ctr, options)
   134  		if err != nil {
   135  			return nil, err
   136  		}
   137  		// Now add the container's dependencies and at the container as a
   138  		// required service of the infra container.
   139  		for _, dep := range dependencies {
   140  			if dep.ID() == infraID {
   141  				ctrInfo.BoundToServices = append(ctrInfo.BoundToServices, podInfo.ServiceName)
   142  			} else {
   143  				_, serviceName := containerServiceName(dep, options)
   144  				ctrInfo.BoundToServices = append(ctrInfo.BoundToServices, serviceName)
   145  			}
   146  		}
   147  		podInfo.RequiredServices = append(podInfo.RequiredServices, ctrInfo.ServiceName)
   148  		containerInfos = append(containerInfos, ctrInfo)
   149  	}
   150  
   151  	units := map[string]string{}
   152  	// Now generate the systemd service for all containers.
   153  	out, err := executePodTemplate(podInfo, options)
   154  	if err != nil {
   155  		return nil, err
   156  	}
   157  	units[podInfo.ServiceName] = out
   158  	for _, info := range containerInfos {
   159  		info.pod = podInfo
   160  		out, err := executeContainerTemplate(info, options)
   161  		if err != nil {
   162  			return nil, err
   163  		}
   164  		units[info.ServiceName] = out
   165  	}
   166  
   167  	return units, nil
   168  }
   169  
   170  func generatePodInfo(pod *libpod.Pod, options entities.GenerateSystemdOptions) (*podInfo, error) {
   171  	// Generate a systemdgen.containerInfo for the infra container. This
   172  	// containerInfo acts as the main service of the pod.
   173  	infraCtr, err := pod.InfraContainer()
   174  	if err != nil {
   175  		return nil, errors.Wrap(err, "could not find infra container")
   176  	}
   177  
   178  	timeout := infraCtr.StopTimeout()
   179  	if options.StopTimeout != nil {
   180  		timeout = *options.StopTimeout
   181  	}
   182  
   183  	config := infraCtr.Config()
   184  	conmonPidFile := config.ConmonPidFile
   185  	if conmonPidFile == "" {
   186  		return nil, errors.Errorf("conmon PID file path is empty, try to recreate the container with --conmon-pidfile flag")
   187  	}
   188  
   189  	createCommand := pod.CreateCommand()
   190  	if options.New && len(createCommand) == 0 {
   191  		return nil, errors.Errorf("cannot use --new on pod %q: no create command found", pod.ID())
   192  	}
   193  
   194  	nameOrID := pod.ID()
   195  	ctrNameOrID := infraCtr.ID()
   196  	if options.Name {
   197  		nameOrID = pod.Name()
   198  		ctrNameOrID = infraCtr.Name()
   199  	}
   200  	serviceName := fmt.Sprintf("%s%s%s", options.PodPrefix, options.Separator, nameOrID)
   201  
   202  	info := podInfo{
   203  		ServiceName:       serviceName,
   204  		InfraNameOrID:     ctrNameOrID,
   205  		RestartPolicy:     options.RestartPolicy,
   206  		PIDFile:           conmonPidFile,
   207  		StopTimeout:       timeout,
   208  		GenerateTimestamp: true,
   209  		CreateCommand:     createCommand,
   210  	}
   211  	return &info, nil
   212  }
   213  
   214  // executePodTemplate executes the pod template on the specified podInfo.  Note
   215  // that the podInfo is also post processed and completed, which allows for an
   216  // easier unit testing.
   217  func executePodTemplate(info *podInfo, options entities.GenerateSystemdOptions) (string, error) {
   218  	if err := validateRestartPolicy(info.RestartPolicy); err != nil {
   219  		return "", err
   220  	}
   221  
   222  	// Make sure the executable is set.
   223  	if info.Executable == "" {
   224  		executable, err := os.Executable()
   225  		if err != nil {
   226  			executable = "/usr/bin/podman"
   227  			logrus.Warnf("Could not obtain podman executable location, using default %s", executable)
   228  		}
   229  		info.Executable = executable
   230  	}
   231  
   232  	info.EnvVariable = EnvVariable
   233  	info.ExecStart = "{{.Executable}} start {{.InfraNameOrID}}"
   234  	info.ExecStop = "{{.Executable}} stop {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}} {{.InfraNameOrID}}"
   235  	info.ExecStopPost = "{{.Executable}} stop {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}} {{.InfraNameOrID}}"
   236  
   237  	// Assemble the ExecStart command when creating a new pod.
   238  	//
   239  	// Note that we cannot catch all corner cases here such that users
   240  	// *must* manually check the generated files.  A pod might have been
   241  	// created via a Python script, which would certainly yield an invalid
   242  	// `info.CreateCommand`.  Hence, we're doing a best effort unit
   243  	// generation and don't try aiming at completeness.
   244  	if options.New {
   245  		info.PIDFile = "%t/" + info.ServiceName + ".pid"
   246  		info.PodIDFile = "%t/" + info.ServiceName + ".pod-id"
   247  
   248  		podCreateIndex := 0
   249  		var podRootArgs, podCreateArgs []string
   250  		switch len(info.CreateCommand) {
   251  		case 0, 1, 2:
   252  			return "", errors.Errorf("pod does not appear to be created via `podman pod create`: %v", info.CreateCommand)
   253  		default:
   254  			// Make sure that pod was created with `pod create` and
   255  			// not something else, such as `run --pod new`.
   256  			for i := 1; i < len(info.CreateCommand); i++ {
   257  				if info.CreateCommand[i-1] == "pod" && info.CreateCommand[i] == "create" {
   258  					podCreateIndex = i
   259  					break
   260  				}
   261  			}
   262  			if podCreateIndex == 0 {
   263  				return "", errors.Errorf("pod does not appear to be created via `podman pod create`: %v", info.CreateCommand)
   264  			}
   265  			podRootArgs = info.CreateCommand[0 : podCreateIndex-2]
   266  			podCreateArgs = filterPodFlags(info.CreateCommand[podCreateIndex+1:])
   267  		}
   268  		// We're hard-coding the first five arguments and append the
   269  		// CreateCommand with a stripped command and subcomand.
   270  		startCommand := []string{info.Executable}
   271  		startCommand = append(startCommand, podRootArgs...)
   272  		startCommand = append(startCommand,
   273  			[]string{"pod", "create",
   274  				"--infra-conmon-pidfile", "{{.PIDFile}}",
   275  				"--pod-id-file", "{{.PodIDFile}}"}...)
   276  
   277  		// Presence check for certain flags/options.
   278  		hasNameParam := false
   279  		hasReplaceParam := false
   280  		for _, p := range podCreateArgs {
   281  			switch p {
   282  			case "--name":
   283  				hasNameParam = true
   284  			case "--replace":
   285  				hasReplaceParam = true
   286  			}
   287  		}
   288  		if hasNameParam && !hasReplaceParam {
   289  			podCreateArgs = append(podCreateArgs, "--replace")
   290  		}
   291  
   292  		startCommand = append(startCommand, podCreateArgs...)
   293  		startCommand = quoteArguments(startCommand)
   294  
   295  		info.ExecStartPre1 = "/bin/rm -f {{.PIDFile}} {{.PodIDFile}}"
   296  		info.ExecStartPre2 = strings.Join(startCommand, " ")
   297  		info.ExecStart = "{{.Executable}} pod start --pod-id-file {{.PodIDFile}}"
   298  		info.ExecStop = "{{.Executable}} pod stop --ignore --pod-id-file {{.PodIDFile}} {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}}"
   299  		info.ExecStopPost = "{{.Executable}} pod rm --ignore -f --pod-id-file {{.PodIDFile}}"
   300  	}
   301  	if info.PodmanVersion == "" {
   302  		info.PodmanVersion = version.Version.String()
   303  	}
   304  	if info.GenerateTimestamp {
   305  		info.TimeStamp = fmt.Sprintf("%v", time.Now().Format(time.UnixDate))
   306  	}
   307  
   308  	// Sort the slices to assure a deterministic output.
   309  	sort.Strings(info.RequiredServices)
   310  
   311  	// Generate the template and compile it.
   312  	//
   313  	// Note that we need a two-step generation process to allow for fields
   314  	// embedding other fields.  This way we can replace `A -> B -> C` and
   315  	// make the code easier to maintain at the cost of a slightly slower
   316  	// generation.  That's especially needed for embedding the PID and ID
   317  	// files in other fields which will eventually get replaced in the 2nd
   318  	// template execution.
   319  	templ, err := template.New("pod_template").Parse(podTemplate)
   320  	if err != nil {
   321  		return "", errors.Wrap(err, "error parsing systemd service template")
   322  	}
   323  
   324  	var buf bytes.Buffer
   325  	if err := templ.Execute(&buf, info); err != nil {
   326  		return "", err
   327  	}
   328  
   329  	// Now parse the generated template (i.e., buf) and execute it.
   330  	templ, err = template.New("pod_template").Parse(buf.String())
   331  	if err != nil {
   332  		return "", errors.Wrap(err, "error parsing systemd service template")
   333  	}
   334  
   335  	buf = bytes.Buffer{}
   336  	if err := templ.Execute(&buf, info); err != nil {
   337  		return "", err
   338  	}
   339  
   340  	return buf.String(), nil
   341  }