Skip to content

Commit 46a3ed4

Browse files
committed
KEP-3857: Recursive Read-only (RRO) mounts
See kubernetes/enhancements issue 3857 (PR 3858) Signed-off-by: Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
1 parent 3a1f73d commit 46a3ed4

File tree

6 files changed

+212
-6
lines changed

6 files changed

+212
-6
lines changed

cmd/crictl/container.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -942,7 +942,7 @@ func ContainerStatus(client internalapi.RuntimeService, id, output string, tmplS
942942

943943
switch output {
944944
case "json", "yaml", "go-template":
945-
return outputStatusInfo(status, r.Info, output, tmplStr)
945+
return outputStatusInfo(status, "", r.Info, output, tmplStr)
946946
case "table": // table output is after this switch block
947947
default:
948948
return fmt.Errorf("output option cannot be %s", output)

cmd/crictl/image.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ var imageStatusCommand = &cli.Command{
323323
}
324324
switch output {
325325
case "json", "yaml", "go-template":
326-
if err := outputStatusInfo(status, r.Info, output, tmplStr); err != nil {
326+
if err := outputStatusInfo(status, "", r.Info, output, tmplStr); err != nil {
327327
return fmt.Errorf("output status for %q: %w", id, err)
328328
}
329329
continue
@@ -521,7 +521,7 @@ var imageFsInfoCommand = &cli.Command{
521521

522522
switch output {
523523
case "json", "yaml", "go-template":
524-
if err := outputStatusInfo(status, nil, output, tmplStr); err != nil {
524+
if err := outputStatusInfo(status, "", nil, output, tmplStr); err != nil {
525525
return fmt.Errorf("output filesystem info: %w", err)
526526
}
527527
return nil

cmd/crictl/info.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package main
1818

1919
import (
2020
"context"
21+
"encoding/json"
2122
"fmt"
2223

2324
"github.com/sirupsen/logrus"
@@ -79,5 +80,9 @@ func Info(cliContext *cli.Context, client internalapi.RuntimeService) error {
7980
if err != nil {
8081
return err
8182
}
82-
return outputStatusInfo(status, r.Info, cliContext.String("output"), cliContext.String("template"))
83+
handlers, err := json.Marshal(r.RuntimeHandlers) // protobufObjectToJSON cannot be used
84+
if err != nil {
85+
return err
86+
}
87+
return outputStatusInfo(status, string(handlers), r.Info, cliContext.String("output"), cliContext.String("template"))
8388
}

cmd/crictl/sandbox.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ func PodSandboxStatus(client internalapi.RuntimeService, id, output string, quie
404404
}
405405
switch output {
406406
case "json", "yaml", "go-template":
407-
return outputStatusInfo(status, r.Info, output, tmplStr)
407+
return outputStatusInfo(status, "", r.Info, output, tmplStr)
408408
case "table": // table output is after this switch block
409409
default:
410410
return fmt.Errorf("output option cannot be %s", output)

cmd/crictl/util.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ func outputProtobufObjAsYAML(obj proto.Message) error {
231231
return nil
232232
}
233233

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

242242
jsonInfo := "{" + "\"status\":" + status + ","
243+
if handlers != "" {
244+
jsonInfo += "\"runtimeHandlers\":" + handlers + ","
245+
}
243246
for _, k := range keys {
244247
var res interface{}
245248
// We attempt to convert key into JSON if possible else use it directly

pkg/validate/container_linux.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,3 +307,201 @@ func createOOMKilledContainer(
307307

308308
return containerID
309309
}
310+
311+
var _ = framework.KubeDescribe("Container Mount Readonly", func() {
312+
f := framework.NewDefaultCRIFramework()
313+
314+
var rc internalapi.RuntimeService
315+
var ic internalapi.ImageManagerService
316+
var runtimeHandler string
317+
318+
BeforeEach(func() {
319+
rc = f.CRIClient.CRIRuntimeClient
320+
ic = f.CRIClient.CRIImageClient
321+
runtimeHandler = framework.TestContext.RuntimeHandler
322+
})
323+
324+
Context("runtime should support readonly mounts", func() {
325+
var podID string
326+
var podConfig *runtimeapi.PodSandboxConfig
327+
328+
BeforeEach(func() {
329+
podID, podConfig = createPrivilegedPodSandbox(rc, true)
330+
})
331+
332+
AfterEach(func() {
333+
By("stop PodSandbox")
334+
rc.StopPodSandbox(context.TODO(), podID)
335+
By("delete PodSandbox")
336+
rc.RemovePodSandbox(context.TODO(), podID)
337+
})
338+
339+
testRRO := func(rc internalapi.RuntimeService, ic internalapi.ImageManagerService, rro bool) {
340+
if rro && !runtimeSupportsRRO(rc, runtimeHandler) {
341+
Skip("runtime does not implement recursive readonly mounts")
342+
return
343+
}
344+
345+
By("create host path")
346+
hostPath, clearHostPath := createHostPathForRROMount(podID)
347+
defer clearHostPath() // clean up the TempDir
348+
349+
By("create container with volume")
350+
containerID := createRROMountContainer(rc, ic, podID, podConfig, hostPath, "/mnt", rro)
351+
352+
By("test start container with volume")
353+
testStartContainer(rc, containerID)
354+
355+
By("check whether `touch /mnt/tmpfs/file` succeeds")
356+
command := []string{"touch", "/mnt/tmpfs/file"}
357+
if rro {
358+
command = []string{"sh", "-c", `touch /mnt/tmpfs/foo 2>&1 | grep -q "Read-only file system"`}
359+
}
360+
execSyncContainer(rc, containerID, command)
361+
}
362+
363+
It("should support non-recursive readonly mounts", func() {
364+
testRRO(rc, ic, false)
365+
})
366+
It("should support recursive readonly mounts", func() {
367+
testRRO(rc, ic, true)
368+
})
369+
testRROInvalidPropagation := func(prop runtimeapi.MountPropagation) {
370+
if !runtimeSupportsRRO(rc, runtimeHandler) {
371+
Skip("runtime does not implement recursive readonly mounts")
372+
return
373+
}
374+
hostPath, clearHostPath := createHostPathForRROMount(podID)
375+
defer clearHostPath() // clean up the TempDir
376+
mounts := []*runtimeapi.Mount{
377+
{
378+
HostPath: hostPath,
379+
ContainerPath: "/mnt",
380+
Readonly: true,
381+
RecursiveReadOnly: true,
382+
SelinuxRelabel: true,
383+
Propagation: prop,
384+
},
385+
}
386+
const expectErr = true
387+
createMountContainer(rc, ic, podID, podConfig, mounts, expectErr)
388+
}
389+
It("should reject a recursive readonly mount with PROPAGATION_HOST_TO_CONTAINER", func() {
390+
testRROInvalidPropagation(runtimeapi.MountPropagation_PROPAGATION_HOST_TO_CONTAINER)
391+
})
392+
It("should reject a recursive readonly mount with PROPAGATION_BIDIRECTIONAL", func() {
393+
testRROInvalidPropagation(runtimeapi.MountPropagation_PROPAGATION_BIDIRECTIONAL)
394+
})
395+
It("should reject a recursive readonly mount with ReadOnly: false", func() {
396+
if !runtimeSupportsRRO(rc, runtimeHandler) {
397+
Skip("runtime does not implement recursive readonly mounts")
398+
return
399+
}
400+
hostPath, clearHostPath := createHostPathForRROMount(podID)
401+
defer clearHostPath() // clean up the TempDir
402+
mounts := []*runtimeapi.Mount{
403+
{
404+
HostPath: hostPath,
405+
ContainerPath: "/mnt",
406+
Readonly: false,
407+
RecursiveReadOnly: true,
408+
SelinuxRelabel: true,
409+
},
410+
}
411+
const expectErr = true
412+
createMountContainer(rc, ic, podID, podConfig, mounts, expectErr)
413+
})
414+
})
415+
})
416+
417+
func runtimeSupportsRRO(rc internalapi.RuntimeService, runtimeHandlerName string) bool {
418+
ctx := context.Background()
419+
status, err := rc.Status(ctx, false)
420+
framework.ExpectNoError(err, "failed to check runtime status")
421+
for _, h := range status.RuntimeHandlers {
422+
if h.Name == runtimeHandlerName {
423+
if f := h.Features; f != nil {
424+
return f.RecursiveReadOnlyMounts
425+
}
426+
}
427+
}
428+
return false
429+
}
430+
431+
// createHostPath creates the hostPath for RRO mount test.
432+
//
433+
// hostPath contains a "tmpfs" directory with tmpfs mounted on it.
434+
func createHostPathForRROMount(podID string) (string, func()) {
435+
hostPath, err := os.MkdirTemp("", "test"+podID)
436+
framework.ExpectNoError(err, "failed to create TempDir %q: %v", hostPath, err)
437+
438+
tmpfsMntPoint := filepath.Join(hostPath, "tmpfs")
439+
err = os.MkdirAll(tmpfsMntPoint, 0700)
440+
framework.ExpectNoError(err, "failed to create tmpfs dir %q: %v", tmpfsMntPoint, err)
441+
442+
err = unix.Mount("none", tmpfsMntPoint, "tmpfs", 0, "")
443+
framework.ExpectNoError(err, "failed to mount tmpfs on dir %q: %v", tmpfsMntPoint, err)
444+
445+
clearHostPath := func() {
446+
By("clean up the TempDir")
447+
err := unix.Unmount(tmpfsMntPoint, unix.MNT_DETACH)
448+
framework.ExpectNoError(err, "failed to unmount \"tmpfsMntPoint\": %v", err)
449+
err = os.RemoveAll(hostPath)
450+
framework.ExpectNoError(err, "failed to remove \"hostPath\": %v", err)
451+
}
452+
453+
return hostPath, clearHostPath
454+
}
455+
456+
func createRROMountContainer(
457+
rc internalapi.RuntimeService,
458+
ic internalapi.ImageManagerService,
459+
podID string,
460+
podConfig *runtimeapi.PodSandboxConfig,
461+
hostPath, containerPath string,
462+
rro bool,
463+
) string {
464+
mounts := []*runtimeapi.Mount{
465+
{
466+
HostPath: hostPath,
467+
ContainerPath: containerPath,
468+
Readonly: true,
469+
RecursiveReadOnly: rro,
470+
SelinuxRelabel: true,
471+
},
472+
}
473+
return createMountContainer(rc, ic, podID, podConfig, mounts, false)
474+
}
475+
476+
func createMountContainer(
477+
rc internalapi.RuntimeService,
478+
ic internalapi.ImageManagerService,
479+
podID string,
480+
podConfig *runtimeapi.PodSandboxConfig,
481+
mounts []*runtimeapi.Mount,
482+
expectErr bool,
483+
) string {
484+
By("create a container with volume and name")
485+
containerName := "test-mount-" + framework.NewUUID()
486+
containerConfig := &runtimeapi.ContainerConfig{
487+
Metadata: framework.BuildContainerMetadata(containerName, framework.DefaultAttempt),
488+
Image: &runtimeapi.ImageSpec{Image: framework.TestContext.TestImageList.DefaultTestContainerImage},
489+
Command: pauseCmd,
490+
Mounts: mounts,
491+
}
492+
493+
if expectErr {
494+
_, err := framework.CreateContainerWithError(rc, ic, containerConfig, podID, podConfig)
495+
Expect(err).To(HaveOccurred())
496+
return ""
497+
}
498+
499+
containerID := framework.CreateContainer(rc, ic, containerConfig, podID, podConfig)
500+
501+
By("verifying container status")
502+
resp, err := rc.ContainerStatus(context.TODO(), containerID, true)
503+
framework.ExpectNoError(err, "unable to get container status")
504+
Expect(len(resp.Status.Mounts), len(mounts))
505+
506+
return containerID
507+
}

0 commit comments

Comments
 (0)