generate systemd

Implement `podman generate systemd` for Podman v2 and enable associated
tests.

Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
This commit is contained in:
Valentin Rothberg
2020-04-27 14:23:23 +02:00
parent bf4efc1953
commit b2414b580e
10 changed files with 297 additions and 1 deletions

View File

@ -0,0 +1,27 @@
package pods
import (
"github.com/containers/libpod/cmd/podman/registry"
"github.com/containers/libpod/pkg/domain/entities"
"github.com/containers/libpod/pkg/util"
"github.com/spf13/cobra"
)
var (
// Command: podman _generate_
generateCmd = &cobra.Command{
Use: "generate",
Short: "Generate structured data based on containers and pods.",
Long: "Generate structured data (e.g., Kubernetes yaml or systemd units) based on containers and pods.",
TraverseChildren: true,
RunE: registry.SubCommandExists,
}
containerConfig = util.DefaultContainerConfig()
)
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Mode: []entities.EngineMode{entities.ABIMode},
Command: generateCmd,
})
}

View File

@ -0,0 +1,57 @@
package pods
import (
"fmt"
"github.com/containers/libpod/cmd/podman/registry"
"github.com/containers/libpod/cmd/podman/utils"
"github.com/containers/libpod/pkg/domain/entities"
"github.com/spf13/cobra"
)
var (
systemdTimeout uint
systemdOptions = entities.GenerateSystemdOptions{}
systemdDescription = `Generate systemd units for a pod or container.
The generated units can later be controlled via systemctl(1).`
systemdCmd = &cobra.Command{
Use: "systemd [flags] CTR|POD",
Short: "Generate systemd units.",
Long: systemdDescription,
RunE: systemd,
Args: cobra.MinimumNArgs(1),
Example: `podman generate systemd CTR
podman generate systemd --new --time 10 CTR
podman generate systemd --files --name POD`,
}
)
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode},
Command: systemdCmd,
Parent: generateCmd,
})
flags := systemdCmd.Flags()
flags.BoolVarP(&systemdOptions.Name, "name", "n", false, "Use container/pod names instead of IDs")
flags.BoolVarP(&systemdOptions.Files, "files", "f", false, "Generate .service files instead of printing to stdout")
flags.UintVarP(&systemdTimeout, "time", "t", containerConfig.Engine.StopTimeout, "Stop timeout override")
flags.StringVar(&systemdOptions.RestartPolicy, "restart-policy", "on-failure", "Systemd restart-policy")
flags.BoolVarP(&systemdOptions.New, "new", "", false, "Create a new container instead of starting an existing one")
flags.SetNormalizeFunc(utils.AliasFlags)
}
func systemd(cmd *cobra.Command, args []string) error {
if cmd.Flags().Changed("time") {
systemdOptions.StopTimeout = &systemdTimeout
}
report, err := registry.ContainerEngine().GenerateSystemd(registry.GetContext(), args[0], systemdOptions)
if err != nil {
return err
}
fmt.Println(report.Output)
return nil
}

View File

@ -4,6 +4,7 @@ import (
"os" "os"
_ "github.com/containers/libpod/cmd/podman/containers" _ "github.com/containers/libpod/cmd/podman/containers"
_ "github.com/containers/libpod/cmd/podman/generate"
_ "github.com/containers/libpod/cmd/podman/healthcheck" _ "github.com/containers/libpod/cmd/podman/healthcheck"
_ "github.com/containers/libpod/cmd/podman/images" _ "github.com/containers/libpod/cmd/podman/images"
_ "github.com/containers/libpod/cmd/podman/manifest" _ "github.com/containers/libpod/cmd/podman/manifest"

View File

@ -41,6 +41,7 @@ type ContainerEngine interface {
ContainerUnpause(ctx context.Context, namesOrIds []string, options PauseUnPauseOptions) ([]*PauseUnpauseReport, error) ContainerUnpause(ctx context.Context, namesOrIds []string, options PauseUnPauseOptions) ([]*PauseUnpauseReport, error)
ContainerWait(ctx context.Context, namesOrIds []string, options WaitOptions) ([]WaitReport, error) ContainerWait(ctx context.Context, namesOrIds []string, options WaitOptions) ([]WaitReport, error)
Events(ctx context.Context, opts EventsOptions) error Events(ctx context.Context, opts EventsOptions) error
GenerateSystemd(ctx context.Context, nameOrID string, opts GenerateSystemdOptions) (*GenerateSystemdReport, error)
HealthCheckRun(ctx context.Context, nameOrId string, options HealthCheckOptions) (*define.HealthCheckResults, error) HealthCheckRun(ctx context.Context, nameOrId string, options HealthCheckOptions) (*define.HealthCheckResults, error)
Info(ctx context.Context) (*define.Info, error) Info(ctx context.Context) (*define.Info, error)
PodCreate(ctx context.Context, opts PodCreateOptions) (*PodCreateReport, error) PodCreate(ctx context.Context, opts PodCreateOptions) (*PodCreateReport, error)

View File

@ -0,0 +1,22 @@
package entities
// GenerateSystemdOptions control the generation of systemd unit files.
type GenerateSystemdOptions struct {
// Files - generate files instead of printing to stdout.
Files bool
// Name - use container/pod name instead of its ID.
Name bool
// New - create a new container instead of starting a new one.
New bool
// RestartPolicy - systemd restart policy.
RestartPolicy string
// StopTimeout - time when stopping the container.
StopTimeout *uint
}
// GenerateSystemdReport
type GenerateSystemdReport struct {
// Output of the generate process. Either the generated files or their
// entire content.
Output string
}

View File

@ -0,0 +1,174 @@
package abi
import (
"context"
"fmt"
"strings"
"github.com/containers/libpod/libpod"
"github.com/containers/libpod/pkg/domain/entities"
"github.com/containers/libpod/pkg/systemd/generate"
"github.com/pkg/errors"
)
func (ic *ContainerEngine) GenerateSystemd(ctx context.Context, nameOrID string, options entities.GenerateSystemdOptions) (*entities.GenerateSystemdReport, error) {
opts := generate.Options{
Files: options.Files,
New: options.New,
}
// First assume it's a container.
if info, found, err := ic.generateSystemdgenContainerInfo(nameOrID, nil, options); found && err != nil {
return nil, err
} else if found && err == nil {
output, err := generate.CreateContainerSystemdUnit(info, opts)
if err != nil {
return nil, err
}
return &entities.GenerateSystemdReport{Output: output}, nil
}
// --new does not support pods.
if options.New {
return nil, errors.Errorf("error generating systemd unit files: cannot generate generic files for a pod")
}
// We're either having a pod or garbage.
pod, err := ic.Libpod.LookupPod(nameOrID)
if err != nil {
return nil, err
}
// Error out if the pod has no infra container, which we require to be the
// main service.
if !pod.HasInfraContainer() {
return nil, 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, nil
}
podInfo, _, err := ic.generateSystemdgenContainerInfo(infraID, pod, options)
if err != nil {
return nil, err
}
// Compute the container-dependency graph for the Pod.
containers, err := pod.AllContainers()
if err != nil {
return nil, err
}
if len(containers) == 0 {
return nil, fmt.Errorf("error generating systemd unit files: Pod %q has no containers", pod.Name())
}
graph, err := libpod.BuildContainerGraph(containers)
if err != nil {
return nil, err
}
// Traverse the dependency graph and create systemdgen.ContainerInfo's for
// each container.
containerInfos := []*generate.ContainerInfo{podInfo}
for ctr, dependencies := range graph.DependencyMap() {
// Skip the infra container as we already generated it.
if ctr.ID() == infraID {
continue
}
ctrInfo, _, err := ic.generateSystemdgenContainerInfo(ctr.ID(), nil, options)
if err != nil {
return nil, 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(dep, nil, options)
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 := generate.CreateContainerSystemdUnit(info, opts)
if err != nil {
return nil, err
}
builder.WriteString(out)
}
return &entities.GenerateSystemdReport{Output: builder.String()}, nil
}
// generateSystemdgenContainerInfo is a helper to generate a
// systemdgen.ContainerInfo for `GenerateSystemd`.
func (ic *ContainerEngine) generateSystemdgenContainerInfo(nameOrID string, pod *libpod.Pod, options entities.GenerateSystemdOptions) (*generate.ContainerInfo, bool, error) {
ctr, err := ic.Libpod.LookupContainer(nameOrID)
if err != nil {
return nil, false, err
}
timeout := ctr.StopTimeout()
if options.StopTimeout != nil {
timeout = *options.StopTimeout
}
config := ctr.Config()
conmonPidFile := config.ConmonPidFile
if conmonPidFile == "" {
return nil, true, errors.Errorf("conmon PID file path is empty, try to recreate the container with --conmon-pidfile flag")
}
createCommand := []string{}
if config.CreateCommand != nil {
createCommand = config.CreateCommand
} else if options.New {
return nil, true, errors.Errorf("cannot use --new on container %q: no create command found", nameOrID)
}
name, serviceName := generateServiceName(ctr, pod, options)
info := &generate.ContainerInfo{
ServiceName: serviceName,
ContainerName: name,
RestartPolicy: options.RestartPolicy,
PIDFile: conmonPidFile,
StopTimeout: timeout,
GenerateTimestamp: true,
CreateCommand: createCommand,
}
return info, true, nil
}
// generateServiceName generates the container name and the service name for systemd service.
func generateServiceName(ctr *libpod.Container, pod *libpod.Pod, options entities.GenerateSystemdOptions) (string, string) {
var kind, name, ctrName string
if pod == nil {
kind = "container"
name = ctr.ID()
if options.Name {
name = ctr.Name()
}
ctrName = name
} else {
kind = "pod"
name = pod.ID()
ctrName = ctr.ID()
if options.Name {
name = pod.Name()
ctrName = ctr.Name()
}
}
return ctrName, fmt.Sprintf("%s-%s", kind, name)
}

View File

@ -0,0 +1,12 @@
package tunnel
import (
"context"
"github.com/containers/libpod/pkg/domain/entities"
"github.com/pkg/errors"
)
func (ic *ContainerEngine) GenerateSystemd(ctx context.Context, nameOrID string, options entities.GenerateSystemdOptions) (*entities.GenerateSystemdReport, error) {
return nil, errors.New("not implemented for tunnel")
}

View File

@ -76,6 +76,7 @@ func MakeContainer(ctx context.Context, rt *libpod.Runtime, s *specgen.SpecGener
} }
options := []libpod.CtrCreateOption{} options := []libpod.CtrCreateOption{}
options = append(options, libpod.WithCreateCommand())
var newImage *image.Image var newImage *image.Image
if s.Rootfs != "" { if s.Rootfs != "" {

View File

@ -18,7 +18,6 @@ var _ = Describe("Podman generate systemd", func() {
) )
BeforeEach(func() { BeforeEach(func() {
Skip(v2fail)
tempdir, err = CreateTempDirInTempDir() tempdir, err = CreateTempDirInTempDir()
if err != nil { if err != nil {
os.Exit(1) os.Exit(1)

View File

@ -10,6 +10,8 @@ SERVICE_NAME="podman_test_$(random_string)"
UNIT_DIR="$HOME/.config/systemd/user" UNIT_DIR="$HOME/.config/systemd/user"
UNIT_FILE="$UNIT_DIR/$SERVICE_NAME.service" UNIT_FILE="$UNIT_DIR/$SERVICE_NAME.service"
# FIXME: the must run as root (because of CI). It's also broken...
function setup() { function setup() {
skip_if_not_systemd skip_if_not_systemd
skip_if_remote skip_if_remote