diff --git a/internal/ldconfig/ldconfig.go b/internal/ldconfig/ldconfig.go index 4c9939f1e..03dabf1b2 100644 --- a/internal/ldconfig/ldconfig.go +++ b/internal/ldconfig/ldconfig.go @@ -37,6 +37,15 @@ const ( // higher precedence than other libraries on the system, but lower than // the 00-cuda-compat that is included in some containers. ldsoconfdFilenamePattern = "00-nvcr-*.conf" + // defaultTopLevelLdsoconfFilePath is the standard location of the top-level ld.so.conf file. + // Most container images based on a distro will have this file, but distroless container images + // may not. + defaultTopLevelLdsoconfFilePath = "/etc/ld.so.conf" + // defaultLdsoconfdDir is the standard location for the ld.so.conf.d drop-in directory. Most + // container images based on a distro will have this directory included by the top-level + // ld.so.conf file, but some may not. And some container images may not have a top-level + // ld.so.conf file at all. + defaultLdsoconfdDir = "/etc/ld.so.conf.d" ) type Ldconfig struct { @@ -115,20 +124,22 @@ func (l *Ldconfig) UpdateLDCache() error { // Explicitly specify using /etc/ld.so.conf since the host's ldconfig may // be configured to use a different config file by default. - const topLevelLdsoconfFilePath = "/etc/ld.so.conf" - filteredDirectories, err := l.filterDirectories(topLevelLdsoconfFilePath, l.directories...) + filteredDirectories, err := l.filterDirectories(defaultTopLevelLdsoconfFilePath, l.directories...) if err != nil { return err } args := []string{ filepath.Base(ldconfigPath), - "-f", topLevelLdsoconfFilePath, + "-f", defaultTopLevelLdsoconfFilePath, "-C", "/etc/ld.so.cache", } - if err := createLdsoconfdFile(ldsoconfdFilenamePattern, filteredDirectories...); err != nil { - return fmt.Errorf("failed to update ld.so.conf.d: %w", err) + if err := ensureLdsoconfFile(defaultTopLevelLdsoconfFilePath, defaultLdsoconfdDir); err != nil { + return fmt.Errorf("failed to ensure ld.so.conf file: %w", err) + } + if err := createLdsoconfdFile(defaultLdsoconfdDir, ldsoconfdFilenamePattern, filteredDirectories...); err != nil { + return fmt.Errorf("failed to create ld.so.conf.d drop-in file: %w", err) } return SafeExec(ldconfigPath, args, nil) @@ -179,19 +190,15 @@ func (l *Ldconfig) filterDirectories(configFilePath string, directories ...strin return filtered, nil } -// createLdsoconfdFile creates a file at /etc/ld.so.conf.d/. -// The file is created at /etc/ld.so.conf.d/{{ .pattern }} using `CreateTemp` and -// contains the specified directories on each line. -func createLdsoconfdFile(pattern string, dirs ...string) error { +// createLdsoconfdFile creates a ld.so.conf.d drop-in file with the specified directories on each +// line. The file is created at `ldsoconfdDir`/{{ .pattern }} using `CreateTemp`. +func createLdsoconfdFile(ldsoconfdDir, pattern string, dirs ...string) error { if len(dirs) == 0 { return nil } - - ldsoconfdDir := "/etc/ld.so.conf.d" if err := os.MkdirAll(ldsoconfdDir, 0755); err != nil { return fmt.Errorf("failed to create ld.so.conf.d: %w", err) } - configFile, err := os.CreateTemp(ldsoconfdDir, pattern) if err != nil { return fmt.Errorf("failed to create config file: %w", err) @@ -205,7 +212,7 @@ func createLdsoconfdFile(pattern string, dirs ...string) error { if added[dir] { continue } - _, err = fmt.Fprintf(configFile, "%s\n", dir) + _, err := fmt.Fprintf(configFile, "%s\n", dir) if err != nil { return fmt.Errorf("failed to update config file: %w", err) } @@ -220,6 +227,25 @@ func createLdsoconfdFile(pattern string, dirs ...string) error { return nil } +// ensureLdsoconfFile creates a "standard" top-level ld.so.conf file if none exists. +// +// The created file will contain a single include statement for "`ldsoconfdDir`/*.conf". +func ensureLdsoconfFile(topLevelLdsoconfFilePath, ldsoconfdDir string) error { + configFile, err := os.OpenFile(topLevelLdsoconfFilePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0644) + if err != nil { + if os.IsExist(err) { + return nil + } + return fmt.Errorf("failed to create top-level ld.so.conf file: %w", err) + } + defer configFile.Close() + _, err = configFile.WriteString("include " + ldsoconfdDir + "/*.conf\n") + if err != nil { + return fmt.Errorf("failed to write to top-level ld.so.conf file: %w", err) + } + return nil +} + // getLdsoconfDirectories returns a map of ldsoconf directories to the conf // files that refer to the directory. func (l *Ldconfig) getLdsoconfDirectories(configFilePath string) (map[string]struct{}, error) { diff --git a/internal/ldconfig/ldconfig_test.go b/internal/ldconfig/ldconfig_test.go index a22876719..c983e7f4f 100644 --- a/internal/ldconfig/ldconfig_test.go +++ b/internal/ldconfig/ldconfig_test.go @@ -19,6 +19,7 @@ package ldconfig import ( "os" + "path/filepath" "strings" "testing" @@ -124,3 +125,124 @@ include INCLUDED_PATTERN* }) } } + +func TestCreateLdsoconfdFile(t *testing.T) { + testCases := []struct { + description string + pattern string + dirs []string + expectedContent []string + }{ + { + description: "empty directories", + pattern: "test-*.conf", + dirs: []string{}, + expectedContent: nil, + }, + { + description: "single directory", + pattern: "test-*.conf", + dirs: []string{"/usr/local/lib"}, + expectedContent: []string{ + "/usr/local/lib", + }, + }, + { + description: "multiple directories", + pattern: "test-*.conf", + dirs: []string{"/usr/local/lib", "/opt/lib", "/usr/lib64"}, + expectedContent: []string{ + "/usr/local/lib", + "/opt/lib", + "/usr/lib64", + }, + }, + { + description: "duplicate directories", + pattern: "test-*.conf", + dirs: []string{"/usr/local/lib", "/opt/lib", "/usr/local/lib"}, + expectedContent: []string{ + "/usr/local/lib", + "/opt/lib", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + tmpDir := t.TempDir() + + err := createLdsoconfdFile(tmpDir, tc.pattern, tc.dirs...) + require.NoError(t, err) + + if len(tc.expectedContent) == 0 { + entries, err := os.ReadDir(tmpDir) + require.NoError(t, err) + require.Empty(t, entries) + return + } + + entries, err := os.ReadDir(tmpDir) + require.NoError(t, err) + require.Len(t, entries, 1) + createdFile := filepath.Join(tmpDir, entries[0].Name()) + + info, err := os.Stat(createdFile) + require.NoError(t, err) + require.Equal(t, os.FileMode(0644), info.Mode().Perm()) + + content, err := os.ReadFile(createdFile) + require.NoError(t, err) + lines := strings.Split(strings.TrimSpace(string(content)), "\n") + require.Equal(t, tc.expectedContent, lines) + }) + } +} + +func TestEnsureLdsoconfFile(t *testing.T) { + testCases := []struct { + description string + existingContent string + ldsoconfdDir string + expectCreation bool + expectedContent string + }{ + { + description: "creates file when none exists", + existingContent: "", + ldsoconfdDir: "/custom/ld.so.conf.d", + expectCreation: true, + expectedContent: "include /custom/ld.so.conf.d/*.conf\n", + }, + { + description: "does not modify existing file", + existingContent: "# custom config\n/usr/local/lib\n", + ldsoconfdDir: "/etc/ld.so.conf.d", + expectCreation: false, + expectedContent: "# custom config\n/usr/local/lib\n", + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + tmpDir := t.TempDir() + confFilePath := filepath.Join(tmpDir, "ld.so.conf") + + if tc.existingContent != "" { + err := os.WriteFile(confFilePath, []byte(tc.existingContent), 0644) //nolint:gosec + require.NoError(t, err) + } + + err := ensureLdsoconfFile(confFilePath, tc.ldsoconfdDir) + require.NoError(t, err) + + info, err := os.Stat(confFilePath) + require.NoError(t, err) + require.Equal(t, os.FileMode(0644), info.Mode().Perm()) + + content, err := os.ReadFile(confFilePath) + require.NoError(t, err) + require.Equal(t, tc.expectedContent, string(content)) + }) + } +}