github.com/containers/podman/v2@v2.2.2-0.20210501105131-c1e07d070c4c/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/v2/libpod" 13 "github.com/containers/podman/v2/pkg/domain/entities" 14 "github.com/containers/podman/v2/version" 15 "github.com/pkg/errors" 16 "github.com/sirupsen/logrus" 17 ) 18 19 // podInfo contains data required for generating a pod's systemd 20 // unit file. 21 type podInfo struct { 22 // ServiceName of the systemd service. 23 ServiceName string 24 // Name or ID of the infra container. 25 InfraNameOrID string 26 // StopTimeout sets the timeout Podman waits before killing the container 27 // during service stop. 28 StopTimeout uint 29 // RestartPolicy of the systemd unit (e.g., no, on-failure, always). 30 RestartPolicy string 31 // PIDFile of the service. Required for forking services. Must point to the 32 // PID of the associated conmon process. 33 PIDFile string 34 // PodIDFile of the unit. 35 PodIDFile string 36 // GenerateTimestamp, if set the generated unit file has a time stamp. 37 GenerateTimestamp bool 38 // RequiredServices are services this service requires. Note that this 39 // service runs before them. 40 RequiredServices []string 41 // PodmanVersion for the header. Will be set internally. Will be auto-filled 42 // if left empty. 43 PodmanVersion string 44 // Executable is the path to the podman executable. Will be auto-filled if 45 // left empty. 46 Executable string 47 // TimeStamp at the time of creating the unit file. Will be set internally. 48 TimeStamp string 49 // CreateCommand is the full command plus arguments of the process the 50 // container has been created with. 51 CreateCommand []string 52 // PodCreateCommand - a post-processed variant of CreateCommand to use 53 // when creating the pod. 54 PodCreateCommand string 55 // EnvVariable is generate.EnvVariable and must not be set. 56 EnvVariable string 57 // ExecStartPre1 of the unit. 58 ExecStartPre1 string 59 // ExecStartPre2 of the unit. 60 ExecStartPre2 string 61 // ExecStart of the unit. 62 ExecStart string 63 // ExecStop of the unit. 64 ExecStop string 65 // ExecStopPost of the unit. 66 ExecStopPost string 67 } 68 69 const podTemplate = headerTemplate + `Requires={{- range $index, $value := .RequiredServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}} 70 Before={{- range $index, $value := .RequiredServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}} 71 72 [Service] 73 Environment={{.EnvVariable}}=%n 74 Restart={{.RestartPolicy}} 75 {{- if .ExecStartPre1}} 76 ExecStartPre={{.ExecStartPre1}} 77 {{- end}} 78 {{- if .ExecStartPre2}} 79 ExecStartPre={{.ExecStartPre2}} 80 {{- end}} 81 ExecStart={{.ExecStart}} 82 ExecStop={{.ExecStop}} 83 ExecStopPost={{.ExecStopPost}} 84 PIDFile={{.PIDFile}} 85 KillMode=none 86 Type=forking 87 88 [Install] 89 WantedBy=multi-user.target default.target 90 ` 91 92 // PodUnits generates systemd units for the specified pod and its containers. 93 // Based on the options, the return value might be the content of all units or 94 // the files they been written to. 95 func PodUnits(pod *libpod.Pod, options entities.GenerateSystemdOptions) (map[string]string, error) { 96 // Error out if the pod has no infra container, which we require to be the 97 // main service. 98 if !pod.HasInfraContainer() { 99 return nil, errors.Errorf("error generating systemd unit files: Pod %q has no infra container", pod.Name()) 100 } 101 102 podInfo, err := generatePodInfo(pod, options) 103 if err != nil { 104 return nil, err 105 } 106 107 infraID, err := pod.InfraContainerID() 108 if err != nil { 109 return nil, err 110 } 111 112 // Compute the container-dependency graph for the Pod. 113 containers, err := pod.AllContainers() 114 if err != nil { 115 return nil, err 116 } 117 if len(containers) == 0 { 118 return nil, errors.Errorf("error generating systemd unit files: Pod %q has no containers", pod.Name()) 119 } 120 graph, err := libpod.BuildContainerGraph(containers) 121 if err != nil { 122 return nil, err 123 } 124 125 // Traverse the dependency graph and create systemdgen.containerInfo's for 126 // each container. 127 containerInfos := []*containerInfo{} 128 for ctr, dependencies := range graph.DependencyMap() { 129 // Skip the infra container as we already generated it. 130 if ctr.ID() == infraID { 131 continue 132 } 133 ctrInfo, err := generateContainerInfo(ctr, options) 134 if err != nil { 135 return nil, err 136 } 137 // Now add the container's dependencies and at the container as a 138 // required service of the infra container. 139 for _, dep := range dependencies { 140 if dep.ID() == infraID { 141 ctrInfo.BoundToServices = append(ctrInfo.BoundToServices, podInfo.ServiceName) 142 } else { 143 _, serviceName := containerServiceName(dep, options) 144 ctrInfo.BoundToServices = append(ctrInfo.BoundToServices, serviceName) 145 } 146 } 147 podInfo.RequiredServices = append(podInfo.RequiredServices, ctrInfo.ServiceName) 148 containerInfos = append(containerInfos, ctrInfo) 149 } 150 151 units := map[string]string{} 152 // Now generate the systemd service for all containers. 153 out, err := executePodTemplate(podInfo, options) 154 if err != nil { 155 return nil, err 156 } 157 units[podInfo.ServiceName] = out 158 for _, info := range containerInfos { 159 info.pod = podInfo 160 out, err := executeContainerTemplate(info, options) 161 if err != nil { 162 return nil, err 163 } 164 units[info.ServiceName] = out 165 } 166 167 return units, nil 168 } 169 170 func generatePodInfo(pod *libpod.Pod, options entities.GenerateSystemdOptions) (*podInfo, error) { 171 // Generate a systemdgen.containerInfo for the infra container. This 172 // containerInfo acts as the main service of the pod. 173 infraCtr, err := pod.InfraContainer() 174 if err != nil { 175 return nil, errors.Wrap(err, "could not find infra container") 176 } 177 178 timeout := infraCtr.StopTimeout() 179 if options.StopTimeout != nil { 180 timeout = *options.StopTimeout 181 } 182 183 config := infraCtr.Config() 184 conmonPidFile := config.ConmonPidFile 185 if conmonPidFile == "" { 186 return nil, errors.Errorf("conmon PID file path is empty, try to recreate the container with --conmon-pidfile flag") 187 } 188 189 createCommand := pod.CreateCommand() 190 if options.New && len(createCommand) == 0 { 191 return nil, errors.Errorf("cannot use --new on pod %q: no create command found", pod.ID()) 192 } 193 194 nameOrID := pod.ID() 195 ctrNameOrID := infraCtr.ID() 196 if options.Name { 197 nameOrID = pod.Name() 198 ctrNameOrID = infraCtr.Name() 199 } 200 serviceName := fmt.Sprintf("%s%s%s", options.PodPrefix, options.Separator, nameOrID) 201 202 info := podInfo{ 203 ServiceName: serviceName, 204 InfraNameOrID: ctrNameOrID, 205 RestartPolicy: options.RestartPolicy, 206 PIDFile: conmonPidFile, 207 StopTimeout: timeout, 208 GenerateTimestamp: true, 209 CreateCommand: createCommand, 210 } 211 return &info, nil 212 } 213 214 // executePodTemplate executes the pod template on the specified podInfo. Note 215 // that the podInfo is also post processed and completed, which allows for an 216 // easier unit testing. 217 func executePodTemplate(info *podInfo, options entities.GenerateSystemdOptions) (string, error) { 218 if err := validateRestartPolicy(info.RestartPolicy); err != nil { 219 return "", err 220 } 221 222 // Make sure the executable is set. 223 if info.Executable == "" { 224 executable, err := os.Executable() 225 if err != nil { 226 executable = "/usr/bin/podman" 227 logrus.Warnf("Could not obtain podman executable location, using default %s", executable) 228 } 229 info.Executable = executable 230 } 231 232 info.EnvVariable = EnvVariable 233 info.ExecStart = "{{.Executable}} start {{.InfraNameOrID}}" 234 info.ExecStop = "{{.Executable}} stop {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}} {{.InfraNameOrID}}" 235 info.ExecStopPost = "{{.Executable}} stop {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}} {{.InfraNameOrID}}" 236 237 // Assemble the ExecStart command when creating a new pod. 238 // 239 // Note that we cannot catch all corner cases here such that users 240 // *must* manually check the generated files. A pod might have been 241 // created via a Python script, which would certainly yield an invalid 242 // `info.CreateCommand`. Hence, we're doing a best effort unit 243 // generation and don't try aiming at completeness. 244 if options.New { 245 info.PIDFile = "%t/" + info.ServiceName + ".pid" 246 info.PodIDFile = "%t/" + info.ServiceName + ".pod-id" 247 248 podCreateIndex := 0 249 var podRootArgs, podCreateArgs []string 250 switch len(info.CreateCommand) { 251 case 0, 1, 2: 252 return "", errors.Errorf("pod does not appear to be created via `podman pod create`: %v", info.CreateCommand) 253 default: 254 // Make sure that pod was created with `pod create` and 255 // not something else, such as `run --pod new`. 256 for i := 1; i < len(info.CreateCommand); i++ { 257 if info.CreateCommand[i-1] == "pod" && info.CreateCommand[i] == "create" { 258 podCreateIndex = i 259 break 260 } 261 } 262 if podCreateIndex == 0 { 263 return "", errors.Errorf("pod does not appear to be created via `podman pod create`: %v", info.CreateCommand) 264 } 265 podRootArgs = info.CreateCommand[0 : podCreateIndex-2] 266 podCreateArgs = filterPodFlags(info.CreateCommand[podCreateIndex+1:]) 267 } 268 // We're hard-coding the first five arguments and append the 269 // CreateCommand with a stripped command and subcomand. 270 startCommand := []string{info.Executable} 271 startCommand = append(startCommand, podRootArgs...) 272 startCommand = append(startCommand, 273 []string{"pod", "create", 274 "--infra-conmon-pidfile", "{{.PIDFile}}", 275 "--pod-id-file", "{{.PodIDFile}}"}...) 276 277 // Presence check for certain flags/options. 278 hasNameParam := false 279 hasReplaceParam := false 280 for _, p := range podCreateArgs { 281 switch p { 282 case "--name": 283 hasNameParam = true 284 case "--replace": 285 hasReplaceParam = true 286 } 287 } 288 if hasNameParam && !hasReplaceParam { 289 podCreateArgs = append(podCreateArgs, "--replace") 290 } 291 292 startCommand = append(startCommand, podCreateArgs...) 293 startCommand = quoteArguments(startCommand) 294 295 info.ExecStartPre1 = "/bin/rm -f {{.PIDFile}} {{.PodIDFile}}" 296 info.ExecStartPre2 = strings.Join(startCommand, " ") 297 info.ExecStart = "{{.Executable}} pod start --pod-id-file {{.PodIDFile}}" 298 info.ExecStop = "{{.Executable}} pod stop --ignore --pod-id-file {{.PodIDFile}} {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}}" 299 info.ExecStopPost = "{{.Executable}} pod rm --ignore -f --pod-id-file {{.PodIDFile}}" 300 } 301 if info.PodmanVersion == "" { 302 info.PodmanVersion = version.Version.String() 303 } 304 if info.GenerateTimestamp { 305 info.TimeStamp = fmt.Sprintf("%v", time.Now().Format(time.UnixDate)) 306 } 307 308 // Sort the slices to assure a deterministic output. 309 sort.Strings(info.RequiredServices) 310 311 // Generate the template and compile it. 312 // 313 // Note that we need a two-step generation process to allow for fields 314 // embedding other fields. This way we can replace `A -> B -> C` and 315 // make the code easier to maintain at the cost of a slightly slower 316 // generation. That's especially needed for embedding the PID and ID 317 // files in other fields which will eventually get replaced in the 2nd 318 // template execution. 319 templ, err := template.New("pod_template").Parse(podTemplate) 320 if err != nil { 321 return "", errors.Wrap(err, "error parsing systemd service template") 322 } 323 324 var buf bytes.Buffer 325 if err := templ.Execute(&buf, info); err != nil { 326 return "", err 327 } 328 329 // Now parse the generated template (i.e., buf) and execute it. 330 templ, err = template.New("pod_template").Parse(buf.String()) 331 if err != nil { 332 return "", errors.Wrap(err, "error parsing systemd service template") 333 } 334 335 buf = bytes.Buffer{} 336 if err := templ.Execute(&buf, info); err != nil { 337 return "", err 338 } 339 340 return buf.String(), nil 341 }