Skip to content

Commit

Permalink
Add producer API to write specs
Browse files Browse the repository at this point in the history
This change adds a SpecProducer that can be used by clients
that are only concerned with outputing specs. A spec producer
is configured on construction to allow for the default output
format, file permissions, and spec validation to be specified.

Signed-off-by: Evan Lezar <[email protected]>
  • Loading branch information
elezar committed Oct 22, 2024
1 parent 44fef1d commit 3205f41
Show file tree
Hide file tree
Showing 13 changed files with 802 additions and 210 deletions.
32 changes: 17 additions & 15 deletions pkg/cdi/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (

"github.com/fsnotify/fsnotify"
oci "github.com/opencontainers/runtime-spec/specs-go"
"tags.cncf.io/container-device-interface/pkg/cdi/producer"

Check failure on line 31 in pkg/cdi/cache.go

View workflow job for this annotation

GitHub Actions / lint

could not import tags.cncf.io/container-device-interface/pkg/cdi/producer (-: # tags.cncf.io/container-device-interface/pkg/cdi/producer
cdi "tags.cncf.io/container-device-interface/specs-go"
)

Expand Down Expand Up @@ -281,30 +282,31 @@ func (c *Cache) highestPrioritySpecDir() (string, int) {
// 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
path string
prio int
spec *Spec
err error
)

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

path = filepath.Join(specDir, name)
if ext := filepath.Ext(path); ext != ".json" && ext != ".yaml" {
path += defaultSpecExt
// Ideally we would like to pass the configured spec validator to the
// producer, but we would need to handle the synchronisation.
// Instead we call `validateSpec` here which is a no-op if no validator is
// configured.
if err := validateSpec(raw); err != nil {
return err
}

spec, err = newSpec(raw, path, prio)
path := filepath.Join(specDir, name)

p, err := producer.New(raw,
producer.WithOverwrite(true),
)
if err != nil {
return err
}

return spec.write(true)
if _, err := p.Save(path); err != nil {
return err
}
return nil
}

// RemoveSpec removes a Spec with the given name from the highest
Expand Down
125 changes: 7 additions & 118 deletions pkg/cdi/container-edits.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import (

oci "github.com/opencontainers/runtime-spec/specs-go"
ocigen "github.com/opencontainers/runtime-tools/generate"

"tags.cncf.io/container-device-interface/pkg/cdi/producer/validator"
cdi "tags.cncf.io/container-device-interface/specs-go"
)

Expand All @@ -44,18 +46,6 @@ const (
PoststopHook = "poststop"
)

var (
// Names of recognized hooks.
validHookNames = map[string]struct{}{
PrestartHook: {},
CreateRuntimeHook: {},
CreateContainerHook: {},
StartContainerHook: {},
PoststartHook: {},
PoststopHook: {},
}
)

// ContainerEdits represent updates to be applied to an OCI Spec.
// These updates can be specific to a CDI device, or they can be
// specific to a CDI Spec. In the former case these edits should
Expand Down Expand Up @@ -167,32 +157,7 @@ func (e *ContainerEdits) Validate() error {
if e == nil || e.ContainerEdits == nil {
return nil
}

if err := ValidateEnv(e.Env); err != nil {
return fmt.Errorf("invalid container edits: %w", err)
}
for _, d := range e.DeviceNodes {
if err := (&DeviceNode{d}).Validate(); err != nil {
return err
}
}
for _, h := range e.Hooks {
if err := (&Hook{h}).Validate(); err != nil {
return err
}
}
for _, m := range e.Mounts {
if err := (&Mount{m}).Validate(); err != nil {
return err
}
}
if e.IntelRdt != nil {
if err := (&IntelRdt{e.IntelRdt}).Validate(); err != nil {
return err
}
}

return nil
return validator.Default.ValidateAny(e.ContainerEdits)
}

// Append other edits into this one. If called with a nil receiver,
Expand Down Expand Up @@ -220,71 +185,14 @@ func (e *ContainerEdits) Append(o *ContainerEdits) *ContainerEdits {
return e
}

// isEmpty returns true if these edits are empty. This is valid in a
// global Spec context but invalid in a Device context.
func (e *ContainerEdits) isEmpty() bool {
if e == nil {
return false
}
if len(e.Env) > 0 {
return false
}
if len(e.DeviceNodes) > 0 {
return false
}
if len(e.Hooks) > 0 {
return false
}
if len(e.Mounts) > 0 {
return false
}
if len(e.AdditionalGIDs) > 0 {
return false
}
if e.IntelRdt != nil {
return false
}
return true
}

// ValidateEnv validates the given environment variables.
func ValidateEnv(env []string) error {
for _, v := range env {
if strings.IndexByte(v, byte('=')) <= 0 {
return fmt.Errorf("invalid environment variable %q", v)
}
}
return nil
}

// DeviceNode is a CDI Spec DeviceNode wrapper, used for validating DeviceNodes.
type DeviceNode struct {
*cdi.DeviceNode
}

// Validate a CDI Spec DeviceNode.
func (d *DeviceNode) Validate() error {
validTypes := map[string]struct{}{
"": {},
"b": {},
"c": {},
"u": {},
"p": {},
}

if d.Path == "" {
return errors.New("invalid (empty) device path")
}
if _, ok := validTypes[d.Type]; !ok {
return fmt.Errorf("device %q: invalid type %q", d.Path, d.Type)
}
for _, bit := range d.Permissions {
if bit != 'r' && bit != 'w' && bit != 'm' {
return fmt.Errorf("device %q: invalid permissions %q",
d.Path, d.Permissions)
}
}
return nil
return validator.Default.ValidateAny(d.DeviceNode)
}

// Hook is a CDI Spec Hook wrapper, used for validating hooks.
Expand All @@ -294,16 +202,7 @@ type Hook struct {

// Validate a hook.
func (h *Hook) Validate() error {
if _, ok := validHookNames[h.HookName]; !ok {
return fmt.Errorf("invalid hook name %q", h.HookName)
}
if h.Path == "" {
return fmt.Errorf("invalid hook %q with empty path", h.HookName)
}
if err := ValidateEnv(h.Env); err != nil {
return fmt.Errorf("invalid hook %q: %w", h.HookName, err)
}
return nil
return validator.Default.ValidateAny(h.Hook)
}

// Mount is a CDI Mount wrapper, used for validating mounts.
Expand All @@ -313,13 +212,7 @@ type Mount struct {

// Validate a mount.
func (m *Mount) Validate() error {
if m.HostPath == "" {
return errors.New("invalid mount, empty host path")
}
if m.ContainerPath == "" {
return errors.New("invalid mount, empty container path")
}
return nil
return validator.Default.ValidateAny(m.Mount)
}

// IntelRdt is a CDI IntelRdt wrapper.
Expand All @@ -337,11 +230,7 @@ func ValidateIntelRdt(i *cdi.IntelRdt) error {

// Validate validates the IntelRdt configuration.
func (i *IntelRdt) Validate() error {
// ClosID must be a valid Linux filename
if len(i.ClosID) >= 4096 || i.ClosID == "." || i.ClosID == ".." || strings.ContainsAny(i.ClosID, "/\n") {
return errors.New("invalid ClosID")
}
return nil
return validator.Default.ValidateAny(i.IntelRdt)
}

// Ensure OCI Spec hooks are not nil so we can add hooks.
Expand Down
23 changes: 2 additions & 21 deletions pkg/cdi/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,8 @@
package cdi

import (
"fmt"

oci "github.com/opencontainers/runtime-spec/specs-go"
"tags.cncf.io/container-device-interface/internal/validation"
"tags.cncf.io/container-device-interface/pkg/cdi/producer/validator"
"tags.cncf.io/container-device-interface/pkg/parser"
cdi "tags.cncf.io/container-device-interface/specs-go"
)
Expand Down Expand Up @@ -67,22 +65,5 @@ func (d *Device) edits() *ContainerEdits {

// Validate the device.
func (d *Device) validate() error {
if err := parser.ValidateDeviceName(d.Name); err != nil {
return err
}
name := d.Name
if d.spec != nil {
name = d.GetQualifiedName()
}
if err := validation.ValidateSpecAnnotations(name, d.Annotations); err != nil {
return err
}
edits := d.edits()
if edits.isEmpty() {
return fmt.Errorf("invalid device, empty device edits")
}
if err := edits.Validate(); err != nil {
return fmt.Errorf("invalid device %q: %w", d.Name, err)
}
return nil
return validator.Default.ValidateAny(d.Device)
}
36 changes: 36 additions & 0 deletions pkg/cdi/producer/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
Copyright © 2024 The CDI Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package producer

import cdi "tags.cncf.io/container-device-interface/specs-go"

type SpecFormat string

Check failure on line 21 in pkg/cdi/producer/api.go

View workflow job for this annotation

GitHub Actions / Run go sanity tools (linux, amd64)

exported type SpecFormat should have comment or be unexported

Check failure on line 21 in pkg/cdi/producer/api.go

View workflow job for this annotation

GitHub Actions / Run go sanity tools (windows, amd64)

exported type SpecFormat should have comment or be unexported

const (
// DefaultSpecFormat defines the default encoding used to write CDI specs.
DefaultSpecFormat = SpecFormatYAML

// SpecFormatJSON defines a CDI spec formatted as JSON.
SpecFormatJSON = SpecFormat(".json")
// SpecFormatYAML defines a CDI spec formatted as YAML.
SpecFormatYAML = SpecFormat(".yaml")
)

// A SpecValidator is used to validate a CDI spec.
type SpecValidator interface {
Validate(*cdi.Spec) error
}
75 changes: 75 additions & 0 deletions pkg/cdi/producer/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
Copyright © 2024 The CDI Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package producer

import (
"fmt"
"io/fs"

"tags.cncf.io/container-device-interface/pkg/cdi/producer/validator"
)

// An Option defines a functional option for constructing a producer.
type Option func(*options) error

type options struct {
specFormat SpecFormat
specValidator SpecValidator
overwrite bool
permissions fs.FileMode
}

// WithSpecFormat sets the output format of a CDI specification.
func WithSpecFormat(format SpecFormat) Option {
return func(o *options) error {
switch format {
case SpecFormatJSON, SpecFormatYAML:
o.specFormat = format
default:
return fmt.Errorf("invalid CDI spec format %v", format)
}
return nil
}
}

// WithSpecValidator sets a validator to be used when writing an output spec.
func WithSpecValidator(specValidator SpecValidator) Option {
return func(o *options) error {
if specValidator == nil {
specValidator = validator.Disabled
}
o.specValidator = specValidator
return nil
}
}

// WithOverwrite specifies whether a producer should overwrite a CDI spec when
// saving to file.
func WithOverwrite(overwrite bool) Option {
return func(o *options) error {
o.overwrite = overwrite
return nil
}
}

// WithPermissions sets the file mode to be used for a saved CDI spec.
func WithPermissions(permissions fs.FileMode) Option {
return func(o *options) error {
o.permissions = permissions
return nil
}
}
Loading

0 comments on commit 3205f41

Please sign in to comment.