Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add NRI plugin for CDI device injection #327

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions cmd/plugins/cdi-device-injector/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
ARG GO_VERSION=1.22

FROM golang:${GO_VERSION}-bullseye as builder

ARG IMAGE_VERSION
ARG BUILD_VERSION
ARG BUILD_BUILDID
WORKDIR /go/builder

# Fetch go dependencies in a separate layer for caching
COPY go.mod go.sum ./
RUN go mod download

# Build the nri-cdi-device-injector plugin.
COPY . .

RUN make clean
RUN make IMAGE_VERSION=${IMAGE_VERSION} BUILD_VERSION=${BUILD_VERSION} BUILD_BUILDID=${BUILD_BUILDID} PLUGINS=nri-cdi-device-injector build-plugins-static

FROM gcr.io/distroless/static

COPY --from=builder /go/builder/build/bin/nri-cdi-device-injector /bin/nri-cdi-device-injector

ENTRYPOINT ["/bin/nri-cdi-device-injector", "-idx", "40"]
96 changes: 96 additions & 0 deletions cmd/plugins/cdi-device-injector/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# CDI Device Injector using NRI

The purpose of this NRI plugin is to provide a controlled mechansim for injecting
CDI devices into containers. This can be separated into two aspects:

1. Requesting devices
2. Controlling access to devices


## Requesting devices

For reqesting devices, we use pod annotations to indicate which devices should
be made available to a particular container or containers in a pod. Here a
pre-defined annotation prefix `cdi.nri.io/` is used for these annotations.
Devices are requested for a specific container by including
`container.{{ .ContainerName }}` as a suffix in the annotation key. For a
request targeting ALL containers in a pod `cdi.nri.io/pod` is used as the pod
annotaion key.

In either case, the corresponding annotation value represents a comma-separated
list of fully-qualified CDI device names.

As examples, consider the following pod annotation:
```yaml
apiVersion: v1
kind: Pod
metadata:
namespace: management
name: nri-injection-example
annotations:
cdi.nri.io/container.first-ctr: "example.com/class=device0,example.com/class=device1"
spec:
containers:
- name: first-ctr
image: ubuntu
- name: second-ctr
image: ubuntu
```

This will trigger the injection of the `example.com/class=device0` and
`example.com/class=device1` devices into the `first-ctr` container, but not into
the `second-ctr` container.

When the annotations are updated as follows:
```yaml
apiVersion: v1
kind: Pod
metadata:
namespace: management
name: nri-injection-example
annotations:
cdi.nri.io/pod: "example.com/class=device0,example.com/class=device1"
spec:
containers:
- name: first-ctr
image: ubuntu
- name: second-ctr
image: ubuntu
```

the same `example.com/class=device0` and `example.com/class=device1` devices
will be injected into all (`first-ctr` and `second-ctr`) containers in the pod.

## Controlling Access

In order to control access to specific CDI devices, we make use of namespace
annotations. Here, the same `cdi.nri.io/` prefix is used to identify an
annotation for controlling the injection of CDI devices using NRI. The
pre-defined annotation key `cdi.nri.io/allow` is used to explicitly allow access
to CDI devices.

The value field is interpreted as a filename glob to allow for wildcard matches.

For example:
* `*` will allow any CDI device to be injected
* `example.com/*` will allow any CDI device with the explicit `example.com` to be
injected.
* `example.com/class=*` will allow any CDI devices from vendor `example.com` and
class `class` to be injected.
* `example.com/class=device0` will only allow the specified CDI device to be
injected.

Consider the following example namespace (which was also referenced in the
pod examples above):

```yaml
apiVersion: v1
kind: Namespace
metadata:
name: management
annotations:
cdi.nri.io/allow: "*"
```

This allows the injection of any CDI devices into containers belonging to pods
in this namespace.
203 changes: 203 additions & 0 deletions cmd/plugins/cdi-device-injector/cdi-device-injector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// Copyright The NRI Plugins Authors. All Rights Reserved.
//
// 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 main

import (
"context"
"errors"
"flag"
"fmt"
"path/filepath"
"strings"

"github.com/sirupsen/logrus"
"sigs.k8s.io/yaml"
"tags.cncf.io/container-device-interface/pkg/cdi"
"tags.cncf.io/container-device-interface/pkg/parser"

"github.com/containerd/nri/pkg/api"
"github.com/containerd/nri/pkg/stub"
)

const (
cdiDeviceKey = "cdi.nri.io"
)

var (
log *logrus.Logger
verbose bool
)

// our injector plugin
type plugin struct {
stub stub.Stub
allowedCDIDevicePattern string
cdiCache *cdiCache
}

// CreateContainer handles container creation requests.
func (p *plugin) CreateContainer(ctx context.Context, pod *api.PodSandbox, container *api.Container) (_ *api.ContainerAdjustment, _ []*api.ContainerUpdate, err error) {
defer func() {
if err != nil {
log.Error(err)
}
}()
name := containerName(pod, container)

if verbose {
dump("CreateContainer", "pod", pod, "container", container)
} else {
log.Infof("CreateContainer %s", name)
}

if p.allowedCDIDevicePattern == "" {
return nil, nil, nil
}

cdiDeviceNames, err := parseCdiDevices(pod.Annotations, container.Name)
if err != nil {
return nil, nil, fmt.Errorf("failed to parse CDI Device annotations: %w", err)
}

if len(cdiDeviceNames) == 0 {
return nil, nil, nil
}

var cdiDevices []*api.CDIDevice
for _, cdiDeviceName := range cdiDeviceNames {
match, _ := filepath.Match(p.allowedCDIDevicePattern, cdiDeviceName)
if !match {
continue
}

cdiDevices = append(cdiDevices, &api.CDIDevice{
Name: cdiDeviceName,
})
}

adjust := &api.ContainerAdjustment{
CDIDevices: cdiDevices,
}

return adjust, nil, nil
}

func parseCdiDevices(annotations map[string]string, ctr string) ([]string, error) {
var errs error
var cdiDevices []string

for _, key := range []string{
cdiDeviceKey + "/container." + ctr,
cdiDeviceKey + "/pod",
cdiDeviceKey,
} {
if value, ok := annotations[key]; ok {
for _, device := range strings.Split(value, ",") {
if !parser.IsQualifiedName(device) {
errs = errors.Join(errs, fmt.Errorf("invalid CDI device name %v", device))
continue
}
cdiDevices = append(cdiDevices, device)
}
}
}
return cdiDevices, errs
}

// Construct a container name for log messages.
func containerName(pod *api.PodSandbox, container *api.Container) string {
if pod != nil {
return pod.Namespace + "/" + pod.Name + "/" + container.Name
}
return container.Name
}

// Dump one or more objects, with an optional global prefix and per-object tags.
func dump(args ...interface{}) {
var (
prefix string
idx int
)

if len(args)&0x1 == 1 {
prefix = args[0].(string)
idx++
}

for ; idx < len(args)-1; idx += 2 {
tag, obj := args[idx], args[idx+1]
msg, err := yaml.Marshal(obj)
if err != nil {
log.Infof("%s: %s: failed to dump object: %v", prefix, tag, err)
continue
}

if prefix != "" {
log.Infof("%s: %s:", prefix, tag)
for _, line := range strings.Split(strings.TrimSpace(string(msg)), "\n") {
log.Infof("%s: %s", prefix, line)
}
} else {
log.Infof("%s:", tag)
for _, line := range strings.Split(strings.TrimSpace(string(msg)), "\n") {
log.Infof(" %s", line)
}
}
}
}

func main() {
var (
pluginName string
pluginIdx string
allowedCDIDevicePattern string
opts []stub.Option
err error
)

log = logrus.StandardLogger()
log.SetFormatter(&logrus.TextFormatter{
PadLevelText: true,
})

flag.StringVar(&pluginName, "name", "", "plugin name to register to NRI")
flag.StringVar(&pluginIdx, "idx", "", "plugin index to register to NRI")
flag.StringVar(&allowedCDIDevicePattern, "allowed-cdi-device-pattern", "*", "glob pattern for allowed CDI device names")
flag.BoolVar(&verbose, "verbose", false, "enable (more) verbose logging")
flag.Parse()

if pluginName != "" {
opts = append(opts, stub.WithPluginName(pluginName))
}
if pluginIdx != "" {
opts = append(opts, stub.WithPluginIdx(pluginIdx))
}

p := &plugin{
allowedCDIDevicePattern: allowedCDIDevicePattern,
cdiCache: &cdiCache{
// TODO: We should allow this to be configured
Cache: cdi.GetDefaultCache(),
},
}
if p.stub, err = stub.New(p, opts...); err != nil {
log.Fatalf("failed to create plugin stub: %v", err)
}

err = p.stub.Run(context.Background())
if err != nil {
log.Fatalf("plugin exited with error %v", err)
}
}
Loading
Loading