Skip to content

Commit 2fde79d

Browse files
committed
pkg/cdi: add Spec name generation functions.
Add functions for generating ordinary or transient Spec file names. The generated names can be passed to WriteSpec() to create the actual Spec files and to RemoveSpec() to remove them. Also update the package introduction in the golang docs describing how dynamic Spec files should be generated using the API. Signed-off-by: Krisztian Litkey <[email protected]>
1 parent c5a7189 commit 2fde79d

File tree

3 files changed

+392
-5
lines changed

3 files changed

+392
-5
lines changed

pkg/cdi/cache_test.go

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
package cdi
1818

1919
import (
20+
"fmt"
2021
"os"
2122
"path/filepath"
23+
"strconv"
2224
"strings"
2325
"sync"
2426
"testing"
@@ -29,6 +31,7 @@ import (
2931
oci "github.com/opencontainers/runtime-spec/specs-go"
3032
"github.com/pkg/errors"
3133
"github.com/stretchr/testify/require"
34+
"sigs.k8s.io/yaml"
3235
)
3336

3437
func TestNewCache(t *testing.T) {
@@ -1633,6 +1636,215 @@ containerEdits:
16331636
}
16341637
}
16351638

1639+
func TestCacheTransientSpecs(t *testing.T) {
1640+
type testCase struct {
1641+
name string
1642+
specs []string
1643+
invalid map[int]bool
1644+
expected [][]string
1645+
numSpecFiles []int
1646+
}
1647+
for _, tc := range []*testCase{
1648+
{
1649+
name: "invalid spec",
1650+
specs: []string{
1651+
`
1652+
cdiVersion: "` + cdi.CurrentVersion + `"
1653+
kind: "vendor.comdevice"
1654+
devices:
1655+
- name: "dev1"
1656+
containerEdits:
1657+
deviceNodes:
1658+
- path: "/dev/vendor1-dev1"
1659+
type: b
1660+
major: 10
1661+
minor: 1`,
1662+
},
1663+
invalid: map[int]bool{
1664+
0: true,
1665+
},
1666+
},
1667+
{
1668+
name: "add/remove one valid spec",
1669+
specs: []string{
1670+
`
1671+
cdiVersion: "` + cdi.CurrentVersion + `"
1672+
kind: "vendor.com/device"
1673+
devices:
1674+
- name: "dev1"
1675+
containerEdits:
1676+
deviceNodes:
1677+
- path: "/dev/vendor-dev1"
1678+
type: b
1679+
major: 10
1680+
minor: 1
1681+
`,
1682+
"-0",
1683+
},
1684+
expected: [][]string{
1685+
[]string{
1686+
"vendor.com/device=dev1",
1687+
},
1688+
nil,
1689+
},
1690+
numSpecFiles: []int{
1691+
1,
1692+
0,
1693+
},
1694+
},
1695+
{
1696+
name: "add/remove multiple valid specs",
1697+
specs: []string{
1698+
`
1699+
cdiVersion: "` + cdi.CurrentVersion + `"
1700+
kind: "vendor.com/device"
1701+
devices:
1702+
- name: "dev1"
1703+
containerEdits:
1704+
deviceNodes:
1705+
- path: "/dev/vendor-dev1"
1706+
type: b
1707+
major: 10
1708+
minor: 1
1709+
`,
1710+
`
1711+
cdiVersion: "` + cdi.CurrentVersion + `"
1712+
kind: "vendor.com/device"
1713+
devices:
1714+
- name: "dev2"
1715+
containerEdits:
1716+
deviceNodes:
1717+
- path: "/dev/vendor-dev2"
1718+
type: b
1719+
major: 10
1720+
minor: 2
1721+
`,
1722+
`
1723+
cdiVersion: "` + cdi.CurrentVersion + `"
1724+
kind: "vendor.com/device"
1725+
devices:
1726+
- name: "dev3"
1727+
containerEdits:
1728+
deviceNodes:
1729+
- path: "/dev/vendor-dev3"
1730+
type: b
1731+
major: 10
1732+
minor: 3
1733+
- name: "dev4"
1734+
containerEdits:
1735+
deviceNodes:
1736+
- path: "/dev/vendor-dev4"
1737+
type: b
1738+
major: 10
1739+
minor: 4
1740+
`,
1741+
"-0",
1742+
"-1",
1743+
"-2",
1744+
},
1745+
expected: [][]string{
1746+
[]string{
1747+
"vendor.com/device=dev1",
1748+
},
1749+
[]string{
1750+
"vendor.com/device=dev1",
1751+
"vendor.com/device=dev2",
1752+
},
1753+
[]string{
1754+
"vendor.com/device=dev1",
1755+
"vendor.com/device=dev2",
1756+
"vendor.com/device=dev3",
1757+
"vendor.com/device=dev4",
1758+
},
1759+
[]string{
1760+
"vendor.com/device=dev2",
1761+
"vendor.com/device=dev3",
1762+
"vendor.com/device=dev4",
1763+
},
1764+
[]string{
1765+
"vendor.com/device=dev3",
1766+
"vendor.com/device=dev4",
1767+
},
1768+
nil,
1769+
},
1770+
numSpecFiles: []int{
1771+
1,
1772+
2,
1773+
3,
1774+
2,
1775+
1,
1776+
0,
1777+
},
1778+
},
1779+
} {
1780+
t.Run(tc.name, func(t *testing.T) {
1781+
var (
1782+
dir string
1783+
err error
1784+
cache *Cache
1785+
specFiles []os.DirEntry
1786+
specs = map[int]string{}
1787+
)
1788+
1789+
dir, err = createSpecDirs(t, nil, nil)
1790+
require.NoError(t, err)
1791+
cache, err = NewCache(
1792+
WithSpecDirs(
1793+
filepath.Join(dir, "etc"),
1794+
filepath.Join(dir, "run"),
1795+
),
1796+
WithAutoRefresh(false),
1797+
)
1798+
1799+
require.NoError(t, err)
1800+
require.NotNil(t, cache)
1801+
1802+
for idx, data := range tc.specs {
1803+
var (
1804+
transientID string
1805+
raw *cdi.Spec
1806+
delIdx int
1807+
err error
1808+
)
1809+
1810+
if data[0] == '-' {
1811+
delIdx, err = strconv.Atoi(string(data[1:]))
1812+
require.NoError(t, err)
1813+
1814+
err = cache.RemoveSpec(specs[delIdx])
1815+
require.NoError(t, err)
1816+
} else {
1817+
err = yaml.Unmarshal([]byte(data), &raw)
1818+
require.NoError(t, err)
1819+
1820+
transientID = fmt.Sprintf("id%d", idx)
1821+
specs[idx], err = GenerateNameForTransientSpec(raw, transientID)
1822+
if tc.invalid[idx] {
1823+
require.NotNil(t, err)
1824+
continue
1825+
}
1826+
require.NoError(t, err)
1827+
1828+
err = cache.WriteSpec(raw, specs[idx])
1829+
require.NoError(t, err)
1830+
}
1831+
1832+
err = cache.Refresh()
1833+
require.NoError(t, err)
1834+
1835+
devices := cache.ListDevices()
1836+
require.Equal(t, tc.expected[idx], devices)
1837+
1838+
specFiles, err = os.ReadDir(
1839+
filepath.Join(dir, "run"),
1840+
)
1841+
require.NoError(t, err)
1842+
require.Equal(t, tc.numSpecFiles[idx], len(specFiles))
1843+
}
1844+
})
1845+
}
1846+
}
1847+
16361848
// Create and populate automatically cleaned up spec directories.
16371849
func createSpecDirs(t *testing.T, etc, run map[string]string) (string, error) {
16381850
return mkTestDir(t, map[string]map[string]string{

pkg/cdi/doc.go

Lines changed: 114 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,15 @@
124124
//
125125
// Generated Spec Files, Multiple Directories, Device Precedence
126126
//
127-
// There are systems where the set of available or usable CDI devices
128-
// changes dynamically and this needs to be reflected in the CDI Specs.
129-
// This is done by dynamically regenerating CDI Spec files which are
130-
// affected by these changes.
127+
// It is often necessary to generate Spec files dynamically. On some
128+
// systems the available or usable set of CDI devices might change
129+
// dynamically which then needs to be reflected in CDI Specs. For
130+
// some device classes it makes sense to enumerate the available
131+
// devices at every boot and generate Spec file entries for each
132+
// device found. Some CDI devices might need special client- or
133+
// request-specific configuration which can only be fulfilled by
134+
// dynamically generated client-specific entries in transient Spec
135+
// files.
131136
//
132137
// CDI can collect Spec files from multiple directories. Spec files are
133138
// automatically assigned priorities according to which directory they
@@ -141,7 +146,111 @@
141146
// separating dynamically generated CDI Spec files from static ones.
142147
// The default directories are '/etc/cdi' and '/var/run/cdi'. By putting
143148
// dynamically generated Spec files under '/var/run/cdi', those take
144-
// precedence over static ones in '/etc/cdi'.
149+
// precedence over static ones in '/etc/cdi'. With this scheme, static
150+
// Spec files, typically installed by distro-specific packages, go into
151+
// '/etc/cdi' while all the dynamically generated Spec files, transient
152+
// or other, go into '/var/run/cdi'.
153+
//
154+
// Spec File Generation
155+
//
156+
// CDI offers two functions for writing and removing dynamically generated
157+
// Specs from CDI Spec directories. These functions, WriteSpec() and
158+
// RemoveSpec() implicitly follow the principle of separating dynamic Specs
159+
// from the rest and therefore always write to and remove Specs from the
160+
// last configured directory.
161+
//
162+
// Corresponding functions are also provided for generating names for Spec
163+
// files. These functions follow a simple naming convention to ensure that
164+
// multiple entities generating Spec files simultaneously on the same host
165+
// do not end up using conflicting Spec file names. GenerateSpecName(),
166+
// GenerateNameForSpec(), GenerateTransientSpecName(), and
167+
// GenerateTransientNameForSpec() all generate names which can be passed
168+
// as such to WriteSpec() and subsequently to RemoveSpec().
169+
//
170+
// Generating a Spec file for a vendor/device class can be done with a
171+
// code snippet similar to the following:
172+
//
173+
// import (
174+
// "fmt"
175+
// ...
176+
// "github.com/container-orchestrated-devices/container-device-interface/specs-go"
177+
// "github.com/container-orchestrated-devices/container-device-interface/pkg/cdi"
178+
// )
179+
//
180+
// func generateDeviceSpecs() error {
181+
// registry := cdi.GetRegistry()
182+
// spec := &specs.Spec{
183+
// Version: specVersion,
184+
// Kind: vendor+"/"+class,
185+
// }
186+
//
187+
// for _, dev := range enumerateDevices() {
188+
// spec.Devices = append(spec.Devices, specs.Device{
189+
// Name: dev.Name,
190+
// ContainerEdits: getContainerEditsForDevice(dev),
191+
// })
192+
// }
193+
//
194+
// specName, err := cdi.GenerateNameForSpec(spec)
195+
// if err != nil {
196+
// return fmt.Errorf("failed to generate Spec name: %w", err)
197+
// }
198+
//
199+
// return registry.WriteSpec(spec, specName)
200+
// }
201+
//
202+
// Similary, generating and later cleaning up transient Spec files can be
203+
// done with code fragments similar to the following. These transient Spec
204+
// files are temporary Spec files with container-specific parametrization.
205+
// They are typically created before the associated container is created
206+
// and removed once that container is removed.
207+
//
208+
// import (
209+
// "fmt"
210+
// ...
211+
// "github.com/container-orchestrated-devices/container-device-interface/specs-go"
212+
// "github.com/container-orchestrated-devices/container-device-interface/pkg/cdi"
213+
// )
214+
//
215+
// func generateTransientSpec(ctr Container) error {
216+
// registry := cdi.GetRegistry()
217+
// devices := getContainerDevs(ctr, vendor, class)
218+
// spec := &specs.Spec{
219+
// Version: specVersion,
220+
// Kind: vendor+"/"+class,
221+
// }
222+
//
223+
// for _, dev := range devices {
224+
// spec.Devices = append(spec.Devices, specs.Device{
225+
// // the generated name needs to be unique within the
226+
// // vendor/class domain on the host/node.
227+
// Name: generateUniqueDevName(dev, ctr),
228+
// ContainerEdits: getEditsForContainer(dev),
229+
// })
230+
// }
231+
//
232+
// // transientID is expected to guarantee that the Spec file name
233+
// // generated using <vendor, class, transientID> is unique within
234+
// // the host/node. If more than one device is allocated with the
235+
// // same vendor/class domain, either all generated Spec entries
236+
// // should go to a single Spec file (like in this sample snippet),
237+
// // or transientID should be unique for each generated Spec file.
238+
// transientID := getSomeSufficientlyUniqueIDForContainer(ctr)
239+
// specName, err := cdi.GenerateNameForTransientSpec(vendor, class, transientID)
240+
// if err != nil {
241+
// return fmt.Errorf("failed to generate Spec name: %w", err)
242+
// }
243+
//
244+
// return registry.WriteSpec(spec, specName)
245+
// }
246+
//
247+
// func removeTransientSpec(ctr Container) error {
248+
// registry := cdi.GetRegistry()
249+
// transientID := getSomeSufficientlyUniqueIDForContainer(ctr)
250+
// specName := cdi.GenerateNameForTransientSpec(vendor, class, transientID)
251+
//
252+
// return registry.RemoveSpec(specName)
253+
// }
145254
//
146255
// CDI Spec Validation
147256
//

0 commit comments

Comments
 (0)