github.com/containers/podman/v2@v2.2.2-0.20210501105131-c1e07d070c4c/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/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  // containerInfo contains data required for generating a container's systemd
    20  // unit file.
    21  type containerInfo struct {
    22  	// ServiceName of the systemd service.
    23  	ServiceName string
    24  	// Name or ID of the container.
    25  	ContainerNameOrID 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  	// ContainerIDFile to be used in the unit.
    35  	ContainerIDFile string
    36  	// GenerateTimestamp, if set the generated unit file has a time stamp.
    37  	GenerateTimestamp bool
    38  	// BoundToServices are the services this service binds to.  Note that this
    39  	// service runs after them.
    40  	BoundToServices []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  	// EnvVariable is generate.EnvVariable and must not be set.
    53  	EnvVariable string
    54  	// ExecStartPre of the unit.
    55  	ExecStartPre string
    56  	// ExecStart of the unit.
    57  	ExecStart string
    58  	// ExecStop of the unit.
    59  	ExecStop string
    60  	// ExecStopPost of the unit.
    61  	ExecStopPost string
    62  
    63  	// If not nil, the container is part of the pod.  We can use the
    64  	// podInfo to extract the relevant data.
    65  	pod *podInfo
    66  }
    67  
    68  const containerTemplate = headerTemplate + `
    69  {{- if .BoundToServices}}
    70  BindsTo={{- range $index, $value := .BoundToServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}}
    71  After={{- range $index, $value := .BoundToServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}}
    72  {{- end}}
    73  
    74  [Service]
    75  Environment={{.EnvVariable}}=%n
    76  Restart={{.RestartPolicy}}
    77  {{- if .ExecStartPre}}
    78  ExecStartPre={{.ExecStartPre}}
    79  {{- end}}
    80  ExecStart={{.ExecStart}}
    81  ExecStop={{.ExecStop}}
    82  ExecStopPost={{.ExecStopPost}}
    83  PIDFile={{.PIDFile}}
    84  KillMode=none
    85  Type=forking
    86  
    87  [Install]
    88  WantedBy=multi-user.target default.target
    89  `
    90  
    91  // ContainerUnit generates a systemd unit for the specified container.  Based
    92  // on the options, the return value might be the entire unit or a file it has
    93  // been written to.
    94  func ContainerUnit(ctr *libpod.Container, options entities.GenerateSystemdOptions) (string, string, error) {
    95  	info, err := generateContainerInfo(ctr, options)
    96  	if err != nil {
    97  		return "", "", err
    98  	}
    99  	content, err := executeContainerTemplate(info, options)
   100  	if err != nil {
   101  		return "", "", err
   102  	}
   103  	return info.ServiceName, content, nil
   104  }
   105  
   106  func generateContainerInfo(ctr *libpod.Container, options entities.GenerateSystemdOptions) (*containerInfo, error) {
   107  	timeout := ctr.StopTimeout()
   108  	if options.StopTimeout != nil {
   109  		timeout = *options.StopTimeout
   110  	}
   111  
   112  	config := ctr.Config()
   113  	conmonPidFile := config.ConmonPidFile
   114  	if conmonPidFile == "" {
   115  		return nil, errors.Errorf("conmon PID file path is empty, try to recreate the container with --conmon-pidfile flag")
   116  	}
   117  
   118  	createCommand := []string{}
   119  	if config.CreateCommand != nil {
   120  		createCommand = config.CreateCommand
   121  	} else if options.New {
   122  		return nil, errors.Errorf("cannot use --new on container %q: no create command found", ctr.ID())
   123  	}
   124  
   125  	nameOrID, serviceName := containerServiceName(ctr, options)
   126  
   127  	info := containerInfo{
   128  		ServiceName:       serviceName,
   129  		ContainerNameOrID: nameOrID,
   130  		RestartPolicy:     options.RestartPolicy,
   131  		PIDFile:           conmonPidFile,
   132  		StopTimeout:       timeout,
   133  		GenerateTimestamp: true,
   134  		CreateCommand:     createCommand,
   135  	}
   136  
   137  	return &info, nil
   138  }
   139  
   140  // containerServiceName returns the nameOrID and the service name of the
   141  // container.
   142  func containerServiceName(ctr *libpod.Container, options entities.GenerateSystemdOptions) (string, string) {
   143  	nameOrID := ctr.ID()
   144  	if options.Name {
   145  		nameOrID = ctr.Name()
   146  	}
   147  	serviceName := fmt.Sprintf("%s%s%s", options.ContainerPrefix, options.Separator, nameOrID)
   148  	return nameOrID, serviceName
   149  }
   150  
   151  // executeContainerTemplate executes the container template on the specified
   152  // containerInfo.  Note that the containerInfo is also post processed and
   153  // completed, which allows for an easier unit testing.
   154  func executeContainerTemplate(info *containerInfo, options entities.GenerateSystemdOptions) (string, error) {
   155  	if err := validateRestartPolicy(info.RestartPolicy); err != nil {
   156  		return "", err
   157  	}
   158  
   159  	// Make sure the executable is set.
   160  	if info.Executable == "" {
   161  		executable, err := os.Executable()
   162  		if err != nil {
   163  			executable = "/usr/bin/podman"
   164  			logrus.Warnf("Could not obtain podman executable location, using default %s", executable)
   165  		}
   166  		info.Executable = executable
   167  	}
   168  
   169  	info.EnvVariable = EnvVariable
   170  	info.ExecStart = "{{.Executable}} start {{.ContainerNameOrID}}"
   171  	info.ExecStop = "{{.Executable}} stop {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}} {{.ContainerNameOrID}}"
   172  	info.ExecStopPost = "{{.Executable}} stop {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}} {{.ContainerNameOrID}}"
   173  
   174  	// Assemble the ExecStart command when creating a new container.
   175  	//
   176  	// Note that we cannot catch all corner cases here such that users
   177  	// *must* manually check the generated files.  A container might have
   178  	// been created via a Python script, which would certainly yield an
   179  	// invalid `info.CreateCommand`.  Hence, we're doing a best effort unit
   180  	// generation and don't try aiming at completeness.
   181  	if options.New {
   182  		info.PIDFile = "%t/" + info.ServiceName + ".pid"
   183  		info.ContainerIDFile = "%t/" + info.ServiceName + ".ctr-id"
   184  		// The create command must at least have three arguments:
   185  		// 	/usr/bin/podman run $IMAGE
   186  		index := 2
   187  		if info.CreateCommand[1] == "container" {
   188  			index = 3
   189  		}
   190  		if len(info.CreateCommand) < index+1 {
   191  			return "", errors.Errorf("container's create command is too short or invalid: %v", info.CreateCommand)
   192  		}
   193  		// We're hard-coding the first five arguments and append the
   194  		// CreateCommand with a stripped command and subcomand.
   195  		startCommand := []string{
   196  			info.Executable,
   197  			"run",
   198  			"--conmon-pidfile", "{{.PIDFile}}",
   199  			"--cidfile", "{{.ContainerIDFile}}",
   200  			"--cgroups=no-conmon",
   201  		}
   202  		// If the container is in a pod, make sure that the
   203  		// --pod-id-file is set correctly.
   204  		if info.pod != nil {
   205  			podFlags := []string{"--pod-id-file", info.pod.PodIDFile}
   206  			startCommand = append(startCommand, podFlags...)
   207  			info.CreateCommand = filterPodFlags(info.CreateCommand)
   208  		}
   209  
   210  		// Presence check for certain flags/options.
   211  		hasDetachParam := false
   212  		hasNameParam := false
   213  		hasReplaceParam := false
   214  		for _, p := range info.CreateCommand[index:] {
   215  			switch p {
   216  			case "--detach", "-d":
   217  				hasDetachParam = true
   218  			case "--name":
   219  				hasNameParam = true
   220  			case "--replace":
   221  				hasReplaceParam = true
   222  			}
   223  			if strings.HasPrefix(p, "--name=") {
   224  				hasNameParam = true
   225  			}
   226  		}
   227  
   228  		if !hasDetachParam {
   229  			// Enforce detaching
   230  			//
   231  			// since we use systemd `Type=forking` service @see
   232  			// https://www.freedesktop.org/software/systemd/man/systemd.service.html#Type=
   233  			// when we generated systemd service file with the
   234  			// --new param, `ExecStart` will have `/usr/bin/podman
   235  			// run ...` if `info.CreateCommand` has no `-d` or
   236  			// `--detach` param, podman will run the container in
   237  			// default attached mode, as a result, `systemd start`
   238  			// will wait the `podman run` command exit until failed
   239  			// with timeout error.
   240  			startCommand = append(startCommand, "-d")
   241  		}
   242  		if hasNameParam && !hasReplaceParam {
   243  			// Enforce --replace for named containers.  This will
   244  			// make systemd units more robuts as it allows them to
   245  			// start after system crashes (see
   246  			// github.com/containers/podman/issues/5485).
   247  			startCommand = append(startCommand, "--replace")
   248  		}
   249  		startCommand = append(startCommand, info.CreateCommand[index:]...)
   250  		startCommand = quoteArguments(startCommand)
   251  
   252  		info.ExecStartPre = "/bin/rm -f {{.PIDFile}} {{.ContainerIDFile}}"
   253  		info.ExecStart = strings.Join(startCommand, " ")
   254  		info.ExecStop = "{{.Executable}} stop --ignore --cidfile {{.ContainerIDFile}} {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}}"
   255  		info.ExecStopPost = "{{.Executable}} rm --ignore -f --cidfile {{.ContainerIDFile}}"
   256  	}
   257  
   258  	if info.PodmanVersion == "" {
   259  		info.PodmanVersion = version.Version.String()
   260  	}
   261  	if info.GenerateTimestamp {
   262  		info.TimeStamp = fmt.Sprintf("%v", time.Now().Format(time.UnixDate))
   263  	}
   264  
   265  	// Sort the slices to assure a deterministic output.
   266  	sort.Strings(info.BoundToServices)
   267  
   268  	// Generate the template and compile it.
   269  	//
   270  	// Note that we need a two-step generation process to allow for fields
   271  	// embedding other fields.  This way we can replace `A -> B -> C` and
   272  	// make the code easier to maintain at the cost of a slightly slower
   273  	// generation.  That's especially needed for embedding the PID and ID
   274  	// files in other fields which will eventually get replaced in the 2nd
   275  	// template execution.
   276  	templ, err := template.New("container_template").Parse(containerTemplate)
   277  	if err != nil {
   278  		return "", errors.Wrap(err, "error parsing systemd service template")
   279  	}
   280  
   281  	var buf bytes.Buffer
   282  	if err := templ.Execute(&buf, info); err != nil {
   283  		return "", err
   284  	}
   285  
   286  	// Now parse the generated template (i.e., buf) and execute it.
   287  	templ, err = template.New("container_template").Parse(buf.String())
   288  	if err != nil {
   289  		return "", errors.Wrap(err, "error parsing systemd service template")
   290  	}
   291  
   292  	buf = bytes.Buffer{}
   293  	if err := templ.Execute(&buf, info); err != nil {
   294  		return "", err
   295  	}
   296  
   297  	return buf.String(), nil
   298  }