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