| package symlinks |
| |
| import ( |
| "os" |
| "path/filepath" |
| "strings" |
| "testing" |
| |
| testlog "github.com/sirupsen/logrus/hooks/test" |
| "github.com/stretchr/testify/require" |
| |
| "github.com/NVIDIA/nvidia-container-toolkit/internal/lookup/symlinks" |
| ) |
| |
| func TestLinkExist(t *testing.T) { |
| tmpDir := t.TempDir() |
| require.NoError( |
| t, |
| makeFs(tmpDir, |
| dirOrLink{path: "/a/b/c", target: "d"}, |
| dirOrLink{path: "/a/b/e", target: "/a/b/f"}, |
| ), |
| ) |
| |
| exists, err := linkExists("d", filepath.Join(tmpDir, "/a/b/c")) |
| require.NoError(t, err) |
| require.True(t, exists) |
| |
| exists, err = linkExists("/a/b/f", filepath.Join(tmpDir, "/a/b/e")) |
| require.NoError(t, err) |
| require.True(t, exists) |
| |
| exists, err = linkExists("different-target", filepath.Join(tmpDir, "/a/b/c")) |
| require.NoError(t, err) |
| require.False(t, exists) |
| |
| exists, err = linkExists("/a/b/d", filepath.Join(tmpDir, "/a/b/c")) |
| require.NoError(t, err) |
| require.False(t, exists) |
| |
| exists, err = linkExists("foo", filepath.Join(tmpDir, "/a/b/does-not-exist")) |
| require.NoError(t, err) |
| require.False(t, exists) |
| } |
| |
| func TestCreateLink(t *testing.T) { |
| type link struct { |
| path string |
| target string |
| } |
| type expectedLink struct { |
| link |
| err error |
| } |
| |
| testCases := []struct { |
| description string |
| containerContents []dirOrLink |
| link link |
| expectedCreateError error |
| expectedLinks []expectedLink |
| }{ |
| { |
| description: "link to / resolves to container root", |
| containerContents: []dirOrLink{ |
| {path: "/lib/foo", target: "/"}, |
| }, |
| link: link{ |
| path: "/lib/foo/libfoo.so", |
| target: "libfoo.so.1", |
| }, |
| expectedLinks: []expectedLink{ |
| { |
| link: link{ |
| path: "{{ .containerRoot }}/libfoo.so", |
| target: "libfoo.so.1", |
| }, |
| }, |
| }, |
| }, |
| { |
| description: "link to / resolves to container root; parent relative link", |
| containerContents: []dirOrLink{ |
| {path: "/lib/foo", target: "/"}, |
| }, |
| link: link{ |
| path: "/lib/foo/libfoo.so", |
| target: "../libfoo.so.1", |
| }, |
| expectedLinks: []expectedLink{ |
| { |
| link: link{ |
| path: "{{ .containerRoot }}/libfoo.so", |
| target: "../libfoo.so.1", |
| }, |
| }, |
| }, |
| }, |
| { |
| description: "link to / resolves to container root; absolute link", |
| containerContents: []dirOrLink{ |
| {path: "/lib/foo", target: "/"}, |
| }, |
| link: link{ |
| path: "/lib/foo/libfoo.so", |
| target: "/a-path-in-container/foo/libfoo.so.1", |
| }, |
| expectedLinks: []expectedLink{ |
| { |
| link: link{ |
| path: "{{ .containerRoot }}/libfoo.so", |
| target: "/a-path-in-container/foo/libfoo.so.1", |
| }, |
| }, |
| { |
| // We also check that the target is NOT created. |
| link: link{ |
| path: "{{ .containerRoot }}/a-path-in-container/foo/libfoo.so.1", |
| }, |
| err: os.ErrNotExist, |
| }, |
| }, |
| }, |
| } |
| |
| for _, tc := range testCases { |
| t.Run(tc.description, func(t *testing.T) { |
| tmpDir := t.TempDir() |
| hostRoot := filepath.Join(tmpDir, "/host-root/") |
| containerRoot := filepath.Join(tmpDir, "/container-root") |
| |
| require.NoError(t, makeFs(hostRoot)) |
| require.NoError(t, makeFs(containerRoot, tc.containerContents...)) |
| |
| // nvidia-cdi-hook create-symlinks --link linkSpec |
| err := getTestCommand().createLink(containerRoot, tc.link.target, tc.link.path) |
| // TODO: We may be able to replace this with require.ErrorIs. |
| if tc.expectedCreateError != nil { |
| require.Error(t, err) |
| } else { |
| require.NoError(t, err) |
| } |
| |
| for _, expectedLink := range tc.expectedLinks { |
| path := strings.ReplaceAll(expectedLink.path, "{{ .containerRoot }}", containerRoot) |
| path = strings.ReplaceAll(path, "{{ .hostRoot }}", hostRoot) |
| if expectedLink.target != "" { |
| target, err := symlinks.Resolve(path) |
| require.ErrorIs(t, err, expectedLink.err) |
| require.Equal(t, expectedLink.target, target) |
| } else { |
| _, err := os.Stat(path) |
| require.ErrorIs(t, err, expectedLink.err) |
| } |
| } |
| }) |
| } |
| } |
| |
| func TestCreateLinkRelativePath(t *testing.T) { |
| tmpDir := t.TempDir() |
| hostRoot := filepath.Join(tmpDir, "/host-root/") |
| containerRoot := filepath.Join(tmpDir, "/container-root") |
| |
| require.NoError(t, makeFs(hostRoot)) |
| require.NoError(t, makeFs(containerRoot, dirOrLink{path: "/lib/"})) |
| |
| // nvidia-cdi-hook create-symlinks --link libfoo.so.1::/lib/libfoo.so |
| err := getTestCommand().createLink(containerRoot, "libfoo.so.1", "/lib/libfoo.so") |
| require.NoError(t, err) |
| |
| target, err := symlinks.Resolve(filepath.Join(containerRoot, "/lib/libfoo.so")) |
| require.NoError(t, err) |
| require.Equal(t, "libfoo.so.1", target) |
| } |
| |
| func TestCreateLinkAbsolutePath(t *testing.T) { |
| tmpDir := t.TempDir() |
| hostRoot := filepath.Join(tmpDir, "/host-root/") |
| containerRoot := filepath.Join(tmpDir, "/container-root") |
| |
| require.NoError(t, makeFs(hostRoot)) |
| require.NoError(t, makeFs(containerRoot, dirOrLink{path: "/lib/"})) |
| |
| // nvidia-cdi-hook create-symlinks --link /lib/libfoo.so.1::/lib/libfoo.so |
| err := getTestCommand().createLink(containerRoot, "/lib/libfoo.so.1", "/lib/libfoo.so") |
| require.NoError(t, err) |
| |
| target, err := symlinks.Resolve(filepath.Join(containerRoot, "/lib/libfoo.so")) |
| require.NoError(t, err) |
| require.Equal(t, "/lib/libfoo.so.1", target) |
| } |
| |
| func TestCreateLinkAlreadyExists(t *testing.T) { |
| testCases := []struct { |
| description string |
| containerContents []dirOrLink |
| shouldExist []string |
| }{ |
| { |
| description: "link already exists with correct target", |
| containerContents: []dirOrLink{{path: "/lib/libfoo.so", target: "libfoo.so.1"}}, |
| shouldExist: []string{}, |
| }, |
| { |
| description: "link already exists with different target", |
| containerContents: []dirOrLink{{path: "/lib/libfoo.so", target: "different-target"}, {path: "different-target"}}, |
| shouldExist: []string{"{{ .containerRoot }}/different-target"}, |
| }, |
| } |
| |
| for _, tc := range testCases { |
| t.Run(tc.description, func(t *testing.T) { |
| tmpDir := t.TempDir() |
| hostRoot := filepath.Join(tmpDir, "/host-root/") |
| containerRoot := filepath.Join(tmpDir, "/container-root") |
| require.NoError(t, makeFs(hostRoot)) |
| require.NoError(t, makeFs(containerRoot, tc.containerContents...)) |
| |
| // nvidia-cdi-hook create-symlinks --link libfoo.so.1::/lib/libfoo.so |
| err := getTestCommand().createLink(containerRoot, "libfoo.so.1", "/lib/libfoo.so") |
| require.NoError(t, err) |
| target, err := symlinks.Resolve(filepath.Join(containerRoot, "lib/libfoo.so")) |
| require.NoError(t, err) |
| require.Equal(t, "libfoo.so.1", target) |
| |
| for _, p := range tc.shouldExist { |
| require.DirExists(t, strings.ReplaceAll(p, "{{ .containerRoot }}", containerRoot)) |
| } |
| }) |
| } |
| } |
| |
| func TestCreateLinkOutOfBounds(t *testing.T) { |
| tmpDir := t.TempDir() |
| hostRoot := filepath.Join(tmpDir, "/host-root") |
| containerRoot := filepath.Join(tmpDir, "/container-root") |
| |
| require.NoError(t, |
| makeFs(hostRoot, |
| dirOrLink{path: "libfoo.so"}, |
| ), |
| ) |
| require.NoError(t, |
| makeFs(containerRoot, |
| dirOrLink{path: "/lib"}, |
| dirOrLink{path: "/lib/foo", target: hostRoot}, |
| ), |
| ) |
| |
| path, err := symlinks.Resolve(filepath.Join(containerRoot, "/lib/foo")) |
| require.NoError(t, err) |
| require.Equal(t, hostRoot, path) |
| |
| // nvidia-cdi-hook create-symlinks --link ../libfoo.so.1::/lib/foo/libfoo.so |
| _ = getTestCommand().createLink(containerRoot, "../libfoo.so.1", "/lib/foo/libfoo.so") |
| require.NoError(t, err) |
| |
| target, err := symlinks.Resolve(filepath.Join(containerRoot, hostRoot, "libfoo.so")) |
| require.NoError(t, err) |
| require.Equal(t, "../libfoo.so.1", target) |
| |
| require.DirExists(t, filepath.Join(hostRoot, "libfoo.so")) |
| } |
| |
| type dirOrLink struct { |
| path string |
| target string |
| } |
| |
| func makeFs(tmpdir string, fs ...dirOrLink) error { |
| if err := os.MkdirAll(tmpdir, 0o755); err != nil { |
| return err |
| } |
| for _, s := range fs { |
| s.path = filepath.Join(tmpdir, s.path) |
| if s.target == "" { |
| _ = os.MkdirAll(s.path, 0o755) |
| continue |
| } |
| if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil { |
| return err |
| } |
| if err := os.Symlink(s.target, s.path); err != nil && !os.IsExist(err) { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| // getTestCommand creates a command for running tests against. |
| func getTestCommand() *command { |
| logger, _ := testlog.NewNullLogger() |
| return &command{ |
| logger: logger, |
| } |
| } |