diff options
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/adapter/containers.go | 137 | ||||
-rw-r--r-- | pkg/systemdgen/systemdgen.go | 147 | ||||
-rw-r--r-- | pkg/systemdgen/systemdgen_test.go | 180 |
3 files changed, 373 insertions, 91 deletions
diff --git a/pkg/adapter/containers.go b/pkg/adapter/containers.go index 863640f97..41607145d 100644 --- a/pkg/adapter/containers.go +++ b/pkg/adapter/containers.go @@ -1094,28 +1094,145 @@ func (r *LocalRuntime) Port(c *cliconfig.PortValues) ([]*Container, error) { return portContainers, nil } -// GenerateSystemd creates a unit file for a container -func (r *LocalRuntime) GenerateSystemd(c *cliconfig.GenerateSystemdValues) (string, error) { - ctr, err := r.Runtime.LookupContainer(c.InputArgs[0]) +// generateServiceName generates the container name and the service name for systemd service. +func generateServiceName(c *cliconfig.GenerateSystemdValues, ctr *libpod.Container, pod *libpod.Pod) (string, string) { + var kind, name, ctrName string + if pod == nil { + kind = "container" + name = ctr.ID() + if c.Name { + name = ctr.Name() + } + ctrName = name + } else { + kind = "pod" + name = pod.ID() + ctrName = ctr.ID() + if c.Name { + name = pod.Name() + ctrName = ctr.Name() + } + } + return ctrName, fmt.Sprintf("%s-%s", kind, name) +} + +// generateSystemdgenContainerInfo is a helper to generate a +// systemdgen.ContainerInfo for `GenerateSystemd`. +func (r *LocalRuntime) generateSystemdgenContainerInfo(c *cliconfig.GenerateSystemdValues, nameOrID string, pod *libpod.Pod) (*systemdgen.ContainerInfo, bool, error) { + ctr, err := r.Runtime.LookupContainer(nameOrID) if err != nil { - return "", err + return nil, false, err } + timeout := int(ctr.StopTimeout()) if c.StopTimeout >= 0 { timeout = c.StopTimeout } - name := ctr.ID() - if c.Name { - name = ctr.Name() - } config := ctr.Config() conmonPidFile := config.ConmonPidFile if conmonPidFile == "" { - return "", errors.Errorf("conmon PID file path is empty, try to recreate the container with --conmon-pidfile flag") + return nil, true, errors.Errorf("conmon PID file path is empty, try to recreate the container with --conmon-pidfile flag") + } + + name, serviceName := generateServiceName(c, ctr, pod) + info := &systemdgen.ContainerInfo{ + ServiceName: serviceName, + ContainerName: name, + RestartPolicy: c.RestartPolicy, + PIDFile: conmonPidFile, + StopTimeout: timeout, + GenerateTimestamp: true, + } + + return info, true, nil +} + +// GenerateSystemd creates a unit file for a container or pod. +func (r *LocalRuntime) GenerateSystemd(c *cliconfig.GenerateSystemdValues) (string, error) { + // First assume it's a container. + if info, found, err := r.generateSystemdgenContainerInfo(c, c.InputArgs[0], nil); found && err != nil { + return "", err + } else if found && err == nil { + return systemdgen.CreateContainerSystemdUnit(info, c.Files) + } + + // We're either having a pod or garbage. + pod, err := r.Runtime.LookupPod(c.InputArgs[0]) + if err != nil { + return "", err + } + + // Error out if the pod has no infra container, which we require to be the + // main service. + if !pod.HasInfraContainer() { + return "", fmt.Errorf("error generating systemd unit files: Pod %q has no infra container", pod.Name()) + } + + // Generate a systemdgen.ContainerInfo for the infra container. This + // ContainerInfo acts as the main service of the pod. + infraID, err := pod.InfraContainerID() + if err != nil { + return "", nil + } + podInfo, _, err := r.generateSystemdgenContainerInfo(c, infraID, pod) + if err != nil { + return "", nil + } + + // Compute the container-dependency graph for the Pod. + containers, err := pod.AllContainers() + if err != nil { + return "", err + } + if len(containers) == 0 { + return "", fmt.Errorf("error generating systemd unit files: Pod %q has no containers", pod.Name()) + } + graph, err := libpod.BuildContainerGraph(containers) + if err != nil { + return "", err + } + + // Traverse the dependency graph and create systemdgen.ContainerInfo's for + // each container. + containerInfos := []*systemdgen.ContainerInfo{podInfo} + for ctr, dependencies := range graph.DependencyMap() { + // Skip the infra container as we already generated it. + if ctr.ID() == infraID { + continue + } + ctrInfo, _, err := r.generateSystemdgenContainerInfo(c, ctr.ID(), nil) + if err != nil { + return "", err + } + // Now add the container's dependencies and at the container as a + // required service of the infra container. + for _, dep := range dependencies { + if dep.ID() == infraID { + ctrInfo.BoundToServices = append(ctrInfo.BoundToServices, podInfo.ServiceName) + } else { + _, serviceName := generateServiceName(c, dep, nil) + ctrInfo.BoundToServices = append(ctrInfo.BoundToServices, serviceName) + } + } + podInfo.RequiredServices = append(podInfo.RequiredServices, ctrInfo.ServiceName) + containerInfos = append(containerInfos, ctrInfo) + } + + // Now generate the systemd service for all containers. + builder := strings.Builder{} + for i, info := range containerInfos { + if i > 0 { + builder.WriteByte('\n') + } + out, err := systemdgen.CreateContainerSystemdUnit(info, c.Files) + if err != nil { + return "", err + } + builder.WriteString(out) } - return systemdgen.CreateSystemdUnitAsString(name, ctr.ID(), c.RestartPolicy, conmonPidFile, timeout) + return builder.String(), nil } // GetNamespaces returns namespace information about a container for PS diff --git a/pkg/systemdgen/systemdgen.go b/pkg/systemdgen/systemdgen.go index 06c5ebde5..09d3c6fd5 100644 --- a/pkg/systemdgen/systemdgen.go +++ b/pkg/systemdgen/systemdgen.go @@ -1,29 +1,59 @@ package systemdgen import ( + "bytes" "fmt" + "io/ioutil" "os" + "path/filepath" + "sort" + "text/template" + "time" + "github.com/containers/libpod/version" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) -var template = `[Unit] -Description=%s Podman Container -[Service] -Restart=%s -ExecStart=%s start %s -ExecStop=%s stop -t %d %s -KillMode=none -Type=forking -PIDFile=%s -[Install] -WantedBy=multi-user.target` +// ContainerInfo contains data required for generating a container's systemd +// unit file. +type ContainerInfo struct { + // ServiceName of the systemd service. + ServiceName string + // Name or ID of the container. + ContainerName string + // InfraContainer of the pod. + InfraContainer string + // StopTimeout sets the timeout Podman waits before killing the container + // during service stop. + StopTimeout int + // RestartPolicy of the systemd unit (e.g., no, on-failure, always). + RestartPolicy string + // PIDFile of the service. Required for forking services. Must point to the + // PID of the associated conmon process. + PIDFile string + // GenerateTimestamp, if set the generated unit file has a time stamp. + GenerateTimestamp bool + // BoundToServices are the services this service binds to. Note that this + // service runs after them. + BoundToServices []string + // RequiredServices are services this service requires. Note that this + // service runs before them. + RequiredServices []string + // PodmanVersion for the header. Will be set internally. Will be auto-filled + // if left empty. + PodmanVersion string + // Executable is the path to the podman executable. Will be auto-filled if + // left empty. + Executable string + // TimeStamp at the time of creating the unit file. Will be set internally. + TimeStamp string +} var restartPolicies = []string{"no", "on-success", "on-failure", "on-abnormal", "on-watchdog", "on-abort", "always"} -// ValidateRestartPolicy checks that the user-provided policy is valid -func ValidateRestartPolicy(restart string) error { +// validateRestartPolicy checks that the user-provided policy is valid. +func validateRestartPolicy(restart string) error { for _, i := range restartPolicies { if i == restart { return nil @@ -32,28 +62,87 @@ func ValidateRestartPolicy(restart string) error { return errors.Errorf("%s is not a valid restart policy", restart) } -// CreateSystemdUnitAsString takes variables to create a systemd unit file used to control -// a libpod container -func CreateSystemdUnitAsString(name, cid, restart, pidFile string, stopTimeout int) (string, error) { - podmanExe := getPodmanExecutable() - return createSystemdUnitAsString(podmanExe, name, cid, restart, pidFile, stopTimeout) -} +const containerTemplate = `# {{.ServiceName}}.service +# autogenerated by Podman {{.PodmanVersion}} +{{- if .TimeStamp}} +# {{.TimeStamp}} +{{- end}} + +[Unit] +Description=Podman {{.ServiceName}}.service +Documentation=man:podman-generate-systemd(1) +{{- if .BoundToServices}} +RefuseManualStart=yes +RefuseManualStop=yes +BindsTo={{- range $index, $value := .BoundToServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}} +After={{- range $index, $value := .BoundToServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}} +{{- end}} +{{- if .RequiredServices}} +Requires={{- range $index, $value := .RequiredServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}} +Before={{- range $index, $value := .RequiredServices -}}{{if $index}} {{end}}{{ $value }}.service{{end}} +{{- end}} + +[Service] +Restart={{.RestartPolicy}} +ExecStart={{.Executable}} start {{.ContainerName}} +ExecStop={{.Executable}} stop {{if (ge .StopTimeout 0)}}-t {{.StopTimeout}}{{end}} {{.ContainerName}} +KillMode=none +Type=forking +PIDFile={{.PIDFile}} + +[Install] +WantedBy=multi-user.target` -func createSystemdUnitAsString(exe, name, cid, restart, pidFile string, stopTimeout int) (string, error) { - if err := ValidateRestartPolicy(restart); err != nil { +// CreateContainerSystemdUnit creates a systemd unit file for a container. +func CreateContainerSystemdUnit(info *ContainerInfo, generateFiles bool) (string, error) { + if err := validateRestartPolicy(info.RestartPolicy); err != nil { return "", err } - unit := fmt.Sprintf(template, name, restart, exe, name, exe, stopTimeout, name, pidFile) - return unit, nil -} + // Make sure the executable is set. + if info.Executable == "" { + executable, err := os.Executable() + if err != nil { + executable = "/usr/bin/podman" + logrus.Warnf("Could not obtain podman executable location, using default %s", executable) + } + info.Executable = executable + } + + if info.PodmanVersion == "" { + info.PodmanVersion = version.Version + } + if info.GenerateTimestamp { + info.TimeStamp = fmt.Sprintf("%v", time.Now().Format(time.UnixDate)) + } -func getPodmanExecutable() string { - podmanExe, err := os.Executable() + // Sort the slices to assure a deterministic output. + sort.Strings(info.RequiredServices) + sort.Strings(info.BoundToServices) + + // Generate the template and compile it. + templ, err := template.New("systemd_service_file").Parse(containerTemplate) if err != nil { - podmanExe = "/usr/bin/podman" - logrus.Warnf("Could not obtain podman executable location, using default %s", podmanExe) + return "", errors.Wrap(err, "error parsing systemd service template") } - return podmanExe + var buf bytes.Buffer + if err := templ.Execute(&buf, info); err != nil { + return "", err + } + + if !generateFiles { + return buf.String(), nil + } + + buf.WriteByte('\n') + cwd, err := os.Getwd() + if err != nil { + return "", errors.Wrap(err, "error getting current working directory") + } + path := filepath.Join(cwd, fmt.Sprintf("%s.service", info.ServiceName)) + if err := ioutil.WriteFile(path, buf.Bytes(), 0644); err != nil { + return "", errors.Wrap(err, "error generating systemd unit") + } + return path, nil } diff --git a/pkg/systemdgen/systemdgen_test.go b/pkg/systemdgen/systemdgen_test.go index e413b24ce..1ddb0c514 100644 --- a/pkg/systemdgen/systemdgen_test.go +++ b/pkg/systemdgen/systemdgen_test.go @@ -5,36 +5,41 @@ import ( ) func TestValidateRestartPolicy(t *testing.T) { - type args struct { + type ContainerInfo struct { restart string } tests := []struct { - name string - args args - wantErr bool + name string + ContainerInfo ContainerInfo + wantErr bool }{ - {"good-on", args{restart: "no"}, false}, - {"good-on-success", args{restart: "on-success"}, false}, - {"good-on-failure", args{restart: "on-failure"}, false}, - {"good-on-abnormal", args{restart: "on-abnormal"}, false}, - {"good-on-watchdog", args{restart: "on-watchdog"}, false}, - {"good-on-abort", args{restart: "on-abort"}, false}, - {"good-always", args{restart: "always"}, false}, - {"fail", args{restart: "foobar"}, true}, - {"failblank", args{restart: ""}, true}, + {"good-on", ContainerInfo{restart: "no"}, false}, + {"good-on-success", ContainerInfo{restart: "on-success"}, false}, + {"good-on-failure", ContainerInfo{restart: "on-failure"}, false}, + {"good-on-abnormal", ContainerInfo{restart: "on-abnormal"}, false}, + {"good-on-watchdog", ContainerInfo{restart: "on-watchdog"}, false}, + {"good-on-abort", ContainerInfo{restart: "on-abort"}, false}, + {"good-always", ContainerInfo{restart: "always"}, false}, + {"fail", ContainerInfo{restart: "foobar"}, true}, + {"failblank", ContainerInfo{restart: ""}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := ValidateRestartPolicy(tt.args.restart); (err != nil) != tt.wantErr { + if err := validateRestartPolicy(tt.ContainerInfo.restart); (err != nil) != tt.wantErr { t.Errorf("ValidateRestartPolicy() error = %v, wantErr %v", err, tt.wantErr) } }) } } -func TestCreateSystemdUnitAsString(t *testing.T) { - goodID := `[Unit] -Description=639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401 Podman Container +func TestCreateContainerSystemdUnit(t *testing.T) { + goodID := `# container-639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401.service +# autogenerated by Podman CI + +[Unit] +Description=Podman container-639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401.service +Documentation=man:podman-generate-systemd(1) + [Service] Restart=always ExecStart=/usr/bin/podman start 639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401 @@ -42,11 +47,17 @@ ExecStop=/usr/bin/podman stop -t 10 639c53578af4d84b8800b4635fa4e680ee80fd67e0e6 KillMode=none Type=forking PIDFile=/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid + [Install] WantedBy=multi-user.target` - goodName := `[Unit] -Description=foobar Podman Container + goodName := `# container-foobar.service +# autogenerated by Podman CI + +[Unit] +Description=Podman container-foobar.service +Documentation=man:podman-generate-systemd(1) + [Service] Restart=always ExecStart=/usr/bin/podman start foobar @@ -54,56 +65,121 @@ ExecStop=/usr/bin/podman stop -t 10 foobar KillMode=none Type=forking PIDFile=/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid + +[Install] +WantedBy=multi-user.target` + + goodNameBoundTo := `# container-foobar.service +# autogenerated by Podman CI + +[Unit] +Description=Podman container-foobar.service +Documentation=man:podman-generate-systemd(1) +RefuseManualStart=yes +RefuseManualStop=yes +BindsTo=a.service b.service c.service pod.service +After=a.service b.service c.service pod.service + +[Service] +Restart=always +ExecStart=/usr/bin/podman start foobar +ExecStop=/usr/bin/podman stop -t 10 foobar +KillMode=none +Type=forking +PIDFile=/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid + +[Install] +WantedBy=multi-user.target` + + podGoodName := `# pod-123abc.service +# autogenerated by Podman CI + +[Unit] +Description=Podman pod-123abc.service +Documentation=man:podman-generate-systemd(1) +Requires=container-1.service container-2.service +Before=container-1.service container-2.service + +[Service] +Restart=always +ExecStart=/usr/bin/podman start jadda-jadda-infra +ExecStop=/usr/bin/podman stop -t 10 jadda-jadda-infra +KillMode=none +Type=forking +PIDFile=/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid + [Install] WantedBy=multi-user.target` - type args struct { - exe string - name string - cid string - restart string - pidFile string - stopTimeout int - } tests := []struct { name string - args args + info ContainerInfo want string wantErr bool }{ {"good with id", - args{ - "/usr/bin/podman", - "639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401", - "639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401", - "always", - "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", - 10, + ContainerInfo{ + Executable: "/usr/bin/podman", + ServiceName: "container-639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401", + ContainerName: "639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401", + RestartPolicy: "always", + PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", + StopTimeout: 10, + PodmanVersion: "CI", }, goodID, false, }, {"good with name", - args{ - "/usr/bin/podman", - "foobar", - "639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401", - "always", - "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", - 10, + ContainerInfo{ + Executable: "/usr/bin/podman", + ServiceName: "container-foobar", + ContainerName: "foobar", + RestartPolicy: "always", + PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", + StopTimeout: 10, + PodmanVersion: "CI", }, goodName, false, }, + {"good with name and bound to", + ContainerInfo{ + Executable: "/usr/bin/podman", + ServiceName: "container-foobar", + ContainerName: "foobar", + RestartPolicy: "always", + PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", + StopTimeout: 10, + PodmanVersion: "CI", + BoundToServices: []string{"pod", "a", "b", "c"}, + }, + goodNameBoundTo, + false, + }, + {"pod", + ContainerInfo{ + Executable: "/usr/bin/podman", + ServiceName: "pod-123abc", + ContainerName: "jadda-jadda-infra", + RestartPolicy: "always", + PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", + StopTimeout: 10, + PodmanVersion: "CI", + RequiredServices: []string{"container-1", "container-2"}, + }, + podGoodName, + false, + }, {"bad restart policy", - args{ - "/usr/bin/podman", - "639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401", - "639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401", - "never", - "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", - 10, + ContainerInfo{ + Executable: "/usr/bin/podman", + ServiceName: "639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401", + RestartPolicy: "never", + PIDFile: "/var/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", + StopTimeout: 10, + PodmanVersion: "CI", }, "", true, @@ -111,13 +187,13 @@ WantedBy=multi-user.target` } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := createSystemdUnitAsString(tt.args.exe, tt.args.name, tt.args.cid, tt.args.restart, tt.args.pidFile, tt.args.stopTimeout) + got, err := CreateContainerSystemdUnit(&tt.info, false) if (err != nil) != tt.wantErr { - t.Errorf("CreateSystemdUnitAsString() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("CreateContainerSystemdUnit() error = \n%v, wantErr \n%v", err, tt.wantErr) return } if got != tt.want { - t.Errorf("CreateSystemdUnitAsString() = %v, want %v", got, tt.want) + t.Errorf("CreateContainerSystemdUnit() = \n%v, want \n%v", got, tt.want) } }) } |