Set rootcontext of emptyDir tmpfs mounts correctly

pull/6/head
Paul Morie 2015-04-10 16:56:11 -04:00
parent 1be6847256
commit c98e89fca0
20 changed files with 158 additions and 43 deletions

View File

@ -0,0 +1,47 @@
// +build linux
/*
Copyright 2015 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package kubelet
import (
"github.com/docker/libcontainer/selinux"
)
// getRootContext gets the SELinux context of the kubelet rootDir
// or returns an error.
func (kl *Kubelet) getRootDirContext() (string, error) {
// If SELinux is not enabled, return an empty string
if !selinux.SelinuxEnabled() {
return "", nil
}
// Get the SELinux context of the rootDir.
rootContext, err := selinux.Getfilecon(kl.getRootDir())
if err != nil {
return "", err
}
// There is a libcontainer bug where the null byte is not stripped from
// the result of reading some selinux xattrs; strip it.
//
// TODO: remove when https://github.com/docker/libcontainer/issues/499
// is fixed
rootContext = rootContext[:len(rootContext)-1]
return rootContext, nil
}

View File

@ -0,0 +1,24 @@
// +build !linux
/*
Copyright 2015 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package kubelet
func (kl *Kubelet) getRootDirContext() (string, error) {
// For now, just return a blank security context on unsupported platforms
return "", nil
}

View File

@ -54,8 +54,8 @@ func (vh *volumeHost) GetKubeClient() client.Interface {
return vh.kubelet.kubeClient
}
func (vh *volumeHost) NewWrapperBuilder(spec *api.Volume, podRef *api.ObjectReference) (volume.Builder, error) {
b, err := vh.kubelet.newVolumeBuilderFromPlugins(spec, podRef)
func (vh *volumeHost) NewWrapperBuilder(spec *api.Volume, podRef *api.ObjectReference, opts volume.VolumeOptions) (volume.Builder, error) {
b, err := vh.kubelet.newVolumeBuilderFromPlugins(spec, podRef, opts)
if err == nil && b == nil {
return nil, errUnsupportedVolumeType
}
@ -78,7 +78,7 @@ func (vh *volumeHost) NewWrapperCleaner(spec *api.Volume, podUID types.UID) (vol
return c, nil
}
func (kl *Kubelet) newVolumeBuilderFromPlugins(spec *api.Volume, podRef *api.ObjectReference) (volume.Builder, error) {
func (kl *Kubelet) newVolumeBuilderFromPlugins(spec *api.Volume, podRef *api.ObjectReference, opts volume.VolumeOptions) (volume.Builder, error) {
plugin, err := kl.volumePluginMgr.FindPluginBySpec(spec)
if err != nil {
return nil, fmt.Errorf("can't use volume plugins for %s: %v", spew.Sprintf("%#v", *spec), err)
@ -87,7 +87,7 @@ func (kl *Kubelet) newVolumeBuilderFromPlugins(spec *api.Volume, podRef *api.Obj
// Not found but not an error
return nil, nil
}
builder, err := plugin.NewBuilder(spec, podRef)
builder, err := plugin.NewBuilder(spec, podRef, opts)
if err != nil {
return nil, fmt.Errorf("failed to instantiate volume plugin for %s: %v", spew.Sprintf("%#v", *spec), err)
}
@ -106,8 +106,13 @@ func (kl *Kubelet) mountExternalVolumes(pod *api.Pod) (volumeMap, error) {
return nil, err
}
rootContext, err := kl.getRootDirContext()
if err != nil {
return nil, err
}
// Try to use a plugin for this volume.
builder, err := kl.newVolumeBuilderFromPlugins(volSpec, podRef)
builder, err := kl.newVolumeBuilderFromPlugins(volSpec, podRef, volume.VolumeOptions{rootContext})
if err != nil {
glog.Errorf("Could not create volume builder for pod %s: %v", pod.UID, err)
return nil, err

View File

@ -71,7 +71,7 @@ func (plugin *awsElasticBlockStorePlugin) GetAccessModes() []api.AccessModeType
}
}
func (plugin *awsElasticBlockStorePlugin) NewBuilder(spec *api.Volume, podRef *api.ObjectReference) (volume.Builder, error) {
func (plugin *awsElasticBlockStorePlugin) NewBuilder(spec *api.Volume, podRef *api.ObjectReference, _ volume.VolumeOptions) (volume.Builder, error) {
// Inject real implementations here, test through the internal function.
return plugin.newBuilderInternal(spec, podRef.UID, &AWSDiskUtil{}, mount.New())
}

View File

@ -25,6 +25,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/mount"
"github.com/GoogleCloudPlatform/kubernetes/pkg/volume"
"github.com/golang/glog"
)
// This is the primary entrypoint for volume plugins.
@ -80,12 +81,12 @@ func (plugin *emptyDirPlugin) CanSupport(spec *api.Volume) bool {
return false
}
func (plugin *emptyDirPlugin) NewBuilder(spec *api.Volume, podRef *api.ObjectReference) (volume.Builder, error) {
func (plugin *emptyDirPlugin) NewBuilder(spec *api.Volume, podRef *api.ObjectReference, opts volume.VolumeOptions) (volume.Builder, error) {
// Inject real implementations here, test through the internal function.
return plugin.newBuilderInternal(spec, podRef, plugin.mounter, &realMountDetector{plugin.mounter})
return plugin.newBuilderInternal(spec, podRef, plugin.mounter, &realMountDetector{plugin.mounter}, opts)
}
func (plugin *emptyDirPlugin) newBuilderInternal(spec *api.Volume, podRef *api.ObjectReference, mounter mount.Interface, mountDetector mountDetector) (volume.Builder, error) {
func (plugin *emptyDirPlugin) newBuilderInternal(spec *api.Volume, podRef *api.ObjectReference, mounter mount.Interface, mountDetector mountDetector, opts volume.VolumeOptions) (volume.Builder, error) {
if plugin.legacyMode {
// Legacy mode instances can be cleaned up but not created anew.
return nil, fmt.Errorf("legacy mode: can not create new instances")
@ -102,6 +103,7 @@ func (plugin *emptyDirPlugin) newBuilderInternal(spec *api.Volume, podRef *api.O
mountDetector: mountDetector,
plugin: plugin,
legacyMode: false,
rootContext: opts.RootContext,
}, nil
}
@ -154,6 +156,7 @@ type emptyDir struct {
mountDetector mountDetector
plugin *emptyDirPlugin
legacyMode bool
rootContext string
}
// SetUp creates new directory.
@ -192,10 +195,29 @@ func (ed *emptyDir) setupTmpfs(dir string) error {
if err != nil {
return err
}
// If the directory is a mountpoint with medium memory, there is no
// work to do since we are already in the desired state.
if isMnt && medium == mediumMemory {
return nil // current state is what we expect
return nil
}
return ed.mounter.Mount("tmpfs", dir, "tmpfs", 0, "")
// By default a tmpfs mount will receive a different SELinux context
// from that of the Kubelet root directory which is not readable from
// the SELinux context of a docker container.
//
// getTmpfsMountOptions gets the mount option to set the context of
// the tmpfs mount so that it can be read from the SELinux context of
// the container.
opts := ed.getTmpfsMountOptions()
glog.V(3).Infof("pod %v: mounting tmpfs for volume %v with opts %v", ed.podUID, ed.volName, opts)
return ed.mounter.Mount("tmpfs", dir, "tmpfs", 0, opts)
}
func (ed *emptyDir) getTmpfsMountOptions() string {
if ed.rootContext == "" {
return ""
}
return fmt.Sprintf("rootcontext=\"%v\"", ed.rootContext)
}
func (ed *emptyDir) GetPath() string {

View File

@ -74,7 +74,7 @@ func TestPlugin(t *testing.T) {
}
mounter := mount.FakeMounter{}
mountDetector := fakeMountDetector{}
builder, err := plug.(*emptyDirPlugin).newBuilderInternal(spec, &api.ObjectReference{UID: types.UID("poduid")}, &mounter, &mountDetector)
builder, err := plug.(*emptyDirPlugin).newBuilderInternal(spec, &api.ObjectReference{UID: types.UID("poduid")}, &mounter, &mountDetector, volume.VolumeOptions{""})
if err != nil {
t.Errorf("Failed to make a new Builder: %v", err)
}
@ -133,7 +133,7 @@ func TestPluginTmpfs(t *testing.T) {
}
mounter := mount.FakeMounter{}
mountDetector := fakeMountDetector{}
builder, err := plug.(*emptyDirPlugin).newBuilderInternal(spec, &api.ObjectReference{UID: types.UID("poduid")}, &mounter, &mountDetector)
builder, err := plug.(*emptyDirPlugin).newBuilderInternal(spec, &api.ObjectReference{UID: types.UID("poduid")}, &mounter, &mountDetector, volume.VolumeOptions{""})
if err != nil {
t.Errorf("Failed to make a new Builder: %v", err)
}
@ -197,7 +197,7 @@ func TestPluginBackCompat(t *testing.T) {
spec := &api.Volume{
Name: "vol1",
}
builder, err := plug.NewBuilder(spec, &api.ObjectReference{UID: types.UID("poduid")})
builder, err := plug.NewBuilder(spec, &api.ObjectReference{UID: types.UID("poduid")}, volume.VolumeOptions{""})
if err != nil {
t.Errorf("Failed to make a new Builder: %v", err)
}
@ -222,7 +222,7 @@ func TestPluginLegacy(t *testing.T) {
}
spec := api.Volume{VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}
if _, err := plug.(*emptyDirPlugin).newBuilderInternal(&spec, &api.ObjectReference{UID: types.UID("poduid")}, &mount.FakeMounter{}, &fakeMountDetector{}); err == nil {
if _, err := plug.(*emptyDirPlugin).newBuilderInternal(&spec, &api.ObjectReference{UID: types.UID("poduid")}, &mount.FakeMounter{}, &fakeMountDetector{}, volume.VolumeOptions{""}); err == nil {
t.Errorf("Expected failiure")
}

View File

@ -78,7 +78,7 @@ func (plugin *gcePersistentDiskPlugin) GetAccessModes() []api.AccessModeType {
}
}
func (plugin *gcePersistentDiskPlugin) NewBuilder(spec *api.Volume, podRef *api.ObjectReference) (volume.Builder, error) {
func (plugin *gcePersistentDiskPlugin) NewBuilder(spec *api.Volume, podRef *api.ObjectReference, _ volume.VolumeOptions) (volume.Builder, error) {
// Inject real implementations here, test through the internal function.
return plugin.newBuilderInternal(spec, podRef.UID, &GCEDiskUtil{}, mount.New())
}

View File

@ -167,7 +167,7 @@ func TestPluginLegacy(t *testing.T) {
t.Errorf("Expected false")
}
if _, err := plug.NewBuilder(&api.Volume{VolumeSource: api.VolumeSource{GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{}}}, &api.ObjectReference{UID: types.UID("poduid")}); err == nil {
if _, err := plug.NewBuilder(&api.Volume{VolumeSource: api.VolumeSource{GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{}}}, &api.ObjectReference{UID: types.UID("poduid")}, volume.VolumeOptions{""}); err == nil {
t.Errorf("Expected failiure")
}

View File

@ -70,7 +70,7 @@ func (plugin *gitRepoPlugin) CanSupport(spec *api.Volume) bool {
return false
}
func (plugin *gitRepoPlugin) NewBuilder(spec *api.Volume, podRef *api.ObjectReference) (volume.Builder, error) {
func (plugin *gitRepoPlugin) NewBuilder(spec *api.Volume, podRef *api.ObjectReference, opts volume.VolumeOptions) (volume.Builder, error) {
if plugin.legacyMode {
// Legacy mode instances can be cleaned up but not created anew.
return nil, fmt.Errorf("legacy mode: can not create new instances")
@ -83,6 +83,7 @@ func (plugin *gitRepoPlugin) NewBuilder(spec *api.Volume, podRef *api.ObjectRefe
exec: exec.New(),
plugin: plugin,
legacyMode: false,
opts: opts,
}, nil
}
@ -109,6 +110,7 @@ type gitRepo struct {
exec exec.Interface
plugin *gitRepoPlugin
legacyMode bool
opts volume.VolumeOptions
}
// SetUp creates new directory and clones a git repo.
@ -132,7 +134,7 @@ func (gr *gitRepo) SetUpAt(dir string) error {
}
// Wrap EmptyDir, let it do the setup.
wrapped, err := gr.plugin.host.NewWrapperBuilder(wrappedVolumeSpec, &gr.podRef)
wrapped, err := gr.plugin.host.NewWrapperBuilder(wrappedVolumeSpec, &gr.podRef, gr.opts)
if err != nil {
return err
}

View File

@ -118,7 +118,7 @@ func TestPlugin(t *testing.T) {
},
},
}
builder, err := plug.NewBuilder(spec, &api.ObjectReference{UID: types.UID("poduid")})
builder, err := plug.NewBuilder(spec, &api.ObjectReference{UID: types.UID("poduid")}, volume.VolumeOptions{""})
if err != nil {
t.Errorf("Failed to make a new Builder: %v", err)
}
@ -173,7 +173,7 @@ func TestPluginLegacy(t *testing.T) {
t.Errorf("Expected false")
}
if _, err := plug.NewBuilder(&api.Volume{VolumeSource: api.VolumeSource{GitRepo: &api.GitRepoVolumeSource{}}}, &api.ObjectReference{UID: types.UID("poduid")}); err == nil {
if _, err := plug.NewBuilder(&api.Volume{VolumeSource: api.VolumeSource{GitRepo: &api.GitRepoVolumeSource{}}}, &api.ObjectReference{UID: types.UID("poduid")}, volume.VolumeOptions{""}); err == nil {
t.Errorf("Expected failiure")
}

View File

@ -68,7 +68,7 @@ func (plugin *glusterfsPlugin) GetAccessModes() []api.AccessModeType {
}
}
func (plugin *glusterfsPlugin) NewBuilder(spec *api.Volume, podRef *api.ObjectReference) (volume.Builder, error) {
func (plugin *glusterfsPlugin) NewBuilder(spec *api.Volume, podRef *api.ObjectReference, _ volume.VolumeOptions) (volume.Builder, error) {
ep_name := spec.VolumeSource.Glusterfs.EndpointsName
ns := api.NamespaceDefault
ep, err := plugin.host.GetKubeClient().Endpoints(ns).Get(ep_name)

View File

@ -60,7 +60,7 @@ func (plugin *hostPathPlugin) GetAccessModes() []api.AccessModeType {
}
}
func (plugin *hostPathPlugin) NewBuilder(spec *api.Volume, podRef *api.ObjectReference) (volume.Builder, error) {
func (plugin *hostPathPlugin) NewBuilder(spec *api.Volume, podRef *api.ObjectReference, _ volume.VolumeOptions) (volume.Builder, error) {
return &hostPath{spec.HostPath.Path}, nil
}

View File

@ -68,7 +68,7 @@ func TestPlugin(t *testing.T) {
Name: "vol1",
VolumeSource: api.VolumeSource{HostPath: &api.HostPathVolumeSource{"/vol1"}},
}
builder, err := plug.NewBuilder(spec, &api.ObjectReference{UID: types.UID("poduid")})
builder, err := plug.NewBuilder(spec, &api.ObjectReference{UID: types.UID("poduid")}, volume.VolumeOptions{})
if err != nil {
t.Errorf("Failed to make a new Builder: %v", err)
}

View File

@ -72,7 +72,7 @@ func (plugin *ISCSIPlugin) GetAccessModes() []api.AccessModeType {
}
}
func (plugin *ISCSIPlugin) NewBuilder(spec *api.Volume, podRef *api.ObjectReference) (volume.Builder, error) {
func (plugin *ISCSIPlugin) NewBuilder(spec *api.Volume, podRef *api.ObjectReference, _ volume.VolumeOptions) (volume.Builder, error) {
// Inject real implementations here, test through the internal function.
return plugin.newBuilderInternal(spec, podRef.UID, &ISCSIUtil{}, mount.New())
}

View File

@ -65,7 +65,7 @@ func (plugin *nfsPlugin) GetAccessModes() []api.AccessModeType {
}
}
func (plugin *nfsPlugin) NewBuilder(spec *api.Volume, podRef *api.ObjectReference) (volume.Builder, error) {
func (plugin *nfsPlugin) NewBuilder(spec *api.Volume, podRef *api.ObjectReference, _ volume.VolumeOptions) (volume.Builder, error) {
return plugin.newBuilderInternal(spec, podRef, plugin.mounter)
}

View File

@ -29,6 +29,17 @@ import (
"github.com/golang/glog"
)
// VolumeOptions contains option information about a volume.
//
// Currently, this struct containers only a single field for the
// rootcontext of the volume. This is a temporary measure in order
// to set the rootContext of tmpfs mounts correctly; it will be replaced
// and expanded on by future SecurityContext work.
type VolumeOptions struct {
// The rootcontext to use when performing mounts for a volume.
RootContext string
}
// VolumePlugin is an interface to volume plugins that can be used on a
// kubernetes node (e.g. by kubelet) to instantiate and manage volumes.
type VolumePlugin interface {
@ -51,7 +62,7 @@ type VolumePlugin interface {
// Ownership of the spec pointer in *not* transferred.
// - spec: The api.Volume spec
// - podRef: a reference to the enclosing pod
NewBuilder(spec *api.Volume, podRef *api.ObjectReference) (Builder, error)
NewBuilder(spec *api.Volume, podRef *api.ObjectReference, opts VolumeOptions) (Builder, error)
// NewCleaner creates a new volume.Cleaner from recoverable state.
// - name: The volume name, as per the api.Volume spec.
@ -94,7 +105,7 @@ type VolumeHost interface {
// the provided spec. This is used to implement volume plugins which
// "wrap" other plugins. For example, the "secret" volume is
// implemented in terms of the "emptyDir" volume.
NewWrapperBuilder(spec *api.Volume, podRef *api.ObjectReference) (Builder, error)
NewWrapperBuilder(spec *api.Volume, podRef *api.ObjectReference, opts VolumeOptions) (Builder, error)
// NewWrapperCleaner finds an appropriate plugin with which to handle
// the provided spec. See comments on NewWrapperBuilder for more

View File

@ -58,12 +58,12 @@ func (plugin *secretPlugin) CanSupport(spec *api.Volume) bool {
return false
}
func (plugin *secretPlugin) NewBuilder(spec *api.Volume, podRef *api.ObjectReference) (volume.Builder, error) {
return plugin.newBuilderInternal(spec, podRef)
func (plugin *secretPlugin) NewBuilder(spec *api.Volume, podRef *api.ObjectReference, opts volume.VolumeOptions) (volume.Builder, error) {
return plugin.newBuilderInternal(spec, podRef, opts)
}
func (plugin *secretPlugin) newBuilderInternal(spec *api.Volume, podRef *api.ObjectReference) (volume.Builder, error) {
return &secretVolume{spec.Name, *podRef, plugin, spec.Secret.SecretName}, nil
func (plugin *secretPlugin) newBuilderInternal(spec *api.Volume, podRef *api.ObjectReference, opts volume.VolumeOptions) (volume.Builder, error) {
return &secretVolume{spec.Name, *podRef, plugin, spec.Secret.SecretName, &opts}, nil
}
func (plugin *secretPlugin) NewCleaner(volName string, podUID types.UID) (volume.Cleaner, error) {
@ -71,7 +71,7 @@ func (plugin *secretPlugin) NewCleaner(volName string, podUID types.UID) (volume
}
func (plugin *secretPlugin) newCleanerInternal(volName string, podUID types.UID) (volume.Cleaner, error) {
return &secretVolume{volName, api.ObjectReference{UID: podUID}, plugin, ""}, nil
return &secretVolume{volName, api.ObjectReference{UID: podUID}, plugin, "", nil}, nil
}
// secretVolume handles retrieving secrets from the API server
@ -81,6 +81,7 @@ type secretVolume struct {
podRef api.ObjectReference
plugin *secretPlugin
secretName string
opts *volume.VolumeOptions
}
func (sv *secretVolume) SetUp() error {
@ -97,7 +98,7 @@ func (sv *secretVolume) SetUpAt(dir string) error {
glog.V(3).Infof("Setting up volume %v for pod %v at %v", sv.volName, sv.podRef.UID, dir)
// Wrap EmptyDir, let it do the setup.
wrapped, err := sv.plugin.host.NewWrapperBuilder(wrappedVolumeSpec, &sv.podRef)
wrapped, err := sv.plugin.host.NewWrapperBuilder(wrappedVolumeSpec, &sv.podRef, *sv.opts)
if err != nil {
return err
}
@ -126,7 +127,7 @@ func (sv *secretVolume) SetUpAt(dir string) error {
for name, data := range secret.Data {
hostFilePath := path.Join(dir, name)
glog.V(3).Infof("Writing secret data %v/%v/%v (%v bytes) to host file %v", sv.podRef.Namespace, sv.secretName, name, len(data), hostFilePath)
err := ioutil.WriteFile(hostFilePath, data, 0777)
err := ioutil.WriteFile(hostFilePath, data, 0444)
if err != nil {
glog.Errorf("Error writing secret data to host path: %v, %v", hostFilePath, err)
return err

View File

@ -97,7 +97,7 @@ func TestPlugin(t *testing.T) {
t.Errorf("Can't find the plugin by name")
}
builder, err := plugin.NewBuilder(volumeSpec, &api.ObjectReference{UID: types.UID(testPodUID)})
builder, err := plugin.NewBuilder(volumeSpec, &api.ObjectReference{UID: types.UID(testPodUID)}, volume.VolumeOptions{})
if err != nil {
t.Errorf("Failed to make a new Builder: %v", err)
}

View File

@ -55,12 +55,12 @@ func (f *fakeVolumeHost) GetKubeClient() client.Interface {
return f.kubeClient
}
func (f *fakeVolumeHost) NewWrapperBuilder(spec *api.Volume, podRef *api.ObjectReference) (Builder, error) {
func (f *fakeVolumeHost) NewWrapperBuilder(spec *api.Volume, podRef *api.ObjectReference, opts VolumeOptions) (Builder, error) {
plug, err := f.pluginMgr.FindPluginBySpec(spec)
if err != nil {
return nil, err
}
return plug.NewBuilder(spec, podRef)
return plug.NewBuilder(spec, podRef, opts)
}
func (f *fakeVolumeHost) NewWrapperCleaner(spec *api.Volume, podUID types.UID) (Cleaner, error) {
@ -95,7 +95,7 @@ func (plugin *FakeVolumePlugin) CanSupport(spec *api.Volume) bool {
return true
}
func (plugin *FakeVolumePlugin) NewBuilder(spec *api.Volume, podRef *api.ObjectReference) (Builder, error) {
func (plugin *FakeVolumePlugin) NewBuilder(spec *api.Volume, podRef *api.ObjectReference, opts VolumeOptions) (Builder, error) {
return &FakeVolume{podRef.UID, spec.Name, plugin}, nil
}

View File

@ -83,9 +83,11 @@ var _ = Describe("Secrets", func() {
},
Containers: []api.Container{
{
Name: "catcont",
Image: "gcr.io/google_containers/busybox",
Command: []string{"sh", "-c", "cat /etc/secret-volume/data-1"},
Name: "catcont",
Image: "kubernetes/mounttest:0.1",
Args: []string{
"--file_content=/etc/secret-volume/data-1",
"--file_mode=/etc/secret-volume/data-1"},
VolumeMounts: []api.VolumeMount{
{
Name: volumeName,
@ -100,7 +102,8 @@ var _ = Describe("Secrets", func() {
}
testContainerOutput("consume secrets", c, pod, []string{
"value-1",
"content of file \"/etc/secret-volume/data-1\": value-1",
"mode of file \"/etc/secret-volume/data-1\": -r--r--r--",
})
})
})