diff options
-rw-r--r-- | cmd/podman/system/unshare.go | 21 | ||||
-rw-r--r-- | docs/source/markdown/podman-generate-kube.1.md | 3 | ||||
-rw-r--r-- | docs/source/markdown/podman-play-kube.1.md | 2 | ||||
-rw-r--r-- | docs/source/markdown/podman-unshare.1.md | 29 | ||||
-rw-r--r-- | go.mod | 2 | ||||
-rw-r--r-- | go.sum | 3 | ||||
-rw-r--r-- | libpod/container.go | 5 | ||||
-rw-r--r-- | libpod/kube.go | 33 | ||||
-rw-r--r-- | pkg/domain/entities/play.go | 2 | ||||
-rw-r--r-- | pkg/domain/infra/abi/play.go | 190 | ||||
-rw-r--r-- | pkg/specgen/generate/kube/kube.go | 5 | ||||
-rw-r--r-- | test/e2e/generate_kube_test.go | 72 | ||||
-rw-r--r-- | test/e2e/play_kube_test.go | 54 | ||||
-rw-r--r-- | test/e2e/unshare_test.go | 35 | ||||
-rw-r--r-- | vendor/github.com/opencontainers/selinux/go-selinux/selinux.go | 4 | ||||
-rw-r--r-- | vendor/github.com/opencontainers/selinux/go-selinux/selinux_linux.go | 111 | ||||
-rw-r--r-- | vendor/github.com/opencontainers/selinux/go-selinux/selinux_stub.go | 6 | ||||
-rw-r--r-- | vendor/modules.txt | 2 |
18 files changed, 457 insertions, 122 deletions
diff --git a/cmd/podman/system/unshare.go b/cmd/podman/system/unshare.go index f930d8b62..50230609e 100644 --- a/cmd/podman/system/unshare.go +++ b/cmd/podman/system/unshare.go @@ -2,6 +2,7 @@ package system import ( "os" + "os/exec" "github.com/containers/common/pkg/completion" "github.com/containers/podman/v3/cmd/podman/registry" @@ -50,5 +51,23 @@ func unshare(cmd *cobra.Command, args []string) error { args = []string{shell} } - return registry.ContainerEngine().Unshare(registry.Context(), args, unshareOptions) + err := registry.ContainerEngine().Unshare(registry.Context(), args, unshareOptions) + if err != nil { + if exitError, ok := err.(*exec.ExitError); ok { + // the user command inside the unshare env has failed + // we set the exit code, do not return the error to the user + // otherwise "exit status X" will be printed + registry.SetExitCode(exitError.ExitCode()) + return nil + } + // cmd.Run() can return fs.ErrNotExist, fs.ErrPermission or exec.ErrNotFound + // follow podman run/exec standard with the exit codes + if errors.Is(err, os.ErrNotExist) || errors.Is(err, exec.ErrNotFound) { + registry.SetExitCode(127) + } else if errors.Is(err, os.ErrPermission) { + registry.SetExitCode(126) + } + return err + } + return nil } diff --git a/docs/source/markdown/podman-generate-kube.1.md b/docs/source/markdown/podman-generate-kube.1.md index 2e00b58b6..2e9f68bf3 100644 --- a/docs/source/markdown/podman-generate-kube.1.md +++ b/docs/source/markdown/podman-generate-kube.1.md @@ -16,6 +16,9 @@ Volumes appear in the generated YAML according to two different volume types. Bi Potential name conflicts between volumes are avoided by using a standard naming scheme for each volume type. The *hostPath* volume types are named according to the path on the host machine, replacing forward slashes with hyphens less any leading and trailing forward slashes. The special case of the filesystem root, `/`, translates to the name `root`. Additionally, the name is suffixed with `-host` to avoid naming conflicts with *persistentVolumeClaim* volumes. Each *persistentVolumeClaim* volume type uses the name of its associated named volume suffixed with `-pvc`. +Note that if an init container is created with type `once` and the pod has been started, the init container will not show up in the generated kube YAML as `once` type init containers are deleted after they are run. If the pod has only been created and not started, it will be in the generated kube YAML. +Init containers created with type `always` will always be generated in the kube YAML as they are never deleted, even after running to completion. + Note that the generated Kubernetes YAML file can be used to re-run the deployment via podman-play-kube(1). ## OPTIONS diff --git a/docs/source/markdown/podman-play-kube.1.md b/docs/source/markdown/podman-play-kube.1.md index 6af1bde1d..c170d6495 100644 --- a/docs/source/markdown/podman-play-kube.1.md +++ b/docs/source/markdown/podman-play-kube.1.md @@ -20,6 +20,8 @@ Currently, the supported Kubernetes kinds are: 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: When playing a kube YAML with init containers, the init container will be created with init type value `always`. + 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. diff --git a/docs/source/markdown/podman-unshare.1.md b/docs/source/markdown/podman-unshare.1.md index 2e7adfd34..72821b6e5 100644 --- a/docs/source/markdown/podman-unshare.1.md +++ b/docs/source/markdown/podman-unshare.1.md @@ -37,6 +37,35 @@ connect to a rootless container via IP address (CNI networking). This is otherwi not possible from the host network namespace. _Note: Using this option with more than one unshare session can have unexpected results._ +## Exit Codes + +The exit code from `podman unshare` gives information about why the container +failed to run or why it exited. When `podman unshare` commands exit with a non-zero code, +the exit codes follow the `chroot` standard, see below: + + **125** The error is with podman **_itself_** + + $ podman unshare --foo; echo $? + Error: unknown flag: --foo + 125 + + **126** Executing a _contained command_ and the _command_ cannot be invoked + + $ podman unshare /etc; echo $? + Error: fork/exec /etc: permission denied + 126 + + **127** Executing a _contained command_ and the _command_ cannot be found + + $ podman run busybox foo; echo $? + Error: fork/exec /usr/bin/bogus: no such file or directory + 127 + + **Exit code** _contained command_ exit code + + $ podman run busybox /bin/sh -c 'exit 3'; echo $? + 3 + ## EXAMPLE ``` @@ -50,7 +50,7 @@ require ( github.com/opencontainers/runc v1.0.2 github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 github.com/opencontainers/runtime-tools v0.9.0 - github.com/opencontainers/selinux v1.8.4 + github.com/opencontainers/selinux v1.8.5 github.com/pkg/errors v0.9.1 github.com/pmezard/go-difflib v1.0.0 github.com/rootless-containers/rootlesskit v0.14.5 @@ -756,8 +756,9 @@ github.com/opencontainers/selinux v1.5.1/go.mod h1:yTcKuYAh6R95iDpefGLQaPaRwJFwy github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE= github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= -github.com/opencontainers/selinux v1.8.4 h1:krlgQ6/j9CkCXT5oW0yVXdQFOME3NjKuuAZXuR6O7P4= github.com/opencontainers/selinux v1.8.4/go.mod h1:HTvjPFoGMbpQsG886e3lQwnsRWtE4TC1OF3OUvG9FAo= +github.com/opencontainers/selinux v1.8.5 h1:OkT6bMHOQ1JQQO4ihjQ49sj0+wciDcjziSVTRn8VeTA= +github.com/opencontainers/selinux v1.8.5/go.mod h1:HTvjPFoGMbpQsG886e3lQwnsRWtE4TC1OF3OUvG9FAo= github.com/openshift/imagebuilder v1.2.2-0.20210415181909-87f3e48c2656 h1:WaxyNFpmIDu4i6so9r6LVFIbSaXqsj8oitMitt86ae4= github.com/openshift/imagebuilder v1.2.2-0.20210415181909-87f3e48c2656/go.mod h1:9aJRczxCH0mvT6XQ+5STAQaPWz7OsWcU5/mRkt8IWeo= github.com/ostreedev/ostree-go v0.0.0-20190702140239-759a8c1ac913 h1:TnbXhKzrTOyuvWrjI8W6pcoI9XPbLHFXCdN2dtUw7Rw= diff --git a/libpod/container.go b/libpod/container.go index 0986a0d80..a4bbb5dd0 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -1062,6 +1062,11 @@ func (c *Container) IsInfra() bool { return c.config.IsInfra } +// IsInitCtr returns whether the container is an init container +func (c *Container) IsInitCtr() bool { + return len(c.config.InitContainerType) > 0 +} + // IsReadOnly returns whether the container is running in read only mode func (c *Container) IsReadOnly() bool { return c.config.Spec.Root.Readonly diff --git a/libpod/kube.go b/libpod/kube.go index fff040adb..812bb101b 100644 --- a/libpod/kube.go +++ b/libpod/kube.go @@ -4,6 +4,7 @@ import ( "fmt" "math/rand" "os" + "sort" "strconv" "strings" "time" @@ -220,8 +221,14 @@ func (p *Pod) podWithContainers(containers []*Container, ports []v1.ContainerPor deDupPodVolumes := make(map[string]*v1.Volume) first := true podContainers := make([]v1.Container, 0, len(containers)) + podInitCtrs := []v1.Container{} podAnnotations := make(map[string]string) dnsInfo := v1.PodDNSConfig{} + + // Let's sort the containers in order of created time + // This will ensure that the init containers are defined in the correct order in the kube yaml + sort.Slice(containers, func(i, j int) bool { return containers[i].CreatedTime().Before(containers[j].CreatedTime()) }) + for _, ctr := range containers { if !ctr.IsInfra() { // Convert auto-update labels into kube annotations @@ -229,6 +236,8 @@ func (p *Pod) podWithContainers(containers []*Container, ports []v1.ContainerPor podAnnotations[k] = v } + isInit := ctr.IsInitCtr() + ctr, volumes, _, err := containerToV1Container(ctr) if err != nil { return nil, err @@ -245,6 +254,10 @@ func (p *Pod) podWithContainers(containers []*Container, ports []v1.ContainerPor ctr.Ports = ports first = false } + if isInit { + podInitCtrs = append(podInitCtrs, ctr) + continue + } podContainers = append(podContainers, ctr) // Deduplicate volumes, so if containers in the pod share a volume, it's only // listed in the volumes section once @@ -278,13 +291,14 @@ func (p *Pod) podWithContainers(containers []*Container, ports []v1.ContainerPor return newPodObject( p.Name(), podAnnotations, + podInitCtrs, podContainers, podVolumes, &dnsInfo, hostNetwork), nil } -func newPodObject(podName string, annotations map[string]string, containers []v1.Container, volumes []v1.Volume, dnsOptions *v1.PodDNSConfig, hostNetwork bool) *v1.Pod { +func newPodObject(podName string, annotations map[string]string, initCtrs, containers []v1.Container, volumes []v1.Volume, dnsOptions *v1.PodDNSConfig, hostNetwork bool) *v1.Pod { tm := v12.TypeMeta{ Kind: "Pod", APIVersion: "v1", @@ -304,9 +318,10 @@ func newPodObject(podName string, annotations map[string]string, containers []v1 Annotations: annotations, } ps := v1.PodSpec{ - Containers: containers, - Volumes: volumes, - HostNetwork: hostNetwork, + Containers: containers, + HostNetwork: hostNetwork, + InitContainers: initCtrs, + Volumes: volumes, } if dnsOptions != nil { ps.DNSConfig = dnsOptions @@ -323,6 +338,7 @@ func newPodObject(podName string, annotations map[string]string, containers []v1 // for a single container. we "insert" that container description in a pod. func simplePodWithV1Containers(ctrs []*Container) (*v1.Pod, error) { kubeCtrs := make([]v1.Container, 0, len(ctrs)) + kubeInitCtrs := []v1.Container{} kubeVolumes := make([]v1.Volume, 0) hostNetwork := true podDNS := v1.PodDNSConfig{} @@ -333,6 +349,8 @@ func simplePodWithV1Containers(ctrs []*Container) (*v1.Pod, error) { kubeAnnotations[k] = v } + isInit := ctr.IsInitCtr() + if !ctr.HostNetwork() { hostNetwork = false } @@ -340,7 +358,11 @@ func simplePodWithV1Containers(ctrs []*Container) (*v1.Pod, error) { if err != nil { return nil, err } - kubeCtrs = append(kubeCtrs, kubeCtr) + if isInit { + kubeInitCtrs = append(kubeInitCtrs, kubeCtr) + } else { + kubeCtrs = append(kubeCtrs, kubeCtr) + } kubeVolumes = append(kubeVolumes, kubeVols...) // Combine DNS information in sum'd structure @@ -379,6 +401,7 @@ func simplePodWithV1Containers(ctrs []*Container) (*v1.Pod, error) { return newPodObject( strings.ReplaceAll(ctrs[0].Name(), "_", ""), kubeAnnotations, + kubeInitCtrs, kubeCtrs, kubeVolumes, &podDNS, diff --git a/pkg/domain/entities/play.go b/pkg/domain/entities/play.go index 77329e328..f630b3f24 100644 --- a/pkg/domain/entities/play.go +++ b/pkg/domain/entities/play.go @@ -51,6 +51,8 @@ type PlayKubePod struct { ID string // Containers - the IDs of the containers running in the created pod. Containers []string + // InitContainers - the IDs of the init containers to be run in the created pod. + InitContainers []string // Logs - non-fatal errors and log messages while processing. Logs []string // ContainerErrors - any errors that occurred while starting containers diff --git a/pkg/domain/infra/abi/play.go b/pkg/domain/infra/abi/play.go index c6d5dcc3d..87506f70c 100644 --- a/pkg/domain/infra/abi/play.go +++ b/pkg/domain/infra/abi/play.go @@ -304,76 +304,59 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY } containers := make([]*libpod.Container, 0, len(podYAML.Spec.Containers)) + initContainers := make([]*libpod.Container, 0, len(podYAML.Spec.InitContainers)) cwd, err := os.Getwd() if err != nil { return nil, err } + + for _, initCtr := range podYAML.Spec.InitContainers { + // Init containers cannot have either of lifecycle, livenessProbe, readinessProbe, or startupProbe set + if initCtr.Lifecycle != nil || initCtr.LivenessProbe != nil || initCtr.ReadinessProbe != nil || initCtr.StartupProbe != nil { + return nil, errors.Errorf("cannot create an init container that has either of lifecycle, livenessProbe, readinessProbe, or startupProbe set") + } + pulledImage, labels, err := ic.getImageAndLabelInfo(ctx, cwd, annotations, writer, initCtr, options) + if err != nil { + return nil, err + } + + specgenOpts := kube.CtrSpecGenOptions{ + Container: initCtr, + Image: pulledImage, + Volumes: volumes, + PodID: pod.ID(), + PodName: podName, + PodInfraID: podInfraID, + ConfigMaps: configMaps, + SeccompPaths: seccompPaths, + RestartPolicy: ctrRestartPolicy, + NetNSIsHost: p.NetNS.IsHost(), + SecretsManager: secretsManager, + LogDriver: options.LogDriver, + Labels: labels, + InitContainerType: define.AlwaysInitContainer, + } + specGen, err := kube.ToSpecGen(ctx, &specgenOpts) + if err != nil { + return nil, err + } + rtSpec, spec, opts, err := generate.MakeContainer(ctx, ic.Libpod, specGen) + if err != nil { + return nil, err + } + ctr, err := generate.ExecuteCreate(ctx, ic.Libpod, rtSpec, spec, false, opts...) + if err != nil { + return nil, err + } + + initContainers = append(initContainers, ctr) + } for _, container := range podYAML.Spec.Containers { if !strings.Contains("infra", container.Name) { - // Contains all labels obtained from kube - labels := make(map[string]string) - var pulledImage *libimage.Image - buildFile, err := getBuildFile(container.Image, cwd) + pulledImage, labels, err := ic.getImageAndLabelInfo(ctx, cwd, annotations, writer, container, options) if err != nil { return nil, err } - existsLocally, err := ic.Libpod.LibimageRuntime().Exists(container.Image) - if err != nil { - return nil, err - } - if (len(buildFile) > 0 && !existsLocally) || (len(buildFile) > 0 && options.Build) { - buildOpts := new(buildahDefine.BuildOptions) - commonOpts := new(buildahDefine.CommonBuildOptions) - buildOpts.ConfigureNetwork = buildahDefine.NetworkDefault - buildOpts.Isolation = buildahDefine.IsolationChroot - buildOpts.CommonBuildOpts = commonOpts - buildOpts.Output = container.Image - if _, _, err := ic.Libpod.Build(ctx, *buildOpts, []string{buildFile}...); err != nil { - return nil, err - } - i, _, err := ic.Libpod.LibimageRuntime().LookupImage(container.Image, new(libimage.LookupImageOptions)) - if err != nil { - return nil, err - } - pulledImage = i - } else { - // NOTE: set the pull policy to "newer". This will cover cases - // where the "latest" tag requires a pull and will also - // transparently handle "localhost/" prefixed files which *may* - // refer to a locally built image OR an image running a - // registry on localhost. - pullPolicy := config.PullPolicyNewer - if len(container.ImagePullPolicy) > 0 { - // Make sure to lower the strings since K8s pull policy - // may be capitalized (see bugzilla.redhat.com/show_bug.cgi?id=1985905). - rawPolicy := string(container.ImagePullPolicy) - pullPolicy, err = config.ParsePullPolicy(strings.ToLower(rawPolicy)) - if err != nil { - return nil, err - } - } - pulledImages, err := pullImage(ic, writer, container.Image, options, pullPolicy) - if err != nil { - return nil, err - } - pulledImage = pulledImages[0] - } - - // Handle kube annotations - for k, v := range annotations { - switch k { - // Auto update annotation without container name will apply to - // all containers within the pod - case autoupdate.Label, autoupdate.AuthfileLabel: - labels[k] = v - // Auto update annotation with container name will apply only - // to the specified container - case fmt.Sprintf("%s/%s", autoupdate.Label, container.Name), - fmt.Sprintf("%s/%s", autoupdate.AuthfileLabel, container.Name): - prefixAndCtr := strings.Split(k, "/") - labels[prefixAndCtr[0]] = v - } - } specgenOpts := kube.CtrSpecGenOptions{ Container: container, @@ -394,7 +377,6 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY if err != nil { return nil, err } - rtSpec, spec, opts, err := generate.MakeContainer(ctx, ic.Libpod, specGen) if err != nil { return nil, err @@ -423,12 +405,96 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY for _, ctr := range containers { playKubePod.Containers = append(playKubePod.Containers, ctr.ID()) } + for _, initCtr := range initContainers { + playKubePod.InitContainers = append(playKubePod.InitContainers, initCtr.ID()) + } report.Pods = append(report.Pods, playKubePod) return &report, nil } +// getImageAndLabelInfo returns the image information and how the image should be pulled plus as well as labels to be used for the container in the pod. +// Moved this to a separate function so that it can be used for both init and regular containers when playing a kube yaml. +func (ic *ContainerEngine) getImageAndLabelInfo(ctx context.Context, cwd string, annotations map[string]string, writer io.Writer, container v1.Container, options entities.PlayKubeOptions) (*libimage.Image, map[string]string, error) { + // Contains all labels obtained from kube + labels := make(map[string]string) + var pulledImage *libimage.Image + buildFile, err := getBuildFile(container.Image, cwd) + if err != nil { + return nil, nil, err + } + existsLocally, err := ic.Libpod.LibimageRuntime().Exists(container.Image) + if err != nil { + return nil, nil, err + } + if (len(buildFile) > 0 && !existsLocally) || (len(buildFile) > 0 && options.Build) { + buildOpts := new(buildahDefine.BuildOptions) + commonOpts := new(buildahDefine.CommonBuildOptions) + buildOpts.ConfigureNetwork = buildahDefine.NetworkDefault + buildOpts.Isolation = buildahDefine.IsolationChroot + buildOpts.CommonBuildOpts = commonOpts + buildOpts.Output = container.Image + if _, _, err := ic.Libpod.Build(ctx, *buildOpts, []string{buildFile}...); err != nil { + return nil, nil, err + } + i, _, err := ic.Libpod.LibimageRuntime().LookupImage(container.Image, new(libimage.LookupImageOptions)) + if err != nil { + return nil, nil, err + } + pulledImage = i + } else { + // NOTE: set the pull policy to "newer". This will cover cases + // where the "latest" tag requires a pull and will also + // transparently handle "localhost/" prefixed files which *may* + // refer to a locally built image OR an image running a + // registry on localhost. + pullPolicy := config.PullPolicyNewer + if len(container.ImagePullPolicy) > 0 { + // Make sure to lower the strings since K8s pull policy + // may be capitalized (see bugzilla.redhat.com/show_bug.cgi?id=1985905). + rawPolicy := string(container.ImagePullPolicy) + pullPolicy, err = config.ParsePullPolicy(strings.ToLower(rawPolicy)) + if err != nil { + return nil, nil, err + } + } + // This ensures the image is the image store + pullOptions := &libimage.PullOptions{} + pullOptions.AuthFilePath = options.Authfile + pullOptions.CertDirPath = options.CertDir + pullOptions.SignaturePolicyPath = options.SignaturePolicy + pullOptions.Writer = writer + pullOptions.Username = options.Username + pullOptions.Password = options.Password + pullOptions.InsecureSkipTLSVerify = options.SkipTLSVerify + + pulledImages, err := ic.Libpod.LibimageRuntime().Pull(ctx, container.Image, pullPolicy, pullOptions) + if err != nil { + return nil, nil, err + } + pulledImage = pulledImages[0] + } + + // Handle kube annotations + for k, v := range annotations { + switch k { + // Auto update annotation without container name will apply to + // all containers within the pod + case autoupdate.Label, autoupdate.AuthfileLabel: + labels[k] = v + // Auto update annotation with container name will apply only + // to the specified container + case fmt.Sprintf("%s/%s", autoupdate.Label, container.Name), + fmt.Sprintf("%s/%s", autoupdate.AuthfileLabel, container.Name): + prefixAndCtr := strings.Split(k, "/") + labels[prefixAndCtr[0]] = v + } + } + + return pulledImage, labels, 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 diff --git a/pkg/specgen/generate/kube/kube.go b/pkg/specgen/generate/kube/kube.go index 5188abc3a..c01d7a1f0 100644 --- a/pkg/specgen/generate/kube/kube.go +++ b/pkg/specgen/generate/kube/kube.go @@ -114,6 +114,9 @@ type CtrSpecGenOptions struct { Labels map[string]string // IsInfra bool + // InitContainerType sets what type the init container is + // Note: When playing a kube yaml, the inti container type will be set to "always" only + InitContainerType string } func ToSpecGen(ctx context.Context, opts *CtrSpecGenOptions) (*specgen.SpecGenerator, error) { @@ -135,6 +138,8 @@ func ToSpecGen(ctx context.Context, opts *CtrSpecGenOptions) (*specgen.SpecGener Driver: opts.LogDriver, } + s.InitContainerType = opts.InitContainerType + setupSecurityContext(s, opts.Container) err := setupLivenessProbe(s, opts.Container, opts.RestartPolicy) if err != nil { diff --git a/test/e2e/generate_kube_test.go b/test/e2e/generate_kube_test.go index 8996bf6a0..bf89a0708 100644 --- a/test/e2e/generate_kube_test.go +++ b/test/e2e/generate_kube_test.go @@ -174,6 +174,78 @@ var _ = Describe("Podman generate kube", func() { Expect(string(kube.Out.Contents())).To(ContainSubstring(`name: pod2`)) }) + It("podman generate kube on pod with init containers", func() { + session := podmanTest.Podman([]string{"create", "--pod", "new:toppod", "--init-ctr", "always", ALPINE, "echo", "hello"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + session = podmanTest.Podman([]string{"create", "--pod", "toppod", "--init-ctr", "always", ALPINE, "echo", "world"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + session = podmanTest.Podman([]string{"create", "--pod", "toppod", ALPINE, "top"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + kube := podmanTest.Podman([]string{"generate", "kube", "toppod"}) + kube.WaitWithDefaultTimeout() + Expect(kube).Should(Exit(0)) + + pod := new(v1.Pod) + err := yaml.Unmarshal(kube.Out.Contents(), pod) + Expect(err).To(BeNil()) + Expect(pod.Spec.HostNetwork).To(Equal(false)) + + numContainers := len(pod.Spec.Containers) + len(pod.Spec.InitContainers) + Expect(numContainers).To(Equal(3)) + + // Init container should be in the generated kube yaml if created with "once" type and the pod has not been started + session = podmanTest.Podman([]string{"create", "--pod", "new:toppod-2", "--init-ctr", "once", ALPINE, "echo", "using once type"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + session = podmanTest.Podman([]string{"create", "--pod", "toppod-2", ALPINE, "top"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + kube = podmanTest.Podman([]string{"generate", "kube", "toppod-2"}) + kube.WaitWithDefaultTimeout() + Expect(kube).Should(Exit(0)) + + pod = new(v1.Pod) + err = yaml.Unmarshal(kube.Out.Contents(), pod) + Expect(err).To(BeNil()) + Expect(pod.Spec.HostNetwork).To(Equal(false)) + + numContainers = len(pod.Spec.Containers) + len(pod.Spec.InitContainers) + Expect(numContainers).To(Equal(2)) + + // Init container should not be in the generated kube yaml if created with "once" type and the pod has been started + session = podmanTest.Podman([]string{"create", "--pod", "new:toppod-3", "--init-ctr", "once", ALPINE, "echo", "using once type"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + session = podmanTest.Podman([]string{"create", "--pod", "toppod-3", ALPINE, "top"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + session = podmanTest.Podman([]string{"pod", "start", "toppod-3"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + kube = podmanTest.Podman([]string{"generate", "kube", "toppod-3"}) + kube.WaitWithDefaultTimeout() + Expect(kube).Should(Exit(0)) + + pod = new(v1.Pod) + err = yaml.Unmarshal(kube.Out.Contents(), pod) + Expect(err).To(BeNil()) + Expect(pod.Spec.HostNetwork).To(Equal(false)) + + numContainers = len(pod.Spec.Containers) + len(pod.Spec.InitContainers) + Expect(numContainers).To(Equal(1)) + }) + It("podman generate kube on pod with host network", func() { podSession := podmanTest.Podman([]string{"pod", "create", "--name", "testHostNetwork", "--network", "host"}) podSession.WaitWithDefaultTimeout() diff --git a/test/e2e/play_kube_test.go b/test/e2e/play_kube_test.go index 07f2e65f4..fcda89fbc 100644 --- a/test/e2e/play_kube_test.go +++ b/test/e2e/play_kube_test.go @@ -264,6 +264,17 @@ spec: {{ end }} ip: {{ .IP }} {{ end }} + initContainers: +{{ with .InitCtrs }} + {{ range . }} + - command: + {{ range .Cmd }} + - {{.}} + {{ end }} + image: {{ .Image }} + name: {{ .Name }} + {{ end }} +{{ end }} containers: {{ with .Ctrs }} {{ range . }} @@ -666,6 +677,7 @@ type Pod struct { HostNetwork bool HostAliases []HostAlias Ctrs []*Ctr + InitCtrs []*Ctr Volumes []*Volume Labels map[string]string Annotations map[string]string @@ -687,6 +699,7 @@ func getPod(options ...podOption) *Pod { HostNetwork: false, HostAliases: nil, Ctrs: make([]*Ctr, 0), + InitCtrs: make([]*Ctr, 0), Volumes: make([]*Volume, 0), Labels: make(map[string]string), Annotations: make(map[string]string), @@ -729,6 +742,12 @@ func withCtr(c *Ctr) podOption { } } +func withPodInitCtr(ic *Ctr) podOption { + return func(pod *Pod) { + pod.InitCtrs = append(pod.InitCtrs, ic) + } +} + func withRestartPolicy(policy string) podOption { return func(pod *Pod) { pod.RestartPolicy = policy @@ -848,6 +867,7 @@ type Ctr struct { VolumeReadOnly bool Env []Env EnvFrom []EnvFrom + InitCtrType string } // getCtr takes a list of ctrOptions and returns a Ctr with sane defaults @@ -871,6 +891,7 @@ func getCtr(options ...ctrOption) *Ctr { VolumeReadOnly: false, Env: []Env{}, EnvFrom: []EnvFrom{}, + InitCtrType: "", } for _, option := range options { option(&c) @@ -886,6 +907,12 @@ func withName(name string) ctrOption { } } +func withInitCtr() ctrOption { + return func(c *Ctr) { + c.InitCtrType = define.AlwaysInitContainer + } +} + func withCmd(cmd []string) ctrOption { return func(c *Ctr) { c.Cmd = cmd @@ -1295,6 +1322,33 @@ var _ = Describe("Podman play kube", func() { Expect(inspect.OutputToString()).To(ContainSubstring(`[]`)) }) + // If you have an init container in the pod yaml, podman should create and run the init container with play kube + It("podman play kube test with init containers", func() { + pod := getPod(withPodInitCtr(getCtr(withImage(ALPINE), withCmd([]string{"echo", "hello"}), withInitCtr(), withName("init-test"))), withCtr(getCtr(withImage(ALPINE), withCmd([]string{"top"})))) + err := generateKubeYaml("pod", pod, kubeYaml) + Expect(err).To(BeNil()) + + kube := podmanTest.Podman([]string{"play", "kube", kubeYaml}) + kube.WaitWithDefaultTimeout() + Expect(kube).Should(Exit(0)) + + // Expect the number of containers created to be 3, one init, infra, and regular container + numOfCtrs := podmanTest.NumberOfContainers() + Expect(numOfCtrs).To(Equal(3)) + + // Init container should have exited after running + inspect := podmanTest.Podman([]string{"inspect", "--format", "{{.State.Status}}", "testPod-init-test"}) + inspect.WaitWithDefaultTimeout() + Expect(inspect).Should(Exit(0)) + Expect(inspect.OutputToString()).To(ContainSubstring("exited")) + + // Regular container should be in running state + inspect = podmanTest.Podman([]string{"inspect", "--format", "{{.State.Status}}", "testPod-" + defaultCtrName}) + inspect.WaitWithDefaultTimeout() + Expect(inspect).Should(Exit(0)) + Expect(inspect.OutputToString()).To(ContainSubstring("running")) + }) + // If you supply only args for a Container, the default Entrypoint defined in the Docker image is run with the args that you supplied. It("podman play kube test correct command with only set args in yaml file", func() { pod := getPod(withCtr(getCtr(withImage(registry), withCmd(nil), withArg([]string{"echo", "hello"})))) diff --git a/test/e2e/unshare_test.go b/test/e2e/unshare_test.go index eacdda68a..79ce68e89 100644 --- a/test/e2e/unshare_test.go +++ b/test/e2e/unshare_test.go @@ -47,8 +47,7 @@ var _ = Describe("Podman unshare", func() { session := podmanTest.Podman([]string{"unshare", "readlink", "/proc/self/ns/user"}) session.WaitWithDefaultTimeout() Expect(session).Should(Exit(0)) - ok, _ := session.GrepString(userNS) - Expect(ok).To(BeFalse()) + Expect(session.OutputToString()).ToNot(ContainSubstring(userNS)) }) It("podman unshare --rootles-cni", func() { @@ -57,4 +56,36 @@ var _ = Describe("Podman unshare", func() { Expect(session).Should(Exit(0)) Expect(session.OutputToString()).To(ContainSubstring("tap0")) }) + + It("podman unshare exit codes", func() { + session := podmanTest.Podman([]string{"unshare", "false"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(1)) + Expect(session.OutputToString()).Should(Equal("")) + Expect(session.ErrorToString()).Should(Equal("")) + + session = podmanTest.Podman([]string{"unshare", "/usr/bin/bogus"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(127)) + Expect(session.OutputToString()).Should(Equal("")) + Expect(session.ErrorToString()).Should(ContainSubstring("no such file or directory")) + + session = podmanTest.Podman([]string{"unshare", "bogus"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(127)) + Expect(session.OutputToString()).Should(Equal("")) + Expect(session.ErrorToString()).Should(ContainSubstring("executable file not found in $PATH")) + + session = podmanTest.Podman([]string{"unshare", "/usr"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(126)) + Expect(session.OutputToString()).Should(Equal("")) + Expect(session.ErrorToString()).Should(ContainSubstring("permission denied")) + + session = podmanTest.Podman([]string{"unshare", "--bogus"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(125)) + Expect(session.OutputToString()).Should(Equal("")) + Expect(session.ErrorToString()).Should(ContainSubstring("unknown flag: --bogus")) + }) }) diff --git a/vendor/github.com/opencontainers/selinux/go-selinux/selinux.go b/vendor/github.com/opencontainers/selinux/go-selinux/selinux.go index 9ffd77afa..0eedcaa78 100644 --- a/vendor/github.com/opencontainers/selinux/go-selinux/selinux.go +++ b/vendor/github.com/opencontainers/selinux/go-selinux/selinux.go @@ -38,6 +38,8 @@ var ( // CategoryRange allows the upper bound on the category range to be adjusted CategoryRange = DefaultCategoryRange + + privContainerMountLabel string ) // Context is a representation of the SELinux label broken into 4 parts @@ -280,5 +282,7 @@ func GetDefaultContextWithLevel(user, level, scon string) (string, error) { // PrivContainerMountLabel returns mount label for privileged containers func PrivContainerMountLabel() string { + // Make sure label is initialized. + _ = label("") return privContainerMountLabel } diff --git a/vendor/github.com/opencontainers/selinux/go-selinux/selinux_linux.go b/vendor/github.com/opencontainers/selinux/go-selinux/selinux_linux.go index a804473e4..295b2bc4e 100644 --- a/vendor/github.com/opencontainers/selinux/go-selinux/selinux_linux.go +++ b/vendor/github.com/opencontainers/selinux/go-selinux/selinux_linux.go @@ -12,7 +12,6 @@ import ( "os" "path" "path/filepath" - "regexp" "strconv" "strings" "sync" @@ -34,8 +33,6 @@ const ( xattrNameSelinux = "security.selinux" ) -var policyRoot = filepath.Join(selinuxDir, readConfig(selinuxTypeTag)) - type selinuxState struct { enabledSet bool enabled bool @@ -70,7 +67,6 @@ const ( ) var ( - assignRegex = regexp.MustCompile(`^([^=]+)=(.*)$`) readOnlyFileLabel string state = selinuxState{ mcsList: make(map[string]bool), @@ -79,8 +75,24 @@ var ( // for attrPath() attrPathOnce sync.Once haveThreadSelf bool + + // for policyRoot() + policyRootOnce sync.Once + policyRootVal string + + // for label() + loadLabelsOnce sync.Once + labels map[string]string ) +func policyRoot() string { + policyRootOnce.Do(func() { + policyRootVal = filepath.Join(selinuxDir, readConfig(selinuxTypeTag)) + }) + + return policyRootVal +} + func (s *selinuxState) setEnable(enabled bool) bool { s.Lock() defer s.Unlock() @@ -222,7 +234,7 @@ func readConfig(target string) string { scanner := bufio.NewScanner(in) for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) + line := bytes.TrimSpace(scanner.Bytes()) if len(line) == 0 { // Skip blank lines continue @@ -231,11 +243,12 @@ func readConfig(target string) string { // Skip comments continue } - if groups := assignRegex.FindStringSubmatch(line); groups != nil { - key, val := strings.TrimSpace(groups[1]), strings.TrimSpace(groups[2]) - if key == target { - return strings.Trim(val, "\"") - } + fields := bytes.SplitN(line, []byte{'='}, 2) + if len(fields) != 2 { + continue + } + if bytes.Equal(fields[0], []byte(target)) { + return string(bytes.Trim(fields[1], `"`)) } } return "" @@ -274,12 +287,15 @@ func readCon(fpath string) (string, error) { if err := isProcHandle(in); err != nil { return "", err } + return readConFd(in) +} - var retval string - if _, err := fmt.Fscanf(in, "%s", &retval); err != nil { +func readConFd(in *os.File) (string, error) { + data, err := ioutil.ReadAll(in) + if err != nil { return "", err } - return strings.Trim(retval, "\x00"), nil + return string(bytes.TrimSuffix(data, []byte{0})), nil } // classIndex returns the int index for an object class in the loaded policy, @@ -389,7 +405,7 @@ func writeCon(fpath, val string) error { _, err = out.Write(nil) } if err != nil { - return &os.PathError{Op: "write", Path: fpath, Err: err} + return err } return nil } @@ -664,11 +680,7 @@ func readWriteCon(fpath string, val string) (string, error) { return "", err } - var retval string - if _, err := fmt.Fscanf(f, "%s", &retval); err != nil { - return "", err - } - return strings.Trim(retval, "\x00"), nil + return readConFd(f) } // setExecLabel sets the SELinux label that the kernel will use for any programs @@ -723,10 +735,10 @@ func keyLabel() (string, error) { // get returns the Context as a string func (c Context) get() string { - if c["level"] != "" { - return fmt.Sprintf("%s:%s:%s:%s", c["user"], c["role"], c["type"], c["level"]) + if level := c["level"]; level != "" { + return c["user"] + ":" + c["role"] + ":" + c["type"] + ":" + level } - return fmt.Sprintf("%s:%s:%s", c["user"], c["role"], c["type"]) + return c["user"] + ":" + c["role"] + ":" + c["type"] } // newContext creates a new Context struct from the specified label @@ -891,24 +903,21 @@ func openContextFile() (*os.File, error) { if f, err := os.Open(contextFile); err == nil { return f, nil } - lxcPath := filepath.Join(policyRoot, "/contexts/lxc_contexts") - return os.Open(lxcPath) + return os.Open(filepath.Join(policyRoot(), "/contexts/lxc_contexts")) } -var labels, privContainerMountLabel = loadLabels() - -func loadLabels() (map[string]string, string) { - labels := make(map[string]string) +func loadLabels() { + labels = make(map[string]string) in, err := openContextFile() if err != nil { - return labels, "" + return } defer in.Close() scanner := bufio.NewScanner(in) for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) + line := bytes.TrimSpace(scanner.Bytes()) if len(line) == 0 { // Skip blank lines continue @@ -917,38 +926,47 @@ func loadLabels() (map[string]string, string) { // Skip comments continue } - if groups := assignRegex.FindStringSubmatch(line); groups != nil { - key, val := strings.TrimSpace(groups[1]), strings.TrimSpace(groups[2]) - labels[key] = strings.Trim(val, "\"") + fields := bytes.SplitN(line, []byte{'='}, 2) + if len(fields) != 2 { + continue } + key, val := bytes.TrimSpace(fields[0]), bytes.TrimSpace(fields[1]) + labels[string(key)] = string(bytes.Trim(val, `"`)) } con, _ := NewContext(labels["file"]) con["level"] = fmt.Sprintf("s0:c%d,c%d", maxCategory-2, maxCategory-1) - reserveLabel(con.get()) - return labels, con.get() + privContainerMountLabel = con.get() + reserveLabel(privContainerMountLabel) +} + +func label(key string) string { + loadLabelsOnce.Do(func() { + loadLabels() + }) + return labels[key] } // kvmContainerLabels returns the default processLabel and mountLabel to be used // for kvm containers by the calling process. func kvmContainerLabels() (string, string) { - processLabel := labels["kvm_process"] + processLabel := label("kvm_process") if processLabel == "" { - processLabel = labels["process"] + processLabel = label("process") } - return addMcs(processLabel, labels["file"]) + return addMcs(processLabel, label("file")) } // initContainerLabels returns the default processLabel and file labels to be // used for containers running an init system like systemd by the calling process. func initContainerLabels() (string, string) { - processLabel := labels["init_process"] + processLabel := label("init_process") if processLabel == "" { - processLabel = labels["process"] + processLabel = label("process") } - return addMcs(processLabel, labels["file"]) + return addMcs(processLabel, label("file")) } // containerLabels returns an allocated processLabel and fileLabel to be used for @@ -958,9 +976,9 @@ func containerLabels() (processLabel string, fileLabel string) { return "", "" } - processLabel = labels["process"] - fileLabel = labels["file"] - readOnlyFileLabel = labels["ro_file"] + processLabel = label("process") + fileLabel = label("file") + readOnlyFileLabel = label("ro_file") if processLabel == "" || fileLabel == "" { return "", fileLabel @@ -1180,15 +1198,14 @@ func getDefaultContextFromReaders(c *defaultSECtx) (string, error) { } func getDefaultContextWithLevel(user, level, scon string) (string, error) { - userPath := filepath.Join(policyRoot, selinuxUsersDir, user) - defaultPath := filepath.Join(policyRoot, defaultContexts) - + userPath := filepath.Join(policyRoot(), selinuxUsersDir, user) fu, err := os.Open(userPath) if err != nil { return "", err } defer fu.Close() + defaultPath := filepath.Join(policyRoot(), defaultContexts) fd, err := os.Open(defaultPath) if err != nil { return "", err diff --git a/vendor/github.com/opencontainers/selinux/go-selinux/selinux_stub.go b/vendor/github.com/opencontainers/selinux/go-selinux/selinux_stub.go index b7218a0b6..42657759c 100644 --- a/vendor/github.com/opencontainers/selinux/go-selinux/selinux_stub.go +++ b/vendor/github.com/opencontainers/selinux/go-selinux/selinux_stub.go @@ -2,8 +2,6 @@ package selinux -const privContainerMountLabel = "" - func setDisabled() { } @@ -152,3 +150,7 @@ func disableSecOpt() []string { func getDefaultContextWithLevel(user, level, scon string) (string, error) { return "", nil } + +func label(_ string) string { + return "" +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 35409a743..0edf27275 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -524,7 +524,7 @@ github.com/opencontainers/runtime-tools/generate github.com/opencontainers/runtime-tools/generate/seccomp github.com/opencontainers/runtime-tools/specerror github.com/opencontainers/runtime-tools/validate -# github.com/opencontainers/selinux v1.8.4 +# github.com/opencontainers/selinux v1.8.5 github.com/opencontainers/selinux/go-selinux github.com/opencontainers/selinux/go-selinux/label github.com/opencontainers/selinux/pkg/pwalk |