github.com/containers/libpod@v1.9.4-0.20220419124438-4284fd425507/pkg/systemd/generate/systemdgen.go (about)

     1  package generate
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"sort"
    10  	"strings"
    11  	"text/template"
    12  	"time"
    13  
    14  	"github.com/containers/libpod/version"
    15  	"github.com/pkg/errors"
    16  	"github.com/sirupsen/logrus"
    17  )
    18  
    19  // EnvVariable "PODMAN_SYSTEMD_UNIT" is set in all generated systemd units and
    20  // is set to the unit's (unique) name.
    21  const EnvVariable = "PODMAN_SYSTEMD_UNIT"
    22  
    23  // ContainerInfo contains data required for generating a container's systemd
    24  // unit file.
    25  type ContainerInfo struct {
    26  	// ServiceName of the systemd service.
    27  	ServiceName string
    28  	// Name or ID of the container.
    29  	ContainerName string
    30  	// InfraContainer of the pod.
    31  	InfraContainer string
    32  	// StopTimeout sets the timeout Podman waits before killing the container
    33  	// during service stop.
    34  	StopTimeout uint
    35  	// RestartPolicy of the systemd unit (e.g., no, on-failure, always).
    36  	RestartPolicy string
    37  	// PIDFile of the service. Required for forking services. Must point to the
    38  	// PID of the associated conmon process.
    39  	PIDFile string
    40  	// GenerateTimestamp, if set the generated unit file has a time stamp.
    41  	GenerateTimestamp bool
    42  	// BoundToServices are the services this service binds to.  Note that this
    43  	// service runs after them.
    44  	BoundToServices []string
    45  	// RequiredServices are services this service requires. Note that this
    46  	// service runs before them.
    47  	RequiredServices []string
    48  	// PodmanVersion for the header. Will be set internally. Will be auto-filled
    49  	// if left empty.
    50  	PodmanVersion string
    51  	// Executable is the path to the podman executable. Will be auto-filled if
    52  	// left empty.
    53  	Executable string
    54  	// TimeStamp at the time of creating the unit file. Will be set internally.
    55  	TimeStamp string
    56  	// New controls if a new container is created or if an existing one is started.
    57  	New bool
    58  	// CreateCommand is the full command plus arguments of the process the
    59  	// container has been created with.
    60  	CreateCommand []string
    61  	// RunCommand is a post-processed variant of CreateCommand and used for
    62  	// the ExecStart field in generic unit files.
    63  	RunCommand string
    64  	// EnvVariable is generate.EnvVariable and must not be set.
    65  	EnvVariable string
    66  }
    67  
    68  var restartPolicies = []string{"no", "on-success", "on-failure", "on-abnormal", "on-watchdog", "on-abort", "always"}
    69  
    70  // validateRestartPolicy checks that the user-provided policy is valid.
    71  func validateRestartPolicy(restart string) error {
    72  	for _, i := range restartPolicies {
    73  		if i == restart {
    74  			return nil
    75  		}
    76  	}
    77  	return errors.Errorf("%s is not a valid restart policy", restart)
    78  }
    79  
    80  const containerTemplate = `# {{.ServiceName}}.service
    81  # autogenerated by Podman {{.PodmanVersion}}
    82  {{- if .TimeStamp}}
    83  # {{.TimeStamp}}
    84  {{- end}}
    85  
    86  [Unit]
    87  Description=Podman {{.ServiceName}}.service
    88  Documentation=man:podman-generate-systemd(1)
    89  Wants=network.target
    90  After=network-online.target
    91  {{- if .BoundToServices}}
    92  RefuseManualStart=yes
    93  RefuseManualStop=yes
    94  BindsTo={{- range $index, $value := .BoundToServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}}
    95  After={{- range $index, $value := .BoundToServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}}
    96  {{- end}}
    97  {{- if .RequiredServices}}
    98  Requires={{- range $index, $value := .RequiredServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}}
    99  Before={{- range $index, $value := .RequiredServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}}
   100  {{- end}}
   101  
   102  [Service]
   103  Environment={{.EnvVariable}}=%n
   104  Restart={{.RestartPolicy}}
   105  {{- if .New}}
   106  ExecStartPre=/usr/bin/rm -f %t/%n-pid %t/%n-cid
   107  ExecStart={{.RunCommand}}
   108  ExecStop={{.Executable}} stop --ignore --cidfile %t/%n-cid {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}}
   109  ExecStopPost={{.Executable}} rm --ignore -f --cidfile %t/%n-cid
   110  PIDFile=%t/%n-pid
   111  {{- else}}
   112  ExecStart={{.Executable}} start {{.ContainerName}}
   113  ExecStop={{.Executable}} stop {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}} {{.ContainerName}}
   114  PIDFile={{.PIDFile}}
   115  {{- end}}
   116  KillMode=none
   117  Type=forking
   118  
   119  [Install]
   120  WantedBy=multi-user.target default.target`
   121  
   122  // Options include different options to control the unit file generation.
   123  type Options struct {
   124  	// When set, generate service files in the current working directory and
   125  	// return the paths to these files instead of returning all contents in one
   126  	// big string.
   127  	Files bool
   128  	// New controls if a new container is created or if an existing one is started.
   129  	New bool
   130  }
   131  
   132  // CreateContainerSystemdUnit creates a systemd unit file for a container.
   133  func CreateContainerSystemdUnit(info *ContainerInfo, opts Options) (string, error) {
   134  	if err := validateRestartPolicy(info.RestartPolicy); err != nil {
   135  		return "", err
   136  	}
   137  
   138  	// Make sure the executable is set.
   139  	if info.Executable == "" {
   140  		executable, err := os.Executable()
   141  		if err != nil {
   142  			executable = "/usr/bin/podman"
   143  			logrus.Warnf("Could not obtain podman executable location, using default %s", executable)
   144  		}
   145  		info.Executable = executable
   146  	}
   147  
   148  	info.EnvVariable = EnvVariable
   149  
   150  	// Assemble the ExecStart command when creating a new container.
   151  	//
   152  	// Note that we cannot catch all corner cases here such that users
   153  	// *must* manually check the generated files.  A container might have
   154  	// been created via a Python script, which would certainly yield an
   155  	// invalid `info.CreateCommand`.  Hence, we're doing a best effort unit
   156  	// generation and don't try aiming at completeness.
   157  	if opts.New {
   158  		// The create command must at least have three arguments:
   159  		// 	/usr/bin/podman run $IMAGE
   160  		index := 2
   161  		if info.CreateCommand[1] == "container" {
   162  			index = 3
   163  		}
   164  		if len(info.CreateCommand) < index+1 {
   165  			return "", errors.Errorf("container's create command is too short or invalid: %v", info.CreateCommand)
   166  		}
   167  		// We're hard-coding the first five arguments and append the
   168  		// CreateCommand with a stripped command and subcomand.
   169  		command := []string{
   170  			info.Executable,
   171  			"run",
   172  			"--conmon-pidfile", "%t/%n-pid",
   173  			"--cidfile", "%t/%n-cid",
   174  			"--cgroups=no-conmon",
   175  		}
   176  
   177  		// Enforce detaching
   178  		//
   179  		// since we use systemd `Type=forking` service
   180  		// @see https://www.freedesktop.org/software/systemd/man/systemd.service.html#Type=
   181  		// when we generated systemd service file with the --new param,
   182  		// `ExecStart` will have `/usr/bin/podman run ...`
   183  		// if `info.CreateCommand` has no `-d` or `--detach` param,
   184  		// podman will run the container in default attached mode,
   185  		// as a result, `systemd start` will wait the `podman run` command exit until failed with timeout error.
   186  		hasDetachParam := false
   187  		for _, p := range info.CreateCommand[index:] {
   188  			if p == "--detach" || p == "-d" {
   189  				hasDetachParam = true
   190  			}
   191  		}
   192  		if !hasDetachParam {
   193  			command = append(command, "-d")
   194  		}
   195  
   196  		command = append(command, info.CreateCommand[index:]...)
   197  		info.RunCommand = strings.Join(command, " ")
   198  		info.New = true
   199  	}
   200  
   201  	if info.PodmanVersion == "" {
   202  		info.PodmanVersion = version.Version
   203  	}
   204  	if info.GenerateTimestamp {
   205  		info.TimeStamp = fmt.Sprintf("%v", time.Now().Format(time.UnixDate))
   206  	}
   207  
   208  	// Sort the slices to assure a deterministic output.
   209  	sort.Strings(info.RequiredServices)
   210  	sort.Strings(info.BoundToServices)
   211  
   212  	// Generate the template and compile it.
   213  	templ, err := template.New("systemd_service_file").Parse(containerTemplate)
   214  	if err != nil {
   215  		return "", errors.Wrap(err, "error parsing systemd service template")
   216  	}
   217  
   218  	var buf bytes.Buffer
   219  	if err := templ.Execute(&buf, info); err != nil {
   220  		return "", err
   221  	}
   222  
   223  	if !opts.Files {
   224  		return buf.String(), nil
   225  	}
   226  
   227  	buf.WriteByte('\n')
   228  	cwd, err := os.Getwd()
   229  	if err != nil {
   230  		return "", errors.Wrap(err, "error getting current working directory")
   231  	}
   232  	path := filepath.Join(cwd, fmt.Sprintf("%s.service", info.ServiceName))
   233  	if err := ioutil.WriteFile(path, buf.Bytes(), 0644); err != nil {
   234  		return "", errors.Wrap(err, "error generating systemd unit")
   235  	}
   236  	return path, nil
   237  }