Skip to content

Commit 1f24c77

Browse files
committed
Add commandRunnner interface and a basic test
Signed-off-by: Evan Lezar <[email protected]>
1 parent 678d89c commit 1f24c77

File tree

4 files changed

+206
-13
lines changed

4 files changed

+206
-13
lines changed

pkg/mig/reconfigure/api.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package reconfigure
22

3+
import "os/exec"
4+
35
const (
46
MIGConfigStateLabel = "nvidia.com/mig.config.state"
57
VGPUConfigStateLabel = "nvidia.com/vgpu.config.state"
@@ -9,3 +11,11 @@ const (
911
type Reconfigurer interface {
1012
Reconfigure() error
1113
}
14+
15+
// A commandRunner is used to run a constructed command.
16+
// This interface allows us to inject a runner for testing.
17+
//
18+
//go:generate moq -rm -fmt=goimports -out command-runner_mock.go . commandRunner
19+
type commandRunner interface {
20+
Run(*exec.Cmd) error
21+
}

pkg/mig/reconfigure/command-runner_mock.go

Lines changed: 75 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/mig/reconfigure/reconfigure.go

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ const (
2222
ldPreloadEnvVar = "LD_PRELOAD"
2323
)
2424

25+
type reconfigurer struct {
26+
*reconfigureMIGOptions
27+
commandRunner
28+
}
29+
30+
// A commandWithOutput runs a command and ensures that STDERR and STDOUT are
31+
// set.
32+
type commandWithOutput struct{}
33+
34+
var _ commandRunner = (*commandWithOutput)(nil)
35+
2536
// New creates a MIG Reconfigurer with the supplied options.
2637
func New(opts ...Option) (Reconfigurer, error) {
2738
o := &reconfigureMIGOptions{}
@@ -34,15 +45,20 @@ func New(opts ...Option) (Reconfigurer, error) {
3445
return nil, err
3546
}
3647

37-
return o, nil
48+
r := &reconfigurer{
49+
reconfigureMIGOptions: o,
50+
commandRunner: &commandWithOutput{},
51+
}
52+
53+
return r, nil
3854
}
3955

4056
// Reconfigure configures MIG (Multi-Instance GPU) settings on a Kubernetes
4157
// node. It validates the requested configuration, checks the current state,
4258
// applies MIG mode changes, manages host GPU client services, and handles
4359
// reboots when necessary. The function ensures that MIG configurations are
4460
// applied safely with proper service lifecycle management.
45-
func (opts *reconfigureMIGOptions) Reconfigure() error {
61+
func (opts *reconfigurer) Reconfigure() error {
4662
log.Info("Asserting that the requested configuration is present in the configuration file")
4763
if err := opts.assertValidMIGConfig(); err != nil {
4864
return fmt.Errorf("error validating the selected MIG configuration: %w", err)
@@ -124,7 +140,7 @@ func (opts *reconfigureMIGOptions) Reconfigure() error {
124140
return nil
125141
}
126142

127-
func (opts *reconfigureMIGOptions) assertValidMIGConfig() error {
143+
func (opts *reconfigurer) assertValidMIGConfig() error {
128144
args := []string{
129145
"--debug",
130146
"assert",
@@ -135,7 +151,7 @@ func (opts *reconfigureMIGOptions) assertValidMIGConfig() error {
135151
return opts.runMigParted(args...)
136152
}
137153

138-
func (opts *reconfigureMIGOptions) assertMIGConfig() error {
154+
func (opts *reconfigurer) assertMIGConfig() error {
139155
args := []string{
140156
"--debug",
141157
"assert",
@@ -145,7 +161,7 @@ func (opts *reconfigureMIGOptions) assertMIGConfig() error {
145161
return opts.runMigParted(args...)
146162
}
147163

148-
func (opts *reconfigureMIGOptions) assertMIGModeOnly() error {
164+
func (opts *reconfigurer) assertMIGModeOnly() error {
149165
args := []string{
150166
"--debug",
151167
"assert",
@@ -156,7 +172,7 @@ func (opts *reconfigureMIGOptions) assertMIGModeOnly() error {
156172
return opts.runMigParted(args...)
157173
}
158174

159-
func (opts *reconfigureMIGOptions) applyMIGModeOnly() error {
175+
func (opts *reconfigurer) applyMIGModeOnly() error {
160176
args := []string{
161177
"--debug",
162178
"apply",
@@ -167,7 +183,7 @@ func (opts *reconfigureMIGOptions) applyMIGModeOnly() error {
167183
return opts.runMigParted(args...)
168184
}
169185

170-
func (opts *reconfigureMIGOptions) applyMIGConfig() error {
186+
func (opts *reconfigurer) applyMIGConfig() error {
171187
args := []string{
172188
"--debug",
173189
"apply",
@@ -177,7 +193,7 @@ func (opts *reconfigureMIGOptions) applyMIGConfig() error {
177193
return opts.runMigParted(args...)
178194
}
179195

180-
func (opts *reconfigureMIGOptions) hostPersistConfig() error {
196+
func (opts *reconfigurer) hostPersistConfig() error {
181197
config := fmt.Sprintf(`[Service]
182198
Environment="MIG_PARTED_SELECTED_CONFIG=%s"
183199
`, opts.SelectedMIGConfig)
@@ -189,7 +205,7 @@ Environment="MIG_PARTED_SELECTED_CONFIG=%s"
189205
}
190206

191207
cmd := exec.Command("chroot", opts.HostRootMount, "systemctl", "daemon-reload") // #nosec G204 -- HostRootMount is validated via dirpath validator.
192-
return runCommandWithOutput(cmd)
208+
return opts.Run(cmd)
193209
}
194210

195211
func (opts *reconfigureMIGOptions) hostStopSystemdServices() error {
@@ -217,7 +233,7 @@ func (opts *reconfigureMIGOptions) hostStartSystemdServices() error {
217233
log.Infof("Starting %s", service)
218234
cmd := exec.Command("chroot", opts.HostRootMount, "systemctl", "start", service) // #nosec G204 -- HostRootMount validated via dirpath, service validated via systemd_service_name.
219235
if err := cmd.Run(); err != nil {
220-
serviceError := fmt.Errorf("error starting %q: %w", err)
236+
serviceError := fmt.Errorf("error starting %q: %w", service, err)
221237
log.Errorf("%v; skipping, but continuing...", serviceError)
222238

223239
errs = errors.Join(errs, serviceError)
@@ -315,15 +331,15 @@ func shouldRestartService(opts *reconfigureMIGOptions, service string) bool {
315331
return true
316332
}
317333

318-
func runCommandWithOutput(cmd *exec.Cmd) error {
334+
func (c *commandWithOutput) Run(cmd *exec.Cmd) error {
319335
cmd.Stdout = os.Stdout
320336
cmd.Stderr = os.Stderr
321337
return cmd.Run()
322338
}
323339

324-
func (opts *reconfigureMIGOptions) runMigParted(args ...string) error {
340+
func (opts *reconfigurer) runMigParted(args ...string) error {
325341
cmd := opts.migPartedCmd(args...)
326-
return runCommandWithOutput(cmd)
342+
return opts.commandRunner.Run(cmd)
327343
}
328344

329345
func (opts *reconfigureMIGOptions) migPartedCmd(args ...string) *exec.Cmd {
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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 reconfigure
19+
20+
import (
21+
"fmt"
22+
"os/exec"
23+
"testing"
24+
25+
"github.com/stretchr/testify/require"
26+
)
27+
28+
type commandRunnerWithCLI struct {
29+
mock *commandRunnerMock
30+
calls [][]string
31+
}
32+
33+
func (c *commandRunnerWithCLI) Run(cmd *exec.Cmd) error {
34+
c.calls = append(c.calls, append([]string{cmd.Path}, cmd.Args...))
35+
return c.mock.Run(cmd)
36+
}
37+
38+
func TestReconfigure(t *testing.T) {
39+
testCases := []struct {
40+
description string
41+
options reconfigureMIGOptions
42+
commandRunner *commandRunnerWithCLI
43+
expectedError error
44+
expectedCalls [][]string
45+
}{
46+
{
47+
description: "test command failure",
48+
options: reconfigureMIGOptions{
49+
NodeName: "NodeName",
50+
MIGPartedConfigFile: "/path/to/config/file.yaml",
51+
SelectedMIGConfig: "selected-mig-config",
52+
DriverLibraryPath: "/path/to/libnvidia-ml.so.1",
53+
HostRootMount: "/host/",
54+
},
55+
commandRunner: &commandRunnerWithCLI{
56+
mock: &commandRunnerMock{
57+
RunFunc: func(cmd *exec.Cmd) error {
58+
return fmt.Errorf("error running command %v", cmd.Path)
59+
},
60+
},
61+
},
62+
expectedError: fmt.Errorf("error validating the selected MIG configuration: error running command nvidia-mig-parted"),
63+
expectedCalls: [][]string{
64+
[]string{"nvidia-mig-parted", "nvidia-mig-parted", "--debug", "assert", "--valid-config", "--config-file", "/path/to/config/file.yaml", "--selected-config", "selected-mig-config"},
65+
},
66+
},
67+
}
68+
69+
for _, tc := range testCases {
70+
t.Run(tc.description, func(t *testing.T) {
71+
// TODO: Once we have better mocks in place for the following
72+
// functionality, we can update this.
73+
require.False(t, tc.options.WithReboot)
74+
require.False(t, tc.options.WithShutdownHostGPUClients)
75+
76+
// We test explicit validation in a separate test.
77+
// For now we only ensure that the options are valid.
78+
require.NoError(t, tc.options.Validate())
79+
80+
r := &reconfigurer{
81+
reconfigureMIGOptions: &tc.options,
82+
commandRunner: tc.commandRunner,
83+
}
84+
85+
err := r.Reconfigure()
86+
require.EqualValues(t, tc.expectedError.Error(), err.Error())
87+
88+
require.EqualValues(t, tc.expectedCalls, tc.commandRunner.calls)
89+
90+
})
91+
}
92+
}

0 commit comments

Comments
 (0)