mirror of
https://github.com/containers/podman.git
synced 2025-05-17 23:26:08 +08:00
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:
71
cmd/podman/generate/spec.go
Normal file
71
cmd/podman/generate/spec.go
Normal 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
|
||||
}
|
26
docs/source/markdown/podman-generate-spec.1.md
Normal file
26
docs/source/markdown/podman-generate-spec.1.md
Normal 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.
|
@ -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. |
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
80
test/e2e/generate_spec_test.go
Normal file
80
test/e2e/generate_spec_test.go
Normal 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))
|
||||
})
|
||||
})
|
Reference in New Issue
Block a user