APIv2 add generate systemd endpoint

Add support for generating systemd units
via the api and podman-remote.

Change the GenerateSystemdReport type to return the
units as map[string]string with the unit name as key.

Add `--format` flag to `podman generate systemd`
to allow the output to be formatted as json.

Signed-off-by: Paul Holzinger <paul.holzinger@web.de>
This commit is contained in:
Paul Holzinger
2020-08-05 09:29:59 +02:00
parent 1184cdf03d
commit ebfea2f4f8
13 changed files with 275 additions and 84 deletions

View File

@ -1,15 +1,22 @@
package pods
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/containers/podman/v2/cmd/podman/registry"
"github.com/containers/podman/v2/cmd/podman/utils"
"github.com/containers/podman/v2/pkg/domain/entities"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var (
files bool
format string
systemdTimeout uint
systemdOptions = entities.GenerateSystemdOptions{}
systemdDescription = `Generate systemd units for a pod or container.
@ -29,19 +36,20 @@ var (
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Mode: []entities.EngineMode{entities.ABIMode},
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.BoolVarP(&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.StringVar(&systemdOptions.ContainerPrefix, "container-prefix", "container", "Systemd unit name prefix for containers")
flags.StringVar(&systemdOptions.PodPrefix, "pod-prefix", "pod", "Systemd unit name prefix for pods")
flags.StringVar(&systemdOptions.Separator, "separator", "-", "Systemd unit name separator between name/id and prefix")
flags.StringVar(&format, "format", "", "Print the created units in specified format (json)")
flags.SetNormalizeFunc(utils.AliasFlags)
}
@ -50,11 +58,68 @@ func systemd(cmd *cobra.Command, args []string) error {
systemdOptions.StopTimeout = &systemdTimeout
}
if registry.IsRemote() {
logrus.Warnln("The generated units should be placed on your remote system")
}
report, err := registry.ContainerEngine().GenerateSystemd(registry.GetContext(), args[0], systemdOptions)
if err != nil {
return err
}
fmt.Println(report.Output)
if files {
cwd, err := os.Getwd()
if err != nil {
return errors.Wrap(err, "error getting current working directory")
}
for name, content := range report.Units {
path := filepath.Join(cwd, fmt.Sprintf("%s.service", name))
f, err := os.Create(path)
if err != nil {
return err
}
_, err = f.WriteString(content)
if err != nil {
return err
}
err = f.Close()
if err != nil {
return err
}
// add newline if default format is given
if format == "" {
path += "\n"
}
// modify in place so we can print the
// paths when --files is set
report.Units[name] = path
}
}
switch format {
case "json":
return printJSON(report.Units)
case "":
return printDefault(report.Units)
default:
return errors.Errorf("unknown --format argument: %s", format)
}
}
func printDefault(units map[string]string) error {
for _, content := range units {
fmt.Print(content)
}
return nil
}
func printJSON(units map[string]string) error {
b, err := json.MarshalIndent(units, "", " ")
if err != nil {
return err
}
fmt.Println(string(b))
return nil
}

View File

@ -10,7 +10,7 @@ podman\-generate\-systemd - Generate systemd unit file(s) for a container or pod
**podman generate systemd** will create a systemd unit file that can be used to control a container or pod.
By default, the command will print the content of the unit files to stdout.
Note that this command is not supported for the remote client.
_Note: If you use this command with the remote client, you would still have to place the generated units on the remote system._
## OPTIONS:
@ -20,6 +20,10 @@ Generate files instead of printing to stdout. The generated files are named {co
Note: On a system with SELinux enabled, the generated files will inherit contexts from the current working directory. Depending on the SELinux setup, changes to the generated files using `restorecon`, `chcon`, or `semanage` may be required to allow systemd to access these files. Alternatively, use the `-Z` option when running `mv` or `cp`.
**--format**=*format*
Print the created units in specified format (json). If `--files` is specified the paths to the created files will be printed instead of the unit content.
**--name**, **-n**
Use the name of the container for the start, stop, and description in the unit file

View File

@ -7,10 +7,55 @@ import (
"github.com/containers/podman/v2/pkg/api/handlers/utils"
"github.com/containers/podman/v2/pkg/domain/entities"
"github.com/containers/podman/v2/pkg/domain/infra/abi"
"github.com/containers/podman/v2/pkg/util"
"github.com/gorilla/schema"
"github.com/pkg/errors"
)
func GenerateSystemd(w http.ResponseWriter, r *http.Request) {
runtime := r.Context().Value("runtime").(*libpod.Runtime)
decoder := r.Context().Value("decoder").(*schema.Decoder)
query := struct {
Name bool `schema:"useName"`
New bool `schema:"new"`
RestartPolicy string `schema:"restartPolicy"`
StopTimeout uint `schema:"stopTimeout"`
ContainerPrefix string `schema:"containerPrefix"`
PodPrefix string `schema:"podPrefix"`
Separator string `schema:"separator"`
}{
RestartPolicy: "on-failure",
StopTimeout: util.DefaultContainerConfig().Engine.StopTimeout,
ContainerPrefix: "container",
PodPrefix: "pod",
Separator: "-",
}
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest,
errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String()))
return
}
containerEngine := abi.ContainerEngine{Libpod: runtime}
options := entities.GenerateSystemdOptions{
Name: query.Name,
New: query.New,
RestartPolicy: query.RestartPolicy,
StopTimeout: &query.StopTimeout,
ContainerPrefix: query.ContainerPrefix,
PodPrefix: query.PodPrefix,
Separator: query.Separator,
}
report, err := containerEngine.GenerateSystemd(r.Context(), utils.GetName(r), options)
if err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error generating systemd units"))
return
}
utils.WriteResponse(w, http.StatusOK, report.Units)
}
func GenerateKube(w http.ResponseWriter, r *http.Request) {
runtime := r.Context().Value("runtime").(*libpod.Runtime)
decoder := r.Context().Value("decoder").(*schema.Decoder)

View File

@ -8,6 +8,68 @@ import (
)
func (s *APIServer) registerGenerateHandlers(r *mux.Router) error {
// swagger:operation GET /libpod/generate/{name:.*}/systemd libpod libpodGenerateSystemd
// ---
// tags:
// - containers
// - pods
// summary: Generate Systemd Units
// description: Generate Systemd Units based on a pod or container.
// parameters:
// - in: path
// name: name:.*
// type: string
// required: true
// description: Name or ID of the container or pod.
// - in: query
// name: useName
// type: boolean
// default: false
// description: Use container/pod names instead of IDs.
// - in: query
// name: new
// type: boolean
// default: false
// description: Create a new container instead of starting an existing one.
// - in: query
// name: time
// type: integer
// default: 10
// description: Stop timeout override.
// - in: query
// name: restartPolicy
// default: on-failure
// type: string
// enum: ["no", on-success, on-failure, on-abnormal, on-watchdog, on-abort, always]
// description: Systemd restart-policy.
// - in: query
// name: containerPrefix
// type: string
// default: container
// description: Systemd unit name prefix for containers.
// - in: query
// name: podPrefix
// type: string
// default: pod
// description: Systemd unit name prefix for pods.
// - in: query
// name: separator
// type: string
// default: "-"
// description: Systemd unit name separator between name/id and prefix.
// produces:
// - application/json
// responses:
// 200:
// description: no error
// schema:
// type: object
// additionalProperties:
// type: string
// 500:
// $ref: "#/responses/InternalError"
r.HandleFunc(VersionedPath("/libpod/generate/{name:.*}/systemd"), s.APIHandler(libpod.GenerateSystemd)).Methods(http.MethodGet)
// swagger:operation GET /libpod/generate/{name:.*}/kube libpod libpodGenerateKube
// ---
// tags:

View File

@ -10,6 +10,33 @@ import (
"github.com/containers/podman/v2/pkg/domain/entities"
)
func Systemd(ctx context.Context, nameOrID string, options entities.GenerateSystemdOptions) (*entities.GenerateSystemdReport, error) {
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
params := url.Values{}
params.Set("useName", strconv.FormatBool(options.Name))
params.Set("new", strconv.FormatBool(options.New))
if options.RestartPolicy != "" {
params.Set("restartPolicy", options.RestartPolicy)
}
if options.StopTimeout != nil {
params.Set("stopTimeout", strconv.FormatUint(uint64(*options.StopTimeout), 10))
}
params.Set("containerPrefix", options.ContainerPrefix)
params.Set("podPrefix", options.PodPrefix)
params.Set("separator", options.Separator)
response, err := conn.DoRequest(nil, http.MethodGet, "/generate/%s/systemd", params, nil, nameOrID)
if err != nil {
return nil, err
}
report := &entities.GenerateSystemdReport{}
return report, response.Process(&report.Units)
}
func Kube(ctx context.Context, nameOrID string, options entities.GenerateKubeOptions) (*entities.GenerateKubeReport, error) {
conn, err := bindings.GetClient(ctx)
if err != nil {

View File

@ -4,8 +4,6 @@ import "io"
// 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.
@ -24,9 +22,8 @@ type GenerateSystemdOptions struct {
// GenerateSystemdReport
type GenerateSystemdReport struct {
// Output of the generate process. Either the generated files or their
// entire content.
Output string
// Units of the generate process. key = unit name -> value = unit content
Units map[string]string
}
// GenerateKubeOptions control the generation of Kubernetes YAML files.

View File

@ -19,11 +19,11 @@ func (ic *ContainerEngine) GenerateSystemd(ctx context.Context, nameOrID string,
ctr, ctrErr := ic.Libpod.LookupContainer(nameOrID)
if ctrErr == nil {
// Generate the unit for the container.
s, err := generate.ContainerUnit(ctr, options)
name, content, err := generate.ContainerUnit(ctr, options)
if err != nil {
return nil, err
}
return &entities.GenerateSystemdReport{Output: s}, nil
return &entities.GenerateSystemdReport{Units: map[string]string{name: content}}, nil
}
// If it's not a container, we either have a pod or garbage.
@ -34,11 +34,11 @@ func (ic *ContainerEngine) GenerateSystemd(ctx context.Context, nameOrID string,
}
// Generate the units for the pod and all its containers.
s, err := generate.PodUnits(pod, options)
units, err := generate.PodUnits(pod, options)
if err != nil {
return nil, err
}
return &entities.GenerateSystemdReport{Output: s}, nil
return &entities.GenerateSystemdReport{Units: units}, nil
}
func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrID string, options entities.GenerateKubeOptions) (*entities.GenerateKubeReport, error) {

View File

@ -5,11 +5,10 @@ import (
"github.com/containers/podman/v2/pkg/bindings/generate"
"github.com/containers/podman/v2/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")
return generate.Systemd(ic.ClientCxt, nameOrID, options)
}
func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrID string, options entities.GenerateKubeOptions) (*entities.GenerateKubeReport, error) {

View File

@ -3,9 +3,7 @@ package generate
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"text/template"
@ -87,17 +85,22 @@ KillMode=none
Type=forking
[Install]
WantedBy=multi-user.target default.target`
WantedBy=multi-user.target default.target
`
// ContainerUnit generates a systemd unit for the specified container. Based
// on the options, the return value might be the entire unit or a file it has
// been written to.
func ContainerUnit(ctr *libpod.Container, options entities.GenerateSystemdOptions) (string, error) {
func ContainerUnit(ctr *libpod.Container, options entities.GenerateSystemdOptions) (string, string, error) {
info, err := generateContainerInfo(ctr, options)
if err != nil {
return "", err
return "", "", err
}
return executeContainerTemplate(info, options)
content, err := executeContainerTemplate(info, options)
if err != nil {
return "", "", err
}
return info.ServiceName, content, nil
}
func generateContainerInfo(ctr *libpod.Container, options entities.GenerateSystemdOptions) (*containerInfo, error) {
@ -288,18 +291,5 @@ func executeContainerTemplate(info *containerInfo, options entities.GenerateSyst
return "", err
}
if !options.Files {
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
}

View File

@ -56,7 +56,8 @@ KillMode=none
Type=forking
[Install]
WantedBy=multi-user.target default.target`
WantedBy=multi-user.target default.target
`
goodName := `# container-foobar.service
# autogenerated by Podman CI
@ -78,7 +79,8 @@ KillMode=none
Type=forking
[Install]
WantedBy=multi-user.target default.target`
WantedBy=multi-user.target default.target
`
goodNameBoundTo := `# container-foobar.service
# autogenerated by Podman CI
@ -102,7 +104,8 @@ KillMode=none
Type=forking
[Install]
WantedBy=multi-user.target default.target`
WantedBy=multi-user.target default.target
`
goodWithNameAndGeneric := `# jadda-jadda.service
# autogenerated by Podman CI
@ -125,7 +128,8 @@ KillMode=none
Type=forking
[Install]
WantedBy=multi-user.target default.target`
WantedBy=multi-user.target default.target
`
goodWithExplicitShortDetachParam := `# jadda-jadda.service
# autogenerated by Podman CI
@ -148,7 +152,8 @@ KillMode=none
Type=forking
[Install]
WantedBy=multi-user.target default.target`
WantedBy=multi-user.target default.target
`
goodNameNewWithPodFile := `# jadda-jadda.service
# autogenerated by Podman CI
@ -171,7 +176,8 @@ KillMode=none
Type=forking
[Install]
WantedBy=multi-user.target default.target`
WantedBy=multi-user.target default.target
`
goodNameNewDetach := `# jadda-jadda.service
# autogenerated by Podman CI
@ -194,7 +200,8 @@ KillMode=none
Type=forking
[Install]
WantedBy=multi-user.target default.target`
WantedBy=multi-user.target default.target
`
goodIDNew := `# container-639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401.service
# autogenerated by Podman CI
@ -217,7 +224,8 @@ KillMode=none
Type=forking
[Install]
WantedBy=multi-user.target default.target`
WantedBy=multi-user.target default.target
`
tests := []struct {
name string
@ -375,7 +383,6 @@ WantedBy=multi-user.target default.target`
test := tt
t.Run(tt.name, func(t *testing.T) {
opts := entities.GenerateSystemdOptions{
Files: false,
New: test.new,
}
got, err := executeContainerTemplate(&test.info, opts)

View File

@ -3,9 +3,7 @@ package generate
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"text/template"
@ -88,39 +86,40 @@ KillMode=none
Type=forking
[Install]
WantedBy=multi-user.target default.target`
WantedBy=multi-user.target default.target
`
// PodUnits generates systemd units for the specified pod and its containers.
// Based on the options, the return value might be the content of all units or
// the files they been written to.
func PodUnits(pod *libpod.Pod, options entities.GenerateSystemdOptions) (string, error) {
func PodUnits(pod *libpod.Pod, options entities.GenerateSystemdOptions) (map[string]string, error) {
// Error out if the pod has no infra container, which we require to be the
// main service.
if !pod.HasInfraContainer() {
return "", errors.Errorf("error generating systemd unit files: Pod %q has no infra container", pod.Name())
return nil, errors.Errorf("error generating systemd unit files: Pod %q has no infra container", pod.Name())
}
podInfo, err := generatePodInfo(pod, options)
if err != nil {
return "", err
return nil, err
}
infraID, err := pod.InfraContainerID()
if err != nil {
return "", err
return nil, err
}
// Compute the container-dependency graph for the Pod.
containers, err := pod.AllContainers()
if err != nil {
return "", err
return nil, err
}
if len(containers) == 0 {
return "", errors.Errorf("error generating systemd unit files: Pod %q has no containers", pod.Name())
return nil, errors.Errorf("error generating systemd unit files: Pod %q has no containers", pod.Name())
}
graph, err := libpod.BuildContainerGraph(containers)
if err != nil {
return "", err
return nil, err
}
// Traverse the dependency graph and create systemdgen.containerInfo's for
@ -133,7 +132,7 @@ func PodUnits(pod *libpod.Pod, options entities.GenerateSystemdOptions) (string,
}
ctrInfo, err := generateContainerInfo(ctr, options)
if err != nil {
return "", err
return nil, err
}
// Now add the container's dependencies and at the container as a
// required service of the infra container.
@ -149,24 +148,23 @@ func PodUnits(pod *libpod.Pod, options entities.GenerateSystemdOptions) (string,
containerInfos = append(containerInfos, ctrInfo)
}
units := map[string]string{}
// Now generate the systemd service for all containers.
builder := strings.Builder{}
out, err := executePodTemplate(podInfo, options)
if err != nil {
return "", err
return nil, err
}
builder.WriteString(out)
units[podInfo.ServiceName] = out
for _, info := range containerInfos {
info.pod = podInfo
builder.WriteByte('\n')
out, err := executeContainerTemplate(info, options)
if err != nil {
return "", err
return nil, err
}
builder.WriteString(out)
units[info.ServiceName] = out
}
return builder.String(), nil
return units, nil
}
func generatePodInfo(pod *libpod.Pod, options entities.GenerateSystemdOptions) (*podInfo, error) {
@ -339,18 +337,5 @@ func executePodTemplate(info *podInfo, options entities.GenerateSystemdOptions)
return "", err
}
if !options.Files {
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
}

View File

@ -58,7 +58,8 @@ KillMode=none
Type=forking
[Install]
WantedBy=multi-user.target default.target`
WantedBy=multi-user.target default.target
`
podGoodNamedNew := `# pod-123abc.service
# autogenerated by Podman CI
@ -84,7 +85,8 @@ KillMode=none
Type=forking
[Install]
WantedBy=multi-user.target default.target`
WantedBy=multi-user.target default.target
`
tests := []struct {
name string
@ -130,7 +132,6 @@ WantedBy=multi-user.target default.target`
test := tt
t.Run(tt.name, func(t *testing.T) {
opts := entities.GenerateSystemdOptions{
Files: false,
New: test.new,
}
got, err := executePodTemplate(&test.info, opts)

View File

@ -1,5 +1,3 @@
// +build !remote
package integration
import (
@ -61,7 +59,7 @@ var _ = Describe("Podman generate systemd", func() {
session = podmanTest.Podman([]string{"generate", "systemd", "--restart-policy", "bogus", "foobar"})
session.WaitWithDefaultTimeout()
Expect(session).To(ExitWithError())
found, _ := session.ErrorGrepString("Error: bogus is not a valid restart policy")
found, _ := session.ErrorGrepString("bogus is not a valid restart policy")
Expect(found).Should(BeTrue())
})
@ -383,4 +381,15 @@ var _ = Describe("Podman generate systemd", func() {
found, _ = session.GrepString("pod rm --ignore -f --pod-id-file %t/pod-foo.pod-id")
Expect(found).To(BeTrue())
})
It("podman generate systemd --format json", func() {
n := podmanTest.Podman([]string{"create", "--name", "foo", ALPINE})
n.WaitWithDefaultTimeout()
Expect(n.ExitCode()).To(Equal(0))
session := podmanTest.Podman([]string{"generate", "systemd", "--format", "json", "foo"})
session.WaitWithDefaultTimeout()
Expect(session.ExitCode()).To(Equal(0))
Expect(session.IsJSONOutputValid()).To(BeTrue())
})
})