diff options
author | baude <bbaude@redhat.com> | 2020-12-02 16:06:31 -0600 |
---|---|---|
committer | baude <bbaude@redhat.com> | 2020-12-07 11:34:39 -0600 |
commit | 749ee2a10ed689a06da699659c59872c0ed770d7 (patch) | |
tree | 496d31fdaa07af5778214d6c22ea0222f50ded9b | |
parent | e6f80fa61aa082d2226b8258ea247186451d84d3 (diff) | |
download | podman-749ee2a10ed689a06da699659c59872c0ed770d7.tar.gz podman-749ee2a10ed689a06da699659c59872c0ed770d7.tar.bz2 podman-749ee2a10ed689a06da699659c59872c0ed770d7.zip |
generate kube on multiple containers
add the ability to add multiple containers into a single k8s pod
instead of just one.
also fixed some bugs in the resulting yaml where an empty service
description was being added on error causing the k8s validation to fail.
Signed-off-by: baude <bbaude@redhat.com>
-rw-r--r-- | cmd/podman/generate/kube.go | 8 | ||||
-rw-r--r-- | docs/source/markdown/podman-generate-kube.1.md | 8 | ||||
-rw-r--r-- | libpod/kube.go | 28 | ||||
-rw-r--r-- | pkg/api/handlers/libpod/generate.go | 5 | ||||
-rw-r--r-- | pkg/api/server/register_generate.go | 12 | ||||
-rw-r--r-- | pkg/bindings/generate/generate.go | 11 | ||||
-rw-r--r-- | pkg/domain/entities/engine_container.go | 2 | ||||
-rw-r--r-- | pkg/domain/infra/abi/generate.go | 54 | ||||
-rw-r--r-- | pkg/domain/infra/tunnel/generate.go | 4 | ||||
-rw-r--r-- | test/apiv2/25-containersMore.at | 4 | ||||
-rw-r--r-- | test/e2e/generate_kube_test.go | 70 |
11 files changed, 159 insertions, 47 deletions
diff --git a/cmd/podman/generate/kube.go b/cmd/podman/generate/kube.go index e47bd35b5..0517db19a 100644 --- a/cmd/podman/generate/kube.go +++ b/cmd/podman/generate/kube.go @@ -17,16 +17,16 @@ import ( var ( kubeOptions = entities.GenerateKubeOptions{} kubeFile = "" - kubeDescription = `Command generates Kubernetes pod and service YAML (v1 specification) from a Podman container or pod. + kubeDescription = `Command generates Kubernetes pod and service YAML (v1 specification) from Podman containers or a pod. Whether the input is for a container or pod, Podman will always generate the specification as a pod.` kubeCmd = &cobra.Command{ - Use: "kube [options] CONTAINER | POD", + Use: "kube [options] CONTAINER... | POD", Short: "Generate Kubernetes YAML from a container or pod.", Long: kubeDescription, RunE: kube, - Args: cobra.ExactArgs(1), + Args: cobra.MinimumNArgs(1), ValidArgsFunction: common.AutocompleteContainersAndPods, Example: `podman generate kube ctrID podman generate kube podID @@ -51,7 +51,7 @@ func init() { } func kube(cmd *cobra.Command, args []string) error { - report, err := registry.ContainerEngine().GenerateKube(registry.GetContext(), args[0], kubeOptions) + report, err := registry.ContainerEngine().GenerateKube(registry.GetContext(), args, kubeOptions) if err != nil { return err } diff --git a/docs/source/markdown/podman-generate-kube.1.md b/docs/source/markdown/podman-generate-kube.1.md index 6fad89b1f..ed2143388 100644 --- a/docs/source/markdown/podman-generate-kube.1.md +++ b/docs/source/markdown/podman-generate-kube.1.md @@ -3,12 +3,12 @@ podman-generate-kube - Generate Kubernetes YAML based on a pod or container ## SYNOPSIS -**podman generate kube** [*options*] *container* | *pod* +**podman generate kube** [*options*] *container...* | *pod* ## DESCRIPTION -**podman generate kube** will generate Kubernetes Pod YAML (v1 specification) from a Podman container or pod. Whether -the input is for a container or pod, Podman will always generate the specification as a Pod. The input may be in the form -of a pod or container name or ID. +**podman generate kube** will generate Kubernetes Pod YAML (v1 specification) from Podman one or more containers or a single pod. Whether +the input is for containers or a pod, Podman will always generate the specification as a Pod. The input may be in the form +of a pod or one or more container names or IDs. Note that the generated Kubernetes YAML file can be used to re-run the deployment via podman-play-kube(1). diff --git a/libpod/kube.go b/libpod/kube.go index 067e7827d..bf041112a 100644 --- a/libpod/kube.go +++ b/libpod/kube.go @@ -21,9 +21,9 @@ import ( // GenerateForKube takes a slice of libpod containers and generates // one v1.Pod description that includes just a single container. -func (c *Container) GenerateForKube() (*v1.Pod, error) { +func GenerateForKube(ctrs []*Container) (*v1.Pod, error) { // Generate the v1.Pod yaml description - return simplePodWithV1Container(c) + return simplePodWithV1Containers(ctrs) } // GenerateForKube takes a slice of libpod containers and generates @@ -236,14 +236,20 @@ func addContainersAndVolumesToPodObject(containers []v1.Container, volumes []v1. return &p } -// simplePodWithV1Container is a function used by inspect when kube yaml needs to be generated +// simplePodWithV1Containers is a function used by inspect when kube yaml needs to be generated // for a single container. we "insert" that container description in a pod. -func simplePodWithV1Container(ctr *Container) (*v1.Pod, error) { - kubeCtr, kubeVols, err := containerToV1Container(ctr) - if err != nil { - return nil, err +func simplePodWithV1Containers(ctrs []*Container) (*v1.Pod, error) { + kubeCtrs := make([]v1.Container, 0, len(ctrs)) + kubeVolumes := make([]v1.Volume, 0) + for _, ctr := range ctrs { + kubeCtr, kubeVols, err := containerToV1Container(ctr) + if err != nil { + return nil, err + } + kubeCtrs = append(kubeCtrs, kubeCtr) + kubeVolumes = append(kubeVolumes, kubeVols...) } - return addContainersAndVolumesToPodObject([]v1.Container{kubeCtr}, kubeVols, ctr.Name()), nil + return addContainersAndVolumesToPodObject(kubeCtrs, kubeVolumes, strings.ReplaceAll(ctrs[0].Name(), "_", "")), nil } @@ -294,6 +300,12 @@ func containerToV1Container(c *Container) (v1.Container, []v1.Volume, error) { _, image := c.Image() kubeContainer.Image = image kubeContainer.Stdin = c.Stdin() + + // prepend the entrypoint of the container to command + if ep := c.Entrypoint(); len(c.Entrypoint()) > 0 { + ep = append(ep, containerCommands...) + containerCommands = ep + } kubeContainer.Command = containerCommands // TODO need to figure out how we handle command vs entry point. Kube appears to prefer entrypoint. // right now we just take the container's command diff --git a/pkg/api/handlers/libpod/generate.go b/pkg/api/handlers/libpod/generate.go index 33bb75391..b3b8c1f16 100644 --- a/pkg/api/handlers/libpod/generate.go +++ b/pkg/api/handlers/libpod/generate.go @@ -60,7 +60,8 @@ func GenerateKube(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value("runtime").(*libpod.Runtime) decoder := r.Context().Value("decoder").(*schema.Decoder) query := struct { - Service bool `schema:"service"` + Names []string `schema:"names"` + Service bool `schema:"service"` }{ // Defaults would go here. } @@ -73,7 +74,7 @@ func GenerateKube(w http.ResponseWriter, r *http.Request) { containerEngine := abi.ContainerEngine{Libpod: runtime} options := entities.GenerateKubeOptions{Service: query.Service} - report, err := containerEngine.GenerateKube(r.Context(), utils.GetName(r), options) + report, err := containerEngine.GenerateKube(r.Context(), query.Names, options) if err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error generating YAML")) return diff --git a/pkg/api/server/register_generate.go b/pkg/api/server/register_generate.go index 60e5b03f7..bce5484ab 100644 --- a/pkg/api/server/register_generate.go +++ b/pkg/api/server/register_generate.go @@ -70,7 +70,7 @@ func (s *APIServer) registerGenerateHandlers(r *mux.Router) error { // $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 + // swagger:operation GET /libpod/generate/kube libpod libpodGenerateKube // --- // tags: // - containers @@ -78,9 +78,11 @@ func (s *APIServer) registerGenerateHandlers(r *mux.Router) error { // summary: Generate a Kubernetes YAML file. // description: Generate Kubernetes YAML based on a pod or container. // parameters: - // - in: path - // name: name:.* - // type: string + // - in: query + // name: names + // type: array + // items: + // type: string // required: true // description: Name or ID of the container or pod. // - in: query @@ -98,6 +100,6 @@ func (s *APIServer) registerGenerateHandlers(r *mux.Router) error { // format: binary // 500: // $ref: "#/responses/InternalError" - r.HandleFunc(VersionedPath("/libpod/generate/{name:.*}/kube"), s.APIHandler(libpod.GenerateKube)).Methods(http.MethodGet) + r.HandleFunc(VersionedPath("/libpod/generate/kube"), s.APIHandler(libpod.GenerateKube)).Methods(http.MethodGet) return nil } diff --git a/pkg/bindings/generate/generate.go b/pkg/bindings/generate/generate.go index dde1cc29c..8d0146ec1 100644 --- a/pkg/bindings/generate/generate.go +++ b/pkg/bindings/generate/generate.go @@ -2,6 +2,7 @@ package generate import ( "context" + "errors" "net/http" "net/url" "strconv" @@ -37,15 +38,21 @@ func Systemd(ctx context.Context, nameOrID string, options entities.GenerateSyst return report, response.Process(&report.Units) } -func Kube(ctx context.Context, nameOrID string, options entities.GenerateKubeOptions) (*entities.GenerateKubeReport, error) { +func Kube(ctx context.Context, nameOrIDs []string, options entities.GenerateKubeOptions) (*entities.GenerateKubeReport, error) { conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } + if len(nameOrIDs) < 1 { + return nil, errors.New("must provide the name or ID of one container or pod") + } params := url.Values{} + for _, name := range nameOrIDs { + params.Add("names", name) + } params.Set("service", strconv.FormatBool(options.Service)) - response, err := conn.DoRequest(nil, http.MethodGet, "/generate/%s/kube", params, nil, nameOrID) + response, err := conn.DoRequest(nil, http.MethodGet, "/generate/kube", params, nil) if err != nil { return nil, err } diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index e1f40e307..29d71cc65 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -46,7 +46,7 @@ type ContainerEngine interface { ContainerWait(ctx context.Context, namesOrIds []string, options WaitOptions) ([]WaitReport, error) Events(ctx context.Context, opts EventsOptions) error GenerateSystemd(ctx context.Context, nameOrID string, opts GenerateSystemdOptions) (*GenerateSystemdReport, error) - GenerateKube(ctx context.Context, nameOrID string, opts GenerateKubeOptions) (*GenerateKubeReport, error) + GenerateKube(ctx context.Context, nameOrIDs []string, opts GenerateKubeOptions) (*GenerateKubeReport, error) SystemPrune(ctx context.Context, options SystemPruneOptions) (*SystemPruneReport, error) HealthCheckRun(ctx context.Context, nameOrID string, options HealthCheckOptions) (*define.HealthCheckResults, error) Info(ctx context.Context) (*define.Info, error) diff --git a/pkg/domain/infra/abi/generate.go b/pkg/domain/infra/abi/generate.go index 79bf2291e..79f55e2bd 100644 --- a/pkg/domain/infra/abi/generate.go +++ b/pkg/domain/infra/abi/generate.go @@ -41,28 +41,48 @@ func (ic *ContainerEngine) GenerateSystemd(ctx context.Context, nameOrID string, return &entities.GenerateSystemdReport{Units: units}, nil } -func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrID string, options entities.GenerateKubeOptions) (*entities.GenerateKubeReport, error) { +func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrIDs []string, options entities.GenerateKubeOptions) (*entities.GenerateKubeReport, error) { var ( - pod *libpod.Pod + pods []*libpod.Pod podYAML *k8sAPI.Pod err error - ctr *libpod.Container + ctrs []*libpod.Container servicePorts []k8sAPI.ServicePort serviceYAML k8sAPI.Service ) - // Get the container in question. - ctr, err = ic.Libpod.LookupContainer(nameOrID) - if err != nil { - pod, err = ic.Libpod.LookupPod(nameOrID) + for _, nameOrID := range nameOrIDs { + // Get the container in question + ctr, err := ic.Libpod.LookupContainer(nameOrID) if err != nil { - return nil, err + pod, err := ic.Libpod.LookupPod(nameOrID) + if err != nil { + return nil, err + } + pods = append(pods, pod) + if len(pods) > 1 { + return nil, errors.New("can only generate single pod at a time") + } + } else { + if len(ctr.Dependencies()) > 0 { + return nil, errors.Wrapf(define.ErrNotImplemented, "containers with dependencies") + } + // we cannot deal with ctrs already in a pod + if len(ctr.PodID()) > 0 { + return nil, errors.Errorf("container %s is associated with pod %s: use generate on the pod itself", ctr.ID(), ctr.PodID()) + } + ctrs = append(ctrs, ctr) } - podYAML, servicePorts, err = pod.GenerateForKube() + } + + // check our inputs + if len(pods) > 0 && len(ctrs) > 0 { + return nil, errors.New("cannot generate pods and containers at the same time") + } + + if len(pods) == 1 { + podYAML, servicePorts, err = pods[0].GenerateForKube() } else { - if len(ctr.Dependencies()) > 0 { - return nil, errors.Wrapf(define.ErrNotImplemented, "containers with dependencies") - } - podYAML, err = ctr.GenerateForKube() + podYAML, err = libpod.GenerateForKube(ctrs) } if err != nil { return nil, err @@ -72,7 +92,7 @@ func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrID string, op serviceYAML = libpod.GenerateKubeServiceFromV1Pod(podYAML, servicePorts) } - content, err := generateKubeOutput(podYAML, &serviceYAML) + content, err := generateKubeOutput(podYAML, &serviceYAML, options.Service) if err != nil { return nil, err } @@ -80,7 +100,7 @@ func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrID string, op return &entities.GenerateKubeReport{Reader: bytes.NewReader(content)}, nil } -func generateKubeOutput(podYAML *k8sAPI.Pod, serviceYAML *k8sAPI.Service) ([]byte, error) { +func generateKubeOutput(podYAML *k8sAPI.Pod, serviceYAML *k8sAPI.Service, hasService bool) ([]byte, error) { var ( output []byte marshalledPod []byte @@ -93,7 +113,7 @@ func generateKubeOutput(podYAML *k8sAPI.Pod, serviceYAML *k8sAPI.Service) ([]byt return nil, err } - if serviceYAML != nil { + if hasService { marshalledService, err = yaml.Marshal(serviceYAML) if err != nil { return nil, err @@ -114,7 +134,7 @@ func generateKubeOutput(podYAML *k8sAPI.Pod, serviceYAML *k8sAPI.Service) ([]byt output = append(output, []byte(fmt.Sprintf(header, podmanVersion.Version))...) output = append(output, marshalledPod...) - if serviceYAML != nil { + if hasService { output = append(output, []byte("---\n")...) output = append(output, marshalledService...) } diff --git a/pkg/domain/infra/tunnel/generate.go b/pkg/domain/infra/tunnel/generate.go index 966f707b1..ebbfa143f 100644 --- a/pkg/domain/infra/tunnel/generate.go +++ b/pkg/domain/infra/tunnel/generate.go @@ -11,6 +11,6 @@ func (ic *ContainerEngine) GenerateSystemd(ctx context.Context, nameOrID string, return generate.Systemd(ic.ClientCxt, nameOrID, options) } -func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrID string, options entities.GenerateKubeOptions) (*entities.GenerateKubeReport, error) { - return generate.Kube(ic.ClientCxt, nameOrID, options) +func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrIDs []string, options entities.GenerateKubeOptions) (*entities.GenerateKubeReport, error) { + return generate.Kube(ic.ClientCxt, nameOrIDs, options) } diff --git a/test/apiv2/25-containersMore.at b/test/apiv2/25-containersMore.at index 4f6b80a5f..62b817eb4 100644 --- a/test/apiv2/25-containersMore.at +++ b/test/apiv2/25-containersMore.at @@ -65,13 +65,13 @@ t GET libpod/containers/json?last=1 200 \ cid=$(jq -r '.[0].Id' <<<"$output") -t GET libpod/generate/$cid/kube 200 +t GET libpod/generate/kube?names=$cid 200 like "$output" ".*apiVersion:.*" "Check generated kube yaml - apiVersion" like "$output" ".*kind:\\sPod.*" "Check generated kube yaml - kind: Pod" like "$output" ".*metadata:.*" "Check generated kube yaml - metadata" like "$output" ".*spec:.*" "Check generated kube yaml - spec" -t GET libpod/generate/$cid/kube?service=true 200 +t GET "libpod/generate/kube?service=true&names=$cid" 200 like "$output" ".*apiVersion:.*" "Check generated kube yaml(service=true) - apiVersion" like "$output" ".*kind:\\sPod.*" "Check generated kube yaml(service=true) - kind: Pod" like "$output" ".*metadata:.*" "Check generated kube yaml(service=true) - metadata" diff --git a/test/e2e/generate_kube_test.go b/test/e2e/generate_kube_test.go index c8782c743..0950a9321 100644 --- a/test/e2e/generate_kube_test.go +++ b/test/e2e/generate_kube_test.go @@ -469,4 +469,74 @@ var _ = Describe("Podman generate kube", func() { Expect(inspect.ExitCode()).To(Equal(0)) Expect(inspect.OutputToString()).To(ContainSubstring(`"pid"`)) }) + + It("podman generate kube multiple pods should fail", func() { + pod1 := podmanTest.Podman([]string{"run", "-dt", "--pod", "new:pod1", ALPINE, "top"}) + pod1.WaitWithDefaultTimeout() + Expect(pod1.ExitCode()).To(Equal(0)) + + pod2 := podmanTest.Podman([]string{"run", "-dt", "--pod", "new:pod2", ALPINE, "top"}) + pod2.WaitWithDefaultTimeout() + Expect(pod2.ExitCode()).To(Equal(0)) + + kube := podmanTest.Podman([]string{"generate", "kube", "pod1", "pod2"}) + kube.WaitWithDefaultTimeout() + Expect(kube.ExitCode()).ToNot(Equal(0)) + }) + + It("podman generate kube with pods and containers should fail", func() { + pod1 := podmanTest.Podman([]string{"run", "-dt", "--pod", "new:pod1", ALPINE, "top"}) + pod1.WaitWithDefaultTimeout() + Expect(pod1.ExitCode()).To(Equal(0)) + + pod2 := podmanTest.Podman([]string{"run", "-dt", "--name", "top", ALPINE, "top"}) + pod2.WaitWithDefaultTimeout() + Expect(pod2.ExitCode()).To(Equal(0)) + + kube := podmanTest.Podman([]string{"generate", "kube", "pod1", "top"}) + kube.WaitWithDefaultTimeout() + Expect(kube.ExitCode()).ToNot(Equal(0)) + }) + + It("podman generate kube with containers in a pod should fail", func() { + pod1 := podmanTest.Podman([]string{"pod", "create", "--name", "pod1"}) + pod1.WaitWithDefaultTimeout() + Expect(pod1.ExitCode()).To(Equal(0)) + + con := podmanTest.Podman([]string{"run", "-dt", "--pod", "pod1", "--name", "top", ALPINE, "top"}) + con.WaitWithDefaultTimeout() + Expect(con.ExitCode()).To(Equal(0)) + + kube := podmanTest.Podman([]string{"generate", "kube", "top"}) + kube.WaitWithDefaultTimeout() + Expect(kube.ExitCode()).ToNot(Equal(0)) + }) + + It("podman generate kube with multiple containers", func() { + con1 := podmanTest.Podman([]string{"run", "-dt", "--name", "con1", ALPINE, "top"}) + con1.WaitWithDefaultTimeout() + Expect(con1.ExitCode()).To(Equal(0)) + + con2 := podmanTest.Podman([]string{"run", "-dt", "--name", "con2", ALPINE, "top"}) + con2.WaitWithDefaultTimeout() + Expect(con2.ExitCode()).To(Equal(0)) + + kube := podmanTest.Podman([]string{"generate", "kube", "con1", "con2"}) + kube.WaitWithDefaultTimeout() + Expect(kube.ExitCode()).To(Equal(0)) + }) + + It("podman generate kube with containers in a pod should fail", func() { + pod1 := podmanTest.Podman([]string{"run", "-dt", "--pod", "new:pod1", "--name", "top1", ALPINE, "top"}) + pod1.WaitWithDefaultTimeout() + Expect(pod1.ExitCode()).To(Equal(0)) + + pod2 := podmanTest.Podman([]string{"run", "-dt", "--pod", "new:pod2", "--name", "top2", ALPINE, "top"}) + pod2.WaitWithDefaultTimeout() + Expect(pod2.ExitCode()).To(Equal(0)) + + kube := podmanTest.Podman([]string{"generate", "kube", "pod1", "pod2"}) + kube.WaitWithDefaultTimeout() + Expect(kube.ExitCode()).ToNot(Equal(0)) + }) }) |