diff options
author | Charlie Doern <cdoern@redhat.com> | 2022-07-12 15:23:45 -0400 |
---|---|---|
committer | Charlie Doern <cdoern@redhat.com> | 2022-08-04 13:59:58 -0400 |
commit | 842c6c7c6748f8705698d25a29945f8437f1bed2 (patch) | |
tree | 944a6f4e50f76a9f5a2d5e1eae0f698a69f39fd4 | |
parent | 1cf6afb788a681fc379d58d7f1cb69eb1690dba9 (diff) | |
download | podman-842c6c7c6748f8705698d25a29945f8437f1bed2.tar.gz podman-842c6c7c6748f8705698d25a29945f8437f1bed2.tar.bz2 podman-842c6c7c6748f8705698d25a29945f8437f1bed2.zip |
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>
-rw-r--r-- | cmd/podman/generate/spec.go | 71 | ||||
-rw-r--r-- | docs/source/markdown/podman-generate-spec.1.md | 26 | ||||
-rw-r--r-- | docs/source/markdown/podman-generate.1.md | 3 | ||||
-rw-r--r-- | pkg/domain/entities/engine_container.go | 1 | ||||
-rw-r--r-- | pkg/domain/entities/generate.go | 11 | ||||
-rw-r--r-- | pkg/domain/infra/abi/containers.go | 27 | ||||
-rw-r--r-- | pkg/domain/infra/abi/generate.go | 60 | ||||
-rw-r--r-- | pkg/domain/infra/tunnel/generate.go | 5 | ||||
-rw-r--r-- | pkg/specgen/generate/container.go | 39 | ||||
-rw-r--r-- | pkg/specgen/generate/pod_create.go | 14 | ||||
-rw-r--r-- | test/apiv2/20-containers.at | 21 | ||||
-rw-r--r-- | test/apiv2/40-pods.at | 20 | ||||
-rw-r--r-- | test/e2e/generate_spec_test.go | 80 |
13 files changed, 351 insertions, 27 deletions
diff --git a/cmd/podman/generate/spec.go b/cmd/podman/generate/spec.go new file mode 100644 index 000000000..1cf967424 --- /dev/null +++ b/cmd/podman/generate/spec.go @@ -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 +} diff --git a/docs/source/markdown/podman-generate-spec.1.md b/docs/source/markdown/podman-generate-spec.1.md new file mode 100644 index 000000000..08c939698 --- /dev/null +++ b/docs/source/markdown/podman-generate-spec.1.md @@ -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. diff --git a/docs/source/markdown/podman-generate.1.md b/docs/source/markdown/podman-generate.1.md index 415a53875..8466068f2 100644 --- a/docs/source/markdown/podman-generate.1.md +++ b/docs/source/markdown/podman-generate.1.md @@ -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. | diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index e4eb808b4..6a766eb84 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -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) diff --git a/pkg/domain/entities/generate.go b/pkg/domain/entities/generate.go index 73dd64ecd..cc5fbb6fb 100644 --- a/pkg/domain/entities/generate.go +++ b/pkg/domain/entities/generate.go @@ -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 +} diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index ed149a869..2820032c9 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -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) diff --git a/pkg/domain/infra/abi/generate.go b/pkg/domain/infra/abi/generate.go index 31885ce54..f588f591a 100644 --- a/pkg/domain/infra/abi/generate.go +++ b/pkg/domain/infra/abi/generate.go @@ -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 diff --git a/pkg/domain/infra/tunnel/generate.go b/pkg/domain/infra/tunnel/generate.go index 235d478ec..ed63d363a 100644 --- a/pkg/domain/infra/tunnel/generate.go +++ b/pkg/domain/infra/tunnel/generate.go @@ -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") +} diff --git a/pkg/specgen/generate/container.go b/pkg/specgen/generate/container.go index 8cfac924b..b5d10df8c 100644 --- a/pkg/specgen/generate/container.go +++ b/pkg/specgen/generate/container.go @@ -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 +} diff --git a/pkg/specgen/generate/pod_create.go b/pkg/specgen/generate/pod_create.go index 4e6362c9b..d6063b9a0 100644 --- a/pkg/specgen/generate/pod_create.go +++ b/pkg/specgen/generate/pod_create.go @@ -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 diff --git a/test/apiv2/20-containers.at b/test/apiv2/20-containers.at index a8d9baef3..ac3626cf1 100644 --- a/test/apiv2/20-containers.at +++ b/test/apiv2/20-containers.at @@ -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 diff --git a/test/apiv2/40-pods.at b/test/apiv2/40-pods.at index 80724a8d9..d21b3d1a9 100644 --- a/test/apiv2/40-pods.at +++ b/test/apiv2/40-pods.at @@ -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 diff --git a/test/e2e/generate_spec_test.go b/test/e2e/generate_spec_test.go new file mode 100644 index 000000000..57cd9546b --- /dev/null +++ b/test/e2e/generate_spec_test.go @@ -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)) + }) +}) |