diff options
-rw-r--r-- | cmd/podman/common/completion.go | 14 | ||||
-rw-r--r-- | cmd/podman/generate/generate.go | 4 | ||||
-rw-r--r-- | cmd/podman/generate/kube.go | 14 | ||||
-rw-r--r-- | cmd/podman/play/kube.go | 13 | ||||
-rw-r--r-- | cmd/podman/play/play.go | 4 | ||||
-rw-r--r-- | docs/source/markdown/podman-generate-kube.1.md | 12 | ||||
-rw-r--r-- | docs/source/markdown/podman-generate.1.md | 6 | ||||
-rw-r--r-- | docs/source/markdown/podman-play-kube.1.md | 22 | ||||
-rw-r--r-- | docs/source/markdown/podman-play.1.md | 6 | ||||
-rw-r--r-- | docs/source/markdown/podman.1.md | 4 | ||||
-rw-r--r-- | libpod/kube.go | 45 | ||||
-rw-r--r-- | pkg/domain/entities/play.go | 8 | ||||
-rw-r--r-- | pkg/domain/infra/abi/generate.go | 172 | ||||
-rw-r--r-- | pkg/domain/infra/abi/play.go | 105 | ||||
-rw-r--r-- | pkg/util/kube.go | 16 | ||||
-rw-r--r-- | test/e2e/generate_kube_test.go | 58 | ||||
-rw-r--r-- | test/e2e/play_kube_test.go | 182 |
17 files changed, 596 insertions, 89 deletions
diff --git a/cmd/podman/common/completion.go b/cmd/podman/common/completion.go index bc106263c..d110fb1b5 100644 --- a/cmd/podman/common/completion.go +++ b/cmd/podman/common/completion.go @@ -405,6 +405,20 @@ func AutocompletePodsRunning(cmd *cobra.Command, args []string, toComplete strin return getPods(cmd, toComplete, completeDefault, "running", "degraded") } +// AutocompleteForKube - Autocomplete all Podman objects supported by kube generate. +func AutocompleteForKube(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if !validCurrentCmdLine(cmd, args, toComplete) { + return nil, cobra.ShellCompDirectiveNoFileComp + } + containers, _ := getContainers(cmd, toComplete, completeDefault) + pods, _ := getPods(cmd, toComplete, completeDefault) + volumes, _ := getVolumes(cmd, toComplete) + objs := containers + objs = append(objs, pods...) + objs = append(objs, volumes...) + return objs, cobra.ShellCompDirectiveNoFileComp +} + // AutocompleteContainersAndPods - Autocomplete container names and pod names. func AutocompleteContainersAndPods(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if !validCurrentCmdLine(cmd, args, toComplete) { diff --git a/cmd/podman/generate/generate.go b/cmd/podman/generate/generate.go index c559b9e1b..a6d39bbbe 100644 --- a/cmd/podman/generate/generate.go +++ b/cmd/podman/generate/generate.go @@ -12,8 +12,8 @@ var ( // Command: podman _generate_ generateCmd = &cobra.Command{ Use: "generate", - Short: "Generate structured data based on containers and pods.", - Long: "Generate structured data (e.g., Kubernetes yaml or systemd units) based on containers and pods.", + Short: "Generate structured data based on containers, pods or volumes.", + Long: "Generate structured data (e.g., Kubernetes YAML or systemd units) based on containers, pods or volumes.", RunE: validate.SubCommandExists, } containerConfig = util.DefaultContainerConfig() diff --git a/cmd/podman/generate/kube.go b/cmd/podman/generate/kube.go index 90e58271f..9767b0e06 100644 --- a/cmd/podman/generate/kube.go +++ b/cmd/podman/generate/kube.go @@ -17,20 +17,22 @@ import ( var ( kubeOptions = entities.GenerateKubeOptions{} kubeFile = "" - kubeDescription = `Command generates Kubernetes pod and service YAML (v1 specification) from Podman containers or a pod. + kubeDescription = `Command generates Kubernetes Pod, Service or PersistenVolumeClaim YAML (v1 specification) from Podman containers, pods or volumes. -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{ - Use: "kube [options] {CONTAINER...|POD}", - Short: "Generate Kubernetes YAML from a container or pod.", + Use: "kube [options] {CONTAINER...|POD...|VOLUME...}", + Short: "Generate Kubernetes YAML from containers, pods or volumes.", Long: kubeDescription, RunE: kube, Args: cobra.MinimumNArgs(1), - ValidArgsFunction: common.AutocompleteContainersAndPods, + ValidArgsFunction: common.AutocompleteForKube, Example: `podman generate kube ctrID podman generate kube podID - podman generate kube --service podID`, + podman generate kube --service podID + podman generate kube volumeName + podman generate kube ctrID podID volumeName --service`, } ) diff --git a/cmd/podman/play/kube.go b/cmd/podman/play/kube.go index 4c0f7f39e..ddba5dc0f 100644 --- a/cmd/podman/play/kube.go +++ b/cmd/podman/play/kube.go @@ -32,11 +32,11 @@ var ( kubeOptions = playKubeOptionsWrapper{} kubeDescription = `Command reads in a structured file of Kubernetes YAML. - It creates the pod and containers described in the YAML. The containers within the pod are then started and the ID of the new Pod is output.` + It creates pods or volumes based on the Kubernetes kind described in the YAML. Supported kinds are Pods, Deployments and PersistentVolumeClaims.` kubeCmd = &cobra.Command{ Use: "kube [options] KUBEFILE|-", - Short: "Play a pod based on Kubernetes YAML.", + Short: "Play a pod or volume based on Kubernetes YAML.", Long: kubeDescription, RunE: kube, Args: cobra.ExactArgs(1), @@ -129,6 +129,15 @@ func kube(cmd *cobra.Command, args []string) error { return err } + // Print volumes report + for i, volume := range report.Volumes { + if i == 0 { + fmt.Println("Volumes:") + } + fmt.Println(volume.Name) + } + + // Print pods report for _, pod := range report.Pods { for _, l := range pod.Logs { fmt.Fprint(os.Stderr, l) diff --git a/cmd/podman/play/play.go b/cmd/podman/play/play.go index 92f87ad80..89c7e0139 100644 --- a/cmd/podman/play/play.go +++ b/cmd/podman/play/play.go @@ -11,8 +11,8 @@ var ( // Command: podman _play_ playCmd = &cobra.Command{ Use: "play", - Short: "Play a pod and its containers from a structured file.", - Long: "Play structured data (e.g., Kubernetes pod or service yaml) based on containers and pods.", + Short: "Play containers, pods or volumes from a structured file.", + Long: "Play structured data (e.g., Kubernetes YAML) based on containers, pods or volumes.", RunE: validate.SubCommandExists, } ) diff --git a/docs/source/markdown/podman-generate-kube.1.md b/docs/source/markdown/podman-generate-kube.1.md index 90b4c59fb..a426d2c3c 100644 --- a/docs/source/markdown/podman-generate-kube.1.md +++ b/docs/source/markdown/podman-generate-kube.1.md @@ -1,14 +1,16 @@ % podman-generate-kube(1) ## NAME -podman-generate-kube - Generate Kubernetes YAML based on a pod or container +podman-generate-kube - Generate Kubernetes YAML based on containers, pods or volumes ## SYNOPSIS -**podman generate kube** [*options*] *container...* | *pod* +**podman generate kube** [*options*] *container...* | *pod...* | *volume...* ## DESCRIPTION -**podman generate kube** will generate Kubernetes Pod YAML (v1 specification) from Podman from 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. +**podman generate kube** will generate Kubernetes YAML (v1 specification) from Podman containers, pods or volumes. Whether +the input is for containers or pods, Podman will always generate the specification as a Pod. The input may be in the form +of one or more containers, pods or volumes names or IDs. + +`Podman Containers or Pods` Volumes appear in the generated YAML according to two different volume types. Bind-mounted volumes become *hostPath* volume types and named volumes become *persistentVolumeClaim* volume types. Generated *hostPath* volume types will be one of three subtypes depending on the state of the host path: *DirectoryOrCreate* when no file or directory exists at the host, *Directory* when host path is a directory, or *File* when host path is a file. The value for *claimName* for a *persistentVolumeClaim* is the name of the named volume registered in Podman. diff --git a/docs/source/markdown/podman-generate.1.md b/docs/source/markdown/podman-generate.1.md index 82c67fdb1..c2060d1a4 100644 --- a/docs/source/markdown/podman-generate.1.md +++ b/docs/source/markdown/podman-generate.1.md @@ -1,19 +1,19 @@ % podman-generate(1) ## NAME -podman\-generate - Generate structured data based for a containers and pods +podman\-generate - Generate structured data based on containers, pods or volumes ## SYNOPSIS **podman generate** *subcommand* ## DESCRIPTION -The generate command will create structured output (like YAML) based on a container or pod. +The generate command will create structured output (like YAML) based on a container, pod or volume. ## COMMANDS | Command | Man Page | Description | |---------|------------------------------------------------------------|-------------------------------------------------------------------------------------| -| kube | [podman-generate-kube(1)](podman-generate-kube.1.md) | Generate Kubernetes YAML based on a pod or container. | +| kube | [podman-generate-kube(1)](podman-generate-kube.1.md) | Generate Kubernetes YAML based on containers, pods or volumes. | | systemd | [podman-generate-systemd(1)](podman-generate-systemd.1.md) | Generate systemd unit file(s) for a container or pod. | diff --git a/docs/source/markdown/podman-play-kube.1.md b/docs/source/markdown/podman-play-kube.1.md index 6206a2ea9..91899a8bd 100644 --- a/docs/source/markdown/podman-play-kube.1.md +++ b/docs/source/markdown/podman-play-kube.1.md @@ -1,22 +1,40 @@ % podman-play-kube(1) ## NAME -podman-play-kube - Create pods and containers based on Kubernetes YAML +podman-play-kube - Create containers, pods or volumes based on Kubernetes YAML ## SYNOPSIS **podman play kube** [*options*] *file.yml|-* ## DESCRIPTION -**podman play kube** will read in a structured file of Kubernetes YAML. It will then recreate the pod and containers described in the YAML. The containers within the pod are then started and the ID of the new Pod is output. If the yaml file is specified as "-" then `podman play kube` with read the yaml file from stdin. +**podman play kube** will read in a structured file of Kubernetes YAML. It will then recreate the containers, pods or volumes described in the YAML. Containers within a pod are then started and the ID of the new Pod or the name of the new Volume is output. If the yaml file is specified as "-" then `podman play kube` will read the YAML file from stdin. Ideally the input file would be one created by Podman (see podman-generate-kube(1)). This would guarantee a smooth import and expected results. +Currently, the supported Kubernetes kinds are: +- Pod +- Deployment +- PersistentVolumeClaim + +`Kubernetes Pods or Deployments` + Only two volume types are supported by play kube, the *hostPath* and *persistentVolumeClaim* volume types. For the *hostPath* volume type, only the *default (empty)*, *DirectoryOrCreate*, *Directory*, *FileOrCreate*, *File*, and *Socket* subtypes are supported. The *CharDevice* and *BlockDevice* subtypes are not supported. Podman interprets the value of *hostPath* *path* as a file path when it contains at least one forward slash, otherwise Podman treats the value as the name of a named volume. When using a *persistentVolumeClaim*, the value for *claimName* is the name for the Podman named volume. Note: *hostPath* volume types created by play kube will be given an SELinux private label (Z) Note: If the `:latest` tag is used, Podman will attempt to pull the image from a registry. If the image was built locally with Podman or Buildah, it will have `localhost` as the domain, in that case, Podman will use the image from the local store even if it has the `:latest` tag. +`Kubernetes PersistentVolumeClaims` + +A Kubernetes PersistentVolumeClaim represents a Podman named volume. Only the PersistentVolumeClaim name is required by Podman to create a volume. Kubernetes annotations can be used to make use of the available options for Podman volumes. + +- volume.podman.io/driver +- volume.podman.io/device +- volume.podman.io/type +- volume.podman.io/uid +- volume.podman.io/gid +- volume.podman.io/mount-options + ## OPTIONS #### **\-\-authfile**=*path* diff --git a/docs/source/markdown/podman-play.1.md b/docs/source/markdown/podman-play.1.md index 364baad60..39101ad31 100644 --- a/docs/source/markdown/podman-play.1.md +++ b/docs/source/markdown/podman-play.1.md @@ -1,20 +1,20 @@ % podman-play(1) ## NAME -podman\-play - Play pods and containers based on a structured input file +podman\-play - Play containers, pods or volumes based on a structured input file ## SYNOPSIS **podman play** *subcommand* ## DESCRIPTION -The play command will recreate pods and containers based on the input from a structured (like YAML) +The play command will recreate containers, pods or volumes based on the input from a structured (like YAML) file input. Containers will be automatically started. ## COMMANDS | Command | Man Page | Description | | ------- | --------------------------------------------------- | ---------------------------------------------------------------------------- | -| kube | [podman-play-kube(1)](podman-play-kube.1.md) | Create pods and containers based on Kubernetes YAML. | +| kube | [podman-play-kube(1)](podman-play-kube.1.md) | Create containers, pods or volumes based on Kubernetes YAML. | ## SEE ALSO podman, podman-pod(1), podman-container(1), podman-generate(1), podman-play(1), podman-play-kube(1) diff --git a/docs/source/markdown/podman.1.md b/docs/source/markdown/podman.1.md index 5755b45ac..87bcd8802 100644 --- a/docs/source/markdown/podman.1.md +++ b/docs/source/markdown/podman.1.md @@ -223,7 +223,7 @@ the exit codes follow the `chroot` standard, see below: | [podman-events(1)](podman-events.1.md) | Monitor Podman events | | [podman-exec(1)](podman-exec.1.md) | Execute a command in a running container. | | [podman-export(1)](podman-export.1.md) | Export a container's filesystem contents as a tar archive. | -| [podman-generate(1)](podman-generate.1.md) | Generate structured data based for a containers and pods. | +| [podman-generate(1)](podman-generate.1.md) | Generate structured data based on containers, pods or volumes. | | [podman-healthcheck(1)](podman-healthcheck.1.md) | Manage healthchecks for containers | | [podman-history(1)](podman-history.1.md) | Show the history of an image. | | [podman-image(1)](podman-image.1.md) | Manage images. | @@ -242,7 +242,7 @@ the exit codes follow the `chroot` standard, see below: | [podman-mount(1)](podman-mount.1.md) | Mount a working container's root filesystem. | | [podman-network(1)](podman-network.1.md) | Manage Podman CNI networks. | | [podman-pause(1)](podman-pause.1.md) | Pause one or more containers. | -| [podman-play(1)](podman-play.1.md) | Play pods and containers based on a structured input file. | +| [podman-play(1)](podman-play.1.md) | Play containers, pods or volumes based on a structured input file. | | [podman-pod(1)](podman-pod.1.md) | Management tool for groups of containers, called pods. | | [podman-port(1)](podman-port.1.md) | List port mappings for a container. | | [podman-ps(1)](podman-ps.1.md) | Prints out information about containers. | diff --git a/libpod/kube.go b/libpod/kube.go index b4dd4f10a..11ccaeadc 100644 --- a/libpod/kube.go +++ b/libpod/kube.go @@ -16,6 +16,7 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" v12 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -113,6 +114,50 @@ func (p *Pod) getInfraContainer() (*Container, error) { return p.runtime.GetContainer(infraID) } +// GenerateForKube generates a v1.PersistentVolumeClaim from a libpod volume. +func (v *Volume) GenerateForKube() *v1.PersistentVolumeClaim { + annotations := make(map[string]string) + annotations[util.VolumeDriverAnnotation] = v.Driver() + + for k, v := range v.Options() { + switch k { + case "o": + annotations[util.VolumeMountOptsAnnotation] = v + case "device": + annotations[util.VolumeDeviceAnnotation] = v + case "type": + annotations[util.VolumeTypeAnnotation] = v + case "UID": + annotations[util.VolumeUIDAnnotation] = v + case "GID": + annotations[util.VolumeGIDAnnotation] = v + } + } + + return &v1.PersistentVolumeClaim{ + TypeMeta: v12.TypeMeta{ + Kind: "PersistentVolumeClaim", + APIVersion: "v1", + }, + ObjectMeta: v12.ObjectMeta{ + Name: v.Name(), + Labels: v.Labels(), + Annotations: annotations, + CreationTimestamp: v12.Now(), + }, + Spec: v1.PersistentVolumeClaimSpec{ + Resources: v1.ResourceRequirements{ + Requests: map[v1.ResourceName]resource.Quantity{ + v1.ResourceStorage: resource.MustParse("1Gi"), + }, + }, + AccessModes: []v1.PersistentVolumeAccessMode{ + v1.ReadWriteOnce, + }, + }, + } +} + // GenerateKubeServiceFromV1Pod creates a v1 service object from a v1 pod object func GenerateKubeServiceFromV1Pod(pod *v1.Pod, servicePorts []v1.ServicePort) v1.Service { service := v1.Service{} diff --git a/pkg/domain/entities/play.go b/pkg/domain/entities/play.go index 6883fe6c5..cd8bb9506 100644 --- a/pkg/domain/entities/play.go +++ b/pkg/domain/entities/play.go @@ -45,8 +45,16 @@ type PlayKubePod struct { ContainerErrors []string } +// PlayKubeVolume represents a single volume created by play kube. +type PlayKubeVolume struct { + // Name - Name of the volume created by play kube. + Name string +} + // PlayKubeReport contains the results of running play kube. type PlayKubeReport struct { // Pods - pods created by play kube. Pods []PlayKubePod + // Volumes - volumes created by play kube. + Volumes []PlayKubeVolume } diff --git a/pkg/domain/infra/abi/generate.go b/pkg/domain/infra/abi/generate.go index 94f649e15..b0853b554 100644 --- a/pkg/domain/infra/abi/generate.go +++ b/pkg/domain/infra/abi/generate.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "strings" "github.com/containers/podman/v3/libpod" "github.com/containers/podman/v3/libpod/define" @@ -43,120 +44,174 @@ func (ic *ContainerEngine) GenerateSystemd(ctx context.Context, nameOrID string, func (ic *ContainerEngine) GenerateKube(ctx context.Context, nameOrIDs []string, options entities.GenerateKubeOptions) (*entities.GenerateKubeReport, error) { var ( - pods []*libpod.Pod - ctrs []*libpod.Container - kubePods []*k8sAPI.Pod - kubeServices []k8sAPI.Service - content []byte + pods []*libpod.Pod + ctrs []*libpod.Container + vols []*libpod.Volume + podContent [][]byte + content [][]byte ) + + // Lookup for podman objects. for _, nameOrID := range nameOrIDs { - // Get the container in question + // Let's assume it's a container, so get the container. ctr, err := ic.Libpod.LookupContainer(nameOrID) if err != nil { - pod, err := ic.Libpod.LookupPod(nameOrID) - if err != nil { + if !strings.Contains(err.Error(), "no such container") { return nil, err } - pods = append(pods, pod) } else { if len(ctr.Dependencies()) > 0 { return nil, errors.Wrapf(define.ErrNotImplemented, "containers with dependencies") } - // we cannot deal with ctrs already in a pod + // 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) + continue + } + + // Maybe it's a pod. + pod, err := ic.Libpod.LookupPod(nameOrID) + if err != nil { + if !strings.Contains(err.Error(), "no such pod") { + return nil, err + } + } else { + pods = append(pods, pod) + continue + } + + // Or volume. + vol, err := ic.Libpod.LookupVolume(nameOrID) + if err != nil { + if !strings.Contains(err.Error(), "no such volume") { + return nil, err + } + } else { + vols = append(vols, vol) + continue } + + // If it reaches here is because the name or id did not exist. + return nil, errors.Errorf("Name or ID %q not found", nameOrID) } - // check our inputs - if len(pods) > 0 && len(ctrs) > 0 { - return nil, errors.New("cannot generate pods and containers at the same time") + // Generate kube persistent volume claims from volumes. + if len(vols) >= 1 { + pvs, err := getKubePVCs(vols) + if err != nil { + return nil, err + } + + content = append(content, pvs...) } + // Generate kube pods and services from pods. if len(pods) >= 1 { pos, svcs, err := getKubePods(pods, options.Service) if err != nil { return nil, err } - kubePods = append(kubePods, pos...) + podContent = append(podContent, pos...) if options.Service { - kubeServices = append(kubeServices, svcs...) + content = append(content, svcs...) } - } else { + } + + // Generate the kube pods from containers. + if len(ctrs) >= 1 { po, err := libpod.GenerateForKube(ctrs) if err != nil { return nil, err } - kubePods = append(kubePods, po) + b, err := generateKubeYAML(po) + if err != nil { + return nil, err + } + + podContent = append(podContent, b) if options.Service { - kubeServices = append(kubeServices, libpod.GenerateKubeServiceFromV1Pod(po, []k8sAPI.ServicePort{})) + b, err := generateKubeYAML(libpod.GenerateKubeServiceFromV1Pod(po, []k8sAPI.ServicePort{})) + if err != nil { + return nil, err + } + content = append(content, b) } } - content, err := generateKubeOutput(kubePods, kubeServices, options.Service) + // Content order is based on helm install order (secret, persistentVolumeClaim, service, pod). + content = append(content, podContent...) + + // Generate kube YAML file from all kube kinds. + k, err := generateKubeOutput(content) if err != nil { return nil, err } - return &entities.GenerateKubeReport{Reader: bytes.NewReader(content)}, nil + return &entities.GenerateKubeReport{Reader: bytes.NewReader(k)}, nil } -func getKubePods(pods []*libpod.Pod, getService bool) ([]*k8sAPI.Pod, []k8sAPI.Service, error) { - kubePods := make([]*k8sAPI.Pod, 0) - kubeServices := make([]k8sAPI.Service, 0) +// getKubePods returns kube pod and service YAML files from podman pods. +func getKubePods(pods []*libpod.Pod, getService bool) ([][]byte, [][]byte, error) { + pos := [][]byte{} + svcs := [][]byte{} for _, p := range pods { - po, svc, err := p.GenerateForKube() + po, sp, err := p.GenerateForKube() + if err != nil { + return nil, nil, err + } + + b, err := generateKubeYAML(po) if err != nil { return nil, nil, err } + pos = append(pos, b) - kubePods = append(kubePods, po) if getService { - kubeServices = append(kubeServices, libpod.GenerateKubeServiceFromV1Pod(po, svc)) + b, err := generateKubeYAML(libpod.GenerateKubeServiceFromV1Pod(po, sp)) + if err != nil { + return nil, nil, err + } + svcs = append(svcs, b) } } - return kubePods, kubeServices, nil + return pos, svcs, nil } -func generateKubeOutput(kubePods []*k8sAPI.Pod, kubeServices []k8sAPI.Service, hasService bool) ([]byte, error) { - output := make([]byte, 0) - marshalledPods := make([]byte, 0) - marshalledServices := make([]byte, 0) +// getKubePVCs returns kube persistent volume claim YAML files from podman volumes. +func getKubePVCs(volumes []*libpod.Volume) ([][]byte, error) { + pvs := [][]byte{} - for i, p := range kubePods { - if i != 0 { - marshalledPods = append(marshalledPods, []byte("---\n")...) - } - - b, err := yaml.Marshal(p) + for _, v := range volumes { + b, err := generateKubeYAML(v.GenerateForKube()) if err != nil { return nil, err } - - marshalledPods = append(marshalledPods, b...) + pvs = append(pvs, b) } - if hasService { - for i, s := range kubeServices { - if i != 0 { - marshalledServices = append(marshalledServices, []byte("---\n")...) - } - - b, err := yaml.Marshal(s) - if err != nil { - return nil, err - } + return pvs, nil +} - marshalledServices = append(marshalledServices, b...) - } +// generateKubeYAML marshalls a kube kind into a YAML file. +func generateKubeYAML(kubeKind interface{}) ([]byte, error) { + b, err := yaml.Marshal(kubeKind) + if err != nil { + return nil, err } + return b, nil +} + +// generateKubeOutput generates kube YAML file containing multiple kube kinds. +func generateKubeOutput(content [][]byte) ([]byte, error) { + output := make([]byte, 0) + header := `# Generation of Kubernetes YAML is still under development! # # Save the output of this file and use kubectl create -f to import @@ -169,13 +224,18 @@ func generateKubeOutput(kubePods []*k8sAPI.Pod, kubeServices []k8sAPI.Service, h return nil, err } + // Add header to kube YAML file. output = append(output, []byte(fmt.Sprintf(header, podmanVersion.Version))...) - // kube generate order is based on helm install order (service, pod...) - if hasService { - output = append(output, marshalledServices...) - output = append(output, []byte("---\n")...) + + // kube generate order is based on helm install order (secret, persistentVolume, service, pod...). + // Add kube kinds. + for i, b := range content { + if i != 0 { + output = append(output, []byte("---\n")...) + } + + output = append(output, b...) } - output = append(output, marshalledPods...) return output, nil } diff --git a/pkg/domain/infra/abi/play.go b/pkg/domain/infra/abi/play.go index 3b5c141d7..52f759f13 100644 --- a/pkg/domain/infra/abi/play.go +++ b/pkg/domain/infra/abi/play.go @@ -7,6 +7,7 @@ import ( "io" "io/ioutil" "os" + "strconv" "strings" "github.com/containers/common/pkg/secrets" @@ -43,6 +44,12 @@ func (ic *ContainerEngine) PlayKube(ctx context.Context, path string, options en return nil, err } + // sort kube kinds + documentList, err = sortKubeKinds(documentList) + if err != nil { + return nil, errors.Wrapf(err, "unable to sort kube kinds in %q", path) + } + // create pod on each document if it is a pod or deployment // any other kube kind will be skipped for _, document := range documentList { @@ -84,6 +91,20 @@ func (ic *ContainerEngine) PlayKube(ctx context.Context, path string, options en report.Pods = append(report.Pods, r.Pods...) validKinds++ + case "PersistentVolumeClaim": + var pvcYAML v1.PersistentVolumeClaim + + if err := yaml.Unmarshal(document, &pvcYAML); err != nil { + return nil, errors.Wrapf(err, "unable to read YAML %q as Kube PersistentVolumeClaim", path) + } + + r, err := ic.playKubePVC(ctx, &pvcYAML, options) + if err != nil { + return nil, err + } + + report.Volumes = append(report.Volumes, r.Volumes...) + validKinds++ default: logrus.Infof("kube kind %s not supported", kind) continue @@ -313,6 +334,68 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY return &report, nil } +// playKubePVC creates a podman volume from a kube persistent volume claim. +func (ic *ContainerEngine) playKubePVC(ctx context.Context, pvcYAML *v1.PersistentVolumeClaim, options entities.PlayKubeOptions) (*entities.PlayKubeReport, error) { + var report entities.PlayKubeReport + opts := make(map[string]string) + + // Get pvc name. + // This is the only required pvc attribute to create a podman volume. + name := pvcYAML.GetName() + if strings.TrimSpace(name) == "" { + return nil, fmt.Errorf("persistent volume claim name can not be empty") + } + + // Create podman volume options. + volOptions := []libpod.VolumeCreateOption{ + libpod.WithVolumeName(name), + libpod.WithVolumeLabels(pvcYAML.GetLabels()), + } + + // Get pvc annotations and create remaining podman volume options if available. + // These are podman volume options that do not match any of the persistent volume claim + // attributes, so they can be configured using annotations since they will not affect k8s. + for k, v := range pvcYAML.GetAnnotations() { + switch k { + case util.VolumeDriverAnnotation: + volOptions = append(volOptions, libpod.WithVolumeDriver(v)) + case util.VolumeDeviceAnnotation: + opts["device"] = v + case util.VolumeTypeAnnotation: + opts["type"] = v + case util.VolumeUIDAnnotation: + uid, err := strconv.Atoi(v) + if err != nil { + return nil, errors.Wrapf(err, "cannot convert uid %s to integer", v) + } + volOptions = append(volOptions, libpod.WithVolumeUID(uid)) + opts["UID"] = v + case util.VolumeGIDAnnotation: + gid, err := strconv.Atoi(v) + if err != nil { + return nil, errors.Wrapf(err, "cannot convert gid %s to integer", v) + } + volOptions = append(volOptions, libpod.WithVolumeGID(gid)) + opts["GID"] = v + case util.VolumeMountOptsAnnotation: + opts["o"] = v + } + } + volOptions = append(volOptions, libpod.WithVolumeOptions(opts)) + + // Create volume. + vol, err := ic.Libpod.NewVolume(ctx, volOptions...) + if err != nil { + return nil, err + } + + report.Volumes = append(report.Volumes, entities.PlayKubeVolume{ + Name: vol.Name(), + }) + + return &report, nil +} + // readConfigMapFromFile returns a kubernetes configMap obtained from --configmap flag func readConfigMapFromFile(r io.Reader) (v1.ConfigMap, error) { var cm v1.ConfigMap @@ -374,3 +457,25 @@ func getKubeKind(obj []byte) (string, error) { return kubeObject.Kind, nil } + +// sortKubeKinds adds the correct creation order for the kube kinds. +// Any pod dependecy will be created first like volumes, secrets, etc. +func sortKubeKinds(documentList [][]byte) ([][]byte, error) { + var sortedDocumentList [][]byte + + for _, document := range documentList { + kind, err := getKubeKind(document) + if err != nil { + return nil, err + } + + switch kind { + case "Pod", "Deployment": + sortedDocumentList = append(sortedDocumentList, document) + default: + sortedDocumentList = append([][]byte{document}, sortedDocumentList...) + } + } + + return sortedDocumentList, nil +} diff --git a/pkg/util/kube.go b/pkg/util/kube.go new file mode 100644 index 000000000..1255cdfc5 --- /dev/null +++ b/pkg/util/kube.go @@ -0,0 +1,16 @@ +package util + +const ( + // Kube annotation for podman volume driver. + VolumeDriverAnnotation = "volume.podman.io/driver" + // Kube annotation for podman volume type. + VolumeTypeAnnotation = "volume.podman.io/type" + // Kube annotation for podman volume device. + VolumeDeviceAnnotation = "volume.podman.io/device" + // Kube annotation for podman volume UID. + VolumeUIDAnnotation = "volume.podman.io/uid" + // Kube annotation for podman volume GID. + VolumeGIDAnnotation = "volume.podman.io/gid" + // Kube annotation for podman volume mount options. + VolumeMountOptsAnnotation = "volume.podman.io/mount-options" +) diff --git a/test/e2e/generate_kube_test.go b/test/e2e/generate_kube_test.go index 1c53307bd..c3586d9b6 100644 --- a/test/e2e/generate_kube_test.go +++ b/test/e2e/generate_kube_test.go @@ -6,6 +6,7 @@ import ( "path/filepath" "strconv" + "github.com/containers/podman/v3/pkg/util" . "github.com/containers/podman/v3/test/utils" "github.com/ghodss/yaml" . "github.com/onsi/ginkgo" @@ -554,7 +555,7 @@ var _ = Describe("Podman generate kube", func() { Expect(inspect.OutputToString()).To(ContainSubstring(`"pid"`)) }) - It("podman generate kube with pods and containers should fail", func() { + It("podman generate kube with pods and containers", func() { pod1 := podmanTest.Podman([]string{"run", "-dt", "--pod", "new:pod1", ALPINE, "top"}) pod1.WaitWithDefaultTimeout() Expect(pod1.ExitCode()).To(Equal(0)) @@ -565,7 +566,7 @@ var _ = Describe("Podman generate kube", func() { kube := podmanTest.Podman([]string{"generate", "kube", "pod1", "top"}) kube.WaitWithDefaultTimeout() - Expect(kube.ExitCode()).ToNot(Equal(0)) + Expect(kube.ExitCode()).To(Equal(0)) }) It("podman generate kube with containers in a pod should fail", func() { @@ -630,7 +631,7 @@ var _ = Describe("Podman generate kube", func() { Expect(*pod.Spec.DNSConfig.Options[0].Value).To(Equal("blue")) }) - It("podman generate kube multiple contianer dns servers and options are cumulative", func() { + It("podman generate kube multiple container dns servers and options are cumulative", func() { top1 := podmanTest.Podman([]string{"run", "-dt", "--name", "top1", "--dns", "8.8.8.8", "--dns-search", "foobar.com", ALPINE, "top"}) top1.WaitWithDefaultTimeout() Expect(top1.ExitCode()).To(BeZero()) @@ -798,4 +799,55 @@ USER test1` Expect(*pod.Spec.Containers[0].SecurityContext.RunAsUser).To(Equal(int64(10001))) }) + It("podman generate kube on named volume", func() { + vol := "simple-named-volume" + + session := podmanTest.Podman([]string{"volume", "create", vol}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + kube := podmanTest.Podman([]string{"generate", "kube", vol}) + kube.WaitWithDefaultTimeout() + Expect(kube.ExitCode()).To(Equal(0)) + + pvc := new(v1.PersistentVolumeClaim) + err := yaml.Unmarshal(kube.Out.Contents(), pvc) + Expect(err).To(BeNil()) + Expect(pvc.GetName()).To(Equal(vol)) + Expect(pvc.Spec.AccessModes[0]).To(Equal(v1.ReadWriteOnce)) + Expect(pvc.Spec.Resources.Requests.Storage().String()).To(Equal("1Gi")) + }) + + It("podman generate kube on named volume with options", func() { + vol := "complex-named-volume" + volDevice := "tmpfs" + volType := "tmpfs" + volOpts := "nodev,noexec" + + session := podmanTest.Podman([]string{"volume", "create", "--opt", "device=" + volDevice, "--opt", "type=" + volType, "--opt", "o=" + volOpts, vol}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + kube := podmanTest.Podman([]string{"generate", "kube", vol}) + kube.WaitWithDefaultTimeout() + Expect(kube.ExitCode()).To(Equal(0)) + + pvc := new(v1.PersistentVolumeClaim) + err := yaml.Unmarshal(kube.Out.Contents(), pvc) + Expect(err).To(BeNil()) + Expect(pvc.GetName()).To(Equal(vol)) + Expect(pvc.Spec.AccessModes[0]).To(Equal(v1.ReadWriteOnce)) + Expect(pvc.Spec.Resources.Requests.Storage().String()).To(Equal("1Gi")) + + for k, v := range pvc.GetAnnotations() { + switch k { + case util.VolumeDeviceAnnotation: + Expect(v).To(Equal(volDevice)) + case util.VolumeTypeAnnotation: + Expect(v).To(Equal(volType)) + case util.VolumeMountOptsAnnotation: + Expect(v).To(Equal(volOpts)) + } + } + }) }) diff --git a/test/e2e/play_kube_test.go b/test/e2e/play_kube_test.go index 93c8426a7..41afd9f75 100644 --- a/test/e2e/play_kube_test.go +++ b/test/e2e/play_kube_test.go @@ -10,6 +10,7 @@ import ( "strings" "text/template" + "github.com/containers/podman/v3/pkg/util" . "github.com/containers/podman/v3/test/utils" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -83,6 +84,26 @@ data: {{ end }} ` +var persistentVolumeClaimYamlTemplate = ` +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ .Name }} +{{ with .Annotations }} + annotations: + {{ range $key, $value := . }} + {{ $key }}: {{ $value }} + {{ end }} +{{ end }} +spec: + accessModes: + - "ReadWriteOnce" + resources: + requests: + storage: "1Gi" + storageClassName: default +` + var podYamlTemplate = ` apiVersion: v1 kind: Pod @@ -337,10 +358,31 @@ spec: privileged: false readOnlyRootFilesystem: false workingDir: / + volumeMounts: + {{ if .VolumeMount }} + - name: {{.VolumeName}} + mountPath: {{ .VolumeMountPath }} + readonly: {{.VolumeReadOnly}} + {{ end }} {{ end }} {{ end }} {{ end }} - {{ end }} + {{ with .Volumes }} + volumes: + {{ range . }} + - name: {{ .Name }} + {{- if (eq .VolumeType "HostPath") }} + hostPath: + path: {{ .HostPath.Path }} + type: {{ .HostPath.Type }} + {{- end }} + {{- if (eq .VolumeType "PersistentVolumeClaim") }} + persistentVolumeClaim: + claimName: {{ .PersistentVolumeClaim.ClaimName }} + {{- end }} + {{ end }} + {{ end }} +{{ end }} ` var ( @@ -352,6 +394,7 @@ var ( defaultVolName = "testVol" defaultDeploymentName = "testDeployment" defaultConfigMapName = "testConfigMap" + defaultPVCName = "testPVC" seccompPwdEPERM = []byte(`{"defaultAction":"SCMP_ACT_ALLOW","syscalls":[{"name":"getcwd","action":"SCMP_ACT_ERRNO"}]}`) // CPU Period in ms defaultCPUPeriod = 100 @@ -386,6 +429,8 @@ func getKubeYaml(kind string, object interface{}) (string, error) { yamlTemplate = podYamlTemplate case "deployment": yamlTemplate = deploymentYamlTemplate + case "persistentVolumeClaim": + yamlTemplate = persistentVolumeClaimYamlTemplate default: return "", fmt.Errorf("unsupported kubernetes kind") } @@ -467,6 +512,39 @@ func withConfigMapData(k, v string) configMapOption { } } +// PVC describes the options a kube yaml can be configured at persistent volume claim level +type PVC struct { + Name string + Annotations map[string]string +} + +func getPVC(options ...pvcOption) *PVC { + pvc := PVC{ + Name: defaultPVCName, + Annotations: map[string]string{}, + } + + for _, option := range options { + option(&pvc) + } + + return &pvc +} + +type pvcOption func(*PVC) + +func withPVCName(name string) pvcOption { + return func(pvc *PVC) { + pvc.Name = name + } +} + +func withPVCAnnotations(k, v string) pvcOption { + return func(pvc *PVC) { + pvc.Annotations[k] = v + } +} + // Pod describes the options a kube yaml can be configured at pod level type Pod struct { Name string @@ -1941,8 +2019,106 @@ MemoryReservation: {{ .HostConfig.MemoryReservation }}`}) Expect(inspect.OutputToString()).To(Equal("true")) }) + It("podman play kube persistentVolumeClaim", func() { + volName := "myvol" + volDevice := "tmpfs" + volType := "tmpfs" + volOpts := "nodev,noexec" + + pvc := getPVC(withPVCName(volName), + withPVCAnnotations(util.VolumeDeviceAnnotation, volDevice), + withPVCAnnotations(util.VolumeTypeAnnotation, volType), + withPVCAnnotations(util.VolumeMountOptsAnnotation, volOpts)) + err = generateKubeYaml("persistentVolumeClaim", pvc, kubeYaml) + Expect(err).To(BeNil()) + + kube := podmanTest.Podman([]string{"play", "kube", kubeYaml}) + kube.WaitWithDefaultTimeout() + Expect(kube.ExitCode()).To(Equal(0)) + + inspect := podmanTest.Podman([]string{"inspect", volName, "--format", ` +Name: {{ .Name }} +Device: {{ .Options.device }} +Type: {{ .Options.type }} +o: {{ .Options.o }}`}) + inspect.WaitWithDefaultTimeout() + Expect(inspect.ExitCode()).To(Equal(0)) + Expect(inspect.OutputToString()).To(ContainSubstring("Name: " + volName)) + Expect(inspect.OutputToString()).To(ContainSubstring("Device: " + volDevice)) + Expect(inspect.OutputToString()).To(ContainSubstring("Type: " + volType)) + Expect(inspect.OutputToString()).To(ContainSubstring("o: " + volOpts)) + }) + // Multi doc related tests - It("podman play kube multi doc yaml", func() { + It("podman play kube multi doc yaml with persistentVolumeClaim, service and deployment", func() { + yamlDocs := []string{} + + serviceTemplate := `apiVersion: v1 +kind: Service +metadata: + name: %s +spec: + ports: + - port: 80 + protocol: TCP + targetPort: 9376 + selector: + app: %s +` + // generate persistentVolumeClaim + volName := "multiFoo" + pvc := getPVC(withPVCName(volName)) + + // generate deployment + deploymentName := "multiFoo" + podName := "multiFoo" + ctrName := "ctr-01" + ctr := getCtr(withVolumeMount("/test", false)) + ctr.Name = ctrName + pod := getPod(withPodName(podName), withVolume(getPersistentVolumeClaimVolume(volName)), withCtr(ctr)) + deployment := getDeployment(withPod(pod)) + deployment.Name = deploymentName + + // add pvc + k, err := getKubeYaml("persistentVolumeClaim", pvc) + Expect(err).To(BeNil()) + yamlDocs = append(yamlDocs, k) + + // add service + yamlDocs = append(yamlDocs, fmt.Sprintf(serviceTemplate, deploymentName, deploymentName)) + + // add deployment + k, err = getKubeYaml("deployment", deployment) + Expect(err).To(BeNil()) + yamlDocs = append(yamlDocs, k) + + // generate multi doc yaml + err = generateMultiDocKubeYaml(yamlDocs, kubeYaml) + Expect(err).To(BeNil()) + + kube := podmanTest.Podman([]string{"play", "kube", kubeYaml}) + kube.WaitWithDefaultTimeout() + Expect(kube.ExitCode()).To(Equal(0)) + + inspectVolume := podmanTest.Podman([]string{"inspect", volName, "--format", "'{{ .Name }}'"}) + inspectVolume.WaitWithDefaultTimeout() + Expect(inspectVolume.ExitCode()).To(Equal(0)) + Expect(inspectVolume.OutputToString()).To(ContainSubstring(volName)) + + inspectPod := podmanTest.Podman([]string{"inspect", podName + "-pod-0", "--format", "'{{ .State }}'"}) + inspectPod.WaitWithDefaultTimeout() + Expect(inspectPod.ExitCode()).To(Equal(0)) + Expect(inspectPod.OutputToString()).To(ContainSubstring(`Running`)) + + inspectMounts := podmanTest.Podman([]string{"inspect", podName + "-pod-0-" + ctrName, "--format", "{{ (index .Mounts 0).Type }}:{{ (index .Mounts 0).Name }}"}) + inspectMounts.WaitWithDefaultTimeout() + Expect(inspectMounts.ExitCode()).To(Equal(0)) + + correct := fmt.Sprintf("volume:%s", volName) + Expect(inspectMounts.OutputToString()).To(Equal(correct)) + }) + + It("podman play kube multi doc yaml with multiple services, pods and deployments", func() { yamlDocs := []string{} podNames := []string{} @@ -1958,7 +2134,7 @@ spec: selector: app: %s ` - // generate servies, pods and deployments + // generate services, pods and deployments for i := 0; i < 2; i++ { podName := fmt.Sprintf("testPod%d", i) deploymentName := fmt.Sprintf("testDeploy%d", i) |