Skip to content

Commit e853e41

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 e853e41

File tree

6 files changed

+210
-6
lines changed

6 files changed

+210
-6
lines changed

cmd/crictl/container.go

+1-1
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

+2-2
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

+6-1
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

+1-1
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

+4-1
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

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

0 commit comments

Comments
 (0)