Skip to content

Commit 8abef17

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. Signed-off-by: Krisztian Litkey <krisztian.litkey@intel.com>
1 parent d83f864 commit 8abef17

File tree

2 files changed

+285
-0
lines changed

2 files changed

+285
-0
lines changed

pkg/cdi/cache_test.go

Lines changed: 213 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
"syscall"
@@ -30,6 +32,7 @@ import (
3032
oci "github.com/opencontainers/runtime-spec/specs-go"
3133
"github.com/pkg/errors"
3234
"github.com/stretchr/testify/require"
35+
"sigs.k8s.io/yaml"
3336
)
3437

3538
func TestNewCache(t *testing.T) {
@@ -1634,6 +1637,216 @@ containerEdits:
16341637
}
16351638
}
16361639

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

pkg/cdi/spec.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,3 +262,75 @@ func validateSpec(raw *cdi.Spec) error {
262262
}
263263
return nil
264264
}
265+
266+
// GenerateSpecName generates a vendor+class scoped Spec file name. The
267+
// name can be passed to WriteSpec() to write a Spec file to the file
268+
// system.
269+
//
270+
// vendor and class should match the vendor and class of the CDI Spec.
271+
// The file name is generated without a ".json" or ".yaml" extension.
272+
// The caller can append the desired extension to choose a particular
273+
// encoding. Otherwise WriteSpec() will use its default encoding.
274+
//
275+
// This function always returns the same name for the same vendor/class
276+
// combination. Therefore it cannot be used as such to generate multiple
277+
// Spec file names for a single vendor and class.
278+
func GenerateSpecName(vendor, class string) string {
279+
return vendor + "-" + class
280+
}
281+
282+
// GenerateTransientSpecName generates a vendor+class scoped transient
283+
// Spec file name. The name can be passed to WriteSpec() to write a Spec
284+
// file to the file system.
285+
//
286+
// Transient Specs are those whose lifecycle is tied to that of some
287+
// external entity, for instance a container. vendor and class should
288+
// match the vendor and class of the CDI Spec. driver can be left empty
289+
// if only a single CDI user is generating transient Spec files for the
290+
// given vendor/class combination on the host. Otherwise driver should
291+
// uniquely identify the caller among those that use the same vendor
292+
// and class. transientID should uniquely identify the external entity
293+
// within the scope of the caller/driver.
294+
//
295+
// The file name is generated without a ".json" or ".yaml" extension.
296+
// The caller can append the desired extension to choose a particular
297+
// encoding. Otherwise WriteSpec() will use its default encoding.
298+
func GenerateTransientSpecName(vendor, class, driver, transientID string) string {
299+
name := vendor + "-" + class
300+
301+
if driver != "" {
302+
name += "+" + driver
303+
}
304+
305+
name = name + ".transient_" + transientID
306+
307+
return name
308+
}
309+
310+
// GenerateNameForSpec generates a name for the given Spec using
311+
// GenerateSpecName with the vendor and class taken from the Spec.
312+
// On success it returns the generated name and a nil error. If
313+
// the Spec does not contain a valid vendor or class, it returns
314+
// an empty name and a non-nil error.
315+
func GenerateNameForSpec(raw *cdi.Spec) (string, error) {
316+
vendor, class := ParseQualifier(raw.Kind)
317+
if vendor == "" {
318+
return "", errors.Errorf("invalid vendor/class %q in Spec", raw.Kind)
319+
}
320+
321+
return GenerateSpecName(vendor, class), nil
322+
}
323+
324+
// GenerateNameForTransientSpec generates a name for the given transient
325+
// Spec using GenerateTransientSpecName with the vendor and class taken
326+
// from the Spec. On success it returns the generated name and a nil error.
327+
// If the Spec does not contain a valid vendor or class, it returns an
328+
// an empty name and a non-nil error.
329+
func GenerateNameForTransientSpec(raw *cdi.Spec, driver, transientID string) (string, error) {
330+
vendor, class := ParseQualifier(raw.Kind)
331+
if vendor == "" {
332+
return "", errors.Errorf("invalid vendor/class %q in Spec", raw.Kind)
333+
}
334+
335+
return GenerateTransientSpecName(vendor, class, driver, transientID), nil
336+
}

0 commit comments

Comments
 (0)