github.com/hanks177/podman/v4@v4.1.3-0.20220613032544-16d90015bc83/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/hanks177/podman/v4/libpod"
    13  	"github.com/hanks177/podman/v4/pkg/domain/entities"
    14  	"github.com/hanks177/podman/v4/pkg/systemd/define"
    15  	"github.com/hanks177/podman/v4/version"
    16  	"github.com/pkg/errors"
    17  	"github.com/sirupsen/logrus"
    18  	"github.com/spf13/pflag"
    19  )
    20  
    21  // podInfo contains data required for generating a pod's systemd
    22  // unit file.
    23  type podInfo struct {
    24  	// ServiceName of the systemd service.
    25  	ServiceName string
    26  	// Name or ID of the infra container.
    27  	InfraNameOrID string
    28  	// StopTimeout sets the timeout Podman waits before killing the container
    29  	// during service stop.
    30  	StopTimeout uint
    31  	// RestartPolicy of the systemd unit (e.g., no, on-failure, always).
    32  	RestartPolicy string
    33  	// RestartSec of the systemd unit. Configures the time to sleep before restarting a service.
    34  	RestartSec uint
    35  	// PIDFile of the service. Required for forking services. Must point to the
    36  	// PID of the associated conmon process.
    37  	PIDFile string
    38  	// PodIDFile of the unit.
    39  	PodIDFile string
    40  	// GenerateTimestamp, if set the generated unit file has a time stamp.
    41  	GenerateTimestamp bool
    42  	// RequiredServices are services this service requires. Note that this
    43  	// service runs before them.
    44  	RequiredServices []string
    45  	// PodmanVersion for the header. Will be set internally. Will be auto-filled
    46  	// if left empty.
    47  	PodmanVersion string
    48  	// Executable is the path to the podman executable. Will be auto-filled if
    49  	// left empty.
    50  	Executable string
    51  	// RootFlags contains the root flags which were used to create the container
    52  	// Only used with --new
    53  	RootFlags string
    54  	// TimeStamp at the time of creating the unit file. Will be set internally.
    55  	TimeStamp string
    56  	// CreateCommand is the full command plus arguments of the process the
    57  	// container has been created with.
    58  	CreateCommand []string
    59  	// PodCreateCommand - a post-processed variant of CreateCommand to use
    60  	// when creating the pod.
    61  	PodCreateCommand string
    62  	// EnvVariable is generate.EnvVariable and must not be set.
    63  	EnvVariable string
    64  	// ExecStartPre1 of the unit.
    65  	ExecStartPre1 string
    66  	// ExecStartPre2 of the unit.
    67  	ExecStartPre2 string
    68  	// ExecStart of the unit.
    69  	ExecStart string
    70  	// TimeoutStopSec of the unit.
    71  	TimeoutStopSec uint
    72  	// ExecStop of the unit.
    73  	ExecStop string
    74  	// ExecStopPost of the unit.
    75  	ExecStopPost string
    76  	// Removes autogenerated by Podman and timestamp if set to true
    77  	GenerateNoHeader bool
    78  	// Location of the GraphRoot for the pod.  Required for ensuring the
    79  	// volume has finished mounting when coming online at boot.
    80  	GraphRoot string
    81  	// Location of the RunRoot for the pod.  Required for ensuring the tmpfs
    82  	// or volume exists and is mounted when coming online at boot.
    83  	RunRoot string
    84  	// Add %i and %I to description and execute parts - this should not be used
    85  	IdentifySpecifier bool
    86  	// Wants are the list of services that this service is (weak) dependent on. This
    87  	// option does not influence the order in which services are started or stopped.
    88  	Wants []string
    89  	// After ordering dependencies between the list of services and this service.
    90  	After []string
    91  	// Similar to Wants, but declares a stronger requirement dependency.
    92  	Requires []string
    93  }
    94  
    95  const podTemplate = headerTemplate + `Requires={{{{- range $index, $value := .RequiredServices -}}}}{{{{if $index}}}} {{{{end}}}}{{{{ $value }}}}.service{{{{end}}}}
    96  Before={{{{- range $index, $value := .RequiredServices -}}}}{{{{if $index}}}} {{{{end}}}}{{{{ $value }}}}.service{{{{end}}}}
    97  {{{{- if or .Wants .After .Requires }}}}
    98  
    99  # User-defined dependencies
   100  {{{{- end}}}}
   101  {{{{- if .Wants}}}}
   102  Wants={{{{- range $index, $value := .Wants }}}}{{{{ if $index}}}} {{{{end}}}}{{{{ $value }}}}{{{{end}}}}
   103  {{{{- end}}}}
   104  {{{{- if .After}}}}
   105  After={{{{- range $index, $value := .After }}}}{{{{ if $index}}}} {{{{end}}}}{{{{ $value }}}}{{{{end}}}}
   106  {{{{- end}}}}
   107  {{{{- if .Requires}}}}
   108  Requires={{{{- range $index, $value := .Requires }}}}{{{{ if $index}}}} {{{{end}}}}{{{{ $value }}}}{{{{end}}}}
   109  {{{{- end}}}}
   110  
   111  [Service]
   112  Environment={{{{.EnvVariable}}}}=%n
   113  Restart={{{{.RestartPolicy}}}}
   114  {{{{- if .RestartSec}}}}
   115  RestartSec={{{{.RestartSec}}}}
   116  {{{{- end}}}}
   117  TimeoutStopSec={{{{.TimeoutStopSec}}}}
   118  {{{{- if .ExecStartPre1}}}}
   119  ExecStartPre={{{{.ExecStartPre1}}}}
   120  {{{{- end}}}}
   121  {{{{- if .ExecStartPre2}}}}
   122  ExecStartPre={{{{.ExecStartPre2}}}}
   123  {{{{- end}}}}
   124  ExecStart={{{{.ExecStart}}}}
   125  ExecStop={{{{.ExecStop}}}}
   126  ExecStopPost={{{{.ExecStopPost}}}}
   127  PIDFile={{{{.PIDFile}}}}
   128  Type=forking
   129  
   130  [Install]
   131  WantedBy=default.target
   132  `
   133  
   134  // PodUnits generates systemd units for the specified pod and its containers.
   135  // Based on the options, the return value might be the content of all units or
   136  // the files they been written to.
   137  func PodUnits(pod *libpod.Pod, options entities.GenerateSystemdOptions) (map[string]string, error) {
   138  	if options.TemplateUnitFile {
   139  		return nil, errors.New("--template is not supported for pods")
   140  	}
   141  	// Error out if the pod has no infra container, which we require to be the
   142  	// main service.
   143  	if !pod.HasInfraContainer() {
   144  		return nil, errors.Errorf("generating systemd unit files: Pod %q has no infra container", pod.Name())
   145  	}
   146  
   147  	podInfo, err := generatePodInfo(pod, options)
   148  	if err != nil {
   149  		return nil, err
   150  	}
   151  
   152  	infraID, err := pod.InfraContainerID()
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  
   157  	// Compute the container-dependency graph for the Pod.
   158  	containers, err := pod.AllContainers()
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  	if len(containers) == 0 {
   163  		return nil, errors.Errorf("generating systemd unit files: Pod %q has no containers", pod.Name())
   164  	}
   165  	graph, err := libpod.BuildContainerGraph(containers)
   166  	if err != nil {
   167  		return nil, err
   168  	}
   169  
   170  	// Traverse the dependency graph and create systemdgen.containerInfo's for
   171  	// each container.
   172  	containerInfos := []*containerInfo{}
   173  	for ctr, dependencies := range graph.DependencyMap() {
   174  		// Skip the infra container as we already generated it.
   175  		if ctr.ID() == infraID {
   176  			continue
   177  		}
   178  		ctrInfo, err := generateContainerInfo(ctr, options)
   179  		if err != nil {
   180  			return nil, err
   181  		}
   182  		// Now add the container's dependencies and at the container as a
   183  		// required service of the infra container.
   184  		for _, dep := range dependencies {
   185  			if dep.ID() == infraID {
   186  				ctrInfo.BoundToServices = append(ctrInfo.BoundToServices, podInfo.ServiceName)
   187  			} else {
   188  				_, serviceName := containerServiceName(dep, options)
   189  				ctrInfo.BoundToServices = append(ctrInfo.BoundToServices, serviceName)
   190  			}
   191  		}
   192  		podInfo.RequiredServices = append(podInfo.RequiredServices, ctrInfo.ServiceName)
   193  		containerInfos = append(containerInfos, ctrInfo)
   194  	}
   195  
   196  	units := map[string]string{}
   197  	// Now generate the systemd service for all containers.
   198  	out, err := executePodTemplate(podInfo, options)
   199  	if err != nil {
   200  		return nil, err
   201  	}
   202  	units[podInfo.ServiceName] = out
   203  	for _, info := range containerInfos {
   204  		info.Pod = podInfo
   205  		out, err := executeContainerTemplate(info, options)
   206  		if err != nil {
   207  			return nil, err
   208  		}
   209  		units[info.ServiceName] = out
   210  	}
   211  
   212  	return units, nil
   213  }
   214  
   215  func generatePodInfo(pod *libpod.Pod, options entities.GenerateSystemdOptions) (*podInfo, error) {
   216  	// Generate a systemdgen.containerInfo for the infra container. This
   217  	// containerInfo acts as the main service of the pod.
   218  	infraCtr, err := pod.InfraContainer()
   219  	if err != nil {
   220  		return nil, errors.Wrap(err, "could not find infra container")
   221  	}
   222  
   223  	stopTimeout := infraCtr.StopTimeout()
   224  	if options.StopTimeout != nil {
   225  		stopTimeout = *options.StopTimeout
   226  	}
   227  
   228  	config := infraCtr.Config()
   229  	conmonPidFile := config.ConmonPidFile
   230  	if conmonPidFile == "" {
   231  		return nil, errors.Errorf("conmon PID file path is empty, try to recreate the container with --conmon-pidfile flag")
   232  	}
   233  
   234  	createCommand := pod.CreateCommand()
   235  	if options.New && len(createCommand) == 0 {
   236  		return nil, errors.Errorf("cannot use --new on pod %q: no create command found", pod.ID())
   237  	}
   238  
   239  	nameOrID := pod.ID()
   240  	ctrNameOrID := infraCtr.ID()
   241  	if options.Name {
   242  		nameOrID = pod.Name()
   243  		ctrNameOrID = infraCtr.Name()
   244  	}
   245  
   246  	serviceName := getServiceName(options.PodPrefix, options.Separator, nameOrID)
   247  
   248  	info := podInfo{
   249  		ServiceName:       serviceName,
   250  		InfraNameOrID:     ctrNameOrID,
   251  		PIDFile:           conmonPidFile,
   252  		StopTimeout:       stopTimeout,
   253  		GenerateTimestamp: true,
   254  		CreateCommand:     createCommand,
   255  	}
   256  	return &info, nil
   257  }
   258  
   259  // Unless already specified, the pod's exit policy to "stop".
   260  func setPodExitPolicy(cmd []string) []string {
   261  	for _, arg := range cmd {
   262  		if strings.HasPrefix(arg, "--exit-policy=") || arg == "--exit-policy" {
   263  			return cmd
   264  		}
   265  	}
   266  	return append(cmd, "--exit-policy=stop")
   267  }
   268  
   269  // executePodTemplate executes the pod template on the specified podInfo.  Note
   270  // that the podInfo is also post processed and completed, which allows for an
   271  // easier unit testing.
   272  func executePodTemplate(info *podInfo, options entities.GenerateSystemdOptions) (string, error) {
   273  	info.RestartPolicy = define.DefaultRestartPolicy
   274  	if options.RestartPolicy != nil {
   275  		if err := validateRestartPolicy(*options.RestartPolicy); err != nil {
   276  			return "", err
   277  		}
   278  		info.RestartPolicy = *options.RestartPolicy
   279  	}
   280  
   281  	if options.RestartSec != nil {
   282  		info.RestartSec = *options.RestartSec
   283  	}
   284  
   285  	// Make sure the executable is set.
   286  	if info.Executable == "" {
   287  		executable, err := os.Executable()
   288  		if err != nil {
   289  			executable = "/usr/bin/podman"
   290  			logrus.Warnf("Could not obtain podman executable location, using default %s: %v", executable, err)
   291  		}
   292  		info.Executable = executable
   293  	}
   294  
   295  	info.EnvVariable = define.EnvVariable
   296  	info.ExecStart = "{{{{.Executable}}}} start {{{{.InfraNameOrID}}}}"
   297  	info.ExecStop = "{{{{.Executable}}}} stop {{{{if (ge .StopTimeout 0)}}}}-t {{{{.StopTimeout}}}}{{{{end}}}} {{{{.InfraNameOrID}}}}"
   298  	info.ExecStopPost = "{{{{.Executable}}}} stop {{{{if (ge .StopTimeout 0)}}}}-t {{{{.StopTimeout}}}}{{{{end}}}} {{{{.InfraNameOrID}}}}"
   299  
   300  	// Assemble the ExecStart command when creating a new pod.
   301  	//
   302  	// Note that we cannot catch all corner cases here such that users
   303  	// *must* manually check the generated files.  A pod might have been
   304  	// created via a Python script, which would certainly yield an invalid
   305  	// `info.CreateCommand`.  Hence, we're doing a best effort unit
   306  	// generation and don't try aiming at completeness.
   307  	if options.New {
   308  		info.PIDFile = "%t/" + info.ServiceName + ".pid"
   309  		info.PodIDFile = "%t/" + info.ServiceName + ".pod-id"
   310  
   311  		podCreateIndex := 0
   312  		var podRootArgs, podCreateArgs []string
   313  		switch len(info.CreateCommand) {
   314  		case 0, 1, 2:
   315  			return "", errors.Errorf("pod does not appear to be created via `podman pod create`: %v", info.CreateCommand)
   316  		default:
   317  			// Make sure that pod was created with `pod create` and
   318  			// not something else, such as `run --pod new`.
   319  			for i := 1; i < len(info.CreateCommand); i++ {
   320  				if info.CreateCommand[i-1] == "pod" && info.CreateCommand[i] == "create" {
   321  					podCreateIndex = i
   322  					break
   323  				}
   324  			}
   325  			if podCreateIndex == 0 {
   326  				return "", errors.Errorf("pod does not appear to be created via `podman pod create`: %v", info.CreateCommand)
   327  			}
   328  			podRootArgs = info.CreateCommand[1 : podCreateIndex-1]
   329  			info.RootFlags = strings.Join(escapeSystemdArguments(podRootArgs), " ")
   330  			podCreateArgs = filterPodFlags(info.CreateCommand[podCreateIndex+1:], 0)
   331  		}
   332  		// We're hard-coding the first five arguments and append the
   333  		// CreateCommand with a stripped command and subcommand.
   334  		startCommand := []string{info.Executable}
   335  		startCommand = append(startCommand, podRootArgs...)
   336  		startCommand = append(startCommand,
   337  			"pod", "create",
   338  			"--infra-conmon-pidfile", "{{{{.PIDFile}}}}",
   339  			"--pod-id-file", "{{{{.PodIDFile}}}}")
   340  
   341  		// Presence check for certain flags/options.
   342  		fs := pflag.NewFlagSet("args", pflag.ContinueOnError)
   343  		fs.ParseErrorsWhitelist.UnknownFlags = true
   344  		fs.Usage = func() {}
   345  		fs.SetInterspersed(false)
   346  		fs.String("name", "", "")
   347  		fs.Bool("replace", false, "")
   348  		if err := fs.Parse(podCreateArgs); err != nil {
   349  			return "", fmt.Errorf("parsing remaining command-line arguments: %w", err)
   350  		}
   351  
   352  		hasNameParam := fs.Lookup("name").Changed
   353  		hasReplaceParam, err := fs.GetBool("replace")
   354  		if err != nil {
   355  			return "", err
   356  		}
   357  		if hasNameParam && !hasReplaceParam {
   358  			if fs.Changed("replace") {
   359  				// this can only happen if --replace=false is set
   360  				// in that case we need to remove it otherwise we
   361  				// would overwrite the previous replace arg to false
   362  				podCreateArgs = removeReplaceArg(podCreateArgs, fs.NArg())
   363  			}
   364  			podCreateArgs = append(podCreateArgs, "--replace")
   365  		}
   366  
   367  		startCommand = append(startCommand, podCreateArgs...)
   368  		startCommand = setPodExitPolicy(startCommand)
   369  		startCommand = escapeSystemdArguments(startCommand)
   370  
   371  		info.ExecStartPre1 = "/bin/rm -f {{{{.PIDFile}}}} {{{{.PodIDFile}}}}"
   372  		info.ExecStartPre2 = strings.Join(startCommand, " ")
   373  		info.ExecStart = "{{{{.Executable}}}} {{{{if .RootFlags}}}}{{{{ .RootFlags}}}} {{{{end}}}}pod start --pod-id-file {{{{.PodIDFile}}}}"
   374  		info.ExecStop = "{{{{.Executable}}}} {{{{if .RootFlags}}}}{{{{ .RootFlags}}}} {{{{end}}}}pod stop --ignore --pod-id-file {{{{.PodIDFile}}}} {{{{if (ge .StopTimeout 0)}}}}-t {{{{.StopTimeout}}}}{{{{end}}}}"
   375  		info.ExecStopPost = "{{{{.Executable}}}} {{{{if .RootFlags}}}}{{{{ .RootFlags}}}} {{{{end}}}}pod rm --ignore -f --pod-id-file {{{{.PodIDFile}}}}"
   376  	}
   377  	info.TimeoutStopSec = minTimeoutStopSec + info.StopTimeout
   378  
   379  	if info.PodmanVersion == "" {
   380  		info.PodmanVersion = version.Version.String()
   381  	}
   382  
   383  	if options.NoHeader {
   384  		info.GenerateNoHeader = true
   385  		info.GenerateTimestamp = false
   386  	}
   387  
   388  	if info.GenerateTimestamp {
   389  		info.TimeStamp = fmt.Sprintf("%v", time.Now().Format(time.UnixDate))
   390  	}
   391  
   392  	// Sort the slices to assure a deterministic output.
   393  	sort.Strings(info.RequiredServices)
   394  
   395  	// Generate the template and compile it.
   396  	//
   397  	// Note that we need a two-step generation process to allow for fields
   398  	// embedding other fields.  This way we can replace `A -> B -> C` and
   399  	// make the code easier to maintain at the cost of a slightly slower
   400  	// generation.  That's especially needed for embedding the PID and ID
   401  	// files in other fields which will eventually get replaced in the 2nd
   402  	// template execution.
   403  	templ, err := template.New("pod_template").Delims("{{{{", "}}}}").Parse(podTemplate)
   404  	if err != nil {
   405  		return "", errors.Wrap(err, "error parsing systemd service template")
   406  	}
   407  
   408  	var buf bytes.Buffer
   409  	if err := templ.Execute(&buf, info); err != nil {
   410  		return "", err
   411  	}
   412  
   413  	// Now parse the generated template (i.e., buf) and execute it.
   414  	templ, err = template.New("pod_template").Delims("{{{{", "}}}}").Parse(buf.String())
   415  	if err != nil {
   416  		return "", errors.Wrap(err, "error parsing systemd service template")
   417  	}
   418  
   419  	buf = bytes.Buffer{}
   420  	if err := templ.Execute(&buf, info); err != nil {
   421  		return "", err
   422  	}
   423  
   424  	return buf.String(), nil
   425  }