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>
This commit is contained in:
baude
2020-12-02 16:06:31 -06:00
parent e6f80fa61a
commit 749ee2a10e
11 changed files with 159 additions and 47 deletions

View File

@ -17,16 +17,16 @@ import (
var ( var (
kubeOptions = entities.GenerateKubeOptions{} kubeOptions = entities.GenerateKubeOptions{}
kubeFile = "" 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.` Whether the input is for a container or pod, Podman will always generate the specification as a pod.`
kubeCmd = &cobra.Command{ kubeCmd = &cobra.Command{
Use: "kube [options] CONTAINER | POD", Use: "kube [options] CONTAINER... | POD",
Short: "Generate Kubernetes YAML from a container or pod.", Short: "Generate Kubernetes YAML from a container or pod.",
Long: kubeDescription, Long: kubeDescription,
RunE: kube, RunE: kube,
Args: cobra.ExactArgs(1), Args: cobra.MinimumNArgs(1),
ValidArgsFunction: common.AutocompleteContainersAndPods, ValidArgsFunction: common.AutocompleteContainersAndPods,
Example: `podman generate kube ctrID Example: `podman generate kube ctrID
podman generate kube podID podman generate kube podID
@ -51,7 +51,7 @@ func init() {
} }
func kube(cmd *cobra.Command, args []string) error { 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 { if err != nil {
return err return err
} }

View File

@ -3,12 +3,12 @@
podman-generate-kube - Generate Kubernetes YAML based on a pod or container podman-generate-kube - Generate Kubernetes YAML based on a pod or container
## SYNOPSIS ## SYNOPSIS
**podman generate kube** [*options*] *container* | *pod* **podman generate kube** [*options*] *container...* | *pod*
## DESCRIPTION ## DESCRIPTION
**podman generate kube** will generate Kubernetes Pod YAML (v1 specification) from a Podman container or pod. Whether **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 a container or pod, Podman will always generate the specification as a Pod. The input may be in the form 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 container name or ID. 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). Note that the generated Kubernetes YAML file can be used to re-run the deployment via podman-play-kube(1).

View File

@ -21,9 +21,9 @@ import (
// GenerateForKube takes a slice of libpod containers and generates // GenerateForKube takes a slice of libpod containers and generates
// one v1.Pod description that includes just a single container. // 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 // Generate the v1.Pod yaml description
return simplePodWithV1Container(c) return simplePodWithV1Containers(ctrs)
} }
// GenerateForKube takes a slice of libpod containers and generates // GenerateForKube takes a slice of libpod containers and generates
@ -236,14 +236,20 @@ func addContainersAndVolumesToPodObject(containers []v1.Container, volumes []v1.
return &p 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. // for a single container. we "insert" that container description in a pod.
func simplePodWithV1Container(ctr *Container) (*v1.Pod, error) { 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) kubeCtr, kubeVols, err := containerToV1Container(ctr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return addContainersAndVolumesToPodObject([]v1.Container{kubeCtr}, kubeVols, ctr.Name()), nil kubeCtrs = append(kubeCtrs, kubeCtr)
kubeVolumes = append(kubeVolumes, kubeVols...)
}
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() _, image := c.Image()
kubeContainer.Image = image kubeContainer.Image = image
kubeContainer.Stdin = c.Stdin() 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 kubeContainer.Command = containerCommands
// TODO need to figure out how we handle command vs entry point. Kube appears to prefer entrypoint. // 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 // right now we just take the container's command

View File

@ -60,6 +60,7 @@ func GenerateKube(w http.ResponseWriter, r *http.Request) {
runtime := r.Context().Value("runtime").(*libpod.Runtime) runtime := r.Context().Value("runtime").(*libpod.Runtime)
decoder := r.Context().Value("decoder").(*schema.Decoder) decoder := r.Context().Value("decoder").(*schema.Decoder)
query := struct { query := struct {
Names []string `schema:"names"`
Service bool `schema:"service"` Service bool `schema:"service"`
}{ }{
// Defaults would go here. // Defaults would go here.
@ -73,7 +74,7 @@ func GenerateKube(w http.ResponseWriter, r *http.Request) {
containerEngine := abi.ContainerEngine{Libpod: runtime} containerEngine := abi.ContainerEngine{Libpod: runtime}
options := entities.GenerateKubeOptions{Service: query.Service} 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 { if err != nil {
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error generating YAML")) utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "error generating YAML"))
return return

View File

@ -70,7 +70,7 @@ func (s *APIServer) registerGenerateHandlers(r *mux.Router) error {
// $ref: "#/responses/InternalError" // $ref: "#/responses/InternalError"
r.HandleFunc(VersionedPath("/libpod/generate/{name:.*}/systemd"), s.APIHandler(libpod.GenerateSystemd)).Methods(http.MethodGet) 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: // tags:
// - containers // - containers
@ -78,8 +78,10 @@ func (s *APIServer) registerGenerateHandlers(r *mux.Router) error {
// summary: Generate a Kubernetes YAML file. // summary: Generate a Kubernetes YAML file.
// description: Generate Kubernetes YAML based on a pod or container. // description: Generate Kubernetes YAML based on a pod or container.
// parameters: // parameters:
// - in: path // - in: query
// name: name:.* // name: names
// type: array
// items:
// type: string // type: string
// required: true // required: true
// description: Name or ID of the container or pod. // description: Name or ID of the container or pod.
@ -98,6 +100,6 @@ func (s *APIServer) registerGenerateHandlers(r *mux.Router) error {
// format: binary // format: binary
// 500: // 500:
// $ref: "#/responses/InternalError" // $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 return nil
} }

View File

@ -2,6 +2,7 @@ package generate
import ( import (
"context" "context"
"errors"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
@ -37,15 +38,21 @@ func Systemd(ctx context.Context, nameOrID string, options entities.GenerateSyst
return report, response.Process(&report.Units) 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) conn, err := bindings.GetClient(ctx)
if err != nil { if err != nil {
return nil, err 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{} params := url.Values{}
for _, name := range nameOrIDs {
params.Add("names", name)
}
params.Set("service", strconv.FormatBool(options.Service)) 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 { if err != nil {
return nil, err return nil, err
} }

View File

@ -46,7 +46,7 @@ type ContainerEngine interface {
ContainerWait(ctx context.Context, namesOrIds []string, options WaitOptions) ([]WaitReport, error) ContainerWait(ctx context.Context, namesOrIds []string, options WaitOptions) ([]WaitReport, error)
Events(ctx context.Context, opts EventsOptions) error Events(ctx context.Context, opts EventsOptions) error
GenerateSystemd(ctx context.Context, nameOrID string, opts GenerateSystemdOptions) (*GenerateSystemdReport, 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) SystemPrune(ctx context.Context, options SystemPruneOptions) (*SystemPruneReport, error)
HealthCheckRun(ctx context.Context, nameOrID string, options HealthCheckOptions) (*define.HealthCheckResults, error) HealthCheckRun(ctx context.Context, nameOrID string, options HealthCheckOptions) (*define.HealthCheckResults, error)
Info(ctx context.Context) (*define.Info, error) Info(ctx context.Context) (*define.Info, error)

View File

@ -41,28 +41,48 @@ func (ic *ContainerEngine) GenerateSystemd(ctx context.Context, nameOrID string,
return &entities.GenerateSystemdReport{Units: units}, nil 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 ( var (
pod *libpod.Pod pods []*libpod.Pod
podYAML *k8sAPI.Pod podYAML *k8sAPI.Pod
err error err error
ctr *libpod.Container ctrs []*libpod.Container
servicePorts []k8sAPI.ServicePort servicePorts []k8sAPI.ServicePort
serviceYAML k8sAPI.Service serviceYAML k8sAPI.Service
) )
// Get the container in question. for _, nameOrID := range nameOrIDs {
ctr, err = ic.Libpod.LookupContainer(nameOrID) // Get the container in question
ctr, err := ic.Libpod.LookupContainer(nameOrID)
if err != nil { if err != nil {
pod, err = ic.Libpod.LookupPod(nameOrID) pod, err := ic.Libpod.LookupPod(nameOrID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
podYAML, servicePorts, err = pod.GenerateForKube() pods = append(pods, pod)
if len(pods) > 1 {
return nil, errors.New("can only generate single pod at a time")
}
} else { } else {
if len(ctr.Dependencies()) > 0 { if len(ctr.Dependencies()) > 0 {
return nil, errors.Wrapf(define.ErrNotImplemented, "containers with dependencies") return nil, errors.Wrapf(define.ErrNotImplemented, "containers with dependencies")
} }
podYAML, err = ctr.GenerateForKube() // 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)
}
}
// 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 {
podYAML, err = libpod.GenerateForKube(ctrs)
} }
if err != nil { if err != nil {
return nil, err return nil, err
@ -72,7 +92,7 @@ func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrID string, op
serviceYAML = libpod.GenerateKubeServiceFromV1Pod(podYAML, servicePorts) serviceYAML = libpod.GenerateKubeServiceFromV1Pod(podYAML, servicePorts)
} }
content, err := generateKubeOutput(podYAML, &serviceYAML) content, err := generateKubeOutput(podYAML, &serviceYAML, options.Service)
if err != nil { if err != nil {
return nil, err 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 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 ( var (
output []byte output []byte
marshalledPod []byte marshalledPod []byte
@ -93,7 +113,7 @@ func generateKubeOutput(podYAML *k8sAPI.Pod, serviceYAML *k8sAPI.Service) ([]byt
return nil, err return nil, err
} }
if serviceYAML != nil { if hasService {
marshalledService, err = yaml.Marshal(serviceYAML) marshalledService, err = yaml.Marshal(serviceYAML)
if err != nil { if err != nil {
return nil, err 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, []byte(fmt.Sprintf(header, podmanVersion.Version))...)
output = append(output, marshalledPod...) output = append(output, marshalledPod...)
if serviceYAML != nil { if hasService {
output = append(output, []byte("---\n")...) output = append(output, []byte("---\n")...)
output = append(output, marshalledService...) output = append(output, marshalledService...)
} }

View File

@ -11,6 +11,6 @@ func (ic *ContainerEngine) GenerateSystemd(ctx context.Context, nameOrID string,
return generate.Systemd(ic.ClientCxt, nameOrID, options) return generate.Systemd(ic.ClientCxt, nameOrID, options)
} }
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) {
return generate.Kube(ic.ClientCxt, nameOrID, options) return generate.Kube(ic.ClientCxt, nameOrIDs, options)
} }

View File

@ -65,13 +65,13 @@ t GET libpod/containers/json?last=1 200 \
cid=$(jq -r '.[0].Id' <<<"$output") 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" ".*apiVersion:.*" "Check generated kube yaml - apiVersion"
like "$output" ".*kind:\\sPod.*" "Check generated kube yaml - kind: Pod" like "$output" ".*kind:\\sPod.*" "Check generated kube yaml - kind: Pod"
like "$output" ".*metadata:.*" "Check generated kube yaml - metadata" like "$output" ".*metadata:.*" "Check generated kube yaml - metadata"
like "$output" ".*spec:.*" "Check generated kube yaml - spec" 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" ".*apiVersion:.*" "Check generated kube yaml(service=true) - apiVersion"
like "$output" ".*kind:\\sPod.*" "Check generated kube yaml(service=true) - kind: Pod" like "$output" ".*kind:\\sPod.*" "Check generated kube yaml(service=true) - kind: Pod"
like "$output" ".*metadata:.*" "Check generated kube yaml(service=true) - metadata" like "$output" ".*metadata:.*" "Check generated kube yaml(service=true) - metadata"

View File

@ -469,4 +469,74 @@ var _ = Describe("Podman generate kube", func() {
Expect(inspect.ExitCode()).To(Equal(0)) Expect(inspect.ExitCode()).To(Equal(0))
Expect(inspect.OutputToString()).To(ContainSubstring(`"pid"`)) 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))
})
}) })