nri_device_injector/nri_device_injector.go (155 lines of code) (raw):
// Copyright 2023 Google Inc. 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"
"fmt"
"os"
"golang.org/x/sys/unix"
"sigs.k8s.io/yaml"
"github.com/containerd/nri/pkg/api"
"github.com/containerd/nri/pkg/stub"
log "github.com/sirupsen/logrus"
)
const (
deviceKeyPrefix = "devices.gke.io"
// Key prefix for device injection to a container, followed by container name
ctrDeviceKeyPrefix = deviceKeyPrefix + "/container."
pluginName = "device_injector_nri"
pluginIdx = "10"
// Device types.
blockDevice = "b"
charDevice = "c"
fifoDevice = "p"
)
type device struct {
Path string `json:"path"`
Type string `json:"type"`
Major int64 `json:"major"`
Minor int64 `json:"minor"`
FileMode uint32 `json:"file_mode"`
UID uint32 `json:"uid"`
GID uint32 `json:"gid"`
}
type plugin struct {
stub stub.Stub
}
func main() {
var (
opts []stub.Option
err error
)
opts = append(opts, stub.WithPluginName(pluginName))
opts = append(opts, stub.WithPluginIdx(pluginIdx))
p := &plugin{}
if p.stub, err = stub.New(p, append(opts, stub.WithOnClose(p.onClose))...); err != nil {
log.Errorf("Failed to create plugin stub: %v", err)
os.Exit(1)
}
err = p.stub.Run(context.Background())
if err != nil {
log.Errorf("plugin exited with error %v", err)
os.Exit(1)
}
}
func (p *plugin) onClose() {
log.Info("NRI connection closed")
}
// CreateContainer handles CreateContainer requests relayed to the plugin by containerd NRI.
// The plugin makes adjustment on containers with device injection annotations.
// When multiple annotations annotate devices with the same path, only the first one will be injected.
func (p *plugin) CreateContainer(_ context.Context, pod *api.PodSandbox, container *api.Container) (*api.ContainerAdjustment, []*api.ContainerUpdate, error) {
if pod == nil {
return nil, nil, nil
}
var (
ctrName = container.Name
l = log.WithFields(log.Fields{"container": ctrName, "pod": pod.Name, "namespace": pod.Namespace})
devices []device
err error
)
defer l.Info("Finished CreateContainer")
l.Info("Started CreateContainer")
devices, err = getDevices(ctrName, pod.Annotations)
if err != nil {
l.WithError(err).Warn("Failed to get device from pod annotation")
return nil, nil, err
}
adjust := &api.ContainerAdjustment{}
if len(devices) == 0 {
l.Debug("No devices annotated...")
return adjust, nil, nil
}
for _, d := range devices {
l.WithField("device", d.Path).Info("Annotated device")
deviceNRI, err := d.toNRIDevice()
if err != nil {
l.WithField("device", d.Path).WithError(err).Warn("Failed to get device from path")
return nil, nil, err
}
adjust.AddDevice(deviceNRI)
l.WithField("device", d.Path).Info("Injected device")
}
return adjust, nil, nil
}
// getDevices returns parsed devices from pod annotations of device injections.
func getDevices(ctrName string, podAnnotations map[string]string) ([]device, error) {
var (
deviceKey string = ctrDeviceKeyPrefix + ctrName
annotation []byte
parsedDevices []device
devices []device
)
if value, ok := podAnnotations[deviceKey]; ok {
annotation = []byte(value)
}
if annotation == nil {
return nil, nil
}
if err := yaml.Unmarshal(annotation, &parsedDevices); err != nil {
return nil, fmt.Errorf("invalid device annotation %q: %w", deviceKey, err)
}
paths := make(map[string]bool)
for _, d := range parsedDevices {
if _, got := paths[d.Path]; got {
continue
} else {
paths[d.Path] = true
devices = append(devices, d)
}
}
return devices, nil
}
// toNRIDevice retrieves device's major, minor and type from its path, and returns a NRI device
func (d *device) toNRIDevice() (*api.LinuxDevice, error) {
var (
stat unix.Stat_t
)
if err := unix.Lstat(d.Path, &stat); err != nil {
return nil, fmt.Errorf("failed to get info from device path %s: %v", d.Path, err)
}
var (
devNumber = uint64(stat.Rdev)
major = unix.Major(devNumber)
minor = unix.Minor(devNumber)
mode = stat.Mode
devType string
)
switch mode & unix.S_IFMT {
case unix.S_IFBLK:
devType = blockDevice
case unix.S_IFCHR:
devType = charDevice
case unix.S_IFIFO:
devType = fifoDevice
default:
return nil, fmt.Errorf("invalid device type %v from device path %v", mode, d.Path)
}
apiDev := &api.LinuxDevice{
Path: d.Path,
Type: devType,
Major: int64(major),
Minor: int64(minor),
}
if d.FileMode != 0 {
apiDev.FileMode = api.FileMode(d.FileMode)
}
if d.UID != 0 {
apiDev.Uid = api.UInt32(d.UID)
}
if d.GID != 0 {
apiDev.Gid = api.UInt32(d.GID)
}
return apiDev, nil
}