From d26a6c5af139da1fcbb2e9be1caebc016626d608 Mon Sep 17 00:00:00 2001 From: Jake Correnti Date: Wed, 12 Jul 2023 09:48:27 -0400 Subject: [PATCH] Add `--podmanonly` flag to `podman generate kube` Adds an `--podmanonly` flag to `podman generate kube` to allow for reserved annotations to be included in the generated YAML file. Associated with: #19102 Signed-off-by: Jake Correnti --- cmd/podman/kube/generate.go | 3 + .../source/markdown/podman-kube-generate.1.md | 4 + libpod/kube.go | 16 +- pkg/api/handlers/libpod/generate.go | 12 +- pkg/api/server/register_kube.go | 5 + pkg/bindings/generate/types.go | 2 + pkg/bindings/generate/types_kube_options.go | 15 ++ pkg/domain/entities/generate.go | 2 + pkg/domain/infra/abi/generate.go | 4 +- pkg/domain/infra/tunnel/kube.go | 2 +- test/e2e/generate_kube_test.go | 181 ++++++++++++++++++ 11 files changed, 230 insertions(+), 16 deletions(-) diff --git a/cmd/podman/kube/generate.go b/cmd/podman/kube/generate.go index 6da5ebf25fee..709cf69508de 100644 --- a/cmd/podman/kube/generate.go +++ b/cmd/podman/kube/generate.go @@ -84,6 +84,9 @@ func generateFlags(cmd *cobra.Command, podmanConfig *entities.PodmanConfig) { noTruncAnnotationsFlagName := "no-trunc" flags.BoolVar(&generateOptions.UseLongAnnotations, noTruncAnnotationsFlagName, false, "Don't truncate annotations to Kubernetes length (63 chars)") + podmanonlyFlagName := "podmanonly" + flags.BoolVar(&generateOptions.PodmanOnly, podmanonlyFlagName, false, "Add podman-only reserved annotations to the generated YAML file (Cannot be used by Kubernetes") + flags.SetNormalizeFunc(utils.AliasFlags) } diff --git a/docs/source/markdown/podman-kube-generate.1.md b/docs/source/markdown/podman-kube-generate.1.md index 4cc19d1574f7..ec48deed4d79 100644 --- a/docs/source/markdown/podman-kube-generate.1.md +++ b/docs/source/markdown/podman-kube-generate.1.md @@ -30,6 +30,10 @@ Note that the generated Kubernetes YAML file can be used to re-run the deploymen ## OPTIONS +#### **--podmanonly** + +Add podman-only reserved annotations in generated YAML file (Cannot be used by Kubernetes) + #### **--filename**, **-f**=*filename* Output to the given file instead of STDOUT. If the file already exists, `kube generate` refuses to replace it and returns an error. diff --git a/libpod/kube.go b/libpod/kube.go index 2dc6298a616a..e9c06d2ac541 100644 --- a/libpod/kube.go +++ b/libpod/kube.go @@ -34,14 +34,14 @@ import ( // GenerateForKube takes a slice of libpod containers and generates // one v1.Pod description that includes just a single container. -func GenerateForKube(ctx context.Context, ctrs []*Container, getService, useLongAnnotations bool) (*v1.Pod, error) { +func GenerateForKube(ctx context.Context, ctrs []*Container, getService, useLongAnnotations, podmanOnly bool) (*v1.Pod, error) { // Generate the v1.Pod yaml description - return simplePodWithV1Containers(ctx, ctrs, getService, useLongAnnotations) + return simplePodWithV1Containers(ctx, ctrs, getService, useLongAnnotations, podmanOnly) } // GenerateForKube takes a slice of libpod containers and generates // one v1.Pod description -func (p *Pod) GenerateForKube(ctx context.Context, getService, useLongAnnotations bool) (*v1.Pod, []v1.ServicePort, error) { +func (p *Pod) GenerateForKube(ctx context.Context, getService, useLongAnnotations, podmanOnly bool) (*v1.Pod, []v1.ServicePort, error) { // Generate the v1.Pod yaml description var ( ports []v1.ContainerPort @@ -91,7 +91,7 @@ func (p *Pod) GenerateForKube(ctx context.Context, getService, useLongAnnotation hostNetwork = infraContainer.NetworkMode() == string(namespaces.NetworkMode(specgen.Host)) hostUsers = infraContainer.IDMappings().HostUIDMapping && infraContainer.IDMappings().HostGIDMapping } - pod, err := p.podWithContainers(ctx, allContainers, ports, hostNetwork, hostUsers, getService, useLongAnnotations) + pod, err := p.podWithContainers(ctx, allContainers, ports, hostNetwork, hostUsers, getService, useLongAnnotations, podmanOnly) if err != nil { return nil, servicePorts, err } @@ -426,7 +426,7 @@ func containersToServicePorts(containers []v1.Container) ([]v1.ServicePort, erro return sps, nil } -func (p *Pod) podWithContainers(ctx context.Context, containers []*Container, ports []v1.ContainerPort, hostNetwork, hostUsers, getService, useLongAnnotations bool) (*v1.Pod, error) { +func (p *Pod) podWithContainers(ctx context.Context, containers []*Container, ports []v1.ContainerPort, hostNetwork, hostUsers, getService, useLongAnnotations, podmanOnly bool) (*v1.Pod, error) { deDupPodVolumes := make(map[string]*v1.Volume) first := true podContainers := make([]v1.Container, 0, len(containers)) @@ -442,7 +442,7 @@ func (p *Pod) podWithContainers(ctx context.Context, containers []*Container, po for _, ctr := range containers { if !ctr.IsInfra() { for k, v := range ctr.config.Spec.Annotations { - if define.IsReservedAnnotation(k) || annotations.IsReservedAnnotation(k) { + if !podmanOnly && (define.IsReservedAnnotation(k) || annotations.IsReservedAnnotation(k)) { continue } podAnnotations[fmt.Sprintf("%s/%s", k, removeUnderscores(ctr.Name()))] = truncateKubeAnnotation(v, useLongAnnotations) @@ -574,7 +574,7 @@ func newPodObject(podName string, annotations map[string]string, initCtrs, conta // 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 simplePodWithV1Containers(ctx context.Context, ctrs []*Container, getService, useLongAnnotations bool) (*v1.Pod, error) { +func simplePodWithV1Containers(ctx context.Context, ctrs []*Container, getService, useLongAnnotations, podmanOnly bool) (*v1.Pod, error) { kubeCtrs := make([]v1.Container, 0, len(ctrs)) kubeInitCtrs := []v1.Container{} kubeVolumes := make([]v1.Volume, 0) @@ -588,7 +588,7 @@ func simplePodWithV1Containers(ctx context.Context, ctrs []*Container, getServic for _, ctr := range ctrs { ctrNames = append(ctrNames, removeUnderscores(ctr.Name())) for k, v := range ctr.config.Spec.Annotations { - if define.IsReservedAnnotation(k) || annotations.IsReservedAnnotation(k) { + if !podmanOnly && (define.IsReservedAnnotation(k) || annotations.IsReservedAnnotation(k)) { continue } kubeAnnotations[fmt.Sprintf("%s/%s", k, removeUnderscores(ctr.Name()))] = truncateKubeAnnotation(v, useLongAnnotations) diff --git a/pkg/api/handlers/libpod/generate.go b/pkg/api/handlers/libpod/generate.go index 60450f110589..f9223f40b1c2 100644 --- a/pkg/api/handlers/libpod/generate.go +++ b/pkg/api/handlers/libpod/generate.go @@ -89,11 +89,12 @@ func GenerateKube(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) query := struct { - Names []string `schema:"names"` - Service bool `schema:"service"` - Type string `schema:"type"` - Replicas int32 `schema:"replicas"` - NoTrunc bool `schema:"noTrunc"` + PodmanOnly bool `schema:"podmanOnly"` + Names []string `schema:"names"` + Service bool `schema:"service"` + Type string `schema:"type"` + Replicas int32 `schema:"replicas"` + NoTrunc bool `schema:"noTrunc"` }{ // Defaults would go here. Replicas: 1, @@ -117,6 +118,7 @@ func GenerateKube(w http.ResponseWriter, r *http.Request) { containerEngine := abi.ContainerEngine{Libpod: runtime} options := entities.GenerateKubeOptions{ + PodmanOnly: query.PodmanOnly, Service: query.Service, Type: generateType, Replicas: query.Replicas, diff --git a/pkg/api/server/register_kube.go b/pkg/api/server/register_kube.go index 22c30ad65614..f75807da76d8 100644 --- a/pkg/api/server/register_kube.go +++ b/pkg/api/server/register_kube.go @@ -108,6 +108,11 @@ func (s *APIServer) registerKubeHandlers(r *mux.Router) error { // description: Generate Kubernetes YAML based on a pod or container. // parameters: // - in: query + // name: podmanOnly + // type: boolean + // default: false + // description: add podman-only reserved annotations in generated YAML file (Cannot be used by Kubernetes) + // - in: query // name: names // type: array // items: diff --git a/pkg/bindings/generate/types.go b/pkg/bindings/generate/types.go index 3910638b1bc9..9c611dbe8aa5 100644 --- a/pkg/bindings/generate/types.go +++ b/pkg/bindings/generate/types.go @@ -4,6 +4,8 @@ package generate // //go:generate go run ../generator/generator.go KubeOptions type KubeOptions struct { + // PodmanOnly - add podman-only reserved annotations to generated YAML file (Cannot be used by Kubernetes) + PodmanOnly *bool // Service - generate YAML for a Kubernetes _service_ object. Service *bool // Type - the k8s kind to be generated i.e Pod or Deployment diff --git a/pkg/bindings/generate/types_kube_options.go b/pkg/bindings/generate/types_kube_options.go index 6a863cc23c15..b015949578d2 100644 --- a/pkg/bindings/generate/types_kube_options.go +++ b/pkg/bindings/generate/types_kube_options.go @@ -17,6 +17,21 @@ func (o *KubeOptions) ToParams() (url.Values, error) { return util.ToParams(o) } +// WithPodmanOnly set field PodmanOnly to given value +func (o *KubeOptions) WithPodmanOnly(value bool) *KubeOptions { + o.PodmanOnly = &value + return o +} + +// GetPodmanOnly returns value of field PodmanOnly +func (o *KubeOptions) GetPodmanOnly() bool { + if o.PodmanOnly == nil { + var z bool + return z + } + return *o.PodmanOnly +} + // WithService set field Service to given value func (o *KubeOptions) WithService(value bool) *KubeOptions { o.Service = &value diff --git a/pkg/domain/entities/generate.go b/pkg/domain/entities/generate.go index eaa20bb98d75..e67c5bb8d991 100644 --- a/pkg/domain/entities/generate.go +++ b/pkg/domain/entities/generate.go @@ -29,6 +29,8 @@ type GenerateSystemdReport struct { // GenerateKubeOptions control the generation of Kubernetes YAML files. type GenerateKubeOptions struct { + // PodmanOnly - add podman-only reserved annotations in the generated YAML file (Cannot be used by Kubernetes) + PodmanOnly bool // Service - generate YAML for a Kubernetes _service_ object. Service bool // Type - the k8s kind to be generated i.e Pod or Deployment diff --git a/pkg/domain/infra/abi/generate.go b/pkg/domain/infra/abi/generate.go index a8d30bb91686..9d80f7a11fdd 100644 --- a/pkg/domain/infra/abi/generate.go +++ b/pkg/domain/infra/abi/generate.go @@ -207,7 +207,7 @@ func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrIDs []string, // Generate the kube pods from containers. if len(ctrs) >= 1 { - po, err := libpod.GenerateForKube(ctx, ctrs, options.Service, options.UseLongAnnotations) + po, err := libpod.GenerateForKube(ctx, ctrs, options.Service, options.UseLongAnnotations, options.PodmanOnly) if err != nil { return nil, err } @@ -273,7 +273,7 @@ func getKubePods(ctx context.Context, pods []*libpod.Pod, options entities.Gener svcs := [][]byte{} for _, p := range pods { - po, sp, err := p.GenerateForKube(ctx, options.Service, options.UseLongAnnotations) + po, sp, err := p.GenerateForKube(ctx, options.Service, options.UseLongAnnotations, options.PodmanOnly) if err != nil { return nil, nil, err } diff --git a/pkg/domain/infra/tunnel/kube.go b/pkg/domain/infra/tunnel/kube.go index fff83a594638..802879e24bb6 100644 --- a/pkg/domain/infra/tunnel/kube.go +++ b/pkg/domain/infra/tunnel/kube.go @@ -46,7 +46,7 @@ func (ic *ContainerEngine) GenerateSystemd(ctx context.Context, nameOrID string, // // Note: Caller is responsible for closing returned Reader func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrIDs []string, opts entities.GenerateKubeOptions) (*entities.GenerateKubeReport, error) { - options := new(generate.KubeOptions).WithService(opts.Service).WithType(opts.Type).WithReplicas(opts.Replicas).WithNoTrunc(opts.UseLongAnnotations) + options := new(generate.KubeOptions).WithService(opts.Service).WithType(opts.Type).WithReplicas(opts.Replicas).WithNoTrunc(opts.UseLongAnnotations).WithPodmanOnly(opts.PodmanOnly) return generate.Kube(ic.ClientCtx, nameOrIDs, options) } diff --git a/test/e2e/generate_kube_test.go b/test/e2e/generate_kube_test.go index 09ebec6592af..2a52f8205025 100644 --- a/test/e2e/generate_kube_test.go +++ b/test/e2e/generate_kube_test.go @@ -1,6 +1,7 @@ package integration import ( + "fmt" "os" "os/user" "path/filepath" @@ -1657,4 +1658,184 @@ USER test1` Expect(pod.Annotations).To(HaveKeyWithValue(define.BindMountPrefix, vol1[:define.MaxKubeAnnotation])) Expect(pod.Annotations).To(Not(HaveKeyWithValue(define.BindMountPrefix, vol1+":Z"))) }) + + It("podman kube generate --podmanonly on container with --volumes-from", func() { + ctr1 := "ctr1" + ctr2 := "ctr2" + vol1 := filepath.Join(podmanTest.TempDir, "vol-test1") + + err := os.MkdirAll(vol1, 0755) + Expect(err).ToNot(HaveOccurred()) + + session := podmanTest.Podman([]string{"create", "--name", ctr1, "-v", vol1, ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + session = podmanTest.Podman([]string{"create", "--volumes-from", ctr1, "--name", ctr2, ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + kube := podmanTest.Podman([]string{"kube", "generate", "--podmanonly", ctr2}) + kube.WaitWithDefaultTimeout() + Expect(kube).Should(Exit(0)) + + pod := new(v1.Pod) + err = yaml.Unmarshal(kube.Out.Contents(), pod) + Expect(err).ToNot(HaveOccurred()) + Expect(pod.Annotations).To(HaveKeyWithValue(define.InspectAnnotationVolumesFrom+"/"+ctr2, ctr1)) + }) + + It("podman kube generate --podmanonly on container with --rm", func() { + ctr := "ctr" + + session := podmanTest.Podman([]string{"create", "--rm", "--name", ctr, ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + kube := podmanTest.Podman([]string{"kube", "generate", "--podmanonly", ctr}) + kube.WaitWithDefaultTimeout() + Expect(kube).Should(Exit(0)) + + pod := new(v1.Pod) + err = yaml.Unmarshal(kube.Out.Contents(), pod) + Expect(err).ToNot(HaveOccurred()) + Expect(pod.Annotations).To(HaveKeyWithValue(define.InspectAnnotationAutoremove+"/"+ctr, define.InspectResponseTrue)) + }) + + It("podman kube generate --podmanonly on container with --privileged", func() { + ctr := "ctr" + + session := podmanTest.Podman([]string{"create", "--privileged", "--name", ctr, ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + kube := podmanTest.Podman([]string{"kube", "generate", "--podmanonly", ctr}) + kube.WaitWithDefaultTimeout() + Expect(kube).Should(Exit(0)) + + pod := new(v1.Pod) + err = yaml.Unmarshal(kube.Out.Contents(), pod) + Expect(err).ToNot(HaveOccurred()) + Expect(pod.Annotations).To(HaveKeyWithValue(define.InspectAnnotationPrivileged+"/"+ctr, define.InspectResponseTrue)) + }) + + It("podman kube generate --podmanonly on container with --init", func() { + ctr := "ctr" + + session := podmanTest.Podman([]string{"create", "--init", "--name", ctr, ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + kube := podmanTest.Podman([]string{"kube", "generate", "--podmanonly", ctr}) + kube.WaitWithDefaultTimeout() + Expect(kube).Should(Exit(0)) + + pod := new(v1.Pod) + err = yaml.Unmarshal(kube.Out.Contents(), pod) + Expect(err).ToNot(HaveOccurred()) + Expect(pod.Annotations).To(HaveKeyWithValue(define.InspectAnnotationInit+"/"+ctr, define.InspectResponseTrue)) + }) + + It("podman kube generate --podmanonly on container with --cidfile", func() { + ctr := "ctr" + cidFile := filepath.Join(podmanTest.TempDir, RandomString(10)+".txt") + + session := podmanTest.Podman([]string{"create", "--cidfile", cidFile, "--name", ctr, ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + kube := podmanTest.Podman([]string{"kube", "generate", "--podmanonly", ctr}) + kube.WaitWithDefaultTimeout() + Expect(kube).Should(Exit(0)) + + pod := new(v1.Pod) + err = yaml.Unmarshal(kube.Out.Contents(), pod) + Expect(err).ToNot(HaveOccurred()) + Expect(pod.Annotations).To(HaveKeyWithValue(define.InspectAnnotationCIDFile+"/"+ctr, cidFile)) + }) + + It("podman kube generate --podmanonly on container with --security-opt seccomp=unconfined", func() { + ctr := "ctr" + + session := podmanTest.Podman([]string{"create", "--security-opt", "seccomp=unconfined", "--name", ctr, ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + kube := podmanTest.Podman([]string{"kube", "generate", "--podmanonly", ctr}) + kube.WaitWithDefaultTimeout() + Expect(kube).Should(Exit(0)) + + pod := new(v1.Pod) + err = yaml.Unmarshal(kube.Out.Contents(), pod) + Expect(err).ToNot(HaveOccurred()) + Expect(pod.Annotations).To(HaveKeyWithValue(define.InspectAnnotationSeccomp+"/"+ctr, "unconfined")) + }) + + It("podman kube generate --podmanonly on container with --security-opt apparmor=unconfined", func() { + ctr := "ctr" + + session := podmanTest.Podman([]string{"create", "--security-opt", "apparmor=unconfined", "--name", ctr, ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + kube := podmanTest.Podman([]string{"kube", "generate", "--podmanonly", ctr}) + kube.WaitWithDefaultTimeout() + Expect(kube).Should(Exit(0)) + + pod := new(v1.Pod) + err = yaml.Unmarshal(kube.Out.Contents(), pod) + Expect(err).ToNot(HaveOccurred()) + Expect(pod.Annotations).To(HaveKeyWithValue(define.InspectAnnotationApparmor+"/"+ctr, "unconfined")) + }) + + It("podman kube generate --podmanonly on container with --security-opt label=level:s0", func() { + ctr := "ctr" + + session := podmanTest.Podman([]string{"create", "--security-opt", "label=level:s0", "--name", ctr, ALPINE}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + kube := podmanTest.Podman([]string{"kube", "generate", "--podmanonly", ctr}) + kube.WaitWithDefaultTimeout() + Expect(kube).Should(Exit(0)) + + pod := new(v1.Pod) + err = yaml.Unmarshal(kube.Out.Contents(), pod) + Expect(err).ToNot(HaveOccurred()) + Expect(pod.Annotations).To(HaveKeyWithValue(define.InspectAnnotationLabel+"/"+ctr, "level:s0")) + }) + + It("podman kube generate --podmanonly on container with --publish-all", func() { + podmanTest.AddImageToRWStore(ALPINE) + dockerfile := fmt.Sprintf(`FROM %s +EXPOSE 2002 +EXPOSE 2001-2003 +EXPOSE 2004-2005/tcp`, ALPINE) + imageName := "testimg" + podmanTest.BuildImage(dockerfile, imageName, "false") + + // Verify that the buildah is just passing through the EXPOSE keys + inspect := podmanTest.Podman([]string{"inspect", imageName}) + inspect.WaitWithDefaultTimeout() + image := inspect.InspectImageJSON() + Expect(image).To(HaveLen(1)) + Expect(image[0].Config.ExposedPorts).To(HaveLen(3)) + Expect(image[0].Config.ExposedPorts).To(HaveKey("2002/tcp")) + Expect(image[0].Config.ExposedPorts).To(HaveKey("2001-2003/tcp")) + Expect(image[0].Config.ExposedPorts).To(HaveKey("2004-2005/tcp")) + + ctr := "ctr" + session := podmanTest.Podman([]string{"create", "--publish-all", "--name", ctr, imageName, "true"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + kube := podmanTest.Podman([]string{"kube", "generate", "--podmanonly", ctr}) + kube.WaitWithDefaultTimeout() + Expect(kube).Should(Exit(0)) + + pod := new(v1.Pod) + err = yaml.Unmarshal(kube.Out.Contents(), pod) + Expect(err).ToNot(HaveOccurred()) + Expect(pod.Annotations).To(HaveKeyWithValue(define.InspectAnnotationPublishAll+"/"+ctr, define.InspectResponseTrue)) + }) })