Skip to content

Commit 8328180

Browse files
authored
Merge pull request #773 from elezar/fix-libcuda-symlink
Force creation of symlinks in create-symlink hook
2 parents ab0a4ea + 324096c commit 8328180

File tree

3 files changed

+87
-52
lines changed

3 files changed

+87
-52
lines changed

cmd/nvidia-cdi-hook/create-symlinks/create-symlinks.go

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func (m command) build() *cli.Command {
6363
c.Flags = []cli.Flag{
6464
&cli.StringSliceFlag{
6565
Name: "link",
66-
Usage: "Specify a specific link to create. The link is specified as target::link",
66+
Usage: "Specify a specific link to create. The link is specified as target::link. If the link exists in the container root, it is removed.",
6767
Destination: &cfg.links,
6868
},
6969
// The following flags are testing-only flags.
@@ -112,18 +112,19 @@ func (m command) run(c *cli.Context, cfg *config) error {
112112
// createLink creates a symbolic link in the specified container root.
113113
// This is equivalent to:
114114
//
115-
// chroot {{ .containerRoot }} ln -s {{ .target }} {{ .link }}
115+
// chroot {{ .containerRoot }} ln -f -s {{ .target }} {{ .link }}
116116
//
117117
// If the specified link already exists and points to the same target, this
118-
// operation is a no-op. If the link points to a different target, an error is
119-
// returned.
118+
// operation is a no-op.
119+
// If a file exists at the link path or the link points to a different target
120+
// this file is removed before creating the link.
120121
//
121122
// Note that if the link path resolves to an absolute path oudside of the
122123
// specified root, this is treated as an absolute path in this root.
123124
func (m command) createLink(containerRoot string, targetPath string, link string) error {
124125
linkPath := filepath.Join(containerRoot, link)
125126

126-
exists, err := doesLinkExist(targetPath, linkPath)
127+
exists, err := linkExists(targetPath, linkPath)
127128
if err != nil {
128129
return fmt.Errorf("failed to check if link exists: %w", err)
129130
}
@@ -132,27 +133,31 @@ func (m command) createLink(containerRoot string, targetPath string, link string
132133
return nil
133134
}
134135

135-
resolvedLinkPath, err := symlink.FollowSymlinkInScope(linkPath, containerRoot)
136+
// We resolve the parent of the symlink that we're creating in the container root.
137+
// If we resolve the full link path, an existing link at the location itself
138+
// is also resolved here and we are unable to force create the link.
139+
resolvedLinkParent, err := symlink.FollowSymlinkInScope(filepath.Dir(linkPath), containerRoot)
136140
if err != nil {
137141
return fmt.Errorf("failed to follow path for link %v relative to %v: %w", link, containerRoot, err)
138142
}
143+
resolvedLinkPath := filepath.Join(resolvedLinkParent, filepath.Base(linkPath))
139144

140145
m.logger.Infof("Symlinking %v to %v", resolvedLinkPath, targetPath)
141146
err = os.MkdirAll(filepath.Dir(resolvedLinkPath), 0755)
142147
if err != nil {
143148
return fmt.Errorf("failed to create directory: %v", err)
144149
}
145-
err = os.Symlink(targetPath, resolvedLinkPath)
150+
err = symlinks.ForceCreate(targetPath, resolvedLinkPath)
146151
if err != nil {
147152
return fmt.Errorf("failed to create symlink: %v", err)
148153
}
149154

150155
return nil
151156
}
152157

153-
// doesLinkExist returns true if link exists and points to target.
154-
// An error is returned if link exists but points to a different target.
155-
func doesLinkExist(target string, link string) (bool, error) {
158+
// linkExists checks whether the specified link exists.
159+
// A link exists if the path exists, is a symlink, and points to the specified target.
160+
func linkExists(target string, link string) (bool, error) {
156161
currentTarget, err := symlinks.Resolve(link)
157162
if errors.Is(err, os.ErrNotExist) {
158163
return false, nil
@@ -163,5 +168,5 @@ func doesLinkExist(target string, link string) (bool, error) {
163168
if currentTarget == target {
164169
return true, nil
165170
}
166-
return true, fmt.Errorf("unexpected link target: %s", currentTarget)
171+
return false, nil
167172
}

cmd/nvidia-cdi-hook/create-symlinks/create-symlinks_test.go

Lines changed: 56 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
"github.com/NVIDIA/nvidia-container-toolkit/internal/lookup/symlinks"
1313
)
1414

15-
func TestDoesLinkExist(t *testing.T) {
15+
func TestLinkExist(t *testing.T) {
1616
tmpDir := t.TempDir()
1717
require.NoError(
1818
t,
@@ -22,21 +22,23 @@ func TestDoesLinkExist(t *testing.T) {
2222
),
2323
)
2424

25-
exists, err := doesLinkExist("d", filepath.Join(tmpDir, "/a/b/c"))
25+
exists, err := linkExists("d", filepath.Join(tmpDir, "/a/b/c"))
2626
require.NoError(t, err)
2727
require.True(t, exists)
2828

29-
exists, err = doesLinkExist("/a/b/f", filepath.Join(tmpDir, "/a/b/e"))
29+
exists, err = linkExists("/a/b/f", filepath.Join(tmpDir, "/a/b/e"))
3030
require.NoError(t, err)
3131
require.True(t, exists)
3232

33-
_, err = doesLinkExist("different-target", filepath.Join(tmpDir, "/a/b/c"))
34-
require.Error(t, err)
33+
exists, err = linkExists("different-target", filepath.Join(tmpDir, "/a/b/c"))
34+
require.NoError(t, err)
35+
require.False(t, exists)
3536

36-
_, err = doesLinkExist("/a/b/d", filepath.Join(tmpDir, "/a/b/c"))
37-
require.Error(t, err)
37+
exists, err = linkExists("/a/b/d", filepath.Join(tmpDir, "/a/b/c"))
38+
require.NoError(t, err)
39+
require.False(t, exists)
3840

39-
exists, err = doesLinkExist("foo", filepath.Join(tmpDir, "/a/b/does-not-exist"))
41+
exists, err = linkExists("foo", filepath.Join(tmpDir, "/a/b/does-not-exist"))
4042
require.NoError(t, err)
4143
require.False(t, exists)
4244
}
@@ -190,43 +192,55 @@ func TestCreateLinkAbsolutePath(t *testing.T) {
190192
}
191193

192194
func TestCreateLinkAlreadyExists(t *testing.T) {
193-
tmpDir := t.TempDir()
194-
hostRoot := filepath.Join(tmpDir, "/host-root/")
195-
containerRoot := filepath.Join(tmpDir, "/container-root")
196-
197-
require.NoError(t, makeFs(hostRoot))
198-
require.NoError(t, makeFs(containerRoot, dirOrLink{path: "/lib/libfoo.so", target: "libfoo.so.1"}))
199-
200-
// nvidia-cdi-hook create-symlinks --link libfoo.so.1::/lib/libfoo.so
201-
err := getTestCommand().createLink(containerRoot, "libfoo.so.1", "/lib/libfoo.so")
202-
require.NoError(t, err)
203-
target, err := symlinks.Resolve(filepath.Join(containerRoot, "lib/libfoo.so"))
204-
require.NoError(t, err)
205-
require.Equal(t, "libfoo.so.1", target)
206-
}
195+
testCases := []struct {
196+
description string
197+
containerContents []dirOrLink
198+
shouldExist []string
199+
}{
200+
{
201+
description: "link already exists with correct target",
202+
containerContents: []dirOrLink{{path: "/lib/libfoo.so", target: "libfoo.so.1"}},
203+
shouldExist: []string{},
204+
},
205+
{
206+
description: "link already exists with different target",
207+
containerContents: []dirOrLink{{path: "/lib/libfoo.so", target: "different-target"}, {path: "different-target"}},
208+
shouldExist: []string{"{{ .containerRoot }}/different-target"},
209+
},
210+
}
207211

208-
func TestCreateLinkAlreadyExistsDifferentTarget(t *testing.T) {
209-
tmpDir := t.TempDir()
210-
hostRoot := filepath.Join(tmpDir, "/host-root/")
211-
containerRoot := filepath.Join(tmpDir, "/container-root")
212+
for _, tc := range testCases {
213+
t.Run(tc.description, func(t *testing.T) {
214+
tmpDir := t.TempDir()
215+
hostRoot := filepath.Join(tmpDir, "/host-root/")
216+
containerRoot := filepath.Join(tmpDir, "/container-root")
217+
require.NoError(t, makeFs(hostRoot))
218+
require.NoError(t, makeFs(containerRoot, tc.containerContents...))
212219

213-
require.NoError(t, makeFs(hostRoot))
214-
require.NoError(t, makeFs(containerRoot, dirOrLink{path: "/lib/libfoo.so", target: "different-target"}))
220+
// nvidia-cdi-hook create-symlinks --link libfoo.so.1::/lib/libfoo.so
221+
err := getTestCommand().createLink(containerRoot, "libfoo.so.1", "/lib/libfoo.so")
222+
require.NoError(t, err)
223+
target, err := symlinks.Resolve(filepath.Join(containerRoot, "lib/libfoo.so"))
224+
require.NoError(t, err)
225+
require.Equal(t, "libfoo.so.1", target)
215226

216-
// nvidia-cdi-hook create-symlinks --link libfoo.so.1::/lib/libfoo.so
217-
err := getTestCommand().createLink(containerRoot, "libfoo.so.1", "/lib/libfoo.so")
218-
require.Error(t, err)
219-
target, err := symlinks.Resolve(filepath.Join(containerRoot, "lib/libfoo.so"))
220-
require.NoError(t, err)
221-
require.Equal(t, "different-target", target)
227+
for _, p := range tc.shouldExist {
228+
require.DirExists(t, strings.ReplaceAll(p, "{{ .containerRoot }}", containerRoot))
229+
}
230+
})
231+
}
222232
}
223233

224234
func TestCreateLinkOutOfBounds(t *testing.T) {
225235
tmpDir := t.TempDir()
226-
hostRoot := filepath.Join(tmpDir, "/host-root/")
236+
hostRoot := filepath.Join(tmpDir, "/host-root")
227237
containerRoot := filepath.Join(tmpDir, "/container-root")
228238

229-
require.NoError(t, makeFs(hostRoot))
239+
require.NoError(t,
240+
makeFs(hostRoot,
241+
dirOrLink{path: "libfoo.so"},
242+
),
243+
)
230244
require.NoError(t,
231245
makeFs(containerRoot,
232246
dirOrLink{path: "/lib"},
@@ -240,12 +254,13 @@ func TestCreateLinkOutOfBounds(t *testing.T) {
240254

241255
// nvidia-cdi-hook create-symlinks --link ../libfoo.so.1::/lib/foo/libfoo.so
242256
_ = getTestCommand().createLink(containerRoot, "../libfoo.so.1", "/lib/foo/libfoo.so")
243-
// TODO: We need to enabled this check once we have updated the implementation.
244-
// require.Error(t, err)
245-
_, err = os.Lstat(filepath.Join(hostRoot, "libfoo.so"))
246-
require.ErrorIs(t, err, os.ErrNotExist)
247-
_, err = os.Lstat(filepath.Join(containerRoot, hostRoot, "libfoo.so"))
248257
require.NoError(t, err)
258+
259+
target, err := symlinks.Resolve(filepath.Join(containerRoot, hostRoot, "libfoo.so"))
260+
require.NoError(t, err)
261+
require.Equal(t, "../libfoo.so.1", target)
262+
263+
require.DirExists(t, filepath.Join(hostRoot, "libfoo.so"))
249264
}
250265

251266
type dirOrLink struct {

internal/lookup/symlinks/symlink.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,18 @@ func Resolve(filename string) (string, error) {
3333

3434
return os.Readlink(filename)
3535
}
36+
37+
// ForceCreate creates a specified symlink.
38+
// If a file (or empty directory) exists at the path it is removed.
39+
func ForceCreate(target string, link string) error {
40+
_, err := os.Lstat(link)
41+
if err != nil && !os.IsNotExist(err) {
42+
return fmt.Errorf("failed to get file info: %w", err)
43+
}
44+
if !os.IsNotExist(err) {
45+
if err := os.Remove(link); err != nil {
46+
return fmt.Errorf("failed to remove existing file: %w", err)
47+
}
48+
}
49+
return os.Symlink(target, link)
50+
}

0 commit comments

Comments
 (0)