Skip to content

Commit

Permalink
Merge pull request #77 from klihub/transient-specs
Browse files Browse the repository at this point in the history
pkg/cdi: add functions for generating Spec names and removing Spec files.
  • Loading branch information
elezar authored Oct 31, 2022
2 parents 5609688 + a5fbc2f commit cbac83c
Show file tree
Hide file tree
Showing 6 changed files with 485 additions and 33 deletions.
65 changes: 54 additions & 11 deletions pkg/cdi/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@
package cdi

import (
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"sync"

stderr "errors"

cdi "github.com/container-orchestrated-devices/container-device-interface/specs-go"
"github.com/fsnotify/fsnotify"
"github.com/hashicorp/go-multierror"
Expand Down Expand Up @@ -255,11 +259,22 @@ func (c *Cache) InjectDevices(ociSpec *oci.Spec, devices ...string) ([]string, e
return nil, nil
}

// WriteSpec writes a Spec file with the given content. Priority is used
// as an index into the list of Spec directories to pick a directory for
// the file, adjusting for any under- or overflows. If name has a "json"
// or "yaml" extension it choses the encoding. Otherwise JSON encoding
// is used with a "json" extension.
// highestPrioritySpecDir returns the Spec directory with highest priority
// and its priority.
func (c *Cache) highestPrioritySpecDir() (string, int) {
if len(c.specDirs) == 0 {
return "", -1
}

prio := len(c.specDirs) - 1
dir := c.specDirs[prio]

return dir, prio
}

// WriteSpec writes a Spec file with the given content into the highest
// priority Spec directory. If name has a "json" or "yaml" extension it
// choses the encoding. Otherwise the default YAML encoding is used.
func (c *Cache) WriteSpec(raw *cdi.Spec, name string) error {
var (
specDir string
Expand All @@ -269,23 +284,51 @@ func (c *Cache) WriteSpec(raw *cdi.Spec, name string) error {
err error
)

if len(c.specDirs) == 0 {
specDir, prio = c.highestPrioritySpecDir()
if specDir == "" {
return errors.New("no Spec directories to write to")
}

prio = len(c.specDirs) - 1
specDir = c.specDirs[prio]
path = filepath.Join(specDir, name)
if ext := filepath.Ext(path); ext != ".json" && ext != ".yaml" {
path += ".json"
path += defaultSpecExt
}

spec, err = NewSpec(raw, path, prio)
spec, err = newSpec(raw, path, prio)
if err != nil {
return err
}

return spec.Write(true)
return spec.write(true)
}

// RemoveSpec removes a Spec with the given name from the highest
// priority Spec directory. This function can be used to remove a
// Spec previously written by WriteSpec(). If the file exists and
// its removal fails RemoveSpec returns an error.
func (c *Cache) RemoveSpec(name string) error {
var (
specDir string
path string
err error
)

specDir, _ = c.highestPrioritySpecDir()
if specDir == "" {
return errors.New("no Spec directories to remove from")
}

path = filepath.Join(specDir, name)
if ext := filepath.Ext(path); ext != ".json" && ext != ".yaml" {
path += defaultSpecExt
}

err = os.Remove(path)
if err != nil && stderr.Is(err, fs.ErrNotExist) {
err = nil
}

return err
}

// GetDevice returns the cached device for the given qualified name.
Expand Down
212 changes: 212 additions & 0 deletions pkg/cdi/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
package cdi

import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"testing"
Expand All @@ -29,6 +31,7 @@ import (
oci "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
"sigs.k8s.io/yaml"
)

func TestNewCache(t *testing.T) {
Expand Down Expand Up @@ -1633,6 +1636,215 @@ containerEdits:
}
}

func TestCacheTransientSpecs(t *testing.T) {
type testCase struct {
name string
specs []string
invalid map[int]bool
expected [][]string
numSpecFiles []int
}
for _, tc := range []*testCase{
{
name: "invalid spec",
specs: []string{
`
cdiVersion: "` + cdi.CurrentVersion + `"
kind: "vendor.comdevice"
devices:
- name: "dev1"
containerEdits:
deviceNodes:
- path: "/dev/vendor1-dev1"
type: b
major: 10
minor: 1`,
},
invalid: map[int]bool{
0: true,
},
},
{
name: "add/remove one valid spec",
specs: []string{
`
cdiVersion: "` + cdi.CurrentVersion + `"
kind: "vendor.com/device"
devices:
- name: "dev1"
containerEdits:
deviceNodes:
- path: "/dev/vendor-dev1"
type: b
major: 10
minor: 1
`,
"-0",
},
expected: [][]string{
[]string{
"vendor.com/device=dev1",
},
nil,
},
numSpecFiles: []int{
1,
0,
},
},
{
name: "add/remove multiple valid specs",
specs: []string{
`
cdiVersion: "` + cdi.CurrentVersion + `"
kind: "vendor.com/device"
devices:
- name: "dev1"
containerEdits:
deviceNodes:
- path: "/dev/vendor-dev1"
type: b
major: 10
minor: 1
`,
`
cdiVersion: "` + cdi.CurrentVersion + `"
kind: "vendor.com/device"
devices:
- name: "dev2"
containerEdits:
deviceNodes:
- path: "/dev/vendor-dev2"
type: b
major: 10
minor: 2
`,
`
cdiVersion: "` + cdi.CurrentVersion + `"
kind: "vendor.com/device"
devices:
- name: "dev3"
containerEdits:
deviceNodes:
- path: "/dev/vendor-dev3"
type: b
major: 10
minor: 3
- name: "dev4"
containerEdits:
deviceNodes:
- path: "/dev/vendor-dev4"
type: b
major: 10
minor: 4
`,
"-0",
"-1",
"-2",
},
expected: [][]string{
[]string{
"vendor.com/device=dev1",
},
[]string{
"vendor.com/device=dev1",
"vendor.com/device=dev2",
},
[]string{
"vendor.com/device=dev1",
"vendor.com/device=dev2",
"vendor.com/device=dev3",
"vendor.com/device=dev4",
},
[]string{
"vendor.com/device=dev2",
"vendor.com/device=dev3",
"vendor.com/device=dev4",
},
[]string{
"vendor.com/device=dev3",
"vendor.com/device=dev4",
},
nil,
},
numSpecFiles: []int{
1,
2,
3,
2,
1,
0,
},
},
} {
t.Run(tc.name, func(t *testing.T) {
var (
dir string
err error
cache *Cache
specFiles []os.DirEntry
specs = map[int]string{}
)

dir, err = createSpecDirs(t, nil, nil)
require.NoError(t, err)
cache, err = NewCache(
WithSpecDirs(
filepath.Join(dir, "etc"),
filepath.Join(dir, "run"),
),
WithAutoRefresh(false),
)

require.NoError(t, err)
require.NotNil(t, cache)

for idx, data := range tc.specs {
var (
transientID string
raw *cdi.Spec
delIdx int
err error
)

if data[0] == '-' {
delIdx, err = strconv.Atoi(string(data[1:]))
require.NoError(t, err)

err = cache.RemoveSpec(specs[delIdx])
require.NoError(t, err)
} else {
err = yaml.Unmarshal([]byte(data), &raw)
require.NoError(t, err)

transientID = fmt.Sprintf("id%d", idx)
specs[idx], err = GenerateNameForTransientSpec(raw, transientID)
if tc.invalid[idx] {
require.NotNil(t, err)
continue
}
require.NoError(t, err)

err = cache.WriteSpec(raw, specs[idx])
require.NoError(t, err)
}

err = cache.Refresh()
require.NoError(t, err)

devices := cache.ListDevices()
require.Equal(t, tc.expected[idx], devices)

specFiles, err = os.ReadDir(
filepath.Join(dir, "run"),
)
require.NoError(t, err)
require.Equal(t, tc.numSpecFiles[idx], len(specFiles))
}
})
}
}

// Create and populate automatically cleaned up spec directories.
func createSpecDirs(t *testing.T, etc, run map[string]string) (string, error) {
return mkTestDir(t, map[string]map[string]string{
Expand Down
Loading

0 comments on commit cbac83c

Please sign in to comment.