Skip to content

Commit

Permalink
KEP-3857: Recursive Read-only (RRO) mounts
Browse files Browse the repository at this point in the history
See kubernetes/enhancements issue 3857 (PR 3858)

Signed-off-by: Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
  • Loading branch information
AkihiroSuda committed Feb 29, 2024
1 parent 3a1f73d commit 46a3ed4
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 6 deletions.
2 changes: 1 addition & 1 deletion cmd/crictl/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -942,7 +942,7 @@ func ContainerStatus(client internalapi.RuntimeService, id, output string, tmplS

switch output {
case "json", "yaml", "go-template":
return outputStatusInfo(status, r.Info, output, tmplStr)
return outputStatusInfo(status, "", r.Info, output, tmplStr)
case "table": // table output is after this switch block
default:
return fmt.Errorf("output option cannot be %s", output)
Expand Down
4 changes: 2 additions & 2 deletions cmd/crictl/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ var imageStatusCommand = &cli.Command{
}
switch output {
case "json", "yaml", "go-template":
if err := outputStatusInfo(status, r.Info, output, tmplStr); err != nil {
if err := outputStatusInfo(status, "", r.Info, output, tmplStr); err != nil {
return fmt.Errorf("output status for %q: %w", id, err)
}
continue
Expand Down Expand Up @@ -521,7 +521,7 @@ var imageFsInfoCommand = &cli.Command{

switch output {
case "json", "yaml", "go-template":
if err := outputStatusInfo(status, nil, output, tmplStr); err != nil {
if err := outputStatusInfo(status, "", nil, output, tmplStr); err != nil {
return fmt.Errorf("output filesystem info: %w", err)
}
return nil
Expand Down
7 changes: 6 additions & 1 deletion cmd/crictl/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package main

import (
"context"
"encoding/json"
"fmt"

"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -79,5 +80,9 @@ func Info(cliContext *cli.Context, client internalapi.RuntimeService) error {
if err != nil {
return err
}
return outputStatusInfo(status, r.Info, cliContext.String("output"), cliContext.String("template"))
handlers, err := json.Marshal(r.RuntimeHandlers) // protobufObjectToJSON cannot be used
if err != nil {
return err
}
return outputStatusInfo(status, string(handlers), r.Info, cliContext.String("output"), cliContext.String("template"))
}
2 changes: 1 addition & 1 deletion cmd/crictl/sandbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ func PodSandboxStatus(client internalapi.RuntimeService, id, output string, quie
}
switch output {
case "json", "yaml", "go-template":
return outputStatusInfo(status, r.Info, output, tmplStr)
return outputStatusInfo(status, "", r.Info, output, tmplStr)
case "table": // table output is after this switch block
default:
return fmt.Errorf("output option cannot be %s", output)
Expand Down
5 changes: 4 additions & 1 deletion cmd/crictl/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ func outputProtobufObjAsYAML(obj proto.Message) error {
return nil
}

func outputStatusInfo(status string, info map[string]string, format string, tmplStr string) error {
func outputStatusInfo(status, handlers string, info map[string]string, format string, tmplStr string) error {
// Sort all keys
keys := []string{}
for k := range info {
Expand All @@ -240,6 +240,9 @@ func outputStatusInfo(status string, info map[string]string, format string, tmpl
sort.Strings(keys)

jsonInfo := "{" + "\"status\":" + status + ","
if handlers != "" {
jsonInfo += "\"runtimeHandlers\":" + handlers + ","
}
for _, k := range keys {
var res interface{}
// We attempt to convert key into JSON if possible else use it directly
Expand Down
198 changes: 198 additions & 0 deletions pkg/validate/container_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,3 +307,201 @@ func createOOMKilledContainer(

return containerID
}

var _ = framework.KubeDescribe("Container Mount Readonly", func() {
f := framework.NewDefaultCRIFramework()

var rc internalapi.RuntimeService
var ic internalapi.ImageManagerService
var runtimeHandler string

BeforeEach(func() {
rc = f.CRIClient.CRIRuntimeClient
ic = f.CRIClient.CRIImageClient
runtimeHandler = framework.TestContext.RuntimeHandler
})

Context("runtime should support readonly mounts", func() {
var podID string
var podConfig *runtimeapi.PodSandboxConfig

BeforeEach(func() {
podID, podConfig = createPrivilegedPodSandbox(rc, true)
})

AfterEach(func() {
By("stop PodSandbox")
rc.StopPodSandbox(context.TODO(), podID)
By("delete PodSandbox")
rc.RemovePodSandbox(context.TODO(), podID)
})

testRRO := func(rc internalapi.RuntimeService, ic internalapi.ImageManagerService, rro bool) {
if rro && !runtimeSupportsRRO(rc, runtimeHandler) {
Skip("runtime does not implement recursive readonly mounts")
return
}

By("create host path")
hostPath, clearHostPath := createHostPathForRROMount(podID)
defer clearHostPath() // clean up the TempDir

By("create container with volume")
containerID := createRROMountContainer(rc, ic, podID, podConfig, hostPath, "/mnt", rro)

By("test start container with volume")
testStartContainer(rc, containerID)

By("check whether `touch /mnt/tmpfs/file` succeeds")
command := []string{"touch", "/mnt/tmpfs/file"}
if rro {
command = []string{"sh", "-c", `touch /mnt/tmpfs/foo 2>&1 | grep -q "Read-only file system"`}
}
execSyncContainer(rc, containerID, command)
}

It("should support non-recursive readonly mounts", func() {
testRRO(rc, ic, false)
})
It("should support recursive readonly mounts", func() {
testRRO(rc, ic, true)
})
testRROInvalidPropagation := func(prop runtimeapi.MountPropagation) {
if !runtimeSupportsRRO(rc, runtimeHandler) {
Skip("runtime does not implement recursive readonly mounts")
return
}
hostPath, clearHostPath := createHostPathForRROMount(podID)
defer clearHostPath() // clean up the TempDir
mounts := []*runtimeapi.Mount{
{
HostPath: hostPath,
ContainerPath: "/mnt",
Readonly: true,
RecursiveReadOnly: true,
SelinuxRelabel: true,
Propagation: prop,
},
}
const expectErr = true
createMountContainer(rc, ic, podID, podConfig, mounts, expectErr)
}
It("should reject a recursive readonly mount with PROPAGATION_HOST_TO_CONTAINER", func() {
testRROInvalidPropagation(runtimeapi.MountPropagation_PROPAGATION_HOST_TO_CONTAINER)
})
It("should reject a recursive readonly mount with PROPAGATION_BIDIRECTIONAL", func() {
testRROInvalidPropagation(runtimeapi.MountPropagation_PROPAGATION_BIDIRECTIONAL)
})
It("should reject a recursive readonly mount with ReadOnly: false", func() {
if !runtimeSupportsRRO(rc, runtimeHandler) {
Skip("runtime does not implement recursive readonly mounts")
return
}
hostPath, clearHostPath := createHostPathForRROMount(podID)
defer clearHostPath() // clean up the TempDir
mounts := []*runtimeapi.Mount{
{
HostPath: hostPath,
ContainerPath: "/mnt",
Readonly: false,
RecursiveReadOnly: true,
SelinuxRelabel: true,
},
}
const expectErr = true
createMountContainer(rc, ic, podID, podConfig, mounts, expectErr)
})
})
})

func runtimeSupportsRRO(rc internalapi.RuntimeService, runtimeHandlerName string) bool {
ctx := context.Background()
status, err := rc.Status(ctx, false)
framework.ExpectNoError(err, "failed to check runtime status")
for _, h := range status.RuntimeHandlers {
if h.Name == runtimeHandlerName {
if f := h.Features; f != nil {
return f.RecursiveReadOnlyMounts
}
}
}
return false
}

// createHostPath creates the hostPath for RRO mount test.
//
// hostPath contains a "tmpfs" directory with tmpfs mounted on it.
func createHostPathForRROMount(podID string) (string, func()) {
hostPath, err := os.MkdirTemp("", "test"+podID)
framework.ExpectNoError(err, "failed to create TempDir %q: %v", hostPath, err)

tmpfsMntPoint := filepath.Join(hostPath, "tmpfs")
err = os.MkdirAll(tmpfsMntPoint, 0700)
framework.ExpectNoError(err, "failed to create tmpfs dir %q: %v", tmpfsMntPoint, err)

err = unix.Mount("none", tmpfsMntPoint, "tmpfs", 0, "")
framework.ExpectNoError(err, "failed to mount tmpfs on dir %q: %v", tmpfsMntPoint, err)

clearHostPath := func() {
By("clean up the TempDir")
err := unix.Unmount(tmpfsMntPoint, unix.MNT_DETACH)
framework.ExpectNoError(err, "failed to unmount \"tmpfsMntPoint\": %v", err)
err = os.RemoveAll(hostPath)
framework.ExpectNoError(err, "failed to remove \"hostPath\": %v", err)
}

return hostPath, clearHostPath
}

func createRROMountContainer(
rc internalapi.RuntimeService,
ic internalapi.ImageManagerService,
podID string,
podConfig *runtimeapi.PodSandboxConfig,
hostPath, containerPath string,
rro bool,
) string {
mounts := []*runtimeapi.Mount{
{
HostPath: hostPath,
ContainerPath: containerPath,
Readonly: true,
RecursiveReadOnly: rro,
SelinuxRelabel: true,
},
}
return createMountContainer(rc, ic, podID, podConfig, mounts, false)
}

func createMountContainer(
rc internalapi.RuntimeService,
ic internalapi.ImageManagerService,
podID string,
podConfig *runtimeapi.PodSandboxConfig,
mounts []*runtimeapi.Mount,
expectErr bool,
) string {
By("create a container with volume and name")
containerName := "test-mount-" + framework.NewUUID()
containerConfig := &runtimeapi.ContainerConfig{
Metadata: framework.BuildContainerMetadata(containerName, framework.DefaultAttempt),
Image: &runtimeapi.ImageSpec{Image: framework.TestContext.TestImageList.DefaultTestContainerImage},
Command: pauseCmd,
Mounts: mounts,
}

if expectErr {
_, err := framework.CreateContainerWithError(rc, ic, containerConfig, podID, podConfig)
Expect(err).To(HaveOccurred())
return ""
}

containerID := framework.CreateContainer(rc, ic, containerConfig, podID, podConfig)

By("verifying container status")
resp, err := rc.ContainerStatus(context.TODO(), containerID, true)
framework.ExpectNoError(err, "unable to get container status")
Expect(len(resp.Status.Mounts), len(mounts))

return containerID
}

0 comments on commit 46a3ed4

Please sign in to comment.