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 }