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