Skip to content

Commit 37a16a7

Browse files
committed
[no-relnote] Refactor creation of discoverer for tegra systems
This change updates the way we construct a discoverer for tegra systems to be more flexible in terms of how the SOURCES of the mount specs can be specified. This allows for subsequent changes like adding (or removing) mount specs at the point of construction. Signed-off-by: Evan Lezar <[email protected]>
1 parent 3b57dbe commit 37a16a7

File tree

7 files changed

+270
-145
lines changed

7 files changed

+270
-145
lines changed

internal/platform-support/tegra/csv.go

Lines changed: 31 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,36 +17,30 @@
1717
package tegra
1818

1919
import (
20-
"fmt"
21-
2220
"github.com/NVIDIA/nvidia-container-toolkit/internal/discover"
2321
"github.com/NVIDIA/nvidia-container-toolkit/internal/logger"
2422
"github.com/NVIDIA/nvidia-container-toolkit/internal/lookup"
2523
"github.com/NVIDIA/nvidia-container-toolkit/internal/platform-support/tegra/csv"
2624
)
2725

28-
// newDiscovererFromCSVFiles creates a discoverer for the specified CSV files. A logger is also supplied.
29-
// The constructed discoverer is comprised of a list, with each element in the list being associated with a
30-
// single CSV files.
31-
func (o tegraOptions) newDiscovererFromCSVFiles() (discover.Discover, error) {
32-
if len(o.csvFiles) == 0 {
33-
o.logger.Warningf("No CSV files specified")
26+
func (o options) newDiscovererFromMountSpecs() (discover.Discover, error) {
27+
pathsByType := o.MountSpecPathsByType()
28+
if len(pathsByType) == 0 {
29+
o.logger.Warningf("No mount specs specified")
3430
return discover.None{}, nil
3531
}
3632

37-
targetsByType := getTargetsFromCSVFiles(o.logger, o.csvFiles)
38-
3933
devices := discover.NewCharDeviceDiscoverer(
4034
o.logger,
4135
o.devRoot,
42-
targetsByType[csv.MountSpecDev],
36+
pathsByType[csv.MountSpecDev],
4337
)
4438

4539
directories := discover.NewMounts(
4640
o.logger,
4741
lookup.NewDirectoryLocator(lookup.WithLogger(o.logger), lookup.WithRoot(o.driverRoot)),
4842
o.driverRoot,
49-
targetsByType[csv.MountSpecDir],
43+
pathsByType[csv.MountSpecDir],
5044
)
5145

5246
// We create a discoverer for mounted libraries and add additional .so
@@ -57,14 +51,14 @@ func (o tegraOptions) newDiscovererFromCSVFiles() (discover.Discover, error) {
5751
o.logger,
5852
o.symlinkLocator,
5953
o.driverRoot,
60-
targetsByType[csv.MountSpecLib],
54+
pathsByType[csv.MountSpecLib],
6155
),
6256
"",
6357
o.hookCreator,
6458
)
6559

6660
// We process the explicitly requested symlinks.
67-
symlinkTargets := o.ignorePatterns.Apply(targetsByType[csv.MountSpecSym]...)
61+
symlinkTargets := pathsByType[csv.MountSpecSym]
6862
o.logger.Debugf("Filtered symlink targets: %v", symlinkTargets)
6963
symlinks := discover.NewMounts(
7064
o.logger,
@@ -85,35 +79,34 @@ func (o tegraOptions) newDiscovererFromCSVFiles() (discover.Discover, error) {
8579
return d, nil
8680
}
8781

88-
// getTargetsFromCSVFiles returns the list of mount specs from the specified CSV files.
89-
// These are aggregated by mount spec type.
90-
// TODO: We use a function variable here to allow this to be overridden for testing.
91-
// This should be properly mocked.
92-
var getTargetsFromCSVFiles = func(logger logger.Interface, files []string) map[csv.MountSpecType][]string {
93-
targetsByType := make(map[csv.MountSpecType][]string)
94-
for _, filename := range files {
95-
targets, err := loadCSVFile(logger, filename)
96-
if err != nil {
97-
logger.Warningf("Skipping CSV file %v: %v", filename, err)
98-
continue
99-
}
100-
for _, t := range targets {
101-
targetsByType[t.Type] = append(targetsByType[t.Type], t.Path)
102-
}
82+
// MountSpecsFromCSVFiles returns a MountSpecPathsByTyper for the specified list
83+
// of CSV files.
84+
func MountSpecsFromCSVFiles(logger logger.Interface, csvFiles ...string) MountSpecPathsByTyper {
85+
var tts []MountSpecPathsByTyper
86+
87+
for _, filename := range csvFiles {
88+
tts = append(tts, &fromCSVFile{logger, filename})
10389
}
104-
return targetsByType
90+
return Merge(tts...)
10591
}
10692

107-
// loadCSVFile loads the specified CSV file and returns the list of mount specs
108-
func loadCSVFile(logger logger.Interface, filename string) ([]*csv.MountSpec, error) {
93+
type fromCSVFile struct {
94+
logger logger.Interface
95+
filename string
96+
}
97+
98+
// MountSpecPathsByType returns mountspecs defined in the specified CSV file.
99+
func (t *fromCSVFile) MountSpecPathsByType() MountSpecPathsByType {
109100
// Create a discoverer for each file-kind combination
110-
targets, err := csv.NewCSVFileParser(logger, filename).Parse()
101+
targets, err := csv.NewCSVFileParser(t.logger, t.filename).Parse()
111102
if err != nil {
112-
return nil, fmt.Errorf("failed to parse CSV file: %v", err)
113-
}
114-
if len(targets) == 0 {
115-
return nil, fmt.Errorf("CSV file is empty")
103+
t.logger.Warningf("failed to parse CSV file %v: %v", t.filename, err)
104+
return nil
116105
}
117106

118-
return targets, nil
107+
targetsByType := make(MountSpecPathsByType)
108+
for _, t := range targets {
109+
targetsByType[t.Type] = append(targetsByType[t.Type], t.Path)
110+
}
111+
return targetsByType
119112
}

internal/platform-support/tegra/csv_test.go

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import (
2424
"github.com/stretchr/testify/require"
2525

2626
"github.com/NVIDIA/nvidia-container-toolkit/internal/discover"
27-
"github.com/NVIDIA/nvidia-container-toolkit/internal/logger"
2827
"github.com/NVIDIA/nvidia-container-toolkit/internal/lookup"
2928

3029
"github.com/NVIDIA/nvidia-container-toolkit/internal/platform-support/tegra/csv"
@@ -34,7 +33,7 @@ func TestDiscovererFromCSVFiles(t *testing.T) {
3433
logger, _ := testlog.NewNullLogger()
3534
testCases := []struct {
3635
description string
37-
moutSpecs map[csv.MountSpecType][]string
36+
moutSpecs MountSpecPathsByType
3837
ignorePatterns []string
3938
symlinkLocator lookup.Locator
4039
symlinkChainLocator lookup.Locator
@@ -186,19 +185,19 @@ func TestDiscovererFromCSVFiles(t *testing.T) {
186185
hookCreator := discover.NewHookCreator()
187186
for _, tc := range testCases {
188187
t.Run(tc.description, func(t *testing.T) {
189-
defer setGetTargetsFromCSVFiles(tc.moutSpecs)()
190-
191-
o := tegraOptions{
192-
logger: logger,
193-
hookCreator: hookCreator,
194-
csvFiles: []string{"dummy"},
195-
ignorePatterns: tc.ignorePatterns,
188+
o := options{
189+
logger: logger,
190+
hookCreator: hookCreator,
191+
MountSpecPathsByTyper: Filter(
192+
tc.moutSpecs,
193+
Symlinks(tc.ignorePatterns...),
194+
),
196195
symlinkLocator: tc.symlinkLocator,
197196
symlinkChainLocator: tc.symlinkChainLocator,
198197
resolveSymlink: tc.symlinkResolver,
199198
}
200199

201-
d, err := o.newDiscovererFromCSVFiles()
200+
d, err := o.newDiscovererFromMountSpecs()
202201
require.ErrorIs(t, err, tc.expectedError)
203202

204203
hooks, err := d.Hooks()
@@ -212,14 +211,3 @@ func TestDiscovererFromCSVFiles(t *testing.T) {
212211
})
213212
}
214213
}
215-
216-
func setGetTargetsFromCSVFiles(override map[csv.MountSpecType][]string) func() {
217-
original := getTargetsFromCSVFiles
218-
getTargetsFromCSVFiles = func(logger logger.Interface, files []string) map[csv.MountSpecType][]string {
219-
return override
220-
}
221-
222-
return func() {
223-
getTargetsFromCSVFiles = original
224-
}
225-
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
# SPDX-License-Identifier: Apache-2.0
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
**/
17+
18+
package tegra
19+
20+
import "github.com/NVIDIA/nvidia-container-toolkit/internal/platform-support/tegra/csv"
21+
22+
// A MountSpecPathsByTyper provides a function to return mount specs paths by
23+
// mount type.
24+
// The MountSpecTypes are one of: dev, dir, lib, sym and define how these should
25+
// be included in a container (or represented in the associated CDI spec).
26+
type MountSpecPathsByTyper interface {
27+
MountSpecPathsByType() MountSpecPathsByType
28+
}
29+
30+
type MountSpecPathsByType map[csv.MountSpecType][]string
31+
32+
var _ MountSpecPathsByTyper = (MountSpecPathsByType)(nil)
33+
34+
// MountSpecPathsByType for a variable of type MountSpecPathsByType returns the
35+
// underlying data structure.
36+
// This allows for using this type in functions such as Merge and Filter.
37+
func (m MountSpecPathsByType) MountSpecPathsByType() MountSpecPathsByType {
38+
return m
39+
}
40+
41+
type merge []MountSpecPathsByTyper
42+
43+
// Merge combines the MountSpecPathsByType for the specified sources.
44+
func Merge(sources ...MountSpecPathsByTyper) MountSpecPathsByTyper {
45+
return merge(sources)
46+
}
47+
48+
// MountSpecPathsByType for a set of merged mount specs combines the list of
49+
// paths per type.
50+
func (ts merge) MountSpecPathsByType() MountSpecPathsByType {
51+
targetsByType := make(MountSpecPathsByType)
52+
for _, t := range ts {
53+
if t == nil {
54+
continue
55+
}
56+
for tType, targets := range t.MountSpecPathsByType() {
57+
targetsByType[tType] = append(targetsByType[tType], targets...)
58+
}
59+
}
60+
return targetsByType
61+
}
62+
63+
type filterMountSpecs struct {
64+
from MountSpecPathsByTyper
65+
remove MountSpecPathsByTyper
66+
}
67+
68+
// Filter removes the specified MountSpecPaths (by type) from the specified
69+
// set of MountSpecPaths.
70+
// Here the paths in the remove set are treated as patterns, and elements in
71+
// from that match any specified pattern are filtered out.
72+
func Filter(from MountSpecPathsByTyper, remove MountSpecPathsByTyper) MountSpecPathsByTyper {
73+
return filterMountSpecs{
74+
from: from,
75+
remove: remove,
76+
}
77+
}
78+
79+
// MountSpecPathsByType for a filter get the mountspecs defined in the source
80+
// and apply the specified per-type filters.
81+
func (m filterMountSpecs) MountSpecPathsByType() MountSpecPathsByType {
82+
ms := m.from.MountSpecPathsByType()
83+
if len(ms) == 0 {
84+
return ms
85+
}
86+
87+
for t, patterns := range m.remove.MountSpecPathsByType() {
88+
paths := ms[t]
89+
if len(paths) == 0 {
90+
continue
91+
}
92+
filtered := ignoreMountSpecPatterns(patterns).Apply(paths...)
93+
ms[t] = filtered
94+
}
95+
96+
return ms
97+
}
98+
99+
// DeviceNodes creates a set of MountSpecPaths for the specified device nodes.
100+
// These have the MoutSpecDev type.
101+
func DeviceNodes(dn ...string) MountSpecPathsByTyper {
102+
return MountSpecPathsByType{
103+
csv.MountSpecDev: dn,
104+
}
105+
}
106+
107+
// DeviceNodes creates a set of MountSpecPaths for the specified symlinks.
108+
// These have the MountSpecSym type.
109+
func Symlinks(s ...string) MountSpecPathsByTyper {
110+
return MountSpecPathsByType{
111+
csv.MountSpecSym: s,
112+
}
113+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
# SPDX-License-Identifier: Apache-2.0
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
**/
17+
18+
package tegra
19+
20+
import (
21+
"github.com/NVIDIA/nvidia-container-toolkit/internal/discover"
22+
"github.com/NVIDIA/nvidia-container-toolkit/internal/logger"
23+
"github.com/NVIDIA/nvidia-container-toolkit/internal/lookup"
24+
)
25+
26+
type options struct {
27+
logger logger.Interface
28+
driverRoot string
29+
devRoot string
30+
hookCreator discover.HookCreator
31+
ldconfigPath string
32+
librarySearchPaths []string
33+
34+
// The following can be overridden for testing
35+
symlinkLocator lookup.Locator
36+
symlinkChainLocator lookup.Locator
37+
// TODO: This should be replaced by a regular mock
38+
resolveSymlink func(string) (string, error)
39+
40+
MountSpecPathsByTyper
41+
}
42+
43+
// Option defines a functional option for configuring a Tegra discoverer.
44+
type Option func(*options)
45+
46+
// WithLogger sets the logger for the discoverer.
47+
func WithLogger(logger logger.Interface) Option {
48+
return func(o *options) {
49+
o.logger = logger
50+
}
51+
}
52+
53+
// WithDriverRoot sets the driver root for the discoverer.
54+
func WithDriverRoot(driverRoot string) Option {
55+
return func(o *options) {
56+
o.driverRoot = driverRoot
57+
}
58+
}
59+
60+
// WithDevRoot sets the /dev root.
61+
// If this is unset, the driver root is assumed.
62+
func WithDevRoot(devRoot string) Option {
63+
return func(o *options) {
64+
o.devRoot = devRoot
65+
}
66+
}
67+
68+
// WithHookCreator sets the hook creator for the discoverer.
69+
func WithHookCreator(hookCreator discover.HookCreator) Option {
70+
return func(o *options) {
71+
o.hookCreator = hookCreator
72+
}
73+
}
74+
75+
// WithLdconfigPath sets the path to the ldconfig program
76+
func WithLdconfigPath(ldconfigPath string) Option {
77+
return func(o *options) {
78+
o.ldconfigPath = ldconfigPath
79+
}
80+
}
81+
82+
// WithLibrarySearchPaths sets the library search paths for the discoverer.
83+
func WithLibrarySearchPaths(librarySearchPaths ...string) Option {
84+
return func(o *options) {
85+
o.librarySearchPaths = librarySearchPaths
86+
}
87+
}
88+
89+
// WithMountSpecsByPath sets the source of MountSpec paths per type.
90+
// If multiple values are supplied, these are merged.
91+
func WithMountSpecsByPath(msfp ...MountSpecPathsByTyper) Option {
92+
return func(o *options) {
93+
o.MountSpecPathsByTyper = Merge(msfp...)
94+
}
95+
}
96+
97+
// MountSpecPathsByType returns the mounts specs by path configured for these
98+
// options.
99+
// For an unconfigured MountSpecPathsByTyper no mountspecs are returned.
100+
func (o options) MountSpecPathsByType() MountSpecPathsByType {
101+
if o.MountSpecPathsByTyper == nil {
102+
return nil
103+
}
104+
return o.MountSpecPathsByTyper.MountSpecPathsByType()
105+
}

0 commit comments

Comments
 (0)