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}