app_device_policy.go RawHistoryFolder

1package domain
2
3import (
4	"fmt"
5	"regexp"
6	"slices"
7	"strings"
8)
9
10// cdiDeviceNamePattern matches the CDI device-name grammar: an alphanumeric
11// start followed by alphanumerics, '_', '.', or '-'. Host paths and raw
12// device nodes never match.
13var cdiDeviceNamePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.\-]*$`)
14
15// AppDevicePolicy is one named administrative device grant declared under
16// [app_devices.<name>]. The installation maps a logical device name to
17// explicit CDI device IDs and exact app/service allowlists. App manifests
18// request devices by logical name; Gordon resolves the CDI IDs at
19// activation time. Host CDI resolution is never persisted as desired
20// intent: revisions keep the logical names.
21type AppDevicePolicy struct {
22	Name string
23	// CDI holds the explicit CDI device IDs (for example
24	// "nvidia.com/gpu=GPU-..."). Aggregate selectors such as "=all"
25	// are rejected: every granted device must be named explicitly.
26	CDI []string
27	// AllowedApps and AllowedServices are exact, non-empty allowlists.
28	AllowedApps     []string
29	AllowedServices []string
30}
31
32// ValidateDeviceName checks a logical device name (service charset plus -- ban).
33func ValidateDeviceName(name string) error {
34	if !serviceNamePattern.MatchString(name) {
35		return fmt.Errorf("%w: device name %q must match [a-z0-9_.-], max 63", ErrInvalidAppSpec, name)
36	}
37	if strings.Contains(name, "--") {
38		return fmt.Errorf("%w: device name %q must not contain -- (reserved separator)", ErrInvalidAppSpec, name)
39	}
40	return nil
41}
42
43// Validate checks the policy shape without touching the runtime.
44func (p AppDevicePolicy) Validate() error {
45	if err := ValidateDevicePolicyName(p.Name); err != nil {
46		return err
47	}
48	if err := validateDeviceCDIIDs(p.Name, p.CDI); err != nil {
49		return err
50	}
51	if err := validateAllowlist("app", p.AllowedApps, ValidateAppName); err != nil {
52		return fmt.Errorf("%w: device policy %q: %w", ErrDevicePolicy, p.Name, err)
53	}
54	if err := validateAllowlist("service", p.AllowedServices, ValidateServiceName); err != nil {
55		return fmt.Errorf("%w: device policy %q: %w", ErrDevicePolicy, p.Name, err)
56	}
57	return nil
58}
59
60// ValidateDevicePolicyName checks a stable policy name (service charset plus -- ban).
61func ValidateDevicePolicyName(name string) error {
62	if !serviceNamePattern.MatchString(name) {
63		return fmt.Errorf("%w: device policy name %q must match [a-z0-9_.-], max 63", ErrDevicePolicy, name)
64	}
65	if strings.Contains(name, "--") {
66		return fmt.Errorf("%w: device policy name %q must not contain -- (reserved separator)", ErrDevicePolicy, name)
67	}
68	return nil
69}
70
71// validateDeviceCDIIDs requires a non-empty list of unique, well-formed CDI
72// device IDs. Raw /dev paths, empty entries, and aggregate "=all"
73// selectors are rejected.
74func validateDeviceCDIIDs(policy string, ids []string) error {
75	if len(ids) == 0 {
76		return fmt.Errorf("%w: device policy %q: cdi must not be empty", ErrDevicePolicy, policy)
77	}
78	seen := map[string]struct{}{}
79	for _, id := range ids {
80		if err := validateDeviceCDIID(id); err != nil {
81			return fmt.Errorf("%w: device policy %q: %v", ErrDevicePolicy, policy, err)
82		}
83		if _, ok := seen[id]; ok {
84			return fmt.Errorf("%w: device policy %q: duplicate cdi device %q", ErrDevicePolicy, policy, id)
85		}
86		seen[id] = struct{}{}
87	}
88	return nil
89}
90
91// validateDeviceCDIID checks one qualified CDI device ID of the form
92// "<domain>/<kind>=<name>" (for example "nvidia.com/gpu=GPU-...").
93// The device name must not be empty, "all", or a host path.
94func validateDeviceCDIID(id string) error {
95	if id == "" {
96		return fmt.Errorf("cdi device must not be empty")
97	}
98	if strings.HasPrefix(id, "/") {
99		return fmt.Errorf("cdi device %q must be a qualified CDI ID, not a host path", id)
100	}
101	kind, name, ok := strings.Cut(id, "=")
102	if !ok || kind == "" || name == "" {
103		return fmt.Errorf("cdi device %q must be a qualified CDI ID of the form \"<domain>/<kind>=<name>\"", id)
104	}
105	if !strings.Contains(kind, "/") {
106		return fmt.Errorf("cdi device %q must be a qualified CDI ID of the form \"<domain>/<kind>=<name>\"", id)
107	}
108	if name == "all" {
109		return fmt.Errorf("cdi device %q: aggregate \"=all\" selectors are not allowed, name each device explicitly", id)
110	}
111	if !cdiDeviceNamePattern.MatchString(name) {
112		return fmt.Errorf("cdi device %q: name %q must match [A-Za-z0-9][A-Za-z0-9_.-]*, not a host path or raw device node", id, name)
113	}
114	return nil
115}
116
117// ResolveAppDevice resolves the logical device grant under this policy
118// for the given app and service. It validates the policy shape, enforces
119// the exact app/service allowlists, and returns the explicit CDI IDs.
120// The result is runtime-ready: callers encode the IDs as one native CDI
121// DeviceRequest.
122func (p AppDevicePolicy) ResolveAppDevice(app, service string) ([]string, error) {
123	if err := p.Validate(); err != nil {
124		return nil, err
125	}
126	if !allowlistContains(p.AllowedApps, app) {
127		return nil, fmt.Errorf("%w: device policy %q does not allow app %q", ErrDevicePolicy, p.Name, app)
128	}
129	if !allowlistContains(p.AllowedServices, service) {
130		return nil, fmt.Errorf("%w: device policy %q does not allow service %q", ErrDevicePolicy, p.Name, service)
131	}
132	return slices.Clone(p.CDI), nil
133}
134
135// ResolveAppDevices resolves every logical device in names against the
136// given policies for one app service. It returns the deterministic union
137// of CDI IDs (sorted) and rejects duplicate resolved IDs across logical
138// resources. Errors name only the app, service, and logical resource:
139// host inventory never appears.
140func ResolveAppDevices(app, service string, names []string, policies map[string]AppDevicePolicy) ([]string, error) {
141	if len(names) == 0 {
142		return nil, nil
143	}
144	var resolved []string
145	seen := map[string]struct{}{}
146	for _, name := range names {
147		policy, ok := policies[name]
148		if !ok {
149			return nil, fmt.Errorf("%w: app %q service %q device %q: no administrative device policy configured", ErrDevicePolicy, app, service, name)
150		}
151		ids, err := policy.ResolveAppDevice(app, service)
152		if err != nil {
153			return nil, fmt.Errorf("%w: app %q service %q device %q: refused by administrative device policy", ErrDevicePolicy, app, service, name)
154		}
155		for _, id := range ids {
156			if _, dup := seen[id]; dup {
157				return nil, fmt.Errorf("%w: app %q service %q device %q: cdi device already granted by another device", ErrDevicePolicy, app, service, name)
158			}
159			seen[id] = struct{}{}
160			resolved = append(resolved, id)
161		}
162	}
163	slices.Sort(resolved)
164	return resolved, nil
165}

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