podman generate spec

implement a new command `podman generate spec` which can formulate a json specgen to be consumed by both the pod
and container creation API.

supported flags are

--verbose (default true) print output to the terminal
--compact print the json output in a single line format to be piped to the API
--filename put the output in a file
--clone rename the pod/ctr in the spec so it won't conflict w/ an existing entity

Signed-off-by: Charlie Doern <cdoern@redhat.com>
This commit is contained in:
Charlie Doern
2022-07-12 15:23:45 -04:00
parent 1cf6afb788
commit 842c6c7c67
13 changed files with 351 additions and 27 deletions

View File

@ -0,0 +1,71 @@
package pods
import (
"fmt"
"io/ioutil"
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v4/cmd/podman/common"
"github.com/containers/podman/v4/cmd/podman/registry"
"github.com/containers/podman/v4/cmd/podman/utils"
"github.com/containers/podman/v4/pkg/domain/entities"
"github.com/spf13/cobra"
)
var (
specCmd = &cobra.Command{
Use: "spec [options] {CONTAINER|POD}",
Short: "Generate Specgen JSON based on containers or pods",
Long: "Generate Specgen JSON based on containers or pods",
RunE: spec,
Args: cobra.ExactArgs(1),
ValidArgsFunction: common.AutocompleteContainersAndPods,
Example: `podman generate spec ctrID`,
}
)
var (
opts *entities.GenerateSpecOptions
)
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: specCmd,
Parent: generateCmd,
})
opts = &entities.GenerateSpecOptions{}
flags := specCmd.Flags()
filenameFlagName := "filename"
flags.StringVarP(&opts.FileName, filenameFlagName, "f", "", "Write output to the specified path")
_ = specCmd.RegisterFlagCompletionFunc(filenameFlagName, completion.AutocompleteNone)
compactFlagName := "compact"
flags.BoolVarP(&opts.Compact, compactFlagName, "c", false, "Print the json in a compact format for consumption")
nameFlagName := "name"
flags.BoolVarP(&opts.Name, nameFlagName, "n", true, "Specify a new name for the generated spec")
flags.SetNormalizeFunc(utils.AliasFlags)
}
func spec(cmd *cobra.Command, args []string) error {
opts.ID = args[0]
report, err := registry.ContainerEngine().GenerateSpec(registry.GetContext(), opts)
if err != nil {
return err
}
// if we are looking to print the output, do not mess it up by printing the path
// if we are using -v the user probably expects to pipe the output somewhere else
if len(opts.FileName) > 0 {
err = ioutil.WriteFile(opts.FileName, report.Data, 0644)
if err != nil {
return err
}
fmt.Println(opts.FileName)
} else {
fmt.Println(string(report.Data))
}
return nil
}

View File

@ -0,0 +1,26 @@
% podman-generate-spec(1)
## NAME
podman\-generate\-spec - Generate Specgen JSON based on containers or pods
## SYNOPSIS
**podman generate spec** [*options*] *container | *pod*
## DESCRIPTION
**podman generate spec** will generate Specgen JSON from Podman Containers and Pods. This JSON can either be printed to a file, directly to the command line, or both.
This JSON can then be used as input for the Podman API, specifically for Podman container and pod creation. Specgen is Podman's internal structure for formulating new container-related entities.
## OPTIONS
#### **--compact**, **-c**
Print the output in a compact, one line format. This is useful when piping the data to the Podman API
#### **--filename**, **-f**=**filename**
Output to the given file.
#### **--name**, **-n**
Rename the pod or container, so that it does not conflict with the existing entity. This is helpful when the JSON is to be used before the source pod or container is deleted.

View File

@ -13,7 +13,8 @@ The generate command will create structured output (like YAML) based on a contai
| Command | Man Page | Description |
|---------|------------------------------------------------------------|-------------------------------------------------------------------------------------|
| kube | [podman-generate-kube(1)](podman-generate-kube.1.md) | Generate Kubernetes YAML based on containers, pods or volumes. |
| kube | [podman-generate-kube(1)](podman-generate-kube.1.md) | Generate Kubernetes YAML based on containers, pods or volumes. |
| spec | [podman-generate-spec(1)](podman-generate-spec.1.md) | Generate Specgen JSON based on containers or pods. |
| systemd | [podman-generate-systemd(1)](podman-generate-systemd.1.md) | Generate systemd unit file(s) for a container or pod. |

View File

@ -54,6 +54,7 @@ type ContainerEngine interface {
ContainerWait(ctx context.Context, namesOrIds []string, options WaitOptions) ([]WaitReport, error)
Diff(ctx context.Context, namesOrIds []string, options DiffOptions) (*DiffReport, error)
Events(ctx context.Context, opts EventsOptions) error
GenerateSpec(ctx context.Context, opts *GenerateSpecOptions) (*GenerateSpecReport, error)
GenerateSystemd(ctx context.Context, nameOrID string, opts GenerateSystemdOptions) (*GenerateSystemdReport, error)
GenerateKube(ctx context.Context, nameOrIDs []string, opts GenerateKubeOptions) (*GenerateKubeReport, error)
SystemPrune(ctx context.Context, options SystemPruneOptions) (*SystemPruneReport, error)

View File

@ -53,3 +53,14 @@ type GenerateKubeReport struct {
// Reader - the io.Reader to reader the generated YAML file.
Reader io.Reader
}
type GenerateSpecReport struct {
Data []byte
}
type GenerateSpecOptions struct {
ID string
FileName string
Compact bool
Name bool
}

View File

@ -7,7 +7,6 @@ import (
"io/ioutil"
"os"
"strconv"
"strings"
"sync"
"time"
@ -1669,31 +1668,7 @@ func (ic *ContainerEngine) ContainerClone(ctx context.Context, ctrCloneOpts enti
if err == nil {
n += "-clone"
}
switch {
case strings.Contains(n, "-clone"):
ind := strings.Index(n, "-clone") + 6
num, err := strconv.Atoi(n[ind:])
if num == 0 && err != nil { // clone1 is hard to get with this logic, just check for it here.
_, err = ic.Libpod.LookupContainer(n + "1")
if err != nil {
spec.Name = n + "1"
break
}
} else {
n = n[0:ind]
}
err = nil
count := num
for err == nil {
count++
tempN := n + strconv.Itoa(count)
_, err = ic.Libpod.LookupContainer(tempN)
}
n += strconv.Itoa(count)
spec.Name = n
default:
spec.Name = c.Name() + "-clone"
}
spec.Name = generate.CheckName(ic.Libpod, n, true)
}
rtSpec, spec, opts, err := generate.MakeContainer(context.Background(), ic.Libpod, spec, true, c)

View File

@ -3,6 +3,7 @@ package abi
import (
"bytes"
"context"
"encoding/json"
"fmt"
"strings"
@ -10,6 +11,8 @@ import (
"github.com/containers/podman/v4/libpod/define"
"github.com/containers/podman/v4/pkg/domain/entities"
k8sAPI "github.com/containers/podman/v4/pkg/k8s.io/api/core/v1"
"github.com/containers/podman/v4/pkg/specgen"
generateUtils "github.com/containers/podman/v4/pkg/specgen/generate"
"github.com/containers/podman/v4/pkg/systemd/generate"
"github.com/ghodss/yaml"
)
@ -41,6 +44,63 @@ func (ic *ContainerEngine) GenerateSystemd(ctx context.Context, nameOrID string,
return &entities.GenerateSystemdReport{Units: units}, nil
}
func (ic *ContainerEngine) GenerateSpec(ctx context.Context, opts *entities.GenerateSpecOptions) (*entities.GenerateSpecReport, error) {
var spec *specgen.SpecGenerator
var pspec *specgen.PodSpecGenerator
var err error
if _, err := ic.Libpod.LookupContainer(opts.ID); err == nil {
spec = &specgen.SpecGenerator{}
_, _, err = generateUtils.ConfigToSpec(ic.Libpod, spec, opts.ID)
if err != nil {
return nil, err
}
} else if p, err := ic.Libpod.LookupPod(opts.ID); err == nil {
pspec = &specgen.PodSpecGenerator{}
pspec.Name = p.Name()
_, err := generateUtils.PodConfigToSpec(ic.Libpod, pspec, &entities.ContainerCreateOptions{}, opts.ID)
if err != nil {
return nil, err
}
}
if pspec == nil && spec == nil {
return nil, fmt.Errorf("could not find a pod or container with the id %s", opts.ID)
}
// rename if we are looking to consume the output and make a new entity
if opts.Name {
if spec != nil {
spec.Name = generateUtils.CheckName(ic.Libpod, spec.Name, true)
} else {
pspec.Name = generateUtils.CheckName(ic.Libpod, pspec.Name, false)
}
}
j := []byte{}
if spec != nil {
j, err = json.MarshalIndent(spec, "", " ")
if err != nil {
return nil, err
}
} else if pspec != nil {
j, err = json.MarshalIndent(pspec, "", " ")
if err != nil {
return nil, err
}
}
// compact output
if opts.Compact {
compacted := &bytes.Buffer{}
err := json.Compact(compacted, j)
if err != nil {
return nil, err
}
return &entities.GenerateSpecReport{Data: compacted.Bytes()}, nil
}
return &entities.GenerateSpecReport{Data: j}, nil // regular output
}
func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrIDs []string, options entities.GenerateKubeOptions) (*entities.GenerateKubeReport, error) {
var (
pods []*libpod.Pod

View File

@ -2,6 +2,7 @@ package tunnel
import (
"context"
"fmt"
"github.com/containers/podman/v4/pkg/bindings/generate"
"github.com/containers/podman/v4/pkg/domain/entities"
@ -43,3 +44,7 @@ func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrIDs []string,
options := new(generate.KubeOptions).WithService(opts.Service)
return generate.Kube(ic.ClientCtx, nameOrIDs, options)
}
func (ic *ContainerEngine) GenerateSpec(ctx context.Context, opts *entities.GenerateSpecOptions) (*entities.GenerateSpecReport, error) {
return nil, fmt.Errorf("GenerateSpec is not supported on the remote API")
}

View File

@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"os"
"strconv"
"strings"
"time"
@ -555,3 +556,41 @@ func FinishThrottleDevices(s *specgen.SpecGenerator) error {
}
return nil
}
// Check name looks for existing containers/pods with the same name, and modifies the given string until a new name is found
func CheckName(rt *libpod.Runtime, n string, kind bool) string {
switch {
case strings.Contains(n, "-clone"):
ind := strings.Index(n, "-clone") + 6
num, err := strconv.Atoi(n[ind:])
if num == 0 && err != nil { // clone1 is hard to get with this logic, just check for it here.
if kind {
_, err = rt.LookupContainer(n + "1")
} else {
_, err = rt.LookupPod(n + "1")
}
if err != nil {
n += "1"
break
}
} else {
n = n[0:ind]
}
err = nil
count := num
for err == nil {
count++
tempN := n + strconv.Itoa(count)
if kind {
_, err = rt.LookupContainer(tempN)
} else {
_, err = rt.LookupPod(tempN)
}
}
n += strconv.Itoa(count)
default:
n += "-clone"
}
return n
}

View File

@ -2,6 +2,7 @@ package generate
import (
"context"
"encoding/json"
"fmt"
"net"
"os"
@ -327,6 +328,19 @@ func PodConfigToSpec(rt *libpod.Runtime, spec *specgen.PodSpecGenerator, infraOp
}
spec.InfraContainerSpec = infraSpec
matching, err := json.Marshal(infraSpec)
if err != nil {
return nil, err
}
// track name before unmarshal so we do not overwrite w/ infra
name := spec.Name
err = json.Unmarshal(matching, spec)
if err != nil {
return nil, err
}
spec.Name = name
}
// need to reset hostname, name etc of both pod and infra

View File

@ -527,3 +527,24 @@ t GET containers/status-test/json 200 .State.Status="stopping"
sleep 3
t GET containers/status-test/json 200 .State.Status="exited"
# test podman generate spec as input for the api
podman create --name=specgen alpine_labels
TMPD=$(mktemp -d podman-apiv2-test.build.XXXXXXXX)
podman generate spec -f ${TMPD}/input.txt -c specgen
curl -XPOST -o ${TMPD}/response.txt --dump-header ${TMPD}/headers.txt -H content-type:application/json http://$HOST:$PORT/v4.0.0/libpod/containers/create -d "@${TMPD}/input.txt"
if ! grep -q '201 Created' "${TMPD}/headers.txt"; then
cat "${TMPD}/headers.txt"
cat "${TMPD}/response.txt"
echo -e "${red}NOK: container create failed"
rm -rf $TMPD
exit 1
fi
rm -rf $TMPD
podman container rm -fa

View File

@ -136,4 +136,24 @@ t DELETE "libpod/pods/foo (pod has already been deleted)" 404
t_timeout 5 GET "libpod/pods/stats?stream=true&delay=1" 200
podman pod create --name=specgen
TMPD=$(mktemp -d podman-apiv2-test.build.XXXXXXXX)
podman generate spec -f ${TMPD}/input.txt -c specgen
curl -XPOST -o ${TMPD}/response.txt --dump-header ${TMPD}/headers.txt -H content-type:application/json http://$HOST:$PORT/v4.0.0/libpod/pods/create -d "@${TMPD}/input.txt"
if ! grep -q '201 Created' "${TMPD}/headers.txt"; then
cat "${TMPD}/headers.txt"
cat "${TMPD}/response.txt"
echo -e "${red}NOK: pod create failed"
rm -rf $TMPD
exit 1
fi
rm -rf $TMPD
podman pod rm -fa
# vim: filetype=sh

View File

@ -0,0 +1,80 @@
package integration
import (
"os"
"path/filepath"
. "github.com/containers/podman/v4/test/utils"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/gexec"
)
var _ = Describe("Podman generate spec", func() {
var (
tempdir string
err error
podmanTest *PodmanTestIntegration
)
BeforeEach(func() {
SkipIfRemote("podman generate spec is not supported on the remote client")
tempdir, err = CreateTempDirInTempDir()
if err != nil {
os.Exit(1)
}
podmanTest = PodmanTestCreate(tempdir)
podmanTest.Setup()
})
AfterEach(func() {
podmanTest.Cleanup()
f := CurrentGinkgoTestDescription()
processTestResult(f)
})
It("podman generate spec bogus should fail", func() {
session := podmanTest.Podman([]string{"generate", "spec", "foobar"})
session.WaitWithDefaultTimeout()
Expect(session).Should(ExitWithError())
})
It("podman generate spec basic usage", func() {
session := podmanTest.Podman([]string{"create", "--cpus", "5", "--name", "specgen", ALPINE})
session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(0))
session = podmanTest.Podman([]string{"generate", "spec", "--compact", "specgen"})
session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(0))
})
It("podman generate spec file", func() {
session := podmanTest.Podman([]string{"create", "--cpus", "5", "--name", "specgen", ALPINE})
session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(0))
session = podmanTest.Podman([]string{"generate", "spec", "--filename", filepath.Join(tempdir, "out.json"), "specgen"})
session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(0))
path := filepath.Join(tempdir, "out.json")
exec := SystemExec("cat", []string{path})
exec.WaitWithDefaultTimeout()
Expect(exec.OutputToString()).Should(ContainSubstring("specgen-clone"))
Expect(exec.OutputToString()).Should(ContainSubstring("500000"))
})
It("generate spec pod", func() {
session := podmanTest.Podman([]string{"pod", "create", "--cpus", "5", "--name", "podspecgen"})
session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(0))
session = podmanTest.Podman([]string{"generate", "spec", "--compact", "podspecgen"})
session.WaitWithDefaultTimeout()
Expect(session).Should(Exit(0))
})
})