github.com/AbhinandanKurakure/podman/v3@v3.4.10/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/containers/podman/v3/libpod"
    13  	libpodDefine "github.com/containers/podman/v3/libpod/define"
    14  	"github.com/containers/podman/v3/pkg/domain/entities"
    15  	"github.com/containers/podman/v3/pkg/systemd/define"
    16  	"github.com/containers/podman/v3/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  	// TimeoutStopSec of the unit.
    77  	TimeoutStopSec uint
    78  	// ExecStop of the unit.
    79  	ExecStop string
    80  	// ExecStopPost of the unit.
    81  	ExecStopPost string
    82  	// Removes autogenerated by Podman and timestamp if set to true
    83  	GenerateNoHeader bool
    84  	// If not nil, the container is part of the pod.  We can use the
    85  	// podInfo to extract the relevant data.
    86  	Pod *podInfo
    87  	// Location of the GraphRoot for the container.  Required for ensuring the
    88  	// volume has finished mounting when coming online at boot.
    89  	GraphRoot string
    90  	// Location of the RunRoot for the container.  Required for ensuring the tmpfs
    91  	// or volume exists and is mounted when coming online at boot.
    92  	RunRoot string
    93  }
    94  
    95  const containerTemplate = headerTemplate + `
    96  {{{{- if .BoundToServices}}}}
    97  BindsTo={{{{- range $index, $value := .BoundToServices -}}}}{{{{if $index}}}} {{{{end}}}}{{{{ $value }}}}.service{{{{end}}}}
    98  After={{{{- range $index, $value := .BoundToServices -}}}}{{{{if $index}}}} {{{{end}}}}{{{{ $value }}}}.service{{{{end}}}}
    99  {{{{- end}}}}
   100  
   101  [Service]
   102  Environment={{{{.EnvVariable}}}}=%n
   103  {{{{- if .ExtraEnvs}}}}
   104  Environment={{{{- range $index, $value := .ExtraEnvs -}}}}{{{{if $index}}}} {{{{end}}}}{{{{ $value }}}}{{{{end}}}}
   105  {{{{- end}}}}
   106  Restart={{{{.RestartPolicy}}}}
   107  {{{{- if .StartLimitBurst}}}}
   108  StartLimitBurst={{{{.StartLimitBurst}}}}
   109  {{{{- end}}}}
   110  TimeoutStopSec={{{{.TimeoutStopSec}}}}
   111  {{{{- if .ExecStartPre}}}}
   112  ExecStartPre={{{{.ExecStartPre}}}}
   113  {{{{- end}}}}
   114  ExecStart={{{{.ExecStart}}}}
   115  {{{{- if .ExecStop}}}}
   116  ExecStop={{{{.ExecStop}}}}
   117  {{{{- end}}}}
   118  {{{{- if .ExecStopPost}}}}
   119  ExecStopPost={{{{.ExecStopPost}}}}
   120  {{{{- end}}}}
   121  {{{{- if .PIDFile}}}}
   122  PIDFile={{{{.PIDFile}}}}
   123  {{{{- end}}}}
   124  Type={{{{.Type}}}}
   125  {{{{- if .NotifyAccess}}}}
   126  NotifyAccess={{{{.NotifyAccess}}}}
   127  {{{{- end}}}}
   128  
   129  [Install]
   130  WantedBy=default.target
   131  `
   132  
   133  // ContainerUnit generates a systemd unit for the specified container.  Based
   134  // on the options, the return value might be the entire unit or a file it has
   135  // been written to.
   136  func ContainerUnit(ctr *libpod.Container, options entities.GenerateSystemdOptions) (string, string, error) {
   137  	info, err := generateContainerInfo(ctr, options)
   138  	if err != nil {
   139  		return "", "", err
   140  	}
   141  	content, err := executeContainerTemplate(info, options)
   142  	if err != nil {
   143  		return "", "", err
   144  	}
   145  	return info.ServiceName, content, nil
   146  }
   147  
   148  func generateContainerInfo(ctr *libpod.Container, options entities.GenerateSystemdOptions) (*containerInfo, error) {
   149  	timeout := ctr.StopTimeout()
   150  	if options.StopTimeout != nil {
   151  		timeout = *options.StopTimeout
   152  	}
   153  
   154  	config := ctr.Config()
   155  	conmonPidFile := config.ConmonPidFile
   156  	if conmonPidFile == "" {
   157  		return nil, errors.Errorf("conmon PID file path is empty, try to recreate the container with --conmon-pidfile flag")
   158  	}
   159  
   160  	createCommand := []string{}
   161  	if config.CreateCommand != nil {
   162  		createCommand = config.CreateCommand
   163  	} else if options.New {
   164  		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())
   165  	}
   166  
   167  	nameOrID, serviceName := containerServiceName(ctr, options)
   168  
   169  	var runRoot string
   170  	if options.New {
   171  		runRoot = "%t/containers"
   172  	} else {
   173  		runRoot = ctr.Runtime().RunRoot()
   174  		if runRoot == "" {
   175  			return nil, errors.Errorf("could not lookup container's runroot: got empty string")
   176  		}
   177  	}
   178  
   179  	envs := config.Spec.Process.Env
   180  
   181  	info := containerInfo{
   182  		ServiceName:       serviceName,
   183  		ContainerNameOrID: nameOrID,
   184  		RestartPolicy:     define.DefaultRestartPolicy,
   185  		PIDFile:           conmonPidFile,
   186  		StopTimeout:       timeout,
   187  		GenerateTimestamp: true,
   188  		CreateCommand:     createCommand,
   189  		RunRoot:           runRoot,
   190  		containerEnv:      envs,
   191  	}
   192  
   193  	return &info, nil
   194  }
   195  
   196  // containerServiceName returns the nameOrID and the service name of the
   197  // container.
   198  func containerServiceName(ctr *libpod.Container, options entities.GenerateSystemdOptions) (string, string) {
   199  	nameOrID := ctr.ID()
   200  	if options.Name {
   201  		nameOrID = ctr.Name()
   202  	}
   203  	serviceName := fmt.Sprintf("%s%s%s", options.ContainerPrefix, options.Separator, nameOrID)
   204  	return nameOrID, serviceName
   205  }
   206  
   207  // executeContainerTemplate executes the container template on the specified
   208  // containerInfo.  Note that the containerInfo is also post processed and
   209  // completed, which allows for an easier unit testing.
   210  func executeContainerTemplate(info *containerInfo, options entities.GenerateSystemdOptions) (string, error) {
   211  	if options.RestartPolicy != nil {
   212  		if err := validateRestartPolicy(*options.RestartPolicy); err != nil {
   213  			return "", err
   214  		}
   215  		info.RestartPolicy = *options.RestartPolicy
   216  	}
   217  
   218  	// Make sure the executable is set.
   219  	if info.Executable == "" {
   220  		executable, err := os.Executable()
   221  		if err != nil {
   222  			executable = "/usr/bin/podman"
   223  			logrus.Warnf("Could not obtain podman executable location, using default %s", executable)
   224  		}
   225  		info.Executable = executable
   226  	}
   227  
   228  	info.Type = "forking"
   229  	info.EnvVariable = define.EnvVariable
   230  	info.ExecStart = "{{{{.Executable}}}} start {{{{.ContainerNameOrID}}}}"
   231  	info.ExecStop = "{{{{.Executable}}}} stop {{{{if (ge .StopTimeout 0)}}}}-t {{{{.StopTimeout}}}}{{{{end}}}} {{{{.ContainerNameOrID}}}}"
   232  	info.ExecStopPost = "{{{{.Executable}}}} stop {{{{if (ge .StopTimeout 0)}}}}-t {{{{.StopTimeout}}}}{{{{end}}}} {{{{.ContainerNameOrID}}}}"
   233  
   234  	// Assemble the ExecStart command when creating a new container.
   235  	//
   236  	// Note that we cannot catch all corner cases here such that users
   237  	// *must* manually check the generated files.  A container might have
   238  	// been created via a Python script, which would certainly yield an
   239  	// invalid `info.CreateCommand`.  Hence, we're doing a best effort unit
   240  	// generation and don't try aiming at completeness.
   241  	if options.New {
   242  		info.Type = "notify"
   243  		info.NotifyAccess = "all"
   244  		info.PIDFile = ""
   245  		info.ContainerIDFile = "%t/%n.ctr-id"
   246  		info.ExecStartPre = "/bin/rm -f {{{{.ContainerIDFile}}}}"
   247  		info.ExecStop = "{{{{.Executable}}}} stop --ignore --cidfile={{{{.ContainerIDFile}}}}"
   248  		info.ExecStopPost = "{{{{.Executable}}}} rm -f --ignore --cidfile={{{{.ContainerIDFile}}}}"
   249  		// The create command must at least have three arguments:
   250  		// 	/usr/bin/podman run $IMAGE
   251  		index := 0
   252  		for i, arg := range info.CreateCommand {
   253  			if arg == "run" || arg == "create" {
   254  				index = i + 1
   255  				break
   256  			}
   257  		}
   258  		if index == 0 {
   259  			return "", errors.Errorf("container's create command is too short or invalid: %v", info.CreateCommand)
   260  		}
   261  		// We're hard-coding the first five arguments and append the
   262  		// CreateCommand with a stripped command and subcommand.
   263  		startCommand := []string{info.Executable}
   264  		if index > 2 {
   265  			// include root flags
   266  			info.RootFlags = strings.Join(escapeSystemdArguments(info.CreateCommand[1:index-1]), " ")
   267  			startCommand = append(startCommand, info.CreateCommand[1:index-1]...)
   268  		}
   269  		startCommand = append(startCommand,
   270  			"run",
   271  			"--cidfile={{{{.ContainerIDFile}}}}",
   272  			"--cgroups=no-conmon",
   273  			"--rm",
   274  		)
   275  		remainingCmd := info.CreateCommand[index:]
   276  
   277  		// Presence check for certain flags/options.
   278  		fs := pflag.NewFlagSet("args", pflag.ContinueOnError)
   279  		fs.ParseErrorsWhitelist.UnknownFlags = true
   280  		fs.Usage = func() {}
   281  		fs.SetInterspersed(false)
   282  		fs.BoolP("detach", "d", false, "")
   283  		fs.String("name", "", "")
   284  		fs.Bool("replace", false, "")
   285  		fs.StringArrayP("env", "e", nil, "")
   286  		fs.String("sdnotify", "", "")
   287  		fs.String("restart", "", "")
   288  		fs.Parse(remainingCmd)
   289  
   290  		remainingCmd = filterCommonContainerFlags(remainingCmd, fs.NArg())
   291  		// If the container is in a pod, make sure that the
   292  		// --pod-id-file is set correctly.
   293  		if info.Pod != nil {
   294  			podFlags := []string{"--pod-id-file", "{{{{.Pod.PodIDFile}}}}"}
   295  			startCommand = append(startCommand, podFlags...)
   296  			remainingCmd = filterPodFlags(remainingCmd, fs.NArg())
   297  		}
   298  
   299  		hasDetachParam, err := fs.GetBool("detach")
   300  		if err != nil {
   301  			return "", err
   302  		}
   303  		hasNameParam := fs.Lookup("name").Changed
   304  		hasReplaceParam, err := fs.GetBool("replace")
   305  		if err != nil {
   306  			return "", err
   307  		}
   308  
   309  		// Default to --sdnotify=conmon unless already set by the
   310  		// container.
   311  		hasSdnotifyParam := fs.Lookup("sdnotify").Changed
   312  		if !hasSdnotifyParam {
   313  			startCommand = append(startCommand, "--sdnotify=conmon")
   314  		}
   315  
   316  		if !hasDetachParam {
   317  			// Enforce detaching
   318  			//
   319  			// since we use systemd `Type=forking` service @see
   320  			// https://www.freedesktop.org/software/systemd/man/systemd.service.html#Type=
   321  			// when we generated systemd service file with the
   322  			// --new param, `ExecStart` will have `/usr/bin/podman
   323  			// run ...` if `info.CreateCommand` has no `-d` or
   324  			// `--detach` param, podman will run the container in
   325  			// default attached mode, as a result, `systemd start`
   326  			// will wait the `podman run` command exit until failed
   327  			// with timeout error.
   328  			startCommand = append(startCommand, "-d")
   329  
   330  			if fs.Changed("detach") {
   331  				// this can only happen if --detach=false is set
   332  				// in that case we need to remove it otherwise we
   333  				// would overwrite the previous detach arg to false
   334  				remainingCmd = removeDetachArg(remainingCmd, fs.NArg())
   335  			}
   336  		}
   337  		if hasNameParam && !hasReplaceParam {
   338  			// Enforce --replace for named containers.  This will
   339  			// make systemd units more robust as it allows them to
   340  			// start after system crashes (see
   341  			// github.com/containers/podman/issues/5485).
   342  			startCommand = append(startCommand, "--replace")
   343  
   344  			if fs.Changed("replace") {
   345  				// this can only happen if --replace=false is set
   346  				// in that case we need to remove it otherwise we
   347  				// would overwrite the previous replace arg to false
   348  				remainingCmd = removeReplaceArg(remainingCmd, fs.NArg())
   349  			}
   350  		}
   351  
   352  		// Unless the user explicitly set a restart policy, check
   353  		// whether the container was created with a custom one and use
   354  		// it instead of the default.
   355  		if options.RestartPolicy == nil {
   356  			restartPolicy, err := fs.GetString("restart")
   357  			if err != nil {
   358  				return "", err
   359  			}
   360  			if restartPolicy != "" {
   361  				if strings.HasPrefix(restartPolicy, "on-failure:") {
   362  					// Special case --restart=on-failure:5
   363  					spl := strings.Split(restartPolicy, ":")
   364  					restartPolicy = spl[0]
   365  					info.StartLimitBurst = spl[1]
   366  				} else if restartPolicy == libpodDefine.RestartPolicyUnlessStopped {
   367  					restartPolicy = libpodDefine.RestartPolicyAlways
   368  				}
   369  				info.RestartPolicy = restartPolicy
   370  			}
   371  		}
   372  
   373  		envs, err := fs.GetStringArray("env")
   374  		if err != nil {
   375  			return "", err
   376  		}
   377  		for _, env := range envs {
   378  			// if env arg does not contain a equal sign we have to add the envar to the unit
   379  			// because it does try to red the value from the environment
   380  			if !strings.Contains(env, "=") {
   381  				for _, containerEnv := range info.containerEnv {
   382  					split := strings.SplitN(containerEnv, "=", 2)
   383  					if split[0] == env {
   384  						info.ExtraEnvs = append(info.ExtraEnvs, escapeSystemdArg(containerEnv))
   385  					}
   386  				}
   387  			}
   388  		}
   389  
   390  		startCommand = append(startCommand, remainingCmd...)
   391  		startCommand = escapeSystemdArguments(startCommand)
   392  		info.ExecStart = strings.Join(startCommand, " ")
   393  	}
   394  
   395  	info.TimeoutStopSec = minTimeoutStopSec + info.StopTimeout
   396  
   397  	if info.PodmanVersion == "" {
   398  		info.PodmanVersion = version.Version.String()
   399  	}
   400  
   401  	if options.NoHeader {
   402  		info.GenerateNoHeader = true
   403  		info.GenerateTimestamp = false
   404  	}
   405  
   406  	if info.GenerateTimestamp {
   407  		info.TimeStamp = fmt.Sprintf("%v", time.Now().Format(time.UnixDate))
   408  	}
   409  	// Sort the slices to assure a deterministic output.
   410  	sort.Strings(info.BoundToServices)
   411  
   412  	// Generate the template and compile it.
   413  	//
   414  	// Note that we need a two-step generation process to allow for fields
   415  	// embedding other fields.  This way we can replace `A -> B -> C` and
   416  	// make the code easier to maintain at the cost of a slightly slower
   417  	// generation.  That's especially needed for embedding the PID and ID
   418  	// files in other fields which will eventually get replaced in the 2nd
   419  	// template execution.
   420  	templ, err := template.New("container_template").Delims("{{{{", "}}}}").Parse(containerTemplate)
   421  	if err != nil {
   422  		return "", errors.Wrap(err, "error parsing systemd service template")
   423  	}
   424  
   425  	var buf bytes.Buffer
   426  	if err := templ.Execute(&buf, info); err != nil {
   427  		return "", err
   428  	}
   429  
   430  	// Now parse the generated template (i.e., buf) and execute it.
   431  	templ, err = template.New("container_template").Delims("{{{{", "}}}}").Parse(buf.String())
   432  	if err != nil {
   433  		return "", errors.Wrap(err, "error parsing systemd service template")
   434  	}
   435  
   436  	buf = bytes.Buffer{}
   437  	if err := templ.Execute(&buf, info); err != nil {
   438  		return "", err
   439  	}
   440  
   441  	return buf.String(), nil
   442  }