diff options
-rw-r--r-- | cmd/podman/generate.go | 23 | ||||
-rw-r--r-- | cmd/podman/generate_kube.go (renamed from cmd/podman/kube_generate.go) | 67 | ||||
-rw-r--r-- | cmd/podman/kube.go | 23 | ||||
-rw-r--r-- | commands.md | 1 | ||||
-rw-r--r-- | completions/bash/podman | 28 | ||||
-rw-r--r-- | docs/podman-generate-kube.1.md | 119 | ||||
-rw-r--r-- | docs/podman-generate.1.md | 19 | ||||
-rw-r--r-- | libpod/kube.go | 142 | ||||
-rw-r--r-- | test/e2e/generate_kube_test.go | 106 |
9 files changed, 466 insertions, 62 deletions
diff --git a/cmd/podman/generate.go b/cmd/podman/generate.go new file mode 100644 index 000000000..765d0ee70 --- /dev/null +++ b/cmd/podman/generate.go @@ -0,0 +1,23 @@ +package main + +import ( + "github.com/urfave/cli" +) + +var ( + generateSubCommands = []cli.Command{ + containerKubeCommand, + } + + generateDescription = "generate structured data based for a containers and pods" + kubeCommand = cli.Command{ + Name: "generate", + Usage: "generated structured data", + Description: generateDescription, + ArgsUsage: "", + Subcommands: generateSubCommands, + UseShortOptionHandling: true, + OnUsageError: usageErrorHandler, + Hidden: true, + } +) diff --git a/cmd/podman/kube_generate.go b/cmd/podman/generate_kube.go index a18912668..de9f701b0 100644 --- a/cmd/podman/kube_generate.go +++ b/cmd/podman/generate_kube.go @@ -6,10 +6,11 @@ import ( "github.com/containers/libpod/cmd/podman/libpodruntime" "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/rootless" + podmanVersion "github.com/containers/libpod/version" "github.com/ghodss/yaml" "github.com/pkg/errors" - "github.com/sirupsen/logrus" "github.com/urfave/cli" + "k8s.io/api/core/v1" ) var ( @@ -18,16 +19,15 @@ var ( Name: "service, s", Usage: "only generate YAML for kubernetes service object", }, - LatestFlag, } containerKubeDescription = "Generate Kubernetes Pod YAML" containerKubeCommand = cli.Command{ - Name: "generate", - Usage: "Generate Kubernetes pod YAML for a container", + Name: "kube", + Usage: "Generate Kubernetes pod YAML for a container or pod", Description: containerKubeDescription, Flags: sortFlags(containerKubeFlags), Action: generateKubeYAMLCmd, - ArgsUsage: "CONTAINER-NAME", + ArgsUsage: "CONTAINER|POD-NAME", UseShortOptionHandling: true, OnUsageError: usageErrorHandler, } @@ -36,9 +36,13 @@ var ( // generateKubeYAMLCmdgenerates or replays kube func generateKubeYAMLCmd(c *cli.Context) error { var ( - container *libpod.Container - err error - output []byte + podYAML *v1.Pod + container *libpod.Container + err error + output []byte + pod *libpod.Pod + mashalledBytes []byte + servicePorts []v1.ServicePort ) if rootless.IsRootless() { @@ -46,10 +50,7 @@ func generateKubeYAMLCmd(c *cli.Context) error { } args := c.Args() if len(args) > 1 || (len(args) < 1 && !c.Bool("latest")) { - return errors.Errorf("you must provide one container ID or name or --latest") - } - if c.Bool("service") { - return errors.Wrapf(libpod.ErrNotImplemented, "service generation") + return errors.Errorf("you must provide one container|pod ID or name or --latest") } runtime, err := libpodruntime.GetRuntime(c) @@ -59,33 +60,43 @@ func generateKubeYAMLCmd(c *cli.Context) error { defer runtime.Shutdown(false) // Get the container in question - if c.Bool("latest") { - container, err = runtime.GetLatestContainer() + container, err = runtime.LookupContainer(args[0]) + if err != nil { + pod, err = runtime.LookupPod(args[0]) + if err != nil { + return err + } + podYAML, servicePorts, err = pod.GenerateForKube() } else { - container, err = runtime.LookupContainer(args[0]) + if len(container.Dependencies()) > 0 { + return errors.Wrapf(libpod.ErrNotImplemented, "containers with dependencies") + } + podYAML, err = container.GenerateForKube() } if err != nil { return err } - if len(container.Dependencies()) > 0 { - return errors.Wrapf(libpod.ErrNotImplemented, "containers with dependencies") + if c.Bool("service") { + serviceYAML := libpod.GenerateKubeServiceFromV1Pod(podYAML, servicePorts) + mashalledBytes, err = yaml.Marshal(serviceYAML) + } else { + // Marshall the results + mashalledBytes, err = yaml.Marshal(podYAML) } - - podYAML, err := container.InspectForKube() if err != nil { return err } - developmentComment := []byte("# Generation of Kubenetes YAML is still under development!\n") - logrus.Warn("This function is still under heavy development.") - // Marshall the results - b, err := yaml.Marshal(podYAML) - if err != nil { - return err - } - output = append(output, developmentComment...) - output = append(output, b...) + header := `# Generation of Kubenetes YAML is still under development! +# +# Save the output of this file and use kubectl create -f to import +# it into Kubernetes. +# +# Created with podman-%s +` + output = append(output, []byte(fmt.Sprintf(header, podmanVersion.Version))...) + output = append(output, mashalledBytes...) // Output the v1.Pod with the v1.Container fmt.Println(string(output)) diff --git a/cmd/podman/kube.go b/cmd/podman/kube.go deleted file mode 100644 index 2cb407c09..000000000 --- a/cmd/podman/kube.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - "github.com/urfave/cli" -) - -var ( - kubeSubCommands = []cli.Command{ - containerKubeCommand, - } - - kubeDescription = "Work with Kubernetes objects" - kubeCommand = cli.Command{ - Name: "kube", - Usage: "Import and export Kubernetes objections from and to Podman", - Description: containerDescription, - ArgsUsage: "", - Subcommands: kubeSubCommands, - UseShortOptionHandling: true, - OnUsageError: usageErrorHandler, - Hidden: true, - } -) diff --git a/commands.md b/commands.md index 8e1a2dd7a..43796722f 100644 --- a/commands.md +++ b/commands.md @@ -16,6 +16,7 @@ | [podman-diff(1)](/docs/podman-diff.1.md) | Inspect changes on a container or image's filesystem |[![...](/docs/play.png)](https://asciinema.org/a/FXfWB9CKYFwYM4EfqW3NSZy1G)| | [podman-exec(1)](/docs/podman-exec.1.md) | Execute a command in a running container | [podman-export(1)](/docs/podman-export.1.md) | Export container's filesystem contents as a tar archive |[![...](/docs/play.png)](https://asciinema.org/a/913lBIRAg5hK8asyIhhkQVLtV)| +| [podman-generate(1)](/docs/podman-generate.1.md) | Generate structured output based on Podman containers and pods | | | [podman-history(1)](/docs/podman-history.1.md) | Shows the history of an image |[![...](/docs/play.png)](https://asciinema.org/a/bCvUQJ6DkxInMELZdc5DinNSx)| | [podman-image(1)](/docs/podman-image.1.md) | Manage Images|| | [podman-images(1)](/docs/podman-images.1.md) | List images in local storage |[![...](/docs/play.png)](https://asciinema.org/a/133649)| diff --git a/completions/bash/podman b/completions/bash/podman index 3382b6c5a..eab82ec1f 100644 --- a/completions/bash/podman +++ b/completions/bash/podman @@ -876,6 +876,25 @@ _podman_container_wait() { _podman_wait } +_podman_generate() { + local boolean_options=" + --help + -h + " + subcommands=" + kube + " + __podman_subcommands "$subcommands $aliases" && return + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help" -- "$cur" ) ) + ;; + *) + COMPREPLY=( $( compgen -W "$subcommands" -- "$cur" ) ) + ;; + esac +} _podman_container() { local boolean_options=" --help @@ -2219,6 +2238,14 @@ _podman_logout() { _complete_ "$options_with_args" "$boolean_options" } +_podman_generate_kube() { + local options_with_args="" + + local boolean_options=" + -s + --service + " + _podman_container_runlabel() { local options_with_args=" --authfile @@ -2702,6 +2729,7 @@ _podman_podman() { diff exec export + generate history images import diff --git a/docs/podman-generate-kube.1.md b/docs/podman-generate-kube.1.md new file mode 100644 index 000000000..59c3353a5 --- /dev/null +++ b/docs/podman-generate-kube.1.md @@ -0,0 +1,119 @@ +% podman-generate Podman Man Pages +% Brent Baude +% December 2018 +# NAME +podman-generate-kube - Generate Kubernetes YAML + +# SYNOPSIS +**podman generate kube ** +[**-h**|**--help**] +[**-s**][**--service**] +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. + +The **service** option can be used to generate a Service specification for the corresponding Pod ouput. In particular, +if the object has portmap bindings, the service specification will include a NodePort declaration to expose the service. A +random port is assigned by Podman in the specification. + +# OPTIONS: + +**s** **--service** +Generate a service file for the resulting Pod YAML. + +## Examples ## + +Create Kubernetes Pod YAML for a container called `some-mariadb` . +``` +$ sudo podman generate kube some-mariadb +# Generation of Kubenetes YAML is still under development! +# +# Save the output of this file and use kubectl create -f to import +# it into Kubernetes. +# +# Created with podman-0.11.2-dev +apiVersion: v1 +kind: Pod +metadata: + creationTimestamp: 2018-12-03T19:07:59Z + labels: + app: some-mariadb + name: some-mariadb-libpod +spec: + containers: + - command: + - docker-entrypoint.sh + - mysqld + env: + - name: PATH + value: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + - name: TERM + value: xterm + - name: HOSTNAME + - name: container + value: podman + - name: GOSU_VERSION + value: "1.10" + - name: GPG_KEYS + value: "199369E5404BD5FC7D2FE43BCBCB082A1BB943DB \t177F4010FE56CA3336300305F1656F24C74CD1D8 + \t430BDF5C56E7C94E848EE60C1C4CBDCDCD2EFD2A \t4D1BB29D63D98E422B2113B19334A25F8507EFA5" + - name: MARIADB_MAJOR + value: "10.3" + - name: MARIADB_VERSION + value: 1:10.3.10+maria~bionic + - name: MYSQL_ROOT_PASSWORD + value: x + image: quay.io/baude/demodb:latest + name: some-mariadb + ports: + - containerPort: 3306 + hostPort: 36533 + protocol: TCP + resources: {} + securityContext: + allowPrivilegeEscalation: true + privileged: false + readOnlyRootFilesystem: false + tty: true + workingDir: / +status: {} +``` + +Create Kubernetes service YAML for a container called `some-mariabdb` +``` +$ sudo podman generate kube -s some-mariadb +# Generation of Kubenetes YAML is still under development! +# +# Save the output of this file and use kubectl create -f to import +# it into Kubernetes. +# +# Created with podman-0.11.2-dev +apiVersion: v1 +kind: Service +metadata: + creationTimestamp: 2018-12-03T19:08:24Z + labels: + app: some-mariadb + name: some-mariadb-libpod +spec: + ports: + - name: "3306" + nodePort: 30929 + port: 3306 + protocol: TCP + targetPort: 0 + selector: + app: some-mariadb + type: NodePort +status: + loadBalancer: {} +``` + +## SEE ALSO +podman(1), podman-container, podman-pod + +# HISTORY +Decemeber 2018, Originally compiled by Brent Baude (bbaude at redhat dot com) diff --git a/docs/podman-generate.1.md b/docs/podman-generate.1.md new file mode 100644 index 000000000..f19f48511 --- /dev/null +++ b/docs/podman-generate.1.md @@ -0,0 +1,19 @@ +% podman-generate(1) + +## NAME +podman\-container - generate structured data based for a containers and pods + +## SYNOPSIS +**podman generate** *subcommand* + +## DESCRIPTION +The generate command will create structured output (like YAML) based on a container or pod. + +## COMMANDS + +| Command | Man Page | Description | +| ------- | --------------------------------------------------- | ---------------------------------------------------------------------------- | +| kube | [podman-generate-kube(1)](podman-generate-kube.1.md) | Generate Kubernetes YAML based on a pod or container + +## SEE ALSO +podman, podman-pod, podman-container diff --git a/libpod/kube.go b/libpod/kube.go index 1a5f80878..05a6537c4 100644 --- a/libpod/kube.go +++ b/libpod/kube.go @@ -2,7 +2,10 @@ package libpod import ( "fmt" + "math/rand" + "strconv" "strings" + "time" "github.com/containers/libpod/pkg/lookup" "github.com/containers/libpod/pkg/util" @@ -15,23 +18,127 @@ import ( v12 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// InspectForKube 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. -func (c *Container) InspectForKube() (*v1.Pod, error) { +func (c *Container) GenerateForKube() (*v1.Pod, error) { // Generate the v1.Pod yaml description return simplePodWithV1Container(c) } -// simplePodWithV1Container 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) { - var containers []v1.Container - result, err := containerToV1Container(ctr) +// GenerateForKube takes a slice of libpod containers and generates +// one v1.Pod description +func (p *Pod) GenerateForKube() (*v1.Pod, []v1.ServicePort, error) { + // Generate the v1.Pod yaml description + var servicePorts []v1.ServicePort + + allContainers, err := p.allContainers() + if err != nil { + return nil, servicePorts, err + } + // If the pod has no containers, no sense to generate YAML + if len(allContainers) == 0 { + return nil, servicePorts, errors.Errorf("pod %s has no containers", p.ID()) + } + // If only an infra container is present, makes no sense to generate YAML + if len(allContainers) == 1 && p.HasInfraContainer() { + return nil, servicePorts, errors.Errorf("pod %s only has an infra container", p.ID()) + } + + if p.HasInfraContainer() { + infraContainer, err := p.getInfraContainer() + if err != nil { + return nil, servicePorts, err + } + + ports, err := ocicniPortMappingToContainerPort(infraContainer.config.PortMappings) + if err != nil { + return nil, servicePorts, err + } + servicePorts = containerPortsToServicePorts(ports) + } + pod, err := p.podWithContainers(allContainers) + return pod, servicePorts, err +} + +func (p *Pod) getInfraContainer() (*Container, error) { + infraID, err := p.InfraContainerID() if err != nil { return nil, err } - containers = append(containers, result) + return p.runtime.LookupContainer(infraID) +} + +// GenerateKubeServiceFromV1Pod creates a v1 service object from a v1 pod object +func GenerateKubeServiceFromV1Pod(pod *v1.Pod, servicePorts []v1.ServicePort) v1.Service { + service := v1.Service{} + selector := make(map[string]string) + selector["app"] = pod.Labels["app"] + ports := servicePorts + if len(ports) == 0 { + ports = containersToServicePorts(pod.Spec.Containers) + } + serviceSpec := v1.ServiceSpec{ + Ports: ports, + Selector: selector, + Type: v1.ServiceTypeNodePort, + } + service.Spec = serviceSpec + service.ObjectMeta = pod.ObjectMeta + tm := v12.TypeMeta{ + Kind: "Service", + APIVersion: pod.TypeMeta.APIVersion, + } + service.TypeMeta = tm + return service +} +// containerPortsToServicePorts takes a slice of containerports and generates a +// slice of service ports +func containerPortsToServicePorts(containerPorts []v1.ContainerPort) []v1.ServicePort { + var sps []v1.ServicePort + for _, cp := range containerPorts { + nodePort := 30000 + rand.Intn(32767-30000+1) + servicePort := v1.ServicePort{ + Protocol: cp.Protocol, + Port: cp.ContainerPort, + NodePort: int32(nodePort), + Name: strconv.Itoa(int(cp.ContainerPort)), + } + sps = append(sps, servicePort) + } + return sps +} + +// containersToServicePorts takes a slice of v1.Containers and generates an +// inclusive list of serviceports to expose +func containersToServicePorts(containers []v1.Container) []v1.ServicePort { + var sps []v1.ServicePort + // Without the call to rand.Seed, a program will produce the same sequence of pseudo-random numbers + // for each execution. Legal nodeport range is 30000-32767 + rand.Seed(time.Now().UnixNano()) + + for _, ctr := range containers { + sps = append(sps, containerPortsToServicePorts(ctr.Ports)...) + } + return sps +} + +func (p *Pod) podWithContainers(containers []*Container) (*v1.Pod, error) { + var podContainers []v1.Container + for _, ctr := range containers { + result, err := containerToV1Container(ctr) + if err != nil { + return nil, err + } + if !ctr.IsInfra() { + podContainers = append(podContainers, result) + } + } + + return addContainersToPodObject(podContainers, p.Name()), nil +} + +func addContainersToPodObject(containers []v1.Container, podName string) *v1.Pod { tm := v12.TypeMeta{ Kind: "Pod", APIVersion: "v1", @@ -39,10 +146,10 @@ func simplePodWithV1Container(ctr *Container) (*v1.Pod, error) { // Add a label called "app" with the containers name as a value labels := make(map[string]string) - labels["app"] = removeUnderscores(ctr.Name()) + labels["app"] = removeUnderscores(podName) om := v12.ObjectMeta{ // The name of the pod is container_name-libpod - Name: fmt.Sprintf("%s-libpod", removeUnderscores(ctr.Name())), + Name: fmt.Sprintf("%s-libpod", removeUnderscores(podName)), Labels: labels, // CreationTimestamp seems to be required, so adding it; in doing so, the timestamp // will reflect time this is run (not container create time) because the conversion @@ -57,7 +164,20 @@ func simplePodWithV1Container(ctr *Container) (*v1.Pod, error) { ObjectMeta: om, Spec: ps, } - return &p, nil + return &p +} + +// simplePodWithV1Container 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) { + var containers []v1.Container + result, err := containerToV1Container(ctr) + if err != nil { + return nil, err + } + containers = append(containers, result) + return addContainersToPodObject(containers, ctr.Name()), nil + } // containerToV1Container converts information we know about a libpod container diff --git a/test/e2e/generate_kube_test.go b/test/e2e/generate_kube_test.go new file mode 100644 index 000000000..0ee078455 --- /dev/null +++ b/test/e2e/generate_kube_test.go @@ -0,0 +1,106 @@ +package integration + +import ( + "fmt" + "os" + + . "github.com/containers/libpod/test/utils" + "github.com/ghodss/yaml" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Podman generate kube", func() { + var ( + tempdir string + err error + podmanTest *PodmanTestIntegration + ) + + BeforeEach(func() { + tempdir, err = CreateTempDirInTempDir() + if err != nil { + os.Exit(1) + } + podmanTest = PodmanTestCreate(tempdir) + podmanTest.RestoreAllArtifacts() + }) + + AfterEach(func() { + podmanTest.Cleanup() + f := CurrentGinkgoTestDescription() + timedResult := fmt.Sprintf("Test: %s completed in %f seconds", f.TestText, f.Duration.Seconds()) + GinkgoWriter.Write([]byte(timedResult)) + + }) + + It("podman generate pod kube on bogus object", func() { + session := podmanTest.Podman([]string{"generate", "kube", "foobar"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Not(Equal(0))) + }) + + It("podman generate service kube on bogus object", func() { + session := podmanTest.Podman([]string{"generate", "kube", "-s", "foobar"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Not(Equal(0))) + }) + + It("podman generate kube on container", func() { + session := podmanTest.RunTopContainer("top") + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + kube := podmanTest.Podman([]string{"generate", "kube", "top"}) + kube.WaitWithDefaultTimeout() + Expect(kube.ExitCode()).To(Equal(0)) + + _, err := yaml.Marshal(kube.OutputToString()) + Expect(err).To(BeNil()) + }) + + It("podman generate service kube on container", func() { + session := podmanTest.RunTopContainer("top") + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + kube := podmanTest.Podman([]string{"generate", "kube", "-s", "top"}) + kube.WaitWithDefaultTimeout() + Expect(kube.ExitCode()).To(Equal(0)) + + _, err := yaml.Marshal(kube.OutputToString()) + Expect(err).To(BeNil()) + }) + + It("podman generate kube on pod", func() { + _, rc, _ := podmanTest.CreatePod("toppod") + Expect(rc).To(Equal(0)) + + session := podmanTest.RunTopContainerInPod("topcontainer", "toppod") + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + kube := podmanTest.Podman([]string{"generate", "kube", "toppod"}) + kube.WaitWithDefaultTimeout() + Expect(kube.ExitCode()).To(Equal(0)) + + _, err := yaml.Marshal(kube.OutputToString()) + Expect(err).To(BeNil()) + }) + + It("podman generate service kube on pod", func() { + _, rc, _ := podmanTest.CreatePod("toppod") + Expect(rc).To(Equal(0)) + + session := podmanTest.RunTopContainerInPod("topcontainer", "toppod") + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + kube := podmanTest.Podman([]string{"generate", "kube", "-s", "toppod"}) + kube.WaitWithDefaultTimeout() + Expect(kube.ExitCode()).To(Equal(0)) + + _, err := yaml.Marshal(kube.OutputToString()) + Expect(err).To(BeNil()) + }) +}) |