github.com/containers/libpod@v1.9.4-0.20220419124438-4284fd425507/pkg/systemd/generate/systemdgen.go (about) 1 package generate 2 3 import ( 4 "bytes" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "path/filepath" 9 "sort" 10 "strings" 11 "text/template" 12 "time" 13 14 "github.com/containers/libpod/version" 15 "github.com/pkg/errors" 16 "github.com/sirupsen/logrus" 17 ) 18 19 // EnvVariable "PODMAN_SYSTEMD_UNIT" is set in all generated systemd units and 20 // is set to the unit's (unique) name. 21 const EnvVariable = "PODMAN_SYSTEMD_UNIT" 22 23 // ContainerInfo contains data required for generating a container's systemd 24 // unit file. 25 type ContainerInfo struct { 26 // ServiceName of the systemd service. 27 ServiceName string 28 // Name or ID of the container. 29 ContainerName string 30 // InfraContainer of the pod. 31 InfraContainer string 32 // StopTimeout sets the timeout Podman waits before killing the container 33 // during service stop. 34 StopTimeout uint 35 // RestartPolicy of the systemd unit (e.g., no, on-failure, always). 36 RestartPolicy string 37 // PIDFile of the service. Required for forking services. Must point to the 38 // PID of the associated conmon process. 39 PIDFile string 40 // GenerateTimestamp, if set the generated unit file has a time stamp. 41 GenerateTimestamp bool 42 // BoundToServices are the services this service binds to. Note that this 43 // service runs after them. 44 BoundToServices []string 45 // RequiredServices are services this service requires. Note that this 46 // service runs before them. 47 RequiredServices []string 48 // PodmanVersion for the header. Will be set internally. Will be auto-filled 49 // if left empty. 50 PodmanVersion string 51 // Executable is the path to the podman executable. Will be auto-filled if 52 // left empty. 53 Executable string 54 // TimeStamp at the time of creating the unit file. Will be set internally. 55 TimeStamp string 56 // New controls if a new container is created or if an existing one is started. 57 New bool 58 // CreateCommand is the full command plus arguments of the process the 59 // container has been created with. 60 CreateCommand []string 61 // RunCommand is a post-processed variant of CreateCommand and used for 62 // the ExecStart field in generic unit files. 63 RunCommand string 64 // EnvVariable is generate.EnvVariable and must not be set. 65 EnvVariable string 66 } 67 68 var restartPolicies = []string{"no", "on-success", "on-failure", "on-abnormal", "on-watchdog", "on-abort", "always"} 69 70 // validateRestartPolicy checks that the user-provided policy is valid. 71 func validateRestartPolicy(restart string) error { 72 for _, i := range restartPolicies { 73 if i == restart { 74 return nil 75 } 76 } 77 return errors.Errorf("%s is not a valid restart policy", restart) 78 } 79 80 const containerTemplate = `# {{.ServiceName}}.service 81 # autogenerated by Podman {{.PodmanVersion}} 82 {{- if .TimeStamp}} 83 # {{.TimeStamp}} 84 {{- end}} 85 86 [Unit] 87 Description=Podman {{.ServiceName}}.service 88 Documentation=man:podman-generate-systemd(1) 89 Wants=network.target 90 After=network-online.target 91 {{- if .BoundToServices}} 92 RefuseManualStart=yes 93 RefuseManualStop=yes 94 BindsTo={{- range $index, $value := .BoundToServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}} 95 After={{- range $index, $value := .BoundToServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}} 96 {{- end}} 97 {{- if .RequiredServices}} 98 Requires={{- range $index, $value := .RequiredServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}} 99 Before={{- range $index, $value := .RequiredServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}} 100 {{- end}} 101 102 [Service] 103 Environment={{.EnvVariable}}=%n 104 Restart={{.RestartPolicy}} 105 {{- if .New}} 106 ExecStartPre=/usr/bin/rm -f %t/%n-pid %t/%n-cid 107 ExecStart={{.RunCommand}} 108 ExecStop={{.Executable}} stop --ignore --cidfile %t/%n-cid {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}} 109 ExecStopPost={{.Executable}} rm --ignore -f --cidfile %t/%n-cid 110 PIDFile=%t/%n-pid 111 {{- else}} 112 ExecStart={{.Executable}} start {{.ContainerName}} 113 ExecStop={{.Executable}} stop {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}} {{.ContainerName}} 114 PIDFile={{.PIDFile}} 115 {{- end}} 116 KillMode=none 117 Type=forking 118 119 [Install] 120 WantedBy=multi-user.target default.target` 121 122 // Options include different options to control the unit file generation. 123 type Options struct { 124 // When set, generate service files in the current working directory and 125 // return the paths to these files instead of returning all contents in one 126 // big string. 127 Files bool 128 // New controls if a new container is created or if an existing one is started. 129 New bool 130 } 131 132 // CreateContainerSystemdUnit creates a systemd unit file for a container. 133 func CreateContainerSystemdUnit(info *ContainerInfo, opts Options) (string, error) { 134 if err := validateRestartPolicy(info.RestartPolicy); err != nil { 135 return "", err 136 } 137 138 // Make sure the executable is set. 139 if info.Executable == "" { 140 executable, err := os.Executable() 141 if err != nil { 142 executable = "/usr/bin/podman" 143 logrus.Warnf("Could not obtain podman executable location, using default %s", executable) 144 } 145 info.Executable = executable 146 } 147 148 info.EnvVariable = EnvVariable 149 150 // Assemble the ExecStart command when creating a new container. 151 // 152 // Note that we cannot catch all corner cases here such that users 153 // *must* manually check the generated files. A container might have 154 // been created via a Python script, which would certainly yield an 155 // invalid `info.CreateCommand`. Hence, we're doing a best effort unit 156 // generation and don't try aiming at completeness. 157 if opts.New { 158 // The create command must at least have three arguments: 159 // /usr/bin/podman run $IMAGE 160 index := 2 161 if info.CreateCommand[1] == "container" { 162 index = 3 163 } 164 if len(info.CreateCommand) < index+1 { 165 return "", errors.Errorf("container's create command is too short or invalid: %v", info.CreateCommand) 166 } 167 // We're hard-coding the first five arguments and append the 168 // CreateCommand with a stripped command and subcomand. 169 command := []string{ 170 info.Executable, 171 "run", 172 "--conmon-pidfile", "%t/%n-pid", 173 "--cidfile", "%t/%n-cid", 174 "--cgroups=no-conmon", 175 } 176 177 // Enforce detaching 178 // 179 // since we use systemd `Type=forking` service 180 // @see https://www.freedesktop.org/software/systemd/man/systemd.service.html#Type= 181 // when we generated systemd service file with the --new param, 182 // `ExecStart` will have `/usr/bin/podman run ...` 183 // if `info.CreateCommand` has no `-d` or `--detach` param, 184 // podman will run the container in default attached mode, 185 // as a result, `systemd start` will wait the `podman run` command exit until failed with timeout error. 186 hasDetachParam := false 187 for _, p := range info.CreateCommand[index:] { 188 if p == "--detach" || p == "-d" { 189 hasDetachParam = true 190 } 191 } 192 if !hasDetachParam { 193 command = append(command, "-d") 194 } 195 196 command = append(command, info.CreateCommand[index:]...) 197 info.RunCommand = strings.Join(command, " ") 198 info.New = true 199 } 200 201 if info.PodmanVersion == "" { 202 info.PodmanVersion = version.Version 203 } 204 if info.GenerateTimestamp { 205 info.TimeStamp = fmt.Sprintf("%v", time.Now().Format(time.UnixDate)) 206 } 207 208 // Sort the slices to assure a deterministic output. 209 sort.Strings(info.RequiredServices) 210 sort.Strings(info.BoundToServices) 211 212 // Generate the template and compile it. 213 templ, err := template.New("systemd_service_file").Parse(containerTemplate) 214 if err != nil { 215 return "", errors.Wrap(err, "error parsing systemd service template") 216 } 217 218 var buf bytes.Buffer 219 if err := templ.Execute(&buf, info); err != nil { 220 return "", err 221 } 222 223 if !opts.Files { 224 return buf.String(), nil 225 } 226 227 buf.WriteByte('\n') 228 cwd, err := os.Getwd() 229 if err != nil { 230 return "", errors.Wrap(err, "error getting current working directory") 231 } 232 path := filepath.Join(cwd, fmt.Sprintf("%s.service", info.ServiceName)) 233 if err := ioutil.WriteFile(path, buf.Bytes(), 0644); err != nil { 234 return "", errors.Wrap(err, "error generating systemd unit") 235 } 236 return path, nil 237 }