summaryrefslogtreecommitdiff
path: root/pkg/systemd/generate/pods.go
blob: 15b598ae8fb203daab14557479d7aeeb29deb49e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
package generate

import (
	"bytes"
	"fmt"
	"os"
	"sort"
	"strings"
	"text/template"
	"time"

	"github.com/containers/podman/v4/libpod"
	"github.com/containers/podman/v4/pkg/domain/entities"
	"github.com/containers/podman/v4/pkg/systemd/define"
	"github.com/containers/podman/v4/version"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"github.com/spf13/pflag"
)

// podInfo contains data required for generating a pod's systemd
// unit file.
type podInfo struct {
	// ServiceName of the systemd service.
	ServiceName string
	// Name or ID of the infra container.
	InfraNameOrID string
	// StopTimeout sets the timeout Podman waits before killing the container
	// during service stop.
	StopTimeout uint
	// RestartPolicy of the systemd unit (e.g., no, on-failure, always).
	RestartPolicy string
	// RestartSec of the systemd unit. Configures the time to sleep before restarting a service.
	RestartSec uint
	// PIDFile of the service. Required for forking services. Must point to the
	// PID of the associated conmon process.
	PIDFile string
	// PodIDFile of the unit.
	PodIDFile string
	// GenerateTimestamp, if set the generated unit file has a time stamp.
	GenerateTimestamp bool
	// RequiredServices are services this service requires. Note that this
	// service runs before them.
	RequiredServices []string
	// PodmanVersion for the header. Will be set internally. Will be auto-filled
	// if left empty.
	PodmanVersion string
	// Executable is the path to the podman executable. Will be auto-filled if
	// left empty.
	Executable string
	// RootFlags contains the root flags which were used to create the container
	// Only used with --new
	RootFlags string
	// TimeStamp at the time of creating the unit file. Will be set internally.
	TimeStamp string
	// CreateCommand is the full command plus arguments of the process the
	// container has been created with.
	CreateCommand []string
	// PodCreateCommand - a post-processed variant of CreateCommand to use
	// when creating the pod.
	PodCreateCommand string
	// EnvVariable is generate.EnvVariable and must not be set.
	EnvVariable string
	// ExecStartPre1 of the unit.
	ExecStartPre1 string
	// ExecStartPre2 of the unit.
	ExecStartPre2 string
	// ExecStart of the unit.
	ExecStart string
	// TimeoutStopSec of the unit.
	TimeoutStopSec uint
	// ExecStop of the unit.
	ExecStop string
	// ExecStopPost of the unit.
	ExecStopPost string
	// Removes autogenerated by Podman and timestamp if set to true
	GenerateNoHeader bool
	// Location of the GraphRoot for the pod.  Required for ensuring the
	// volume has finished mounting when coming online at boot.
	GraphRoot string
	// Location of the RunRoot for the pod.  Required for ensuring the tmpfs
	// or volume exists and is mounted when coming online at boot.
	RunRoot string
	// Add %i and %I to description and execute parts - this should not be used
	IdentifySpecifier bool
	// Wants are the list of services that this service is (weak) dependent on. This
	// option does not influence the order in which services are started or stopped.
	Wants []string
	// After ordering dependencies between the list of services and this service.
	After []string
	// Similar to Wants, but declares a stronger requirement dependency.
	Requires []string
}

const podTemplate = headerTemplate + `Requires={{{{- range $index, $value := .RequiredServices -}}}}{{{{if $index}}}} {{{{end}}}}{{{{ $value }}}}.service{{{{end}}}}
Before={{{{- range $index, $value := .RequiredServices -}}}}{{{{if $index}}}} {{{{end}}}}{{{{ $value }}}}.service{{{{end}}}}
{{{{- if or .Wants .After .Requires }}}}

# User-defined dependencies
{{{{- end}}}}
{{{{- if .Wants}}}}
Wants={{{{- range $index, $value := .Wants }}}}{{{{ if $index}}}} {{{{end}}}}{{{{ $value }}}}{{{{end}}}}
{{{{- end}}}}
{{{{- if .After}}}}
After={{{{- range $index, $value := .After }}}}{{{{ if $index}}}} {{{{end}}}}{{{{ $value }}}}{{{{end}}}}
{{{{- end}}}}
{{{{- if .Requires}}}}
Requires={{{{- range $index, $value := .Requires }}}}{{{{ if $index}}}} {{{{end}}}}{{{{ $value }}}}{{{{end}}}}
{{{{- end}}}}

[Service]
Environment={{{{.EnvVariable}}}}=%n
Restart={{{{.RestartPolicy}}}}
{{{{- if .RestartSec}}}}
RestartSec={{{{.RestartSec}}}}
{{{{- end}}}}
TimeoutStopSec={{{{.TimeoutStopSec}}}}
{{{{- if .ExecStartPre1}}}}
ExecStartPre={{{{.ExecStartPre1}}}}
{{{{- end}}}}
{{{{- if .ExecStartPre2}}}}
ExecStartPre={{{{.ExecStartPre2}}}}
{{{{- end}}}}
ExecStart={{{{.ExecStart}}}}
ExecStop={{{{.ExecStop}}}}
ExecStopPost={{{{.ExecStopPost}}}}
PIDFile={{{{.PIDFile}}}}
Type=forking

[Install]
WantedBy=default.target
`

// PodUnits generates systemd units for the specified pod and its containers.
// Based on the options, the return value might be the content of all units or
// the files they been written to.
func PodUnits(pod *libpod.Pod, options entities.GenerateSystemdOptions) (map[string]string, error) {
	if options.TemplateUnitFile {
		return nil, errors.New("--template is not supported for pods")
	}
	// Error out if the pod has no infra container, which we require to be the
	// main service.
	if !pod.HasInfraContainer() {
		return nil, errors.Errorf("error generating systemd unit files: Pod %q has no infra container", pod.Name())
	}

	podInfo, err := generatePodInfo(pod, options)
	if err != nil {
		return nil, err
	}

	infraID, err := pod.InfraContainerID()
	if err != nil {
		return nil, err
	}

	// Compute the container-dependency graph for the Pod.
	containers, err := pod.AllContainers()
	if err != nil {
		return nil, err
	}
	if len(containers) == 0 {
		return nil, errors.Errorf("error generating systemd unit files: Pod %q has no containers", pod.Name())
	}
	graph, err := libpod.BuildContainerGraph(containers)
	if err != nil {
		return nil, err
	}

	// Traverse the dependency graph and create systemdgen.containerInfo's for
	// each container.
	containerInfos := []*containerInfo{}
	for ctr, dependencies := range graph.DependencyMap() {
		// Skip the infra container as we already generated it.
		if ctr.ID() == infraID {
			continue
		}
		ctrInfo, err := generateContainerInfo(ctr, options)
		if err != nil {
			return nil, err
		}
		// Now add the container's dependencies and at the container as a
		// required service of the infra container.
		for _, dep := range dependencies {
			if dep.ID() == infraID {
				ctrInfo.BoundToServices = append(ctrInfo.BoundToServices, podInfo.ServiceName)
			} else {
				_, serviceName := containerServiceName(dep, options)
				ctrInfo.BoundToServices = append(ctrInfo.BoundToServices, serviceName)
			}
		}
		podInfo.RequiredServices = append(podInfo.RequiredServices, ctrInfo.ServiceName)
		containerInfos = append(containerInfos, ctrInfo)
	}

	units := map[string]string{}
	// Now generate the systemd service for all containers.
	out, err := executePodTemplate(podInfo, options)
	if err != nil {
		return nil, err
	}
	units[podInfo.ServiceName] = out
	for _, info := range containerInfos {
		info.Pod = podInfo
		out, err := executeContainerTemplate(info, options)
		if err != nil {
			return nil, err
		}
		units[info.ServiceName] = out
	}

	return units, nil
}

func generatePodInfo(pod *libpod.Pod, options entities.GenerateSystemdOptions) (*podInfo, error) {
	// Generate a systemdgen.containerInfo for the infra container. This
	// containerInfo acts as the main service of the pod.
	infraCtr, err := pod.InfraContainer()
	if err != nil {
		return nil, errors.Wrap(err, "could not find infra container")
	}

	stopTimeout := infraCtr.StopTimeout()
	if options.StopTimeout != nil {
		stopTimeout = *options.StopTimeout
	}

	config := infraCtr.Config()
	conmonPidFile := config.ConmonPidFile
	if conmonPidFile == "" {
		return nil, errors.Errorf("conmon PID file path is empty, try to recreate the container with --conmon-pidfile flag")
	}

	createCommand := pod.CreateCommand()
	if options.New && len(createCommand) == 0 {
		return nil, errors.Errorf("cannot use --new on pod %q: no create command found", pod.ID())
	}

	nameOrID := pod.ID()
	ctrNameOrID := infraCtr.ID()
	if options.Name {
		nameOrID = pod.Name()
		ctrNameOrID = infraCtr.Name()
	}

	serviceName := getServiceName(options.PodPrefix, options.Separator, nameOrID)

	info := podInfo{
		ServiceName:       serviceName,
		InfraNameOrID:     ctrNameOrID,
		PIDFile:           conmonPidFile,
		StopTimeout:       stopTimeout,
		GenerateTimestamp: true,
		CreateCommand:     createCommand,
	}
	return &info, nil
}

// executePodTemplate executes the pod template on the specified podInfo.  Note
// that the podInfo is also post processed and completed, which allows for an
// easier unit testing.
func executePodTemplate(info *podInfo, options entities.GenerateSystemdOptions) (string, error) {
	info.RestartPolicy = define.DefaultRestartPolicy
	if options.RestartPolicy != nil {
		if err := validateRestartPolicy(*options.RestartPolicy); err != nil {
			return "", err
		}
		info.RestartPolicy = *options.RestartPolicy
	}

	if options.RestartSec != nil {
		info.RestartSec = *options.RestartSec
	}

	// Make sure the executable is set.
	if info.Executable == "" {
		executable, err := os.Executable()
		if err != nil {
			executable = "/usr/bin/podman"
			logrus.Warnf("Could not obtain podman executable location, using default %s: %v", executable, err)
		}
		info.Executable = executable
	}

	info.EnvVariable = define.EnvVariable
	info.ExecStart = "{{{{.Executable}}}} start {{{{.InfraNameOrID}}}}"
	info.ExecStop = "{{{{.Executable}}}} stop {{{{if (ge .StopTimeout 0)}}}}-t {{{{.StopTimeout}}}}{{{{end}}}} {{{{.InfraNameOrID}}}}"
	info.ExecStopPost = "{{{{.Executable}}}} stop {{{{if (ge .StopTimeout 0)}}}}-t {{{{.StopTimeout}}}}{{{{end}}}} {{{{.InfraNameOrID}}}}"

	// Assemble the ExecStart command when creating a new pod.
	//
	// Note that we cannot catch all corner cases here such that users
	// *must* manually check the generated files.  A pod might have been
	// created via a Python script, which would certainly yield an invalid
	// `info.CreateCommand`.  Hence, we're doing a best effort unit
	// generation and don't try aiming at completeness.
	if options.New {
		info.PIDFile = "%t/" + info.ServiceName + ".pid"
		info.PodIDFile = "%t/" + info.ServiceName + ".pod-id"

		podCreateIndex := 0
		var podRootArgs, podCreateArgs []string
		switch len(info.CreateCommand) {
		case 0, 1, 2:
			return "", errors.Errorf("pod does not appear to be created via `podman pod create`: %v", info.CreateCommand)
		default:
			// Make sure that pod was created with `pod create` and
			// not something else, such as `run --pod new`.
			for i := 1; i < len(info.CreateCommand); i++ {
				if info.CreateCommand[i-1] == "pod" && info.CreateCommand[i] == "create" {
					podCreateIndex = i
					break
				}
			}
			if podCreateIndex == 0 {
				return "", errors.Errorf("pod does not appear to be created via `podman pod create`: %v", info.CreateCommand)
			}
			podRootArgs = info.CreateCommand[1 : podCreateIndex-1]
			info.RootFlags = strings.Join(escapeSystemdArguments(podRootArgs), " ")
			podCreateArgs = filterPodFlags(info.CreateCommand[podCreateIndex+1:], 0)
		}
		// We're hard-coding the first five arguments and append the
		// CreateCommand with a stripped command and subcommand.
		startCommand := []string{info.Executable}
		startCommand = append(startCommand, podRootArgs...)
		startCommand = append(startCommand,
			"pod", "create",
			"--infra-conmon-pidfile", "{{{{.PIDFile}}}}",
			"--pod-id-file", "{{{{.PodIDFile}}}}")

		// Presence check for certain flags/options.
		fs := pflag.NewFlagSet("args", pflag.ContinueOnError)
		fs.ParseErrorsWhitelist.UnknownFlags = true
		fs.Usage = func() {}
		fs.SetInterspersed(false)
		fs.String("name", "", "")
		fs.Bool("replace", false, "")
		if err := fs.Parse(podCreateArgs); err != nil {
			return "", fmt.Errorf("parsing remaining command-line arguments: %w", err)
		}

		hasNameParam := fs.Lookup("name").Changed
		hasReplaceParam, err := fs.GetBool("replace")
		if err != nil {
			return "", err
		}
		if hasNameParam && !hasReplaceParam {
			if fs.Changed("replace") {
				// this can only happen if --replace=false is set
				// in that case we need to remove it otherwise we
				// would overwrite the previous replace arg to false
				podCreateArgs = removeReplaceArg(podCreateArgs, fs.NArg())
			}
			podCreateArgs = append(podCreateArgs, "--replace")
		}

		startCommand = append(startCommand, podCreateArgs...)
		startCommand = escapeSystemdArguments(startCommand)

		info.ExecStartPre1 = "/bin/rm -f {{{{.PIDFile}}}} {{{{.PodIDFile}}}}"
		info.ExecStartPre2 = strings.Join(startCommand, " ")
		info.ExecStart = "{{{{.Executable}}}} {{{{if .RootFlags}}}}{{{{ .RootFlags}}}} {{{{end}}}}pod start --pod-id-file {{{{.PodIDFile}}}}"
		info.ExecStop = "{{{{.Executable}}}} {{{{if .RootFlags}}}}{{{{ .RootFlags}}}} {{{{end}}}}pod stop --ignore --pod-id-file {{{{.PodIDFile}}}} {{{{if (ge .StopTimeout 0)}}}}-t {{{{.StopTimeout}}}}{{{{end}}}}"
		info.ExecStopPost = "{{{{.Executable}}}} {{{{if .RootFlags}}}}{{{{ .RootFlags}}}} {{{{end}}}}pod rm --ignore -f --pod-id-file {{{{.PodIDFile}}}}"
	}
	info.TimeoutStopSec = minTimeoutStopSec + info.StopTimeout

	if info.PodmanVersion == "" {
		info.PodmanVersion = version.Version.String()
	}

	if options.NoHeader {
		info.GenerateNoHeader = true
		info.GenerateTimestamp = false
	}

	if info.GenerateTimestamp {
		info.TimeStamp = fmt.Sprintf("%v", time.Now().Format(time.UnixDate))
	}

	// Sort the slices to assure a deterministic output.
	sort.Strings(info.RequiredServices)

	// Generate the template and compile it.
	//
	// Note that we need a two-step generation process to allow for fields
	// embedding other fields.  This way we can replace `A -> B -> C` and
	// make the code easier to maintain at the cost of a slightly slower
	// generation.  That's especially needed for embedding the PID and ID
	// files in other fields which will eventually get replaced in the 2nd
	// template execution.
	templ, err := template.New("pod_template").Delims("{{{{", "}}}}").Parse(podTemplate)
	if err != nil {
		return "", errors.Wrap(err, "error parsing systemd service template")
	}

	var buf bytes.Buffer
	if err := templ.Execute(&buf, info); err != nil {
		return "", err
	}

	// Now parse the generated template (i.e., buf) and execute it.
	templ, err = template.New("pod_template").Delims("{{{{", "}}}}").Parse(buf.String())
	if err != nil {
		return "", errors.Wrap(err, "error parsing systemd service template")
	}

	buf = bytes.Buffer{}
	if err := templ.Execute(&buf, info); err != nil {
		return "", err
	}

	return buf.String(), nil
}