gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/runsc/boot/mount_hints_test.go (about)

     1  // Copyright 2022 The gVisor Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package boot
    16  
    17  import (
    18  	"slices"
    19  	"strings"
    20  	"testing"
    21  
    22  	specs "github.com/opencontainers/runtime-spec/specs-go"
    23  	"gvisor.dev/gvisor/pkg/sentry/fsimpl/erofs"
    24  	"gvisor.dev/gvisor/runsc/config"
    25  )
    26  
    27  func TestPodMountHintsHappy(t *testing.T) {
    28  	spec := &specs.Spec{
    29  		Annotations: map[string]string{
    30  			MountPrefix + "mount1.source": "foo",
    31  			MountPrefix + "mount1.type":   "tmpfs",
    32  			MountPrefix + "mount1.share":  "pod",
    33  
    34  			MountPrefix + "mount2.source":  "bar",
    35  			MountPrefix + "mount2.type":    "bind",
    36  			MountPrefix + "mount2.share":   "container",
    37  			MountPrefix + "mount2.options": "rw,private",
    38  		},
    39  	}
    40  	podHints, err := NewPodMountHints(spec)
    41  	if err != nil {
    42  		t.Fatalf("newPodMountHints failed: %v", err)
    43  	}
    44  
    45  	// Check that fields were set correctly.
    46  	mount1 := podHints.Mounts["mount1"]
    47  	if want := "mount1"; want != mount1.Name {
    48  		t.Errorf("mount1 name, want: %q, got: %q", want, mount1.Name)
    49  	}
    50  	if want := "foo"; want != mount1.Mount.Source {
    51  		t.Errorf("mount1 source, want: %q, got: %q", want, mount1.Mount.Source)
    52  	}
    53  	if want := "tmpfs"; want != mount1.Mount.Type {
    54  		t.Errorf("mount1 type, want: %q, got: %q", want, mount1.Mount.Type)
    55  	}
    56  	if want := pod; want != mount1.Share {
    57  		t.Errorf("mount1 type, want: %q, got: %q", want, mount1.Share)
    58  	}
    59  	if want := []string(nil); !slices.Equal(want, mount1.Mount.Options) {
    60  		t.Errorf("mount1 type, want: %q, got: %q", want, mount1.Mount.Options)
    61  	}
    62  
    63  	mount2 := podHints.Mounts["mount2"]
    64  	if want := "mount2"; want != mount2.Name {
    65  		t.Errorf("mount2 name, want: %q, got: %q", want, mount2.Name)
    66  	}
    67  	if want := "bar"; want != mount2.Mount.Source {
    68  		t.Errorf("mount2 source, want: %q, got: %q", want, mount2.Mount.Source)
    69  	}
    70  	if want := "bind"; want != mount2.Mount.Type {
    71  		t.Errorf("mount2 type, want: %q, got: %q", want, mount2.Mount.Type)
    72  	}
    73  	if want := container; want != mount2.Share {
    74  		t.Errorf("mount2 type, want: %q, got: %q", want, mount2.Share)
    75  	}
    76  	if want := []string{"rw", "private"}; !slices.Equal(want, mount2.Mount.Options) {
    77  		t.Errorf("mount2 type, want: %q, got: %q", want, mount2.Mount.Options)
    78  	}
    79  }
    80  
    81  func TestPodMountHintsErrors(t *testing.T) {
    82  	for _, tst := range []struct {
    83  		name        string
    84  		annotations map[string]string
    85  		error       string
    86  	}{
    87  		{
    88  			name: "too short",
    89  			annotations: map[string]string{
    90  				MountPrefix + "mount1": "foo",
    91  			},
    92  			error: "invalid mount annotation",
    93  		},
    94  		{
    95  			name: "no name",
    96  			annotations: map[string]string{
    97  				MountPrefix + ".source": "foo",
    98  			},
    99  			error: "invalid mount name",
   100  		},
   101  		{
   102  			name: "duplicate source",
   103  			annotations: map[string]string{
   104  				MountPrefix + "mount1.source": "foo",
   105  				MountPrefix + "mount1.type":   "tmpfs",
   106  				MountPrefix + "mount1.share":  "pod",
   107  
   108  				MountPrefix + "mount2.source": "foo",
   109  				MountPrefix + "mount2.type":   "bind",
   110  				MountPrefix + "mount2.share":  "container",
   111  			},
   112  			error: "have the same mount source",
   113  		},
   114  	} {
   115  		t.Run(tst.name, func(t *testing.T) {
   116  			spec := &specs.Spec{Annotations: tst.annotations}
   117  			podHints, err := NewPodMountHints(spec)
   118  			if err == nil || !strings.Contains(err.Error(), tst.error) {
   119  				t.Errorf("newPodMountHints invalid error, want: .*%s.*, got: %v", tst.error, err)
   120  			}
   121  			if podHints != nil {
   122  				t.Errorf("newPodMountHints must return nil on failure: %+v", podHints)
   123  			}
   124  		})
   125  	}
   126  }
   127  
   128  // Tests that when a required mount annotation is missing, the entire mount
   129  // hint is omitted and ignored.
   130  func TestPodMountHintsIgnore(t *testing.T) {
   131  	for _, tst := range []struct {
   132  		name        string
   133  		annotations map[string]string
   134  	}{
   135  		{
   136  			name: "invalid source",
   137  			annotations: map[string]string{
   138  				MountPrefix + "mount1.source": "",
   139  				MountPrefix + "mount1.type":   "tmpfs",
   140  				MountPrefix + "mount1.share":  "pod",
   141  			},
   142  		},
   143  		{
   144  			name: "invalid type",
   145  			annotations: map[string]string{
   146  				MountPrefix + "mount1.source": "foo",
   147  				MountPrefix + "mount1.type":   "invalid",
   148  				MountPrefix + "mount1.share":  "pod",
   149  			},
   150  		},
   151  		{
   152  			name: "invalid share",
   153  			annotations: map[string]string{
   154  				MountPrefix + "mount1.source": "foo",
   155  				MountPrefix + "mount1.type":   "tmpfs",
   156  				MountPrefix + "mount1.share":  "invalid",
   157  			},
   158  		},
   159  	} {
   160  		t.Run(tst.name, func(t *testing.T) {
   161  			spec := &specs.Spec{Annotations: tst.annotations}
   162  			podHints, err := NewPodMountHints(spec)
   163  			if err != nil {
   164  				t.Errorf("newPodMountHints() failed: %v", err)
   165  			} else if podHints != nil {
   166  				if hint, ok := podHints.Mounts["mount1"]; ok {
   167  					t.Errorf("hint was provided when it should have been omitted: %+v", hint)
   168  				}
   169  			}
   170  		})
   171  	}
   172  }
   173  
   174  func TestIgnoreInvalidMountOptions(t *testing.T) {
   175  	spec := &specs.Spec{
   176  		Annotations: map[string]string{
   177  			MountPrefix + "mount1.source":  "foo",
   178  			MountPrefix + "mount1.type":    "tmpfs",
   179  			MountPrefix + "mount1.share":   "container",
   180  			MountPrefix + "mount1.options": "rw,shared,noexec",
   181  		},
   182  	}
   183  	podHints, err := NewPodMountHints(spec)
   184  	if err != nil {
   185  		t.Fatalf("newPodMountHints failed: %v", err)
   186  	}
   187  	mount1 := podHints.Mounts["mount1"]
   188  	if want := []string{"rw", "noexec"}; !slices.Equal(want, mount1.Mount.Options) {
   189  		t.Errorf("mount2 type, want: %q, got: %q", want, mount1.Mount.Options)
   190  	}
   191  }
   192  
   193  func TestHintsCheckCompatible(t *testing.T) {
   194  	for _, tc := range []struct {
   195  		name        string
   196  		masterOpts  []string
   197  		replicaOpts []string
   198  		err         string
   199  	}{
   200  		{
   201  			name: "empty",
   202  		},
   203  		{
   204  			name:        "same",
   205  			masterOpts:  []string{"ro", "noatime", "noexec"},
   206  			replicaOpts: []string{"ro", "noatime", "noexec"},
   207  		},
   208  		{
   209  			name:        "compatible",
   210  			masterOpts:  []string{"rw", "atime", "exec"},
   211  			replicaOpts: []string{"ro", "noatime", "noexec"},
   212  		},
   213  		{
   214  			name:        "unsupported",
   215  			masterOpts:  []string{"nofoo", "nodev"},
   216  			replicaOpts: []string{"foo", "dev"},
   217  		},
   218  		{
   219  			name:        "incompatible-ro",
   220  			masterOpts:  []string{"ro"},
   221  			replicaOpts: []string{"rw"},
   222  			err:         "read-write",
   223  		},
   224  		{
   225  			name:        "incompatible-atime",
   226  			masterOpts:  []string{"noatime"},
   227  			replicaOpts: []string{"atime"},
   228  			err:         "noatime",
   229  		},
   230  		{
   231  			name:        "incompatible-exec",
   232  			masterOpts:  []string{"noexec"},
   233  			replicaOpts: []string{"exec"},
   234  			err:         "noexec",
   235  		},
   236  	} {
   237  		t.Run(tc.name, func(t *testing.T) {
   238  			master := MountHint{Mount: specs.Mount{Options: tc.masterOpts}}
   239  			replica := specs.Mount{Options: tc.replicaOpts}
   240  			if err := master.checkCompatible(&replica); err != nil {
   241  				if !strings.Contains(err.Error(), tc.err) {
   242  					t.Fatalf("wrong error, want: %q, got: %q", tc.err, err)
   243  				}
   244  			} else {
   245  				if len(tc.err) > 0 {
   246  					t.Fatalf("error %q expected", tc.err)
   247  				}
   248  			}
   249  		})
   250  	}
   251  }
   252  
   253  // TestRootfsHintHappy tests that valid rootfs annotations can be parsed correctly.
   254  func TestRootfsHintHappy(t *testing.T) {
   255  	const imagePath = "/tmp/rootfs.img"
   256  	spec := &specs.Spec{
   257  		Annotations: map[string]string{
   258  			RootfsPrefix + "source":  imagePath,
   259  			RootfsPrefix + "type":    erofs.Name,
   260  			RootfsPrefix + "overlay": config.MemoryOverlay.String(),
   261  		},
   262  	}
   263  	hint, err := NewRootfsHint(spec)
   264  	if err != nil {
   265  		t.Fatalf("NewRootfsHint failed: %v", err)
   266  	}
   267  
   268  	// Check that fields were set correctly.
   269  	if hint.Mount.Source != imagePath {
   270  		t.Errorf("rootfs source, want: %q, got: %q", imagePath, hint.Mount.Source)
   271  	}
   272  	if hint.Mount.Type != erofs.Name {
   273  		t.Errorf("rootfs type, want: %q, got: %q", erofs.Name, hint.Mount.Type)
   274  	}
   275  	if hint.Overlay != config.MemoryOverlay {
   276  		t.Errorf("rootfs overlay, want: %q, got: %q", config.MemoryOverlay, hint.Overlay)
   277  	}
   278  }
   279  
   280  // TestRootfsHintErrors tests that proper errors will be returned when parsing
   281  // invalid rootfs annotations.
   282  func TestRootfsHintErrors(t *testing.T) {
   283  	const imagePath = "/tmp/rootfs.img"
   284  	for _, tst := range []struct {
   285  		name        string
   286  		annotations map[string]string
   287  		error       string
   288  	}{
   289  		{
   290  			name: "invalid source",
   291  			annotations: map[string]string{
   292  				RootfsPrefix + "source": "invalid",
   293  				RootfsPrefix + "type":   erofs.Name,
   294  			},
   295  			error: "invalid rootfs annotation",
   296  		},
   297  		{
   298  			name: "invalid type",
   299  			annotations: map[string]string{
   300  				RootfsPrefix + "source": imagePath,
   301  				RootfsPrefix + "type":   "invalid",
   302  			},
   303  			error: "invalid rootfs annotation",
   304  		},
   305  		{
   306  			name: "invalid overlay",
   307  			annotations: map[string]string{
   308  				RootfsPrefix + "source":  imagePath,
   309  				RootfsPrefix + "type":    erofs.Name,
   310  				RootfsPrefix + "overlay": "invalid",
   311  			},
   312  			error: "invalid rootfs annotation",
   313  		},
   314  		{
   315  			name: "invalid key",
   316  			annotations: map[string]string{
   317  				RootfsPrefix + "invalid": "invalid",
   318  				RootfsPrefix + "source":  imagePath,
   319  				RootfsPrefix + "type":    erofs.Name,
   320  				RootfsPrefix + "overlay": config.MemoryOverlay.String(),
   321  			},
   322  			error: "invalid rootfs annotation",
   323  		},
   324  		{
   325  			name: "missing source",
   326  			annotations: map[string]string{
   327  				RootfsPrefix + "type":    erofs.Name,
   328  				RootfsPrefix + "overlay": config.MemoryOverlay.String(),
   329  			},
   330  			error: "rootfs annotations missing required field",
   331  		},
   332  		{
   333  			name: "missing type",
   334  			annotations: map[string]string{
   335  				RootfsPrefix + "source":  imagePath,
   336  				RootfsPrefix + "overlay": config.MemoryOverlay.String(),
   337  			},
   338  			error: "rootfs annotations missing required field",
   339  		},
   340  	} {
   341  		t.Run(tst.name, func(t *testing.T) {
   342  			spec := &specs.Spec{Annotations: tst.annotations}
   343  			hint, err := NewRootfsHint(spec)
   344  			if err == nil || !strings.Contains(err.Error(), tst.error) {
   345  				t.Errorf("NewRootfsHint invalid error, want: .*%s.*, got: %v", tst.error, err)
   346  			}
   347  			if hint != nil {
   348  				t.Errorf("NewRootfsHint must return nil on failure: %+v", hint)
   349  			}
   350  		})
   351  	}
   352  }