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