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