Skip to content

Commit e34d23a

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 4d45eb2 commit e34d23a

File tree

11 files changed

+1260
-480
lines changed

11 files changed

+1260
-480
lines changed

cmd/crictl/container.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -919,7 +919,7 @@ func ContainerStatus(client internalapi.RuntimeService, id, output string, tmplS
919919

920920
switch output {
921921
case "json", "yaml", "go-template":
922-
return outputStatusInfo(status, r.Info, output, tmplStr)
922+
return outputStatusInfo(status, "", r.Info, output, tmplStr)
923923
case "table": // table output is after this switch block
924924
default:
925925
return fmt.Errorf("output option cannot be %s", output)

cmd/crictl/image.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ var imageStatusCommand = &cli.Command{
308308
}
309309
switch output {
310310
case "json", "yaml", "go-template":
311-
if err := outputStatusInfo(status, r.Info, output, tmplStr); err != nil {
311+
if err := outputStatusInfo(status, "", r.Info, output, tmplStr); err != nil {
312312
return fmt.Errorf("output status for %q: %w", id, err)
313313
}
314314
continue
@@ -501,7 +501,7 @@ var imageFsInfoCommand = &cli.Command{
501501

502502
switch output {
503503
case "json", "yaml", "go-template":
504-
if err := outputStatusInfo(status, nil, output, tmplStr); err != nil {
504+
if err := outputStatusInfo(status, "", nil, output, tmplStr); err != nil {
505505
return fmt.Errorf("output filesystem info: %w", err)
506506
}
507507
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

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ replace (
119119
k8s.io/component-base => k8s.io/kubernetes/staging/src/k8s.io/component-base v0.0.0-20231213084502-3f7a50f38688
120120
k8s.io/component-helpers => k8s.io/kubernetes/staging/src/k8s.io/component-helpers v0.0.0-20231213084502-3f7a50f38688
121121
k8s.io/controller-manager => k8s.io/kubernetes/staging/src/k8s.io/controller-manager v0.0.0-20231213084502-3f7a50f38688
122-
k8s.io/cri-api => k8s.io/kubernetes/staging/src/k8s.io/cri-api v0.0.0-20231213084502-3f7a50f38688
122+
k8s.io/cri-api => k8s.io/kubernetes/staging/src/k8s.io/cri-api v0.0.0-20240217030336-1dce896e2c83
123123
k8s.io/csi-translation-lib => k8s.io/kubernetes/staging/src/k8s.io/csi-translation-lib v0.0.0-20231213084502-3f7a50f38688
124124
k8s.io/dynamic-resource-allocation => k8s.io/kubernetes/staging/src/k8s.io/dynamic-resource-allocation v0.0.0-20231213084502-3f7a50f38688
125125
k8s.io/endpointslice => k8s.io/kubernetes/staging/src/k8s.io/endpointslice v0.0.0-20231213084502-3f7a50f38688

go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -291,8 +291,8 @@ k8s.io/kubernetes/staging/src/k8s.io/client-go v0.0.0-20231213084502-3f7a50f3868
291291
k8s.io/kubernetes/staging/src/k8s.io/client-go v0.0.0-20231213084502-3f7a50f38688/go.mod h1:ZDvAVpZvDeVszpJ60k4vAm6m+lhTLxbr3GY5NbE8X1E=
292292
k8s.io/kubernetes/staging/src/k8s.io/component-base v0.0.0-20231213084502-3f7a50f38688 h1:QpQ05w9A7xLBHLse6EilSkfXqYynkWk/Vkq3SNCMfWY=
293293
k8s.io/kubernetes/staging/src/k8s.io/component-base v0.0.0-20231213084502-3f7a50f38688/go.mod h1:qLO9+0qPsNO/o4U/X1ebazO3IOaQRgQdfTXa7IsRnCE=
294-
k8s.io/kubernetes/staging/src/k8s.io/cri-api v0.0.0-20231213084502-3f7a50f38688 h1:1Wj+ocTpRZfNws4qajSd3hpNABIG0aExtmOvAP5xiow=
295-
k8s.io/kubernetes/staging/src/k8s.io/cri-api v0.0.0-20231213084502-3f7a50f38688/go.mod h1:inilmNAvSChktQm94KP+MNob/cllXUcKfDBcdo8tQ8w=
294+
k8s.io/kubernetes/staging/src/k8s.io/cri-api v0.0.0-20240217030336-1dce896e2c83 h1:2VlK47EjFrqITD1VTOVeOr4aWZRBa8WBYRRx+X39gDw=
295+
k8s.io/kubernetes/staging/src/k8s.io/cri-api v0.0.0-20240217030336-1dce896e2c83/go.mod h1:z2+sP5qP2lt0n6MSzpLoIAkeYx4tR90YTPWli9POTgw=
296296
k8s.io/kubernetes/staging/src/k8s.io/kubectl v0.0.0-20231213084502-3f7a50f38688 h1:yoiaNqQsNGRgNxlzuAL4Zy8MK2KxSHe8hA9YVbEQa+o=
297297
k8s.io/kubernetes/staging/src/k8s.io/kubectl v0.0.0-20231213084502-3f7a50f38688/go.mod h1:rScErriC7ZZ27CUVUI2za1FEpI1BJI/iMBTL9kscr5M=
298298
k8s.io/kubernetes/staging/src/k8s.io/kubelet v0.0.0-20231213084502-3f7a50f38688 h1:7ISXAlR8JDeIo9q+bCtdlkqSWHlHCmYTMemoRH7HPOE=

pkg/validate/container_linux.go

+199
Original file line numberDiff line numberDiff line change
@@ -307,3 +307,202 @@ 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+
if f.RecursiveReadOnlyMounts {
423+
return true
424+
}
425+
return false
426+
}
427+
}
428+
}
429+
return false
430+
}
431+
432+
// createHostPath creates the hostPath for RRO mount test.
433+
//
434+
// hostPath contains a "tmpfs" directory with tmpfs mounted on it.
435+
func createHostPathForRROMount(podID string) (string, func()) {
436+
hostPath, err := os.MkdirTemp("", "test"+podID)
437+
framework.ExpectNoError(err, "failed to create TempDir %q: %v", hostPath, err)
438+
439+
tmpfsMntPoint := filepath.Join(hostPath, "tmpfs")
440+
err = os.MkdirAll(tmpfsMntPoint, 0700)
441+
framework.ExpectNoError(err, "failed to create tmpfs dir %q: %v", tmpfsMntPoint, err)
442+
443+
err = unix.Mount("none", tmpfsMntPoint, "tmpfs", 0, "")
444+
framework.ExpectNoError(err, "failed to mount tmpfs on dir %q: %v", tmpfsMntPoint, err)
445+
446+
clearHostPath := func() {
447+
By("clean up the TempDir")
448+
err := unix.Unmount(tmpfsMntPoint, unix.MNT_DETACH)
449+
framework.ExpectNoError(err, "failed to unmount \"tmpfsMntPoint\": %v", err)
450+
err = os.RemoveAll(hostPath)
451+
framework.ExpectNoError(err, "failed to remove \"hostPath\": %v", err)
452+
}
453+
454+
return hostPath, clearHostPath
455+
}
456+
457+
func createRROMountContainer(
458+
rc internalapi.RuntimeService,
459+
ic internalapi.ImageManagerService,
460+
podID string,
461+
podConfig *runtimeapi.PodSandboxConfig,
462+
hostPath, containerPath string,
463+
rro bool,
464+
) string {
465+
mounts := []*runtimeapi.Mount{
466+
{
467+
HostPath: hostPath,
468+
ContainerPath: containerPath,
469+
Readonly: true,
470+
RecursiveReadOnly: rro,
471+
SelinuxRelabel: true,
472+
},
473+
}
474+
return createMountContainer(rc, ic, podID, podConfig, mounts, false)
475+
}
476+
477+
func createMountContainer(
478+
rc internalapi.RuntimeService,
479+
ic internalapi.ImageManagerService,
480+
podID string,
481+
podConfig *runtimeapi.PodSandboxConfig,
482+
mounts []*runtimeapi.Mount,
483+
expectErr bool,
484+
) string {
485+
By("create a container with volume and name")
486+
containerName := "test-mount-" + framework.NewUUID()
487+
containerConfig := &runtimeapi.ContainerConfig{
488+
Metadata: framework.BuildContainerMetadata(containerName, framework.DefaultAttempt),
489+
Image: &runtimeapi.ImageSpec{Image: framework.TestContext.TestImageList.DefaultTestContainerImage},
490+
Command: pauseCmd,
491+
Mounts: mounts,
492+
}
493+
494+
if expectErr {
495+
_, err := framework.CreateContainerWithError(rc, ic, containerConfig, podID, podConfig)
496+
Expect(err).To(HaveOccurred())
497+
return ""
498+
}
499+
500+
containerID := framework.CreateContainer(rc, ic, containerConfig, podID, podConfig)
501+
502+
By("verifying container status")
503+
resp, err := rc.ContainerStatus(context.TODO(), containerID, true)
504+
framework.ExpectNoError(err, "unable to get container status")
505+
Expect(len(resp.Status.Mounts), len(mounts))
506+
507+
return containerID
508+
}

0 commit comments

Comments
 (0)