main bac21c6

app_device_policy_test.go RawHistoryFolder

1package domain
2
3import (
4	"errors"
5	"testing"
6	"time"
7
8	"github.com/stretchr/testify/assert"
9	"github.com/stretchr/testify/require"
10)
11
12func validDevicePolicy() AppDevicePolicy {
13	return AppDevicePolicy{
14		Name:            "test_gpu",
15		CDI:             []string{"example.com/gpu=GPU-test-uuid"},
16		AllowedApps:     []string{"demo"},
17		AllowedServices: []string{"worker"},
18	}
19}
20
21func TestDevicePolicyValid(t *testing.T) {
22	p := validDevicePolicy()
23	require.NoError(t, p.Validate())
24}
25
26func TestDevicePolicyNameRejected(t *testing.T) {
27	for _, name := range []string{"", "Bad", "a--b", "has space"} {
28		p := validDevicePolicy()
29		p.Name = name
30		err := p.Validate()
31		require.Error(t, err, "name %q", name)
32		assert.True(t, errors.Is(err, ErrDevicePolicy))
33	}
34}
35
36func TestDevicePolicyCDIRejected(t *testing.T) {
37	cases := map[string][]string{
38		"empty":               {},
39		"empty entry":         {""},
40		"raw path":            {"/dev/card0"},
41		"unqualified":         {"gpu0"},
42		"missing name":        {"example.com/gpu="},
43		"missing kind":        {"=gpu0"},
44		"aggregate":           {"example.com/gpu=all"},
45		"duplicate":           {"example.com/gpu=0", "example.com/gpu=0"},
46		"qualified host path": {"example.com/gpu=/dev/card0"},
47	}
48	for name, cdi := range cases {
49		p := validDevicePolicy()
50		p.CDI = cdi
51		err := p.Validate()
52		require.Error(t, err, "case %s", name)
53		assert.True(t, errors.Is(err, ErrDevicePolicy), "case %s", name)
54	}
55}
56
57func TestDevicePolicyAllowlistRejected(t *testing.T) {
58	p := validDevicePolicy()
59	p.AllowedApps = nil
60	err := p.Validate()
61	require.Error(t, err)
62	assert.True(t, errors.Is(err, ErrDevicePolicy))
63
64	p = validDevicePolicy()
65	p.AllowedApps = []string{"demo", "demo"}
66	require.Error(t, p.Validate())
67
68	p = validDevicePolicy()
69	p.AllowedServices = []string{"worker", "worker"}
70	require.Error(t, p.Validate())
71
72	// An invalid allowlist entry is both a policy violation and an invalid
73	// app spec: callers must be able to detect either sentinel.
74	p = validDevicePolicy()
75	p.AllowedServices = []string{"Bad Name"}
76	err = p.Validate()
77	require.Error(t, err)
78	assert.True(t, errors.Is(err, ErrDevicePolicy))
79	assert.True(t, errors.Is(err, ErrInvalidAppSpec), "the invalid-spec chain must survive allowlist validation")
80}
81
82func TestResolveAppDeviceDenyByDefault(t *testing.T) {
83	p := validDevicePolicy()
84	_, err := p.ResolveAppDevice("other", "worker")
85	require.Error(t, err)
86	assert.True(t, errors.Is(err, ErrDevicePolicy))
87
88	_, err = p.ResolveAppDevice("demo", "helper")
89	require.Error(t, err)
90	assert.True(t, errors.Is(err, ErrDevicePolicy))
91}
92
93func TestResolveAppDeviceExactGrant(t *testing.T) {
94	p := validDevicePolicy()
95	ids, err := p.ResolveAppDevice("demo", "worker")
96	require.NoError(t, err)
97	assert.Equal(t, []string{"example.com/gpu=GPU-test-uuid"}, ids)
98	// The returned slice must be a copy: mutating it must not affect
99	// later resolutions.
100	ids[0] = "mutated"
101	again, err := p.ResolveAppDevice("demo", "worker")
102	require.NoError(t, err)
103	assert.Equal(t, []string{"example.com/gpu=GPU-test-uuid"}, again)
104}
105
106func TestResolveAppDevicesUnknownDenied(t *testing.T) {
107	policies := map[string]AppDevicePolicy{"test_gpu": validDevicePolicy()}
108	_, err := ResolveAppDevices("demo", "worker", []string{"unknown"}, policies)
109	require.Error(t, err)
110	assert.True(t, errors.Is(err, ErrDevicePolicy))
111}
112
113func TestResolveAppDevicesDeterministicUnion(t *testing.T) {
114	policies := map[string]AppDevicePolicy{
115		"b": {Name: "b", CDI: []string{"example.com/gpu=GPU-b"}, AllowedApps: []string{"demo"}, AllowedServices: []string{"worker"}},
116		"a": {Name: "a", CDI: []string{"example.com/gpu=GPU-a"}, AllowedApps: []string{"demo"}, AllowedServices: []string{"worker"}},
117	}
118	ids, err := ResolveAppDevices("demo", "worker", []string{"b", "a"}, policies)
119	require.NoError(t, err)
120	assert.Equal(t, []string{"example.com/gpu=GPU-a", "example.com/gpu=GPU-b"}, ids)
121}
122
123func TestResolveAppDevicesDuplicateIDRejected(t *testing.T) {
124	shared := "example.com/gpu=GPU-shared"
125	policies := map[string]AppDevicePolicy{
126		"a": {Name: "a", CDI: []string{shared}, AllowedApps: []string{"demo"}, AllowedServices: []string{"worker"}},
127		"b": {Name: "b", CDI: []string{shared}, AllowedApps: []string{"demo"}, AllowedServices: []string{"worker"}},
128	}
129	_, err := ResolveAppDevices("demo", "worker", []string{"a", "b"}, policies)
130	require.Error(t, err)
131	assert.True(t, errors.Is(err, ErrDevicePolicy))
132	assert.NotContains(t, err.Error(), shared, "duplicate-grant errors must not echo resolved CDI IDs")
133}
134
135func TestResolveAppDevicesEmptyIsNil(t *testing.T) {
136	ids, err := ResolveAppDevices("demo", "worker", nil, nil)
137	require.NoError(t, err)
138	assert.Nil(t, ids)
139}
140
141func TestValidateDeviceName(t *testing.T) {
142	require.NoError(t, ValidateDeviceName("test_gpu"))
143	for _, name := range []string{"", "Bad", "a--b", "/dev/card0"} {
144		require.Error(t, ValidateDeviceName(name), "name %q", name)
145	}
146}
147
148func TestAppServiceDevicesValidation(t *testing.T) {
149	base := AppSpec{Name: "demo", Services: []AppService{{Name: "worker", Image: "registry.example.com/demo/worker:1", StopGrace: 10 * time.Second, Readiness: AppReadiness{Type: "http", Path: "/healthz", Timeout: 30 * time.Second}}}}
150	valid := base
151	valid.Services[0].Devices = []string{"test_gpu"}
152	require.NoError(t, valid.Validate())
153
154	dup := base
155	dup.Services[0].Devices = []string{"test_gpu", "test_gpu"}
156	err := dup.Validate()
157	require.Error(t, err)
158	assert.True(t, errors.Is(err, ErrInvalidAppSpec))
159	assert.Contains(t, err.Error(), "duplicate device")
160
161	bad := base
162	bad.Services[0].Devices = []string{"Bad!"}
163	err = bad.Validate()
164	require.Error(t, err)
165	assert.True(t, errors.Is(err, ErrInvalidAppSpec))
166}
167
168func TestDiffAppSpecDevices(t *testing.T) {
169	svc := func(devices ...string) AppService {
170		return AppService{Name: "worker", Image: "registry.example.com/demo/worker:1", StopGrace: 10 * time.Second, Readiness: AppReadiness{Type: "http", Path: "/healthz", Timeout: 30 * time.Second}, Devices: devices}
171	}
172	mk := func(services ...AppService) AppSpec { return AppSpec{Name: "demo", Services: services} }
173
174	// Add/remove report service/worker/devices.
175	oldSpec, nextSpec := mk(svc()), mk(svc("test_gpu"))
176	diff := DiffAppSpec(oldSpec, nextSpec)
177	assert.Contains(t, diff.Changed, "service/worker/devices")
178
179	// Reorder-only is a no-op.
180	oldSpec, nextSpec = mk(svc("a", "b")), mk(svc("b", "a"))
181	diff = DiffAppSpec(oldSpec, nextSpec)
182	assert.NotContains(t, diff.Changed, "service/worker/devices")
183
184	// Nil vs empty stays a no-op.
185	oldSpec, nextSpec = mk(svc()), mk(svc([]string{}...))
186	diff = DiffAppSpec(oldSpec, nextSpec)
187	assert.NotContains(t, diff.Changed, "service/worker/devices")
188}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close