github.com/hanks177/podman/v4@v4.1.3-0.20220613032544-16d90015bc83/pkg/systemd/generate/containers.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  	libpodDefine "github.com/hanks177/podman/v4/libpod/define"
    14  	"github.com/hanks177/podman/v4/pkg/domain/entities"
    15  	"github.com/hanks177/podman/v4/pkg/systemd/define"
    16  	"github.com/hanks177/podman/v4/version"
    17  	"github.com/pkg/errors"
    18  	"github.com/sirupsen/logrus"
    19  	"github.com/spf13/pflag"
    20  )
    21  
    22  // containerInfo contains data required for generating a container's systemd
    23  // unit file.
    24  type containerInfo struct {
    25  	// ServiceName of the systemd service.
    26  	ServiceName string
    27  	// Name or ID of the container.
    28  	ContainerNameOrID string
    29  	// Type of the unit.
    30  	Type string
    31  	// NotifyAccess of the unit.
    32  	NotifyAccess string
    33  	// StopTimeout sets the timeout Podman waits before killing the container
    34  	// during service stop.
    35  	StopTimeout uint
    36  	// RestartPolicy of the systemd unit (e.g., no, on-failure, always).
    37  	RestartPolicy string
    38  	// Custom number of restart attempts.
    39  	StartLimitBurst string
    40  	// PIDFile of the service. Required for forking services. Must point to the
    41  	// PID of the associated conmon process.
    42  	PIDFile string
    43  	// ContainerIDFile to be used in the unit.
    44  	ContainerIDFile string
    45  	// GenerateTimestamp, if set the generated unit file has a time stamp.
    46  	GenerateTimestamp bool
    47  	// BoundToServices are the services this service binds to.  Note that this
    48  	// service runs after them.
    49  	BoundToServices []string
    50  	// PodmanVersion for the header. Will be set internally. Will be auto-filled
    51  	// if left empty.
    52  	PodmanVersion string
    53  	// Executable is the path to the podman executable. Will be auto-filled if
    54  	// left empty.
    55  	Executable string
    56  	// RootFlags contains the root flags which were used to create the container
    57  	// Only used with --new
    58  	RootFlags string
    59  	// TimeStamp at the time of creating the unit file. Will be set internally.
    60  	TimeStamp string
    61  	// CreateCommand is the full command plus arguments of the process the
    62  	// container has been created with.
    63  	CreateCommand []string
    64  	// containerEnv stores the container environment variables
    65  	containerEnv []string
    66  	// ExtraEnvs contains the container environment variables referenced
    67  	// by only the key in the container create command, e.g. --env FOO.
    68  	// This is only used with --new
    69  	ExtraEnvs []string
    70  	// EnvVariable is generate.EnvVariable and must not be set.
    71  	EnvVariable string
    72  	// ExecStartPre of the unit.
    73  	ExecStartPre string
    74  	// ExecStart of the unit.
    75  	ExecStart string
    76  	// TimeoutStartSec of the unit.
    77  	TimeoutStartSec uint
    78  	// TimeoutStopSec of the unit.
    79  	TimeoutStopSec uint
    80  	// ExecStop of the unit.
    81  	ExecStop string
    82  	// ExecStopPost of the unit.
    83  	ExecStopPost string
    84  	// Removes autogenerated by Podman and timestamp if set to true
    85  	GenerateNoHeader bool
    86  	// If not nil, the container is part of the pod.  We can use the
    87  	// podInfo to extract the relevant data.
    88  	Pod *podInfo
    89  	// Location of the GraphRoot for the container.  Required for ensuring the
    90  	// volume has finished mounting when coming online at boot.
    91  	GraphRoot string
    92  	// Location of the RunRoot for the container.  Required for ensuring the tmpfs
    93  	// or volume exists and is mounted when coming online at boot.
    94  	RunRoot string
    95  	// Add %i and %I to description and execute parts
    96  	IdentifySpecifier bool
    97  	// Wants are the list of services that this service is (weak) dependent on. This
    98  	// option does not influence the order in which services are started or stopped.
    99  	Wants []string
   100  	// After ordering dependencies between the list of services and this service.
   101  	After []string
   102  	// Similar to Wants, but declares a stronger requirement dependency.
   103  	Requires []string
   104  }
   105  
   106  const containerTemplate = headerTemplate + `
   107  {{{{- if .BoundToServices}}}}
   108  BindsTo={{{{- range $index, $value := .BoundToServices -}}}}{{{{if $index}}}} {{{{end}}}}{{{{ $value }}}}.service{{{{end}}}}
   109  After={{{{- range $index, $value := .BoundToServices -}}}}{{{{if $index}}}} {{{{end}}}}{{{{ $value }}}}.service{{{{end}}}}
   110  {{{{- end}}}}
   111  {{{{- if or .Wants .After .Requires }}}}
   112  
   113  # User-defined dependencies
   114  {{{{- end}}}}
   115  {{{{- if .Wants}}}}
   116  Wants={{{{- range $index, $value := .Wants }}}}{{{{ if $index}}}} {{{{end}}}}{{{{ $value }}}}{{{{end}}}}
   117  {{{{- end}}}}
   118  {{{{- if .After}}}}
   119  After={{{{- range $index, $value := .After }}}}{{{{ if $index}}}} {{{{end}}}}{{{{ $value }}}}{{{{end}}}}
   120  {{{{- end}}}}
   121  {{{{- if .Requires}}}}
   122  Requires={{{{- range $index, $value := .Requires }}}}{{{{ if $index}}}} {{{{end}}}}{{{{ $value }}}}{{{{end}}}}
   123  {{{{- end}}}}
   124  
   125  [Service]
   126  Environment={{{{.EnvVariable}}}}=%n{{{{- if (eq .IdentifySpecifier true) }}}}-%i{{{{- end}}}}
   127  {{{{- if .ExtraEnvs}}}}
   128  Environment={{{{- range $index, $value := .ExtraEnvs -}}}}{{{{if $index}}}} {{{{end}}}}{{{{ $value }}}}{{{{end}}}}
   129  {{{{- end}}}}
   130  Restart={{{{.RestartPolicy}}}}
   131  {{{{- if .StartLimitBurst}}}}
   132  StartLimitBurst={{{{.StartLimitBurst}}}}
   133  {{{{- end}}}}
   134  {{{{- if ne .TimeoutStartSec 0}}}}
   135  TimeoutStartSec={{{{.TimeoutStartSec}}}}
   136  {{{{- end}}}}
   137  TimeoutStopSec={{{{.TimeoutStopSec}}}}
   138  {{{{- if .ExecStartPre}}}}
   139  ExecStartPre={{{{.ExecStartPre}}}}
   140  {{{{- end}}}}
   141  ExecStart={{{{.ExecStart}}}}
   142  {{{{- if .ExecStop}}}}
   143  ExecStop={{{{.ExecStop}}}}
   144  {{{{- end}}}}
   145  {{{{- if .ExecStopPost}}}}
   146  ExecStopPost={{{{.ExecStopPost}}}}
   147  {{{{- end}}}}
   148  {{{{- if .PIDFile}}}}
   149  PIDFile={{{{.PIDFile}}}}
   150  {{{{- end}}}}
   151  Type={{{{.Type}}}}
   152  {{{{- if .NotifyAccess}}}}
   153  NotifyAccess={{{{.NotifyAccess}}}}
   154  {{{{- end}}}}
   155  
   156  [Install]
   157  WantedBy=default.target
   158  `
   159  
   160  // ContainerUnit generates a systemd unit for the specified container.  Based
   161  // on the options, the return value might be the entire unit or a file it has
   162  // been written to.
   163  func ContainerUnit(ctr *libpod.Container, options entities.GenerateSystemdOptions) (string, string, error) {
   164  	info, err := generateContainerInfo(ctr, options)
   165  	if err != nil {
   166  		return "", "", err
   167  	}
   168  	content, err := executeContainerTemplate(info, options)
   169  	if err != nil {
   170  		return "", "", err
   171  	}
   172  	return info.ServiceName, content, nil
   173  }
   174  
   175  func generateContainerInfo(ctr *libpod.Container, options entities.GenerateSystemdOptions) (*containerInfo, error) {
   176  	stopTimeout := ctr.StopTimeout()
   177  	if options.StopTimeout != nil {
   178  		stopTimeout = *options.StopTimeout
   179  	}
   180  
   181  	startTimeout := uint(0)
   182  	if options.StartTimeout != nil {
   183  		startTimeout = *options.StartTimeout
   184  	}
   185  
   186  	config := ctr.Config()
   187  	conmonPidFile := config.ConmonPidFile
   188  	if conmonPidFile == "" {
   189  		return nil, errors.Errorf("conmon PID file path is empty, try to recreate the container with --conmon-pidfile flag")
   190  	}
   191  
   192  	createCommand := []string{}
   193  	if config.CreateCommand != nil {
   194  		createCommand = config.CreateCommand
   195  	} else if options.New {
   196  		return nil, errors.Errorf("cannot use --new on container %q: no create command found: only works on containers created directly with podman but not via REST API", ctr.ID())
   197  	}
   198  
   199  	nameOrID, serviceName := containerServiceName(ctr, options)
   200  
   201  	var runRoot string
   202  	if options.New {
   203  		runRoot = "%t/containers"
   204  	} else {
   205  		runRoot = ctr.Runtime().RunRoot()
   206  		if runRoot == "" {
   207  			return nil, errors.Errorf("could not lookup container's runroot: got empty string")
   208  		}
   209  	}
   210  
   211  	envs := config.Spec.Process.Env
   212  
   213  	info := containerInfo{
   214  		ServiceName:       serviceName,
   215  		ContainerNameOrID: nameOrID,
   216  		RestartPolicy:     define.DefaultRestartPolicy,
   217  		PIDFile:           conmonPidFile,
   218  		TimeoutStartSec:   startTimeout,
   219  		StopTimeout:       stopTimeout,
   220  		GenerateTimestamp: true,
   221  		CreateCommand:     createCommand,
   222  		RunRoot:           runRoot,
   223  		containerEnv:      envs,
   224  		Wants:             options.Wants,
   225  		After:             options.After,
   226  		Requires:          options.Requires,
   227  	}
   228  
   229  	return &info, nil
   230  }
   231  
   232  // containerServiceName returns the nameOrID and the service name of the
   233  // container.
   234  func containerServiceName(ctr *libpod.Container, options entities.GenerateSystemdOptions) (string, string) {
   235  	nameOrID := ctr.ID()
   236  	if options.Name {
   237  		nameOrID = ctr.Name()
   238  	}
   239  
   240  	serviceName := getServiceName(options.ContainerPrefix, options.Separator, nameOrID)
   241  
   242  	return nameOrID, serviceName
   243  }
   244  
   245  // setContainerNameForTemplate updates startCommand to contain the name argument with
   246  // a value that includes the identify specifier.
   247  // In case startCommand doesn't contain that argument it's added after "run" and its
   248  // value will be set to info.ServiceName concated with the identify specifier %i.
   249  func setContainerNameForTemplate(startCommand []string, info *containerInfo) ([]string, error) {
   250  	// find the index of "--name" in the command slice
   251  	nameIx := -1
   252  	for argIx, arg := range startCommand {
   253  		if arg == "--name" {
   254  			nameIx = argIx + 1
   255  			break
   256  		}
   257  		if strings.HasPrefix(arg, "--name=") {
   258  			nameIx = argIx
   259  			break
   260  		}
   261  	}
   262  	switch {
   263  	case nameIx == -1:
   264  		// if not found, add --name argument in the command slice before the "run" argument.
   265  		// it's assumed that the command slice contains this argument.
   266  		runIx := -1
   267  		for argIx, arg := range startCommand {
   268  			if arg == "run" {
   269  				runIx = argIx
   270  				break
   271  			}
   272  		}
   273  		if runIx == -1 {
   274  			return startCommand, fmt.Errorf("\"run\" is missing in the command arguments")
   275  		}
   276  		startCommand = append(startCommand[:runIx+1], startCommand[runIx:]...)
   277  		startCommand[runIx+1] = fmt.Sprintf("--name=%s-%%i", info.ServiceName)
   278  	default:
   279  		// append the identity specifier (%i) to the end of the --name value
   280  		startCommand[nameIx] = fmt.Sprintf("%s-%%i", startCommand[nameIx])
   281  	}
   282  	return startCommand, nil
   283  }
   284  
   285  func formatOptions(options []string) string {
   286  	var formatted strings.Builder
   287  	if len(options) == 0 {
   288  		return ""
   289  	}
   290  	formatted.WriteString(options[0])
   291  	for _, o := range options[1:] {
   292  		if strings.HasPrefix(o, "-") {
   293  			formatted.WriteString(" \\\n\t" + o)
   294  			continue
   295  		}
   296  		formatted.WriteString(" " + o)
   297  	}
   298  	return formatted.String()
   299  }
   300  
   301  // executeContainerTemplate executes the container template on the specified
   302  // containerInfo.  Note that the containerInfo is also post processed and
   303  // completed, which allows for an easier unit testing.
   304  func executeContainerTemplate(info *containerInfo, options entities.GenerateSystemdOptions) (string, error) {
   305  	if options.RestartPolicy != nil {
   306  		if err := validateRestartPolicy(*options.RestartPolicy); err != nil {
   307  			return "", err
   308  		}
   309  		info.RestartPolicy = *options.RestartPolicy
   310  	}
   311  
   312  	// Make sure the executable is set.
   313  	if info.Executable == "" {
   314  		executable, err := os.Executable()
   315  		if err != nil {
   316  			executable = "/usr/bin/podman"
   317  			logrus.Warnf("Could not obtain podman executable location, using default %s", executable)
   318  		}
   319  		info.Executable = executable
   320  	}
   321  
   322  	info.Type = "forking"
   323  	info.EnvVariable = define.EnvVariable
   324  	info.ExecStart = "{{{{.Executable}}}} start {{{{.ContainerNameOrID}}}}"
   325  	info.ExecStop = "{{{{.Executable}}}} stop {{{{if (ge .StopTimeout 0)}}}}-t {{{{.StopTimeout}}}}{{{{end}}}} {{{{.ContainerNameOrID}}}}"
   326  	info.ExecStopPost = "{{{{.Executable}}}} stop {{{{if (ge .StopTimeout 0)}}}}-t {{{{.StopTimeout}}}}{{{{end}}}} {{{{.ContainerNameOrID}}}}"
   327  
   328  	// Assemble the ExecStart command when creating a new container.
   329  	//
   330  	// Note that we cannot catch all corner cases here such that users
   331  	// *must* manually check the generated files.  A container might have
   332  	// been created via a Python script, which would certainly yield an
   333  	// invalid `info.CreateCommand`.  Hence, we're doing a best effort unit
   334  	// generation and don't try aiming at completeness.
   335  	if options.New {
   336  		info.Type = "notify"
   337  		info.NotifyAccess = "all"
   338  		info.PIDFile = ""
   339  		info.ContainerIDFile = "%t/%n.ctr-id"
   340  		info.ExecStartPre = "/bin/rm -f {{{{.ContainerIDFile}}}}"
   341  		info.ExecStop = "{{{{.Executable}}}} stop --ignore --cidfile={{{{.ContainerIDFile}}}}"
   342  		info.ExecStopPost = "{{{{.Executable}}}} rm -f --ignore --cidfile={{{{.ContainerIDFile}}}}"
   343  		// The create command must at least have three arguments:
   344  		// 	/usr/bin/podman run $IMAGE
   345  		index := 0
   346  		for i, arg := range info.CreateCommand {
   347  			if arg == "run" || arg == "create" {
   348  				index = i + 1
   349  				break
   350  			}
   351  		}
   352  		if index == 0 {
   353  			return "", errors.Errorf("container's create command is too short or invalid: %v", info.CreateCommand)
   354  		}
   355  		// We're hard-coding the first five arguments and append the
   356  		// CreateCommand with a stripped command and subcommand.
   357  		startCommand := []string{info.Executable}
   358  		if index > 2 {
   359  			// include root flags
   360  			info.RootFlags = strings.Join(escapeSystemdArguments(info.CreateCommand[1:index-1]), " ")
   361  			startCommand = append(startCommand, info.CreateCommand[1:index-1]...)
   362  		}
   363  		startCommand = append(startCommand,
   364  			"run",
   365  			"--cidfile={{{{.ContainerIDFile}}}}",
   366  			"--cgroups=no-conmon",
   367  			"--rm",
   368  		)
   369  		remainingCmd := info.CreateCommand[index:]
   370  		// Presence check for certain flags/options.
   371  		fs := pflag.NewFlagSet("args", pflag.ContinueOnError)
   372  		fs.ParseErrorsWhitelist.UnknownFlags = true
   373  		fs.Usage = func() {}
   374  		fs.SetInterspersed(false)
   375  		fs.BoolP("detach", "d", false, "")
   376  		fs.String("name", "", "")
   377  		fs.Bool("replace", false, "")
   378  		fs.StringArrayP("env", "e", nil, "")
   379  		fs.String("sdnotify", "", "")
   380  		fs.String("restart", "", "")
   381  		if err := fs.Parse(remainingCmd); err != nil {
   382  			return "", fmt.Errorf("parsing remaining command-line arguments: %w", err)
   383  		}
   384  
   385  		remainingCmd = filterCommonContainerFlags(remainingCmd, fs.NArg())
   386  		// If the container is in a pod, make sure that the
   387  		// --pod-id-file is set correctly.
   388  		if info.Pod != nil {
   389  			podFlags := []string{"--pod-id-file", "{{{{.Pod.PodIDFile}}}}"}
   390  			startCommand = append(startCommand, podFlags...)
   391  			remainingCmd = filterPodFlags(remainingCmd, fs.NArg())
   392  		}
   393  
   394  		hasDetachParam, err := fs.GetBool("detach")
   395  		if err != nil {
   396  			return "", err
   397  		}
   398  		hasNameParam := fs.Lookup("name").Changed
   399  		hasReplaceParam, err := fs.GetBool("replace")
   400  		if err != nil {
   401  			return "", err
   402  		}
   403  
   404  		// Default to --sdnotify=conmon unless already set by the
   405  		// container.
   406  		hasSdnotifyParam := fs.Lookup("sdnotify").Changed
   407  		if !hasSdnotifyParam {
   408  			startCommand = append(startCommand, "--sdnotify=conmon")
   409  		}
   410  
   411  		if !hasDetachParam {
   412  			// Enforce detaching
   413  			//
   414  			// since we use systemd `Type=forking` service @see
   415  			// https://www.freedesktop.org/software/systemd/man/systemd.service.html#Type=
   416  			// when we generated systemd service file with the
   417  			// --new param, `ExecStart` will have `/usr/bin/podman
   418  			// run ...` if `info.CreateCommand` has no `-d` or
   419  			// `--detach` param, podman will run the container in
   420  			// default attached mode, as a result, `systemd start`
   421  			// will wait the `podman run` command exit until failed
   422  			// with timeout error.
   423  			startCommand = append(startCommand, "-d")
   424  
   425  			if fs.Changed("detach") {
   426  				// this can only happen if --detach=false is set
   427  				// in that case we need to remove it otherwise we
   428  				// would overwrite the previous detach arg to false
   429  				remainingCmd = removeDetachArg(remainingCmd, fs.NArg())
   430  			}
   431  		}
   432  		if hasNameParam && !hasReplaceParam {
   433  			// Enforce --replace for named containers.  This will
   434  			// make systemd units more robust as it allows them to
   435  			// start after system crashes (see
   436  			// github.com/containers/podman/issues/5485).
   437  			startCommand = append(startCommand, "--replace")
   438  
   439  			if fs.Changed("replace") {
   440  				// this can only happen if --replace=false is set
   441  				// in that case we need to remove it otherwise we
   442  				// would overwrite the previous replace arg to false
   443  				remainingCmd = removeReplaceArg(remainingCmd, fs.NArg())
   444  			}
   445  		}
   446  
   447  		// Unless the user explicitly set a restart policy, check
   448  		// whether the container was created with a custom one and use
   449  		// it instead of the default.
   450  		if options.RestartPolicy == nil {
   451  			restartPolicy, err := fs.GetString("restart")
   452  			if err != nil {
   453  				return "", err
   454  			}
   455  			if restartPolicy != "" {
   456  				if strings.HasPrefix(restartPolicy, "on-failure:") {
   457  					// Special case --restart=on-failure:5
   458  					spl := strings.Split(restartPolicy, ":")
   459  					restartPolicy = spl[0]
   460  					info.StartLimitBurst = spl[1]
   461  				} else if restartPolicy == libpodDefine.RestartPolicyUnlessStopped {
   462  					restartPolicy = libpodDefine.RestartPolicyAlways
   463  				}
   464  				info.RestartPolicy = restartPolicy
   465  			}
   466  		}
   467  
   468  		envs, err := fs.GetStringArray("env")
   469  		if err != nil {
   470  			return "", err
   471  		}
   472  		for _, env := range envs {
   473  			// if env arg does not contain a equal sign we have to add the envar to the unit
   474  			// because it does try to red the value from the environment
   475  			if !strings.Contains(env, "=") {
   476  				for _, containerEnv := range info.containerEnv {
   477  					split := strings.SplitN(containerEnv, "=", 2)
   478  					if split[0] == env {
   479  						info.ExtraEnvs = append(info.ExtraEnvs, escapeSystemdArg(containerEnv))
   480  					}
   481  				}
   482  			}
   483  		}
   484  
   485  		startCommand = append(startCommand, remainingCmd...)
   486  		startCommand = escapeSystemdArguments(startCommand)
   487  		if options.TemplateUnitFile {
   488  			info.IdentifySpecifier = true
   489  			startCommand, err = setContainerNameForTemplate(startCommand, info)
   490  			if err != nil {
   491  				return "", err
   492  			}
   493  		}
   494  		info.ExecStart = formatOptions(startCommand)
   495  	}
   496  	info.TimeoutStopSec = minTimeoutStopSec + info.StopTimeout
   497  
   498  	if info.PodmanVersion == "" {
   499  		info.PodmanVersion = version.Version.String()
   500  	}
   501  
   502  	if options.NoHeader {
   503  		info.GenerateNoHeader = true
   504  		info.GenerateTimestamp = false
   505  	}
   506  
   507  	if info.GenerateTimestamp {
   508  		info.TimeStamp = fmt.Sprintf("%v", time.Now().Format(time.UnixDate))
   509  	}
   510  	// Sort the slices to assure a deterministic output.
   511  	sort.Strings(info.BoundToServices)
   512  
   513  	// Generate the template and compile it.
   514  	//
   515  	// Note that we need a two-step generation process to allow for fields
   516  	// embedding other fields.  This way we can replace `A -> B -> C` and
   517  	// make the code easier to maintain at the cost of a slightly slower
   518  	// generation.  That's especially needed for embedding the PID and ID
   519  	// files in other fields which will eventually get replaced in the 2nd
   520  	// template execution.
   521  	templ, err := template.New("container_template").Delims("{{{{", "}}}}").Parse(containerTemplate)
   522  	if err != nil {
   523  		return "", errors.Wrap(err, "error parsing systemd service template")
   524  	}
   525  
   526  	var buf bytes.Buffer
   527  	if err := templ.Execute(&buf, info); err != nil {
   528  		return "", err
   529  	}
   530  
   531  	// Now parse the generated template (i.e., buf) and execute it.
   532  	templ, err = template.New("container_template").Delims("{{{{", "}}}}").Parse(buf.String())
   533  	if err != nil {
   534  		return "", errors.Wrap(err, "error parsing systemd service template")
   535  	}
   536  
   537  	buf = bytes.Buffer{}
   538  	if err := templ.Execute(&buf, info); err != nil {
   539  		return "", err
   540  	}
   541  
   542  	return buf.String(), nil
   543  }