github.com/containers/podman/v2@v2.2.2-0.20210501105131-c1e07d070c4c/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/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 // containerInfo contains data required for generating a container's systemd 20 // unit file. 21 type containerInfo struct { 22 // ServiceName of the systemd service. 23 ServiceName string 24 // Name or ID of the container. 25 ContainerNameOrID 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 // ContainerIDFile to be used in the unit. 35 ContainerIDFile string 36 // GenerateTimestamp, if set the generated unit file has a time stamp. 37 GenerateTimestamp bool 38 // BoundToServices are the services this service binds to. Note that this 39 // service runs after them. 40 BoundToServices []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 // EnvVariable is generate.EnvVariable and must not be set. 53 EnvVariable string 54 // ExecStartPre of the unit. 55 ExecStartPre string 56 // ExecStart of the unit. 57 ExecStart string 58 // ExecStop of the unit. 59 ExecStop string 60 // ExecStopPost of the unit. 61 ExecStopPost string 62 63 // If not nil, the container is part of the pod. We can use the 64 // podInfo to extract the relevant data. 65 pod *podInfo 66 } 67 68 const containerTemplate = headerTemplate + ` 69 {{- if .BoundToServices}} 70 BindsTo={{- range $index, $value := .BoundToServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}} 71 After={{- range $index, $value := .BoundToServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}} 72 {{- end}} 73 74 [Service] 75 Environment={{.EnvVariable}}=%n 76 Restart={{.RestartPolicy}} 77 {{- if .ExecStartPre}} 78 ExecStartPre={{.ExecStartPre}} 79 {{- end}} 80 ExecStart={{.ExecStart}} 81 ExecStop={{.ExecStop}} 82 ExecStopPost={{.ExecStopPost}} 83 PIDFile={{.PIDFile}} 84 KillMode=none 85 Type=forking 86 87 [Install] 88 WantedBy=multi-user.target default.target 89 ` 90 91 // ContainerUnit generates a systemd unit for the specified container. Based 92 // on the options, the return value might be the entire unit or a file it has 93 // been written to. 94 func ContainerUnit(ctr *libpod.Container, options entities.GenerateSystemdOptions) (string, string, error) { 95 info, err := generateContainerInfo(ctr, options) 96 if err != nil { 97 return "", "", err 98 } 99 content, err := executeContainerTemplate(info, options) 100 if err != nil { 101 return "", "", err 102 } 103 return info.ServiceName, content, nil 104 } 105 106 func generateContainerInfo(ctr *libpod.Container, options entities.GenerateSystemdOptions) (*containerInfo, error) { 107 timeout := ctr.StopTimeout() 108 if options.StopTimeout != nil { 109 timeout = *options.StopTimeout 110 } 111 112 config := ctr.Config() 113 conmonPidFile := config.ConmonPidFile 114 if conmonPidFile == "" { 115 return nil, errors.Errorf("conmon PID file path is empty, try to recreate the container with --conmon-pidfile flag") 116 } 117 118 createCommand := []string{} 119 if config.CreateCommand != nil { 120 createCommand = config.CreateCommand 121 } else if options.New { 122 return nil, errors.Errorf("cannot use --new on container %q: no create command found", ctr.ID()) 123 } 124 125 nameOrID, serviceName := containerServiceName(ctr, options) 126 127 info := containerInfo{ 128 ServiceName: serviceName, 129 ContainerNameOrID: nameOrID, 130 RestartPolicy: options.RestartPolicy, 131 PIDFile: conmonPidFile, 132 StopTimeout: timeout, 133 GenerateTimestamp: true, 134 CreateCommand: createCommand, 135 } 136 137 return &info, nil 138 } 139 140 // containerServiceName returns the nameOrID and the service name of the 141 // container. 142 func containerServiceName(ctr *libpod.Container, options entities.GenerateSystemdOptions) (string, string) { 143 nameOrID := ctr.ID() 144 if options.Name { 145 nameOrID = ctr.Name() 146 } 147 serviceName := fmt.Sprintf("%s%s%s", options.ContainerPrefix, options.Separator, nameOrID) 148 return nameOrID, serviceName 149 } 150 151 // executeContainerTemplate executes the container template on the specified 152 // containerInfo. Note that the containerInfo is also post processed and 153 // completed, which allows for an easier unit testing. 154 func executeContainerTemplate(info *containerInfo, options entities.GenerateSystemdOptions) (string, error) { 155 if err := validateRestartPolicy(info.RestartPolicy); err != nil { 156 return "", err 157 } 158 159 // Make sure the executable is set. 160 if info.Executable == "" { 161 executable, err := os.Executable() 162 if err != nil { 163 executable = "/usr/bin/podman" 164 logrus.Warnf("Could not obtain podman executable location, using default %s", executable) 165 } 166 info.Executable = executable 167 } 168 169 info.EnvVariable = EnvVariable 170 info.ExecStart = "{{.Executable}} start {{.ContainerNameOrID}}" 171 info.ExecStop = "{{.Executable}} stop {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}} {{.ContainerNameOrID}}" 172 info.ExecStopPost = "{{.Executable}} stop {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}} {{.ContainerNameOrID}}" 173 174 // Assemble the ExecStart command when creating a new container. 175 // 176 // Note that we cannot catch all corner cases here such that users 177 // *must* manually check the generated files. A container might have 178 // been created via a Python script, which would certainly yield an 179 // invalid `info.CreateCommand`. Hence, we're doing a best effort unit 180 // generation and don't try aiming at completeness. 181 if options.New { 182 info.PIDFile = "%t/" + info.ServiceName + ".pid" 183 info.ContainerIDFile = "%t/" + info.ServiceName + ".ctr-id" 184 // The create command must at least have three arguments: 185 // /usr/bin/podman run $IMAGE 186 index := 2 187 if info.CreateCommand[1] == "container" { 188 index = 3 189 } 190 if len(info.CreateCommand) < index+1 { 191 return "", errors.Errorf("container's create command is too short or invalid: %v", info.CreateCommand) 192 } 193 // We're hard-coding the first five arguments and append the 194 // CreateCommand with a stripped command and subcomand. 195 startCommand := []string{ 196 info.Executable, 197 "run", 198 "--conmon-pidfile", "{{.PIDFile}}", 199 "--cidfile", "{{.ContainerIDFile}}", 200 "--cgroups=no-conmon", 201 } 202 // If the container is in a pod, make sure that the 203 // --pod-id-file is set correctly. 204 if info.pod != nil { 205 podFlags := []string{"--pod-id-file", info.pod.PodIDFile} 206 startCommand = append(startCommand, podFlags...) 207 info.CreateCommand = filterPodFlags(info.CreateCommand) 208 } 209 210 // Presence check for certain flags/options. 211 hasDetachParam := false 212 hasNameParam := false 213 hasReplaceParam := false 214 for _, p := range info.CreateCommand[index:] { 215 switch p { 216 case "--detach", "-d": 217 hasDetachParam = true 218 case "--name": 219 hasNameParam = true 220 case "--replace": 221 hasReplaceParam = true 222 } 223 if strings.HasPrefix(p, "--name=") { 224 hasNameParam = true 225 } 226 } 227 228 if !hasDetachParam { 229 // Enforce detaching 230 // 231 // since we use systemd `Type=forking` service @see 232 // https://www.freedesktop.org/software/systemd/man/systemd.service.html#Type= 233 // when we generated systemd service file with the 234 // --new param, `ExecStart` will have `/usr/bin/podman 235 // run ...` if `info.CreateCommand` has no `-d` or 236 // `--detach` param, podman will run the container in 237 // default attached mode, as a result, `systemd start` 238 // will wait the `podman run` command exit until failed 239 // with timeout error. 240 startCommand = append(startCommand, "-d") 241 } 242 if hasNameParam && !hasReplaceParam { 243 // Enforce --replace for named containers. This will 244 // make systemd units more robuts as it allows them to 245 // start after system crashes (see 246 // github.com/containers/podman/issues/5485). 247 startCommand = append(startCommand, "--replace") 248 } 249 startCommand = append(startCommand, info.CreateCommand[index:]...) 250 startCommand = quoteArguments(startCommand) 251 252 info.ExecStartPre = "/bin/rm -f {{.PIDFile}} {{.ContainerIDFile}}" 253 info.ExecStart = strings.Join(startCommand, " ") 254 info.ExecStop = "{{.Executable}} stop --ignore --cidfile {{.ContainerIDFile}} {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}}" 255 info.ExecStopPost = "{{.Executable}} rm --ignore -f --cidfile {{.ContainerIDFile}}" 256 } 257 258 if info.PodmanVersion == "" { 259 info.PodmanVersion = version.Version.String() 260 } 261 if info.GenerateTimestamp { 262 info.TimeStamp = fmt.Sprintf("%v", time.Now().Format(time.UnixDate)) 263 } 264 265 // Sort the slices to assure a deterministic output. 266 sort.Strings(info.BoundToServices) 267 268 // Generate the template and compile it. 269 // 270 // Note that we need a two-step generation process to allow for fields 271 // embedding other fields. This way we can replace `A -> B -> C` and 272 // make the code easier to maintain at the cost of a slightly slower 273 // generation. That's especially needed for embedding the PID and ID 274 // files in other fields which will eventually get replaced in the 2nd 275 // template execution. 276 templ, err := template.New("container_template").Parse(containerTemplate) 277 if err != nil { 278 return "", errors.Wrap(err, "error parsing systemd service template") 279 } 280 281 var buf bytes.Buffer 282 if err := templ.Execute(&buf, info); err != nil { 283 return "", err 284 } 285 286 // Now parse the generated template (i.e., buf) and execute it. 287 templ, err = template.New("container_template").Parse(buf.String()) 288 if err != nil { 289 return "", errors.Wrap(err, "error parsing systemd service template") 290 } 291 292 buf = bytes.Buffer{} 293 if err := templ.Execute(&buf, info); err != nil { 294 return "", err 295 } 296 297 return buf.String(), nil 298 }